无服务器计算的机器学习,出路在哪里?

本文重点关注了基于无服务器计算的机器学习的最新研究进展,并通过四篇研究论文表明了无服务器 ML 框架在执行机器学习任务时性能远优于经典的基于粗粒度的 VM 集群的ML框架。

一、机器学习和无服务器学习



1.1、机器学习(ML)在应用场景中遇到了什么问题?


近年来,机器学习(Machine Learning,ML)在图像识别、文本和语音处理等领域中广泛应用,改变了人们工作、生活的方式,带来了巨大的便利性。但同时,ML 用户也面临着几个巨大的挑战,这些挑战极大地阻碍了 ML 的生产力和效率。首先,用户通常需要手动配置许多系统级参数,例如工作服务器 / 参数服务器的数量、内存分配、cpu 数量、物理拓扑等。其次,用户需要指定大量与 ML 相关的参数,如学习率、学习算法、神经网络结构等,这些参数与系统级参数之间还存在各种交互作用。第三,ML 工作流通常由多个阶段组成,包括预处理、训练、超参数搜索等等,每个阶段都有 ML 用户必须考虑的不同计算需求。


由于 ML 的这些特点,在实际应用中经常会导致两个问题:


  • 一是,ML 工作流中不同任务的异构性导致了训练工作流执行过程中资源的严重不平衡。ML 用户需要单独考虑每个阶段的异构资源需求,常常会导致资源过度配置(Resource Overprovisioning)。当前的 ML 框架通常是基于粗粒度的 VM 集群的,而这些集群并不具备 ML 相关工作负载所需的灵活性。CPU 总利用率低至 20% 的情况并不少见[1]。在实践中,开发人员在工作流的不同阶段反复使用不同的 ML 参数进行实验会进一步加剧资源过度配置的问题;

  • 二是,ML 用户需要应对复杂的管理问题,他们面临着为每个 ML 工作负载提供、配置和管理这些资源的挑战。利用 VMs 进行机器学习工作负载的系统通常需要用户重复执行一系列繁重的任务,表 1 中展示了一些任务。这种管理复杂性阻碍了交互和迭代用例,降低了用户生产力和模型的有效性。



在实践中,过度资源调配和显式资源管理负担这两个问题是紧密耦合的:ML 用户在遇到工作流不同阶段所需资源精确分配所带来的难度和人工成本的问题时,通常会采用过度资源调配的方式来应对。


那究竟用什么办法应对 ML 在实践中应用的这些问题呢?在这篇文章中我们一起来探讨一个目前广泛应用且获得了非常好效果的办法: 无服务器计算(Serverless Computing)


表 1. ML 用户在使用 VM 集群时遇到的任务挑战。




1.2、无服务器计算(Serverless Computing)


无服务器计算是云原生计算模型的一种落地状态。云计算的发展在经历了基础设施即服务(Infrastructure as a Service-IaaS)、平台即服务(Platform as a Service-PaaS)、软件即服务(Software as a Service-SaaS)几个阶段后,逐渐进入了无服务器计算的阶段。从与之前几个阶段所能提供的服务进行比较的角度分析,无服务器计算可以提供以下一种或两种服务:


  • 1.  函数即服务 (Functions-as-a-Service-FaaS)。开发人员使用由事件(event) 或 HTTP 请求触发的函数运行和管理应用程序代码,开发人员将这些小的代码单元部署到 FaaS 中,FaaS 按需执行和扩展,开发人员则无需管理服务器或任何其他底层基础设施。

  • 2. 后端即服务(Backend-as-a-Service-BaaS)。提供第三方的基于 API 的服务用于替换应用程序中的核心功能子集。对于开发人员来说,这些 API 是作为一个自动扩缩容和透明操作的服务提供的,所以对于开发人员来说,这种服务方式也是无服务器的。



从技术实现的角度分析,无服务器计算依靠云基础设施而不是用户来自动解决资源调配和管理的挑战。这种方法依赖于一个更受限制的计算单元,例如 AWS Lambda 的无状态 Lambda 函数(the Stateless Lambda Function),该计算单元由开发人员提交,并由云基础设施安排执行。因此,用户无需手动配置、部署和管理长期计算单元(例如 VM)。无服务器模式的优势促进了数据中心、云提供商和开放源代码平台的快速应用。


无服务器计算所提供的服务包括:一种有时间限制的无状态函数作为执行程序逻辑的服务 API,以及,一种管理程序状态的对象存储系统。通过使用服务 API,用户可以运行代码函数 (也称为操作) 并返回每个函数的结果。无服务器计算还提供 HTTPS 终端,允许开发人员检索函数结果,开发人员可以通过 HTTPS 终端输入参数后生成相关函数的触发事件(或链接)。对于能够清晰地分离程序状态和逻辑的应用程序设计人员来说,无服务器计算平台提供了对大型计算能力的即时访问,使得程序设计人员无需进行复杂的集群部署。


在无服务器计算平台中,云服务提供商提供了按需执行函数的能力,并对最终用户隐藏了集群配置和管理开销。除了可用性方面的好处外,这种模式还提高了效率:云提供商可以以比传统集群计算更精细的粒度复用资源,并且用户不需要为空闲资源付费。然而,为了有效地管理资源,云服务提供商对每种资源的使用进行了限制。


  • 计算(computation)。无服务器计算平台中提供的计算资源通常仅限于一个 CPU 核和一个较短的计算窗口。例如,AWS Lambda 在单个 AVX 内核上提供 900 秒的计算时间,可以访问高达 3GB 的内存和 512MB 的磁盘存储。用户可以执行许多并行函数,并且这些执行的聚合计算性能几乎呈线性扩展。函数执行中的线性可伸缩性只在单个 worker 之间没有通信的情况下对并行计算有用。在实际应用中,由于单个 worker 只是瞬时存在的,他们的启动时间可能是错开的,因此传统的类似 MPI 的点对点通信模型无法在这种环境中工作。我们可以考虑利用存储作为 worker 之间的间接通信通道。

  • 存储(Storage)。云服务提供商提供了多种存储选项,从键值存储到关系型数据库。有些服务不完全是弹性的,因为它们需要预先提供资源。然而,像 Amazon S3 或 Google Cloud Storage 这样的分布式对象存储系统提供了无限存储,用户只需按存储的数据量付费。我们可以考虑潜在地将计算期间的中间状态存储在分布式对象存储中,并且仍然可以获得与从其他节点的 RAM 访问时相同的带宽。

  • 控制面(Control Plane)。除了存储服务,云服务提供商还提供发布 - 订阅服务,如 Amazon SQS 或 Google Task Queue。这些服务通常不支持高数据访问带宽,但提供一致性保证,如至少一次传递,并且可以用于 “控制平面” 状态:所有无服务器函数调用之间共享的任务队列。云服务提供商还提供一致的键值存储(例如 DynamoDB),可用于跨无服务器函数调用存储和操作控制平面状态。



由于无服务器计算存在上述约束条件,在实际应用中,无服务器计算也不是 “完美无缺” 的,应用无服务器计算也面临很多问题。以 AWS Lambda 为例,利用无服务器计算的主要挑战是与 Lambda 函数相关联的非常小的本地资源约束(内存、cpu、存储、网络),这是无服务器计算的基础,正因为这些细粒度的计算单元实现了可伸缩性和灵活性。具体的,无服务器计算面临着如下问题:


  • 本地内存和存储空间小(Small local memory and storage)。由于存在计算资源限制,阻止了使用任何未使用这些资源设计的计算框架。例如,我们无法在 AWS Lambda 或具有此类资源受限配置的 VM 上运行 Tensorflow 或 Spark。

  • 低带宽以及缺乏 P2P 通信(Low bandwidth and lack of P2P communication)。与常规 VM 相比,Lambda 函数的可用带宽有限。我们发现,最大的 AWS Lambda 只能维持 60MB/s 的带宽,即使在中型 VM 中,也远远低于 1GB/s 的可用带宽。此外,无服务器计算对通信拓扑施加了进一步的限制。诸如 AWS Lambda 之类的无服务器计算单元不允许对等通信。因此,传统的用于数据中心 ML 的通用通信策略,例如树结构或环结构 AllReduce 通信等等,在这样的环境中都无法有效实现。

  • 短暂且不可预测的加载时间(Short-lived and unpredictable launch times)。Lambda 函数的寿命很短,且启动时间非常多变。例如,AWS Lambda 在加载后可能需要几分钟的时间来启动。这意味着在训练过程中,Lambda 会在不可预知的时间开始,并且有可能在训练中途结束。这就要求 Lambda 的 ML 运行时能够容忍 worker 的频繁离开和到达。

  • 缺乏快速共享存储(Lack of fast shared storage)。因为 Lambda 函数之间不能连接,所以需要使用共享存储。由于 ML 算法有严格的性能要求,这种共享存储需要低延迟、高吞吐量,并针对 ML 工作负载中的通信类型进行优化。然而,到目前为止,还没有能够为云提供所有这些属性的快速无服务器存储。



不过,目前已经有不少无服务器计算的落地应用案例。其中,有代表性的公有云无服务器平台有:


  • AWS Lambda。亚马逊的 AWS Lambda,借助 Lambda,几乎可以为任何类型的应用程序或后端服务运行代码,而且完全无需管理。只需上传代码,Lambda 会处理运行和扩展高可用性代码所需的一切工作。开发人员可以将代码设置为自动从其他 AWS 服务触发,或者直接从任何 Web 或移动应用程序调用。https://aws.amazon.com/cn/lambda/。

  • Microsoft Azure Functions。微软的 Azure 是一个事件驱动(Event-drive)的无服务器计算平台,可以解决复杂的编排问题。本地构建和调试,无需额外设置,在云中大规模部署和操作,并使用触发器和绑定集成服务。https://azure.microsoft.com/en-us/services/functions/。

  • Google Cloud Functions。Google 的 Cloud Functions 是一种事件驱动的计算服务。它具有自动扩展、运行代码以响应事件的能力,仅在代码运行时付费的能力,并且不需要任何服务器管理。用例包括无服务器应用程序后端,实时数据处理和智能应用程序,如虚拟助手,聊天机器人和情绪分析。https://cloud.google.com/functions/

  • 阿里云函数计算(Function Compute)。阿里的函数计算是一个事件驱动的全托管无服务器计算服务,无需管理服务器等基础设施,只需编写代码并上传,函数计算会准备好计算资源,并以弹性、可靠的方式运行代码。所有客户,函数计算都将提供每月 100 万次函数调用、400,000 个函数实例资源的免费无服务器算力支持。https://www.aliyun.com/product/fc?spm=5176.10695662.1112509.1.3b6768bc2OOWFL。



有代表性的私有云无服务器框架有:


  • Fission 。Fission 使用 Kubernetes 构建函数。它允许程序员使用任何编程语言编写函数,并将其与任何事件触发器 (如 HTTP 请求) 进行映射。https://fission.io/。

  • Funktion 。Funktion 是一个开源的容器本地化服务器平台,使用 Kubernetes 构建函数。它允许程序员用任何编程语言编写函数,可以在任何地方、任何云上或在本地运行。https://github.com/funktionio/funktion。

  • Kubeless 。是一个 kubernets 原生的无服务器计算框架。它利用 Kubernetes 资源提供自动缩放、API 路由、监控、故障恢复等功能。https://github.com/kubeless/kubeless。

  • Apache OpenWhisk 。OpenWhisk 使用 Docker 构建函数,它允许程序员使用 Scala 语言编写函数,允许在任何规模的事件响应中执行代码。框架响应类似 HTTP 请求这样的触发事件,然后运行 JavaScript 或 Swift 代码片段。https://openwhisk.apache.org/。

  • Iron Functions 。Iron 使用 Docker、Swarm、Kubernetes 构建函数,它允许程序员使用 Go 语言编写函数。https://github.com/iron-io/functions。

  • OpenLambda。OpenLambda 是一个 Apache 许可的无服务器计算项目,用 Go 编写,基于 Linux 容器。OpenLambda 的主要目标是探索无服务器计算的新方法。https://github.com/open-lambda/open-lambda。

  • OpenFaas 。OpenFaaS 是一个使用 Docker 构建无服务器 (Serverless) 功能的框架,它拥有对指标的一级支持。任何流程都可以打包为一个函数,使你能够使用一系列 web 事件,而无需重复的样板化编码。https://www.oschina.net/p/openfaas?hmsr=aladdin1e1。



有代表性的无服务器平台的包装框架有:


  • Zappa(Python,AWS)。Zappa 极大的简化了在 AWS Lambda + API 网关上发布所有 Python WSGI 应用。相当于是无服务器的部署运行 Python Web 应用。这意味着无限伸缩、零宕机、零维护。https://www.oschina.net/p/python-zappa。

  • Chalice(Python,AWS)。Chalice 允许开发者快速创建和部署应用,采用 Amazon API 网关和 AWS Lambda 。https://www.oschina.net/p/chalice?hmsr=aladdin1e1。

  • Claudia.js(Node,AWS)。方便快速部署 Node.js 项目到 AWS Lambda 和 API 网关。它自动化了所有容易出错的部署和配置任务,并按照 JavaScript 开发人员所期望的开箱即用的方式设置了一切。开发人员可以轻松地开始使用 Lambda 和 API 网关,并专注于解决重要的业务问题,而不是处理 AWS 部署工作流。https://github.com/claudiajs/claudia。



二、引入 ML 的无服务器计算最新研究情况介绍


由上一节的介绍我们知道,目前已经有很多公有云、私有云无服务器计算平台,也有一些无服务器平台的包装框架。可以说,我们想在日常的应用实践中尝试无服务器化,已经是比较容易的一件事了。不过,具体到机器学习的问题,这些无服务器计算平台在 ML 应用场景下都或多或少存在一些问题。


由第一章中的介绍我们可以看到,无服务器计算非常适用于离散化数据中心(Disaggregated Datacenters),但对许多性能关键型应用(Performance critical applications)却不是非常适用,因为无服务器计算方式切断了传统的性能优化途径,例如利用数据局部性进行优化或分层通信等,因此会直接影响性能关键型应用的效果。目前无服务器平台主要用于简单的事件驱动应用程序,如物联网自动化、前端 web 服务和日志处理等等。


最近,一些研究人员将无服务器计算应用在更广泛的场景中,如并行数据分析和分布式视频编码。然而,这些工作负载要么只能简单并行,要么只能跨函数使用简单的通信模式。复杂的通信模式和工作负载如何有效地适应无服务器计算仍然是一个有待研究的问题。我们这篇文章中重点关注的是用于 ML 的无服务器计算。我们知道,ML 包含大量的参数、复杂的处理流程,是典型的 “性能关键型应用”,我们将在这一节中介绍最新的关于“如何将 ML 引入无服务器计算” 这一问题的研究进展。


2.1、A Case for Serverless Machine Learning [2]




本文是来自 Berkeley 的研究人员发表在 NIPS2018 中的一篇文章,具体分析了 ML 工作负载环境下的资源管理问题,探讨了利用无服务器基础设施实现 ML 工作流资源管理自动化的研究方向。作者提出了一个无服务器机器学习框架,该框架专门用于无服务器基础设施和 ML 工作流。


本文所讨论的无服务器计算依赖于 Amazon S3 的无状态 Lambda 函数,这些函数由开发人员提交,并由云基础设施自动调度。因此,它们避免了开发人员显式配置、部署和管理长期计算单元(例如 VM)的需要。与一般的无服务器计算平台不同,无服务器机器学习框架需要满足三个关键目标。首先, 它的 API 需要支持广泛的 ML 任务:数据预处理、训练和超参数优化。为了简化从现有 ML 系统的转换所涉及的工作量,应该用 Python 之类的高级语言开发这样的 API。第二,为了为无状态工作者之间的中间数据和消息传递提供存储, 它需要提供一个具有丰富接口的低延迟可伸缩数据存储。第三,要在资源受限的 Lambda 上高效运行, 它的 Runtime 必须是轻量级和高性能的


为了满足这些条件,作者构建了一个专门用于 ML 的无服务器框架。


  • 首先,该框架为 ML 工作流的所有阶段提供了一个 API,该 API 实用且易于更广泛的 ML 社区使用。(1)API 完全包含在 Python 包中,允许 ML 开发人员轻松调用。(2) API 提供了一个抽象底层系统级资源的高级接口。(3) Python 包提供了一个用户界面,开发人员可以通过该界面可视化工作进度。

  • 然后,该框架包含 Python 前端提供到客户端后端的接口。这个后端负责管理临时计算资源和调度任务。在这个后端中,不同的子模块为 ML 工作流的每个特定阶段的逻辑(例如预处理)进行编码处理。这些子模块启动 Lambda 上的 worker,跟踪计算进度,并在计算完成后将结果返回到 Python 前端。客户端后端使用内部低级调度程序,该调度程序封装了与启动、终止和重新生成在无服务器 Lambda 上运行的任务相关的所有逻辑。这个调度程序还跟踪所有任务的状态。

  • 第三,该框架提供一个轻量级 Runtime,它封装了系统支持的不同计算之间共享的所有函数,从而简化了新算法的开发。Worker runtime 提供两个接口。首先,它提供了一个智能迭代器来训练存储在 S3 中的数据集。这个迭代器在 Lambda 的本地内存中预取和缓冲 mini-batch,与 worker 的计算并行,以减轻访问 S3 的高延迟(>10ms)。它为分布式数据存储提供了一个 API。

  • 最后,该框架为 workers 之间的中间数据和通信提供具有丰富接口的共享存储。此接口有两种类型的 API:(1)用于一般消息传递、中间数据存储和数据缩减的键值存储,以及(2)参数服务器接口。为了达到所需的低延迟,将该数据存储部署在云 VMs 上。为了有效地利用稀缺的网络资源,对数据存储接口进行优化处理,例如:数据压缩、稀疏数据结构、异步通信等。



为了实现简化机器学习工作流执行的目标,理想的系统应该提供一个简单但足够通用的 API。这个 API 需要让用户在一个单一的、集成的框架内执行 ML 任务,例如:(1)数据集加载,支持常用的数据格式,(2)数据预处理,(3)模型训练,(4)大规模的超参数调整。


作者给出了一个例子来展示这个 API 的功能——图 1 中给出基于 Criteo Kaggle 竞争开发模型的过程,该模型用于预测用户点击显示广告数据集的广告的概率。工作流的第一步是加载数据集并将其上载到 Amazon S3。例如,用户可以调用 load_libsvm 方法来加载以 LIBSVM 格式存储的数据集,解析数据后自动为其创建分区,然后将其上载到 Amazon S3。第二步,一旦数据加载到 Amazon S3 中,就可以立即进行预处理。系统应该提供一些开发人员常用的预处理方法。例如,用户可以通过使用 Amazon S3 中数据集的路径调用 normalize 函数来规范化数据集。一旦加载了数据,用户就可以通过查看系统的测试损失来训练不同的模型并查看它们的性能。一旦用户对某个特定的模型获得了合理的损失,他们就可以通过超参数搜索对其进行微调。此外,作者设想这样一个系统允许用户在每个阶段的执行过程中进行多次交互。例如,当超参数搜索任务正在运行时,用户应该能够监视每个单独实验的测试损失。对于表现不好的实验(例如,测试损失发散(test loss is diverging)),用户应该能够终止它们。这个特性可以通过交互环境(比如 Jupyter)中的用户界面来实现。


图 1. API 示例。无服务器 ML 的 API 应该支持 ML 开发工作流的不同阶段:(a)预处理,(b)训练,和(c)超参数调优。


为了评估对 ML 无服务器框架的需求,作者引入两个框架进行性能比对:PyWren[3]和 Bosen[4]。PyWren 是一个专门用于无服务器架构的 Map-reduce 框架。PyWren 提供了可缩放到数千个 workers 的 map 和 reduce 原语。Bosen 是一个分布参数框架,专门用于基于 VM 的 ML 算法。为了进行评估,作者在 PyWren 上实现了一个异步 SGD 训练算法。在 PyWren 基线实现的基础上,作者还进行了一组优化。作者使用了来自 Criteo Kaggle 竞赛的 Criteo 展示广告数据集进行实验。作者在 10 个最大的 AWS Lambda(3GB 内存)上运行 PyWren,在单个 VM(m5.2xlarge1)中的 8 个内核上运行 Bosen。


作者通过记录随时间变化的测试损失来测量这两个系统的性能(图 2)。对于 PyWren,作者在实现每个优化之后报告这个值。作者累计实现了以下优化:(1)跨迭代重用 Lambda;(2)使用异步 SGD 进行小批量预取;(3)使用低延迟存储(Redis)代替 Amazon S3;(4)使用具有多 get 操作的稀疏数据传输。我们观察到这些优化显著改善了 Pyren 在 600 秒后实现的最终测试损失(从 0.61 到 0.57)。尽管有了这些改进,PyWren 仍然比 Bosen 慢得多。进一步的性能分析表明,PyWren 存在一些开销,例如序列化 / 反序列化数据,以及使用接口不适合 ML 工作负载的远程存储(例如 Redis 或 S3)。这一结果表明,在设计无服务器计算框架的早期,需要仔细考虑 ML 工作负载的性能需求。


图 2. PyWren 和 Bosen 在 Criteo-Kaggle 逻辑回归任务中的表现。PyWren 基线通过重用 Lambda、添加预取、切换到异步计算、用更高性能的 Redis 存储后端替换 S3 以及支持在单个 RPC 上获取多个密钥而得到了增量改进。


此外,作者还构建了本文所提出的框架的原型,包括:(1)具有参数服务器接口的高性能数据存储,(2)mini-batch 数据的循环缓冲区预取,(3)逻辑回归 SGD 训练算法。为了充分验证这种设计的好处,作者在相同的逻辑回归任务中对其进行了评估。作者测量了每个 worker 的平均 SGD 迭代时间(见图 3)。这个时间是 worker 性能的一个指标;较低的迭代时间意味着更频繁的模型更新和更快的收敛。作者还将这一次的 SGD 算法分解为四个主要步骤:(1)从数据存储中获取最新模型,(2)从远程存储(例如 S3)中获取一个 minibatch,(3)计算 SGD 梯度,以及(4)将梯度发送到数据存储。作者发现,尽管无服务器计算具有固有的开销,本文所提出的框架原型还是实现了较低的每次迭代时间( 500 μs)--- 与 Bosen 这样的系统不相上下。这种性能源于两种机制:(1)远程 mini-batch 的有效预取和缓冲,以及(2)尽可能与数据存储通信。首先,minibatch 预取机制通过与计算并行进行,有效地掩盖了从 S3 获取 minibatch 所需的时间。实际上,对于中型 / 大型 Lambda,在新的 minibatch 上开始计算所需的时间可以忽略不计,因为大多数情况下,这些数据都是在 worker 需要之前缓存在内存中的。即使从 S3 获取一个 mini-batch 需要 10ms 也是这样的。其次,作者发现与数据存储的通信是有效的(例如,发送梯度的时间可以忽略不计)。由于能够与数据存储异步通信,进一步提升了该框架的性能。


图 3. 本文所提出原型每次 SGD 迭代的时间。具体细分为四个主要步骤:(1)将梯度发送到数据存储,(2)计算梯度,(3)从数据存储获取模型,(4)从 S3 获取 minibatch。


2.2、Cirrus: a Serverless Framework for End-to-end ML Workflows [5]


这篇文章也是节 2.1 中所介绍的 Berkeley 研究小组的研究成果,是对节 2.1 中分析的 NIPS’18 中文章所涉及工作的扩展和延伸。在专门用于无服务器基础设施和 ML 工作流的无服务器 ML 框架原型的基础上,将其封装为一个实现端到端管理的分布式 ML 训练框架 Cirrus,可以直接调用使用(https://github.com/ucbrise/cirrus),并将相关工作内容发表在发表在 SoCC ’19 中。Cirrus 专门用于无服务器云基础设施(如 Amazon AWS Lambda)中的 ML 训练。它提供高级原语来支持 ML 工作流中的一系列任务:数据集预处理、训练和超参数优化。Cirrus 结合了无服务器接口的简单性和无服务器基础设施(具体是指 AWS Lambda 和 S3)的可伸缩性,以最小化用户的工作。


Cirrus 的设计原则是:


  • 自适应的细粒度资源分配。为了避免由于过度配置而造成的资源浪费,Cirrus 应该灵活地调整为每个工作流阶段保留的细粒度资源量。

  • 无状态服务器端后端。为了确保无服务器计算资源的健壮和高效管理,Cirrus 设计了一个无状态的服务器端后端。有关当前部署的函数以及 ML 工作流任务和计算单元之间的映射的信息由客户端后端管理。因此,即使所有云端资源变得不可用,ML 训练工作流也不会失败,并且可以在资源再次可用时恢复其操作。

  • 端到端无服务器 API。模型训练不是 ML 研究人员的唯一任务,数据集预处理、特征工程和参数调整等对于最终生成一个好的模型同样重要。Cirrus 应该提供一个完整的 API,允许开发人员以最小的工作量端到端的大规模地运行这些任务。

  • 高可扩展性。ML 任务是高度计算密集型的,在没有有效并行化的情况下需要很长时间才能完成。因此,Cirrus 应该能够同时运行数千个 workers 和数百个实验。



与节 2.1 中所介绍的工作类似,Cirrus 利用四个系统模块来实现上述原则。首先,Cirrus 为 ML 开发人员提供了 Python 前端。这个前端有两个功能:a)为 ML 训练的所有阶段提供丰富的 API;b)在无服务器的基础设施中执行和管理大规模计算。其次,Cirrus 提供了一个客户端后端。第三,为了克服低延迟无服务器存储的不足,Cirrus 为 worker 共享的所有中间数据提供了低延迟分布式数据存储。第四,Cirrus 提供了一个在无服务器 Lambda 上运行的 worker 运行时(runtime)。该运行时提供了访问 S3 中的训练数据集和分布式数据存储中的中间数据的有效接口。Cirrus 的完整结构见图 4。


图 4. Cirrus 系统结构。系统由(有状态的)客户端(左)和(无状态的)服务器端(右)组成。预处理和面向用户的训练包含一个前端的 API。客户端后端管理云功能和向函数分配任务。服务器端由 Lambda Worker 和高性能数据存储组件组成。Lambda worker 将数据迭代器 API 导出到客户端后端,并包含许多迭代训练算法的有效实现。数据存储用于存储梯度、模型和中间预处理结果。


Cirrus 的整体结构与节 2.1 中是类似的。Cirrus 的前端和客户端后端是用 Python 实现的,方便 Cirrus 与现有的机器学习方法相结合。为了提高效率,分布式数据存储和 worker runtime 用 C++ 实现。表 2 列出了实现的不同组件以及它们的大小和实现语言。Worker runtime 代码包括迭代器接口和数据存储客户端实现。worker runtime 和数据存储通过 TCP 连接进行通信。作者实现了一个共享组件库,其中包括线性代数库、通用实用程序和 ML 算法,这些组件被所有系统组件共享。作者已经公开发布了 Apache 2 开源许可的实现(https://github.com/ucbrise/cirrus)。


表 2. Cirrus 组件。


首先,Cirrus 为 ML 工作流的所有阶段提供了一个 Python 前端 API。前端是一个高度灵活的 thin Python API,默认情况下,它从开发人员那里抽象出所有的细节,同时提供了通过 API 的参数覆盖内部配置参数(例如,优化算法)的能力。前端还提供了一个运行在 Plotly 上的用户界面,供用户监控工作负载的进度和启动 / 停止任务。Cirrus Python API 分为三个子模块。每个子模块都打包了与工作流的每个阶段相关的所有函数和类。(1)预处理。预处理子模块允许用户对存储在 S3 中的训练数据集进行预处理。此子模块允许不同类型的数据集转换:最小 - 最大缩放、标准化和特征散列。(2)训练。Cirrus 的训练子模块支持 ML 模型,这些模型可以通过随机梯度下降进行训练。目前 Cirrus 支持稀疏 Logistic 回归、潜在 Dirichlet 分配、Softmax 和协同过滤。(3)超参数优化。超参数优化子模块允许用户在给定的参数集上运行网格搜索。Cirrus 允许用户改变 ML 训练参数(例如,学习率、正则化率、小批量大小)以及系统参数(例如,Lambda 函数大小、并发 worker 数量、梯度过滤)。


其次,Cirrus 的 Python 前端提供了一个到 Cirrus 客户端后端的接口。这个后端的功能和能够完成的任务与节 2.1 中介绍的框架完全相同。客户端后端从前端算法中抽象出 Lambda 的管理。客户端后台会保存一个当前活动的 Lambda 列表,以及一个 AWS Lambda API 的连接列表(每个连接用于启动一个 Lambda)。在训练期间加载的 Lambda 在其生存期结束时自动重新加载(每 15 分钟一次)。由于 Lambda API 的特殊性,从一台服务器上快速加载数百个 Lambda 是非常困难的。为了解决这个问题,后端保留一个线程池,可用于响应新 Lambda 任务的请求。


第三,Cirrus 提供了分布式存储模块。Cirrus 的数据存储用于存储所有 workers 共享的中间数据。由于现有产品中不允许 Lambda 之间进行交互通信,因此 Lambda 需要共享存储。无服务器 Lambda 的存储需要满足三个条件:首先,它需要低延迟(本文实现低至 300μs),以便能够适应延迟敏感的工作负载,例如用于 ML 训练的工作负载(迭代 SGD)。其次,它需要扩展到数百个 workers,以利用无服务器基础架构几乎线性的可扩展性。第三,它需要一个丰富的接口来支持不同的 ML 用例。例如,数据存储必须支持 multiget(§6.5)、常规键 / 值的 put/get 操作和参数服务器接口。为了实现低延迟,将数据存储部署在云 VMs 中。它实现了低至 300μs 的延迟,而 AWS S3 的延迟约为 10ms。此延迟对于训练阶段最大化模型的更新至关重要。作者使用稀疏表示来表征梯度和模型以实现高达 100 倍的压缩比,以便与存储和批处理请求进行数据交换。为了实现高可伸缩性,Cirrus 包括以下机制:(1)分片存储,(2)高度多线程,(3)数据压缩,(4)梯度滤波器和(5)异步通信。Cirrus 的分布式数据存储提供了一个接口,支持所有在 ML 工作流中存储中间数据的用例。该接口支持键值存储接口(set/get)和参数服务器接口(send 果然啊 dient/get model)。


最后,Cirrus 提供了一个运行时(Runtime),它封装了系统支持的不同计算之间共享的所有函数。如图 5,Cirrus 的 Runtime 为 ML 计算提供了通用抽象(General abstractions)和基本数据类型(Data primitives)用于访问训练数据、参数模型和中间结果。这些可用于向 Cirrus 添加新的 ML 模型。为了简化新算法的开发,Runtime 提供了一组线性代数库。Cirrus 的初始版本使用外部线性代数库如 Eigen 进行梯度计算。为了减少 Eigen 处理序列化和反序列化数据的时间,作者最终开发了自己的线性代数库。对于数据访问,Runtime 提供了一个由本地循环缓冲区支持的基于 minibatch 的迭代器,允许 worker 以低延迟访问训练 minibatch。此外,它还提供了一个高效的 API 来与分布式数据存储进行通信。


图 5. Cirrus Runtime。minibatch 是异步预取的,并在每个 Lambda 的内存中本地缓存(取决于使用的 Lambda 的大小)。将梯度异步发送至参数服务器,每次迭代模型同步从参数服务器中进行检索。


作者给出了 Cirrus 在不同阶段的详细工作方式。


(1) 数据加载和预处理。Cirrus 假设训练数据存储在一个全局存储中,比如 S3。因此,使用 Cirrus 的第一步就是将数据集上传到云端。用户将数据集的路径传递给系统,然后由系统负责解析和上载数据集。在此过程中,Cirrus 将数据集从其原始格式(如 csv)转换为二进制格式。这种压缩消除了在训练和超参数调优阶段进行反序列化的需要,这有助于减少 Lambda 工作进程中的计算负载。其次,Cirrus 生成数据集大小相似的分区,并将其上传到 S3 存储桶(S3 Bucket)。



Cirrus 还可以应用变换(Transformations)来提高模型的性能。例如,对于 Cirrus 实现的异步 SGD 优化方法,对数据集中的特征进行规范化处理能够提高训练的效果。对于这些 transformations,Cirrus 启动了一个大型 Map Reduce 作业:每个输入分区一个 worker。在 map 阶段,每个 worker 计算其分区的统计信息(例如,平均值和标准差)。在 reduce 阶段,这些局部统计信息被聚合以计算全局统计信息。在最后的映射阶段,worker 转换每个分区样本,给出最终的每列统计信息。对于大型数据集,map 和 reduce 阶段会跨大量 worker 和列来聚合每列的统计信息。这会造成每秒生成大量新的写操作和读操作,而超出了 S3 支持的事务吞吐量。基于这个原因,作者使用 Cirrus 的低延迟分布式数据存储来存储映射的中间结果,并减少了计算量。



(2) 模型训练。Cirrus 使用分布式 SGD 算法进行模型训练。在训练期间,worker 运行 Lambda 函数,并迭代计算梯度步长。每个梯度计算需要两个输入:一个 minibatch 和最新的模型。minibatch 是 Cirrus 的运行时通过迭代器从 S3 获取的。因为迭代器在工作内存中缓冲 minibatch,所以检索 minibatch 的延迟非常低。使用数据存储 API(get_sparse_model_X)从数据存储中同步检索最新的模型。对于每个迭代,每个 worker 都计算一个新的梯度。然后将此梯度异步发送到数据存储(send_gradient_X)以更新模型。



(3) 超参数优化。超参数优化是一种模型参数的搜索方式,该模型参数能够保证生成最佳准确度。典型的做法是在多维参数空间上执行网格搜索。搜索可以是暴力破解(Brute-force)搜索或自适应搜索。常见的做法是让网格搜索完整地运行,然后对结果进行后处理,以找到最佳配置。这是一种代价高昂的资源浪费。Cirrus 通过提供超参数搜索仪表板(Hyperparameter search dashboard),来解决这种超时过度配置问题(over-provisioning over time)。Cirrus 超参数仪表板提供了一个统一的界面,用于监控模型随时间变化的损失收敛情况。它允许用户选择单个损失曲线并终止相应的训练实验。因此,Cirrus 提供了:启动超参数搜索的 API 和执行后端;监控模型精度收敛的仪表板;终止单个调优实验的能力,并节省了过度配置成本。



在文献 [2] 工作的基础上,Cirrus 为 ML 用户提供了一个轻量级的 Python API。作者同样给出了一个例子来展示这个 API 的功能。如图 6 所示,这个 API 与图 1 中给出的文献 [2] 中的 API 几乎相同。区别在于本文已经将 Cirrus 封装为模块“cirrus”,可直接在 python 中进行 import。


图 6. Cirrus API 示例。Cirrus 支持 ML 开发工作流的不同阶段:(a)预处理,(b)训练,和(c)超参数调优。


作者利用稀疏逻辑回归任务对比 Cirrus 和两个专门用于基于 VM 的 ML 训练框架:TensorFlow[6]和 Bosen[4]。TensorFlow 是一个用于 ML 计算的通用数据流引擎。Bosen 是一个分布式和多线程参数服务器,由 CMU 开发 Petuum 商业化,它针对大规模分布式集群和机器学习算法的陈旧更新进行了优化。逻辑回归是计算任何给定样本属于两个感兴趣的类的概率问题。本文实验中作者计算网站广告被点击的概率,并利用时间函数评估学习收敛性。使用 Criteo 显示广告数据集[7]。这个数据集包含 45M 个样本,大小为 11GB。每个样本包含 13 个数字特征和 26 个分类特征。在训练之前,对数据集进行了归一化处理,将分类特征哈希为一个大小为 2^20 的稀疏向量。为了评估 Bosen,作者使用 1、2 和 4 个 m5.2xlarge 亚马逊 AWS 实例(每个实例有 8 个 CPU 和 32GB 内存)。对于 Bosen 实验,作者将数据集分区到所有机器上。为了评估 Cirrus,作者使用 Amazon AWS Lambda 作为 worker,m5.large 实例(2 个 CPU,8GB 内存,10Gbps 网络)作为参数服务器,AWS S3 存储用于训练数据和定期备份型。作者报告了尝试两个系统的学习率范围后得到的最佳结果。对于 Bosen,只改变学习率和工人数量。所有其他配置参数都保留默认值。


图 7a 显示了不同数量的服务器(对于 Bosen)和 AWS Lambda(对于 Cirrus)在一段时间内实现的逻辑测试损失。通过对一个包含 50K 样本的数据集上的训练模型评估以得到损失值。作者发现,Cirrus 的收敛速度明显快于 Bosen。Bosen 的性能因为 worker 相互竞争共享本地缓存受到影响,该缓存在将梯度发送到参数服务器之前聚合梯度。这种设计最终导致了 Bosen 收敛速度较慢。在图 7b 中,作者使用相同的数据集和相同的预处理步骤将 Cirrus 与 TensorFlow 进行了比较。同样地,Cirrus 性能优于 TensorFlow。


图 7c 中的实验对比的是 Cirrus 和 Spark 完成协同过滤任务的性能,该实验中使用的是 Netflix 数据库[8]。由图 7c,Cirrus 比 Spark 收敛得更快,测试损耗更低。此外,作者还观察到 Spark 的 ALS 实现受到昂贵的 RDD 开销的影响,因为 Spark 需要将整个数据集加载到内存中。这导致 Spark 花了超过 94% 的时间来做与训练模型不直接相关的工作。相比之下,Cirrus 从 S3 连续向 worker 流式传输数据,这使得他们可以立即开始计算。




图 7. (a) Bosen 和 Cirrus 之间不同设置的时间损失比较。Bosen 达到的最佳损失为 0.485,Cirrus 达到最佳损失的速度至少快了 5 倍(200 秒 vs 1000 秒)。与最先进的 ML 训练框架相比,Cirrus 可以在一个或两个 Lambda 的寿命内(300-600 秒)更快地收敛,并且损失更低。(b) Tensorflow Criteo_tft 基准和 Cirrus 的收敛与时间曲线。Tensorflow 是在 32 核节点上执行的,Cirrus 在 10 个 Lambda 中运行。(c) 运行 Netflix 数据集时,Spark (ALS)和 Cirrus 的 RMSE 随时间变化曲线。Spark 在运行 Netflix 数据集时,前 4 分钟处理数据,并在 ALS 的 5 次迭代中收敛(RMSE=0.85)后终止。Cirrus 能够更快收敛到较低的 RMSE(0.833)。


图 8 中的实验验证的是 Cirrus 的可扩展性(Scalability)。通过设计该系统以实现 3 个维度的扩展:用 S3 存储训练数据,用 Lambda 计算,以及用分布式参数服务器共享内存,来实现扩展性。


存储扩展性:Cirrus 通过将 S3 中的训练数据集分割成中等大小的对象来解决这个问题。作者使用 10MB 的对象,因为作者发现这个大小可以实现良好的网络利用率,同时对于最小尺寸的 Lambda 来说也足够小。通过使用大型对象,减少了每秒的请求数量。因此,当每个 worker 从 S3 消耗 30MB/s 的训练数据时,能够将 S3 的吞吐量线性扩展到 1000 个 Cirrus workers(图 8a)。


计算扩展性:由图 8b,没有模型和参数的同步得情况下 Cirrus 可以通过并行传输输入训练数据和计算梯度来实现线性计算可伸缩性。


参数服务器扩展性:在参数服务器层面,主要挑战来自于每个虚拟机 VM 有限的网络带宽,以及更新模型和 worker 请求服务器所需的计算。Cirrus 通过 1)模型分片,2)稀疏梯度 / 模型,3)数据压缩,4)异步通信来解决这个问题。Cirrus 实现了线性可扩展性,最高可达 600 个 worker(图 8c)。


图 8. AWS 存储(GB / 秒)、AWS 无服务器计算(梯度 / 秒)和 Cirrus 数据存储(样本 / 秒)的可扩展性。每个 worker 消耗 30MB/s 的训练数据。



最后,作者对比了专门的 ML 系统 PyWren 与 Cirrus。PyWren 是一个运行在无服务器 Lambda 上的 map-reduce 框架。它提供了可扩展至数千名 worker 的 map 和 reduce 原语。PyWren 的 Runtime 经过优化可以在 AWS Lambda 上运行,AWS Lambda 也是本文用于 Cirrus 实验的无服务器平台。作者在实验中对 PyWren 进行了优化,使其每次模型更新的平均时间提高了 700 倍(从 14 秒到 0.02),但其模型每秒更新次数仍然远低于 Cirrus(图 9b),并且收敛速度明显慢于 Cirrus(图 9a)。


图 9. PyWren 和 Cirrus 在 10 个 Lambda 上运行时在稀疏逻辑回归工作负载上的性能。由于结合了预取、在模型训练迭代中重复使用 Lambda 以及通过 Cirrus 的快速数据存储进行高效的模型共享,Cirrus 实现了 2 个数量级的模型更新数量增长。训练数据预取解决了 S3 的高访问延迟问题,从而使更新速度增加了 10 倍 / 秒。


2.3、Distributed Machine Learning with a Serverless Architecture [9]


本文作者介绍了一个完全基于无服务器架构的分布式机器学习新框架:SIREN。SIREN 由本地客户端和无服务器云平台(例如 Amazon Lambda)组成,前者使用深度强化学习(Deep Reinforcement Learning,DRL)agent 进行资源调度决策,后者根据这些调度决策为 ML 训练作业加载无状态函数(Stateless Functions)。SIREN 的完整结构框架如图 10。


图 10.SIREN 结构


首先,将一个代码包部署到无服务器云平台中,其中包含用户定义的 ML 模型及其所依赖的库。然后,根据初始资源方案(即函数的数量和内存大小)加载无状态函数群,进行基于 SGD 的第一个 epoch 训练。在第一个 epoch 结束时,收集作业的函数状态和统计数据,并以状态(States)的形式反馈给本地客户端的 DRL agent,DRL agent 将采取行动为下一个 epoch 做出资源调度决策。SIREN 会随着训练作业的 epoch 推进自适应调整资源调度决策:在不同的 epoch 中,可以启动不同数量、不同内存配置的函数。


SIREN 采用的是 SGD 算法,使用 mini-batches 并在多个 Lambda 函数上运行。每个 Lambda 函数的作用就类似于传统参数服务器架构中的 worker。SIREN 与参数服务器架构的一个主要区别是,在 SIREN 中不存在参数服务器来处理模型参数更新。相反,数据和模型都存储在一个共同的数据存储中(例如 Amazon S3),所有函数都可以访问。每个函数从公共存储中读取当前模型,根据 mini-batches 训练数据计算梯度,然后直接用新计算的梯度更新公共存储中的模型。因此,整个架构是无服务器的。在 SIREN 中,作者提出了一种混合同步并行(Hybrid synchronous parallel,HSP)计算模式。如图 11 所示,在每个 epoch 内,所有的函数都可以异步更新模型,同时在每个 epoch 结束时施加一个同步屏障(Synchronization barrier),以便完成下一个 epoch 的资源调度。


已知 epoch 为 t,第 k 个 mini-batch 为Ξ_t,k,更新模型为:




在 epoch t-1 结束时的模型ω与ω_t,0 相同。HSP 在无服务器架构中是高效的,因为加载的函数是同质的,从而导致每个 epoch 的同步代价都很低。在无服务器云平台中,调用和终止函数也是轻量级的。


图 11. 无服务器云上的混合同步并行(HSP)处理。


作者使用 Python 代码实现了 SIREN,支持 AWS Lambda 之上的 ML 模型训练,并全面支持 MXNet APIs。机器学习开发人员可以在 SIREN 上运行他们的传统 MXNet 项目,而无需重构现有代码。如图 10 所示,SIREN 包括三个主要部分:(1)封装 MXNet 机器学习库的代码包;(2)用 AWS SDK boto3 构建本地客户端,调用并管理 AWS Lambda 中的无状态函数;(3)用 TensorFlow 实现 DRL agent,进行动态资源配置决策。此外,还对 AWS Lambda 进行了一系列约束,以保证无状态函数的轻量级和可移植性。


由于 AWS Lambda 的编程 runtime 不支持原生的 ML 训练算法,作者在代码包中引入了一部分 MXNet ML 库。在 AWS Lambda 上,代码包大小限制为 250 MB,这使得直接将任何现成的 ML 库(如 MXNet、TensorFlow)加载到 AWS Lambda 上都是不可行的。为了缩小 MXNet 代码包的大小,作者用不同的编译选项组合重新编译了 MXNet 源代码,并排除了无服务器云中不必要的编译选项。例如,禁用了 USE_CUDA、USE_CUDNN 和 USE_OPENMP 等选项。


在 AWS Lambda 上,单个函数的计算能力也受到限制:要求每个 Lambda 函数最多在 300 秒内执行完毕,最大内存大小为 3GB。但是,由于 AWS Lambda 支持每个 AWS 账户中多达 3000 个函数并发执行,因此 SIREN 通过使用大量 Lambda 函数并行化 ML 训练工作负载实现了高度的并行性。


作者提出了一种深度强化学习(Deep reinforcement learning,DRL)技术,用于完成 SIREN 中的动态资源部署。强化学习 (RL) 是一种经验驱动的方法,agent 通过与动态环境的交互以及执行行动获得奖励来学习如何在动态环境中表现。DRL 利用深度神经网络 (Deep neural network,DNN) 来解决 RL 问题。agent 观察来自动态环境的各种噪声信号,这些信号被称为状态(state),并将这些状态反馈给 DNN 由其产生动作。agent 在环境中采取动作并获得奖励,而奖励又被用来更新 DNN 中的参数,以做出更好的决策。DRL 在一个闭环中工作以改善决策,其最终目标是使总奖励最大化。


作者考虑在一个有 M 个样本的数据集上训练 ML 工作负载,总奖励预算为 B。如果达到一定的损失值 L 或者总奖励预算 B 用完,则训练终止。在任何一个 epoch t,调度器将对并行调用的函数数量(用 n_t 表示)以及每个函数的内存大小 m_t 做出判断。令 f_t,i 表示在第 t 个 epoch 加载第 i 个活跃函数,如图 11 所示。需要注意的是,如果函数 i 已经到了它的运行寿命,则会调用一个新的函数来代替它,且仍然用 f_t,i 来表示,所以在 epoch t 中总会有 n_t 个函数在并发执行。在每一个函数 f_t,i 中,重复计算一个新的 mini-batch 数据的聚合梯度,并根据 HSP 模式下的 SGD 更新模型参数。


在 epoch t 中,假设函数 f_t,i 花费一个完整周期(P^F)_t,i 来获取 mini-batch 数据,(P^C)_t,i 计算梯度,(P^U)_t,i 更新模型参数。函数 i 在 epoch t 的完整执行时间为:




epoch t 在 HSP 的全部持续时间为 P_t=max_i(P_t,i)。在 epoch t 结束时,ML 任务的损失值更新为 l_t。


无服务器云根据函数执行时间和函数内存大小向用户收费。令 c 表示使用 1GB 内存执行一个函数一秒钟的单价。一个 epoch t 的总花费为:






而 ML 任务的总的奖励成本为:




其中,T 表示 epoch 的总数。本文所述任务的目标是最小化作业完成时间,即在一定奖励预算 B 约束下解决以下优化问题:




在每个 epoch t 开始时,DRL agent 决定资源配置计划 (n_t, m_t),即 DRL 任务中的动作 action,具体如图 12。衡量动作(n_t, m_t) 有效性的方法是在每个 epoch t 的结束进行数字 reward 量化计算。计算的依据是这个 epoch 持续的时间 P_t 和任务结束时预算是否透支。


图 12.DNN 策略表示的 DRL。


状态(State):在本文所描述的问题中,epoch t 的状态表示为:




其中,l_t 表示 epoch t 的损失值,(P^F)_t、(P^C)_t、(P^U)_t 分别表示获取、平均计算和平均模型参数更新时间,P_t 为 epoch 的执行时间。u_t 和ω_t 分别表示平均内存和 CPU 的利用情况,b_t 为剩余预算。


动作(Action):在本文所描述的问题中,动作表示为 a_t=(n_t, m_t)。n_t 表示激活的函数数量,m_t 表示每个函数的内存大小。DRL agent 根据策略选择操作,策略定义为给定当前状态下整个操作空间的概率分布π(a | s)。作者使用策略梯度方法,通过参数θ的函数来近似策略π(a | s)。因此,策略π可以写成π(a | s, θ),其中θ是要学习的参数。将策略π定义为实值空间的高斯概率密度:




基于条件概率π(a_t | s_t-1, θ)确定动作 a_t。然后,在一个大的离散作用空间上学习概率质量函数的问题就转化为在一个二维连续空间中寻找参数 (μ(s,θ),σ(s,θ)) 的问题。