一、机器学习和无服务器学习
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,θ)) 的问题。
奖励(Reward):在本文所描述的问题中,每个 epoch 结束时奖励定义为:r_t=-β P_t,其中β为正则化系数。epoch t 的时间越长,agent 得到的奖励就越少。最后一个 epoch T 的奖励为:
换句话说,如果作业成功停止,即在不超出预算 B 的情况下满足收敛阈值 L,则向 agent 分配正 C 的奖励。否则,如果作业失败,即在用完预算 B 之前还没有收敛,则给奖励赋值为负 C。在 DRL 中,agent 学习的是累计折扣奖励:
其中,γ ∈ (0, 1]为未来折扣奖励因子。在整个 DRL 训练过程中,上式中的目标函数引导着 agent 找到最优的估计值。
作者模拟了一个无服务器的云环境,运行由 DRL agent 控制的 mini-batch SGD 算法。作者使用 OpenAI Gym 实现模拟环境(https://gym.openai.com (https://gym.openai.com/)),OpenAI Gym 是一个用于评估强化学习算法的开源接口。实验目的是验证与传统的网格搜索(Grid Search)基线方法所找到的最优(静态)策略相比使用 SIREN 进行调度的优势。作者比较了在 AWS Lambda 上使用 SIREN 和在 EC2 集群上使用 MXNet 训练 ML 作业的完成时间和成本。具体实验中选择了三种类型的 EC2 实例来构建测试集群:m4.large(2 vCPU,8GB 内存)、m4.xlarge(4 vCPU,16GB 内存)和 m4.2xlarge(8 vCPU,32GB 内存),每小时分别收费 0.1 美元、0.2 美元和 0.4 美元。
图 13 给出了 SIREN 与网格搜索最佳函数数量的比较实验。图 13(a)比较了通过网格搜索和 SIREN 实现的训练时间。与网格搜索相比,SIREN 在预算为 300 美元的情况下最多可减少 36% 的训练时间。如图 13(b)所示,网格搜索列举了不同预算下不同数量的函数的总奖励情况。SIREN 能够根据经验动态调整函数数量。图 13(c)给出了分配给每个 epoch 的函数数量。在前几个 epoch 中,SIREN 启动了大量的函数以快速降低损失值;在后几个 epoch,agent 减少了函数数量以节省成本。SIREN 的 DRL agent 通过与模拟的无服务器云的迭代交互进行在线训练。图 13(d)中的学习曲线表明,agent 通过探索不同数量的函数来学习最大化总奖励。agent 的训练在大约 200 次迭代之后完成。
图 13. SIREN 与网格搜索最佳函数数量比较。
图 14. 通过 SIREN 和 EC2 上的 MXNet 对 MNIST 数据集训练 LeNet。
图 14 的实验对比 SIREN 和 EC2 上的 MXNet。图 14(a)显示了使用 12 个 EC2 集群和使用 SIREN 训练 LeNet 的完成时间和相应的成本。由于 EC2 集群的异质性,EC2 上的成本与训练完成时间呈非线性关系。例如,m4.xlarge×6 集群和 m4.2xlarge×6 集群几乎在同一时间完成训练,但后者产生的成本是前者的两倍。相比之下,SIREN 通过更多的投资缩短了完成时间。图 14(b)显示,SIREN 动态调整每个训练 epoch 的函数及其内存。当函数数量减少时,每个函数收到的训练数据分区更大,需要更大的内存来处理数据分区。SIREN 中的 DRL agent 是通过与 AWS Lambda 在线交互进行训练的。从图 14(c)中的学习曲线可以看出,经过 150 次左右的迭代,DRL agent 的训练已经完成。
进一步的,作者在 m4.2xlarge instances 的集群上训练 LeNet、CNN 模型和线性分类模型并确定相应的成本。然后,在成本相同的情况下在 m4.2xlarge×8 集群上用 SIREN 训练同样的模型。图 15 中的实验数据显示,与相同成本的 EC2 集群相比,SIREN 使用这些模型分别减少了 40%、39.4% 和 44.3% 的训练时间。
图 15. 不同模型相同成本预算下 SIREN 与 EC2 的比较。
2.4、Serverless Linear Algebra [10]
本文作者构建了 NumPyWren:一个基于无服务器编程模型的线性代数系统,以及 LAmbdaPACK:一个为高度并行线性代数算法的无服务器执行而设计的领域特定语言。相关工作发表在 SoCC’20 中。
无服务器计算(例如,AWS Lambda、Google Cloud Functions、Azure Functions)是一种编程模型,云提供商在其中管理服务器同时动态管理资源分配。通常,无服务器平台计算会公开一个有时间限制的、无状态的 FaaS API,以及一个管理程序状态的对象存储系统。对于能够清晰地分离程序状态和逻辑的应用程序设计人员来说,无服务器平台提供了对大型计算能力的即时访问,而无需应对复杂集群部署的开销。
本文所研究的内容:密集线性代数(Dense linear algebra)极大地受益于现有的以服务器为中心的数据中心。现有的分布式线性代数框架可以通过利用局部性、网络拓扑和单个服务器内的资源紧密集成来完成高性能计算。在这样的背景下作者提出这样一个问题:这些线性代数算法能否成功地移植到一个分散数据中心中?也就是说,我们能否在无服务器编程模型中实现与基于 MPI 的分布式线性代数框架相当的性能?
本文作者构建了 NumPyWren,一个在无服务器架构上完成线性代数任务的系统。NumPyWren 执行使用 LAmbdaPACK 编写的程序,LAmbdaPACK 是作者构建的一个高级 DSL,可以简洁地表示任意基于分片的线性代数算法。NumPyWren 通过无状态函数执行来执行大规模密集线性代数程序。通过对中间语言 LAmbdaPACK 的分析,作者最终证明了分散式无服务器计算模型(Disaggregated serverless computing model)可以用于具有复杂通信程序的计算密集型程序。
NumPyWren 解决的是类似 Cholesky 分解的线性代数问题。考虑求解线性方程 Ax=b 的问题,其中 a 是对称正定矩阵。我们可以先把 a 分解成两个三角形矩阵 a=LL^T,然后解两个相对简单的 Ly=b 和 L^T x=y 得到解 x。从这个过程中可以看出,分解是该求解问题中计算代价最高的步骤。Communication-Avoiding Cholesky 是一个很好的计算 Cholesky 分解的算法。该算法将矩阵分成若干块,并得出一个计算顺序使总数据传输量最小。具体算法如下:
如图 16,在 outer loop(j)的每一步中,算法首先计算单个块 Ajj 的 Cholesky 分解(图 16(a))。这个结果用来更新由 Aij 下面的列块组成的 "面板(panel)"(图 16(b))。最后,第 j 列右边的所有区块都会根据各自的位置,通过索引更新面板(图 16(c))。通过向下移动对角线重复这一过程(图 16(d))。
图 16. 并行 Cholesky 分解的前 4 个时间步骤:0)对角块 Cholesky 分解,1)并行列更新,2)并行子矩阵更新,3)(后续)对角块 Cholesky 分解。
作者针对 Algorithm 1 提出了两点问题。首先,作者认为 Algorithm 1 在执行过程中展现出了动态并行性。外循环(Outer loop)由三个不同的步骤组成,具有不同的并行度,从 O(1)、O(K)到 O(K2),其中 K 是每个步骤的封闭子矩阵大小。其次,该算法在这三个步骤之间存在细粒度的依赖关系,无论是在一个迭代内还是在多个迭代之间。由此,作者提出了本文所考虑的工作,即:实现适应可用的并行化,作者通过将程序分解为可并行运行的细粒度执行单元来实现这一点。为了在无状态环境中实现这一点,作者建议以分散的方式执行依赖性分析。将描述程序控制流的全局依赖图分发给每个 worker。然后,每个 worker 根据其在全局任务图中的当前位置,对其下游依赖关系进行本地推理。
首先,我们介绍本文提出的 LAmbdaPACK:一种用于实现并行线性代数算法的特定语言。LAmbdaPACK 是生成和使用矩阵块(Tiled matrices)的命令式程序。这些程序可以对标量值执行基本的算术和逻辑运算。它们不能直接读取或写入矩阵值;相反,所有实质性的计算都是通过调用矩阵块上的本机内核来执行的。矩阵块由索引引用,LAmbdaPACK 程序的主要作用是对内核调用排序,并计算每个调用的分块索引。LAmbdaPACK 包括简单的 for 循环和 if 语句,但是没有递归,只有从 LAmbdaPACK 到内核的一级函数调用。每个矩阵块索引只能写入一次,这是许多函数式语言的共同设计原则。LAmbdaPACK 中的原语功能强大,包括 Tall Skinny QR(TSQR)、LU、Cholesky 和奇异值分解等等。LAmbdaPACK 的示例性描述如图 17 所示。
图 17. LAmbdaPACK 语言的示例性描述。
关于 LAmbdaPACK 的算法分析主要包括两个阶段。由于原始未压缩的 DAG 非常大,其节点数可能会随着 Cholesky 或 QR 等算法的输入大小呈立方级增长,因此,第一阶段的任务是分析程序并提取任务的压缩 DAG。DAG 中的每个任务对应一个数组写入,我们还需提取执行此任务所需的内核计算和数组读取。由于每个数组读取都有一个唯一的上游写入任务,因此此过程是可跟踪处理的。第二个阶段发生在 runtime,在执行任务之后,能够动态发现下游任务。使用当前循环变量绑定的信息来查询下游任务的压缩 DAG。图 18 和图 19 分别给出了 LAmbdaPACK 的 DAG 和程序示例。
图 18.LAmbdaPACK DAG 示例。
图 19. LAmbdaPACK 程序示例。
LAmbdaPACK 中没有并行原语,而是 LAmbdaPACK runtime 通过静态分析程序来推断底层依赖关系图。为了并行执行程序,作者从程序产生的依赖结构构造了一个内核调用的 DAG。作者借用并扩展了循环优化技术(loop optimization),将 LAmbdaPACK 程序转换为隐式有向无环图(Implicit DAG)。将程序 DAG 中的每个节点 N 表示为一个元组(line_number, loop_indices)。利用这个信息,可以执行程序迭代空间中的任何语句。
接下来,作者解决推导 DAG 中特定节点的下游依赖关系问题。作者提出在 runtime 处理依赖性分析:每当一个存储位置被写入时,确定从同一存储位置读取的 N(所有行,所有循环索引)中的表达式。每当一个存储位置被写入时,我们确定从同一存储位置读取 N(所有行,所有循环索引)中的表达式。作者将约束建模为一个方程组。假设单个线性代数算法中的行数必然很小,而程序迭代空间通常非常大。当数组仅由循环变量的仿射函数索引时,即形式为 ai+b 的函数,其中 i 是循环变量,a 和 b 是编译时已知的常数,则可以使用循环优化来有效地查找特定节点的依赖关系。
如图 19 中的程序示例,如果在 runtime 一个 worker 正在执行程序的第 7 行,i=0、j=1 和 k=1,以查找下游依赖项,则分析器将扫描这 7 行中的每一行,并计算是否存在一组有效的循环索引,以便在程序中的该点读取 S[1,1,1]。如果是这样,那么元组(line_number, loop_indices)定义了该任务的下游依赖项,并确定为当前任务的子任务。为了便于访问和开发,作者将 LAmbdaPACK 嵌入 Python 中。由于大多数 LAmbdaPACK 调用优化的 BLAS 和 LAPACK 内核,因此使用高级解释语言的性能损失很小。LAmbdaPACK 详细流程见 Algorithm2。
然后,我们介绍本文提出的 NumPyWren 框架。NumPyWren 框架包括五个独立可扩展的主要组件:runtime 状态存储、任务队列、轻量级全局任务调度器、无服务器计算 runtime 和分布式对象存储。图 20 展示了 NumPyWren 框架组件。
图 20. NumPyWren 执行框架的体系结构,具体为 6x6cholesky 分解期间的 runtime 状态。
任务排队(Task Queue):客户端进程将需要执行的第一个任务排队到任务队列中。任务队列是一个发布 - 订阅样式的队列,它包含 DAG 中的所有节点,这些节点的输入依赖关系都已满足并准备好执行。
执行器配置(Executor Provisioning):任务队列的长度由配置者(Provisioner)监控,provisioner 管理计算资源以匹配执行期间的动态并行性。在第一个任务排队后,provisioner 启动一个执行器(executor),并根据任务队列大小维护活动 executor 的数量。由于 provisioner 的角色只是轻量级的,所以它也可以作为 “无服务器” 云函数定期执行。
任务执行(Task Execution):执行器管理 NumPyWren 任务的执行和调度。一旦执行器准备就绪,它就轮询任务队列以获取可用的任务,并执行任务中的编码指令。大多数任务涉及从对象存储读取输入和将输出写入对象存储,以及执行 BLAS/LAPACK 函数等。假定对象存储是一个分布式持久存储系统,它支持单个密钥的先读后写一致性。使用一个带有单一静态赋值语言的持久对象存储,有助于设计容错协议。当执行器接近无服务器系统的 runtime 限制时(AWS Lambda 为 900),执行器自动终止。如果有必要的话,provisioner 将负责雇佣新 worker。容错协议能够实现即使工作进程超过 runtime 限制或是在执行过程中被云提供商杀死,程序仍能在有效状态下运行。
Runtime 状态更新(Runtime state update):一旦任务执行完成并且输出被持久化,执行器就会更新 runtime 状态存储中的任务状态。runtime 状态存储跟踪整个执行的控制状态,并且需要支持每个任务的快速更新。如果已完成的任务具有 “ready” 子任务,则执行器会将该子任务添加到任务队列中。状态存储的原子性保证了每个子任务都能够被调度。这个使用执行器执行调度的过程实现了高效、分散、细粒度的任务调度。由于计算和存储的分离,NumPyWren 中的容错非常容易实现。因为对对象存储的所有写入都是持久的,所以在任务完成后都不需要重新计算。
任务租用(Task Lease):在 NumPyWren 中,所有挂起的和可执行的任务都存储在一个任务队列中。保持一个不变量,即任务只有在完成后才能从队列中删除(例如,runtime 状态存储已更新,输出持久化到对象存储)。当一个 worker 获取一条任务,这个 worker 就获得了该任务的租约(lease)。在租用期间,该任务被标记为不可见,以防止其他 workers 获取这条任务。
故障检测和恢复(Failure Detection and Recovery):在正常操作期间,worker 将使用后台线程续订任务租约,直到任务完成。如果任务完成,worker 将从队列中删除该任务。如果 worker 失败,它将无法再续订租约,并且该任务将对任何可用的 worker 可见。因此,故障检测在租约到期时发生,恢复时间由租约长度决定。
垃圾收集(Garbage collection):由于 NumPyWren 将所有中间状态存储到一个持久对象存储区,因此在不再需要时清除状态是非常必要的。但是,由于在对象存储中存储字节的成本极低,与处理 TB 级中间状态问题的计算成本相比,在程序结束时进行垃圾收集就足够了。使用与程序相关联的唯一 id 标记对象存储中单个程序执行的所有分配。在程序执行终止后,NumPyWren 通过启动一组并行的无服务器任务来异步清理对象存储,以清理与给定程序 id 关联的所有对象。
自动缩放(Autoscaling):与传统的无服务器计算模型(每个新任务分配一个新容器)不同,NumPyWren 中的任务调度和 worker 管理是解耦的。这种解耦允许自动扩展计算资源,以实现更好的性价比权衡。在 NumPyWren 中,作者采用了一个简单的自动缩放启发式算法,能够在保持较低作业完成时间的同时获得很好的利用率。
作者对 4 种线性代数算法:矩阵乘(Matrix Multiply,GEMM)、QR 分解(QR Decomposition,QR)、奇异值分解(SingularValue Decomposition,SVD)、Cholesky 分解(Cholesky Decomposition,Cholesky)进行了实验评价。对于这四种算法,作者将它们与最先进的 MPI 实现进行比较。其中 Cholesky,GEMM 和 SVDwe 使用 ScaLAPACK 实现,ScaLAPACK 是一个工业级 Fortran 库,专为高性能、分布式密集线性代数而设计。对于 QR 分解,则使用了 communication-avoiding QR 分解算法的优化实现。NumPyWren 实现大约有 6000 行 Python 代码,作者将其构建在 Amazon web 服务(AWS)平台上。对于 runtime 状态存储,使用的是 Redis--- 一个由 ElasticCache 提供的键值存储。尽管 ElasticCache 是一种配置的(而不是“无服务器”)服务,但作者发现使用一个实例就足以满足所有工作负载。此外,作者还发现,可以用托管供应商提供的键值存储(如 DynamoDB)来替换 Redis,但性能略有下降。作者将 Amazon 的简单队列服务(Simple queue Service,SQS)用于任务队列,Lambda 或 EC2,使用 Amazon S3 作为远程对象存储。
作者对实验进行了一些特殊的设备选择、环境选择或参数选择。首先,由于不能很容易地控制并发 Lambda 执行的数量或 AWS 提供的硬件类型,出于实验控制的原因,作者通过模仿 EC2 上的无服务器 runtime 来进行大部分评估以便与其他系统进行比较。其次,本文的 Lambda 模拟基于 PyWren 框架中的“独立模式”。PyWren 使用一个单独的 SQS 队列来模拟 Lambda 作业队列,并使用有时间限制的进程来模拟短函数调用。在控制底层硬件(AVX、NIC 等)时,使用 SQS 会导致不确定性。然后,目前 Lambda 的定价是 EC2 现货价格的 10 倍,这使得本文的大规模实验无法在 Lambda 上进行。作者通过实验对比发现,在 EC2 上运行模拟的无服务器环境与在 AWS Lambda 上运行的性能差别最小。最后,模拟环境中的实验还允许修改某些在真实无服务器环境中用户无法控制的系统参数,如函数超时等。
表 3 中给出针对四种密集线性代数方法 NumPyWren 与 MPI 的端到端性能比较。作者对比了在完全相同的硬件条件下(8 个 r4.16xlarge 实例中的 256 个物理核),处理大小为 256k(262144)的方阵时 MPI 和 NumPyWren 的性能。我们可以看到无服务器环境施加的限制导致的性能损失在 1.4x 到 1.6x 之间(按 wall-clock time 计算)。
表 3. 在具有 512 个虚拟核的集群上,在 N=256K 的方阵上运行时,不同算法的 MPI 与 NumPyWren 执行时间的比较。
在表 4 中,作者比较了 NumPyWren 和 MPI 使用的总核秒数(core-seconds)。对于 MPI,core seconds 是指核的总数乘以 wall-clock runtime。对于 NumPyWren,作者只计算“活动核(Active cores)”,因为空闲核是可以被其他任务利用的。作者通过在无服务器核启动和冷却计算过程中每个核的总计算时间中添加一个启动延时γ来计算总核秒数。具体的,作者选择γ=20s 以对系统进行保守的评估。对于 QR 和 Cholesky 这些具有可变并行性的算法,虽然 wall-clock time 相当,但作者发现 NumPyWren 使用的核秒数减少了 1.15 倍。对于 SVD,实验中显示出超过 3 倍的资源节省效果,不过,产生这种差异一部分是由于使用的 SVD 算法不同。然而对于具有固定数量的并行性的算法(如 GEMM),NumPyWren 中过多的通信会导致更高的资源消耗。
表 4. 在一个 256K 大小的方阵上运行算法的 MPI 与 NumPyWren 总 CPU 时间(以核秒为单位)比较。
三、文章小结
本文重点关注了基于无服务器计算的机器学习的最新研究进展。随着云计算的不断发展,开发人员对于按需执行或扩展的需求越来越强烈,越来越希望不去应对服务器或其它底层基础设施,而是集中精力关注于自身应用的开发和调优。无服务器计算的 FaaS 和 BaaS 服务必将迎来更多的关注。但是,正如我们开篇提到的,机器学习的算法或模型中包含大量的参数、复杂的处理流程,是典型的“性能关键型应用”。针对机器学习这种要求复杂通信模式和工作负载的应用如何基于无服务器计算去工作仍然是一个有待研究的问题。
本文关注了三个研究小组的四篇研究论文。其中前两篇文章提出了一种无服务器基础设施和 ML 工作流的无服务器 ML 框架原型,并将其封装为一个实现端到端管理的分布式 ML 训练框架 Cirrus,可以直接调用使用。第三篇文章提出了一个基于无服务器架构的分布式机器学习新框架,以及一种深度强化学习技术,用于实现 SIREN 中的动态资源供应。作者还提出了一种能够在无服务器架构中高效工作的 HSP 计算模式。最后一篇文章重点关注的是无服务器计算的模密集线性代数程序应用,作者提出了一个在无服务器架构上完成线性代数任务的系统 NumPyWren,通过对中间语言 LAmbdaPACK 的分析,作者最终证明了该分散式无服务器计算模型 NumPyWren 可以用于具有复杂通信程序的计算密集型程序。
在几篇文章中,作者都通过实验证明了几种框架在执行机器学习任务时性能远优于经典的基于粗粒度的 VM 集群的 ML 框架。尽管无服务器的机器学习具有敏捷、快速、可伸缩性等优点,但是它对机器学习的集成还处于初级阶段,它自身也面临着工具不全面、不成熟或者配置方式不统一等问题。随着越来越多的研究人员关注,越来越多的应用开发提出成本节约和效率提高的需要,无服务器计算将迎来更快更好的发展。
领取专属 10元无门槛券
私享最新 技术干货