本文最初发布于 Towards Data Science 博客,经原作者 Priansh Shah 授权由 InfoQ 中文站翻译并分享。
现在,关于人工智能的教程多如牛毛:比如,如何进行目标检测、图像分类、自然语言处理、构建聊天机器人等等,这份清单还不止这些。
但当我查找有关如何正确进行大规模部署人工智能的资料时,我发现相关内容屈指可数。更令人惊讶的是,眼下这些少得可怜的资源似乎都重申了同样的这几点:
使用 TensorFlow 之类的可扩展框架构建模型。
要么将其打包到客户端中(如 TF.js Lite、TF-slim 等),要么将其部署为带有容器的微服务。
我对上述第二点更感兴趣,因为我已经开发了一个模型,但让我很惊讶的是,在网上,关于如何实现这点的细节,相关资料寥若晨星。至于每种解决方案有何缺点的信息更是寥寥可数。经过几天的研究并在 Crane.ai 上进行人工智能规模化之后,我整理了更多的信息,如 有关部署、它们的缺点以及如何在低级别上优化 TensorFlow 模型。
其中一项最常用的技术就是使用 TensorFlow、TF Lite 或 TensorFlow Slim 等工具将人工智能打包到你所选择的客户端中。限于篇幅,我并不会赘述这些框架是如何运行的,而是重点阐述它们的缺点。
计算能力。 这些模型的部署问题是它们需要海量内存(我指的是受移动 App 或浏览器的限制,即 > 1~2GB 内存)。很多手机并不具备这种能力,桌面浏览器会延迟 UI 线程,同时也会拖慢电脑、使发热更严重,以致于启动了 CPU 风扇等等。
推理时间。 在计算能力未知的设备上运行模型时,推理时间通常也是未知的;而且这些还不是配置 GPU、大内存、高配 CPU 的机器,而是智能手机、浏览器以及普通电脑上运行的桌面应用程序。使用更大的模型仅需一分钟即可轻松地进行推理,但从用户体验的角度来看,却是一个大大的 “NO”。
大文件。 不幸的是,大多数模型都存储在相当大的文件中(我们说的是动辄几十 MB、几百 MB 那种)。因此,这会拖慢速度,而且加载起来会占用大量内存,并且会大大增加了应用程序包的大小。
不安全。 除非你使用的是开源模型,否则,你应该将你的人工智能和预训练的检查点进行相对保密。然而不幸的是,当你将模型与应用程序打包在一起时,不仅你的推理代码容易遭到反编译,而且 预训练的检查点也会被打包其中,很容易被窃取。
难以更新。 如果想更新模型,那么客户端有两项选择。要么用户通过像 Google Play、Apple 的 App Store 那样的应用商店发布的更新进行升级,但这方式会带来 频繁的大更新(这对于用户来说非常麻烦,而且这一更新可能会被终止或者永远不更新,具体要取决于用户的设置);或者应用程序自身进行检查更新和获取元数据。后者听上去更好一些,但这意味着,你必须通过用户可能不稳定的连接下载一个 100MB 以上的文件;这需要耗费一段时间,因此你的应用程序至少要在后台运行才能完成这一更新过程,而且你还需要支付相当大的网络输出成本(这取决于你采用的云端)。
缺乏可训练性。 针对新用户数据的训练模型提供了一定程度的个性化,同时提高了其准确性,并构建了核心的、高信号的数据集。不幸的是,大多数设备缺乏训练模型的计算能力,就算它们有这种能力,也不可能将训练的效果传送到运行这一应用的服务器或其他设备。
由于这些缺点的存在,在客户端上部署和维护大型神经网络几乎是一个不可能实现的任务。因此,我们剔除了这一选项。
云端 是大规模部署模型的强大工具。你可以根据需求调整完全定制的环境,将你的应用进行容器化,并立即横向扩展,同时提供可与大公司相媲美的 SLA 和正常运行时间。
对大多数 TensorFlow 模型来说,部署周期 是相同的:
将图固化为 Protobuf 二进制
调整推理代码以使用固化图
将应用程序容器化
在上面添加 API 层
第一部分相对简单。“固化”(Freezing)图涉及到创建一个 Protobuf 二进制文件,其中包含检查点所涉及的所有命名节点、权重、体系结构和元数据。这些都可以用各种工具来实现,最为流行的工具就是 TF 自己的工具,可以在 给定输出节点名的情况下,固化任何图。要深入了解这项技术,以及如何完成这一部分的更多信息,可参阅《A Tool Developer's Guide to TensorFlow Model Files》中的 “Freezing” 一节:
https://www.tensorflow.org/guide/extend/model_files#freezing
调整推理代码也不难;多数情况下,你的feed_dict
将保持不变,主要区别在于,添加的加载模型的代码,也许还有输出节点的规范。
容器化也非常简单:只需在 Dockerfile 中设置环境即可(可使用 TF docker 镜像作为基础)。但当我们开始添加 API 层时,事情就开始变得一团糟。通常有两种方法可以做到这一点:
部署运行推理脚本的可扩展容器。 这些容器针对输入运行脚本,脚本启动会话并运行推理,通过管道将输出内容反馈给你作为结果。这是非常有问题的;对大多数云服务提供商来说,添加一个操作容器和管道的 API 层并 不是一件很容易的简单事(例如,虽说 AWS 有 API 网关,但它也远没有你所期望的那么方便),这种方法 效率最低。这里的问题在于 容器启动、硬件分配、会话启动和推理 方面损失了宝贵的时间。如果打开stdin
并保持管道输出,就会加快脚本的速度,但这样一来,就 失去了可扩展性(因为你现在已连接到该容器的 STDIN,它就将无法接受多个请求)。
部署运行 API 层的可扩展容器。 尽管在架构上类似,但出于几种原因,这种方法效率更高;通过 将 API 层放到容器中,可以缓解先前提到的大部分问题。虽然这样一来,需要更多的资源,但它是最小的,并不意味着垂直扩展。它 允许每个容器保持运行,而且这种情况下 API 是分散的,因此将特定的stdin
/stdout
连接到主请求路由也没有问题。这意味着你可以 节约启动时间,并且可以 在服务多个请求时,能够轻松 保持速度和横向扩展。你可以使用 负载均衡器 将容器集中化,并使用 Kubernetes 来保证几乎 100% 的正常运行时间并管理 Fleet。这种方法简单而有效!
通过容器 Fleet 来分散 API 的主要缺点是,这些成本加起来会很快变得昂贵。不幸的是,这对人工智能来说是不可避免的,尽管有一些方法可以缓解这种情况。
重用会话。 你的 Fleet 会根据负载成比例地扩展与缩小,因此你的目标是将运行推理所需时间最小化,以便容器可以腾出时间来处理另一个请求。一种方法是 重用 tf.Session
和 tf.Graph
,一旦初始化后就存储它们并将其作为全局变量进行传递。这就会减少 TF 启动会话和构建图所需的时间,从而大大加快推理任务的速度。这一方法即使在单个容器上也有效,并且被广泛用作 最小化资源重新分配和最大化效率 的技术。
缓存输入,如果可能,也缓存输出。 在人工智能中,动态编程范式最为重要;通过缓存输入,可以节约预处理或从远程获取所需的时间,而通过缓存输出,可以节约运行推理所需的时间。这些都可以在 Python 中轻松完成, 但你应该问问自己它是否适合你的用例 。通常情况下,模型会随着时间的推移而变好,这将极大地影响你的输出缓存机制。在我自己的系统中,我喜欢使用所谓的 “80-20” 规则。当模型的准确率低于 80% 时,我 不会缓存任何输出。一旦达到 80%,我就开始缓存,并 在某个准确度将缓存设置为过期(而不是在某个时间点设置为过期)。这样,随着模型变得更为准确,输出也将会发生变化,但在这个 “80-20” 规则减轻的缓存中,性能和速度之间的权衡较少。
使用任务队列。 通常来说,需要运行的推理任务有大一些的,也有小一些的(在我们的例子中,既有大一些的,也有小一些的,还有复杂的和简单的图)。就用户体验来说,这里使用堆队列并优先处理较小的任务可能会更好,这样运行一个简单步骤的用户只需等待该步骤即可,而无需等待先完成另一个用户的更大推理。(你肯定会想,为什么这里就不能使用横向扩展呢?不是不可以,而是这样一来 会增加成本)
在具有任务队列的专用 GPU 上训练模型。 训练模型是一项长期而艰巨的任务,它需要大量的资源使用,并且模型在其持续期间内无法使用。如果你要将每个交互反馈到模型中进行训练,请 考虑在单独的服务器上运行,也许还要使用 GPU。一旦训练完成之后,你可以将模型(在 AWS 中,可以将模型存储库集中在 S3 中)部署到容器中去。
经过深思熟虑,我们提出了一个大规模部署人工智能的有效工作流程:
固化图,并在 API 之下封包推理
重用 会话和图,并 缓存 输入和输出
使用 Docker(包括 API 层)将应用程序进行 容器化
使用 Kubernetes 在你所选择的云端上大规模 部署 应用程序
从推理中 分离 训练
开发任务队列确定处理较小的任务的 优先级
使用这些技术,你应该能够以最小的成本和开销进行部署,同时最大限度地提高速度和效率!
原文链接:
https://towardsdatascience.com/scaling-ai-2be294368504
如果你喜欢这篇文章,或希望看到更多类似优质报道,记得给我留言和点赞哦!