Lec 18 Ray
阅读资料
Ownership: A Distributed Future System For Fine-Grained Tasks, nsdi'21
首先,为什么要学习这篇分布式计算框架Ray的论文?
Ray是现代版的MapReduce,Spark。Ray使用future高效地移动大量数据,并通过Ownership高效管理分布式future,是个被广泛运用的开源项目(被OpenAI、Anyscale使用),并且能运用在任何规模上。
并行应用场景,需要同时具备函数式(有明确的输入输出、不维护状态)、有状态(保留上下文),并且低延迟。比如:
- 模型服务(论文3a)。需要快速相应、客户端上传的数据量大,router 和模型副本(replica)在多次调用之间保持状态
- 在线视频处理(论文3b)。需要帧与帧之间的连续性,即当前帧处理要考虑上一帧的信息
MapReduce 和 Spark 不适合这种类型,因为他们是批处理系统,设计为无状态的任务并行处理。不擅长保留状态、实时处理和交互式场景。
Outline
- Ray设计的核心思想
Ray设计的核心思想
有3个核心思想:
- actors(跨调用之间维持状态)
- futures(表示计算结果的句柄)
- 根据哪个 actor 拥有某个 future,把 future 的状态按 ownership 进行分片管理。
Future
程序可以异步调用一个函数,返回一个future,并且可以将该future的引用作为参数传递给另外一个函数,或者也可以对齐强制求值。这样的好处在于,系统可以决定何地运行future,何时进行数据移动。
关于Ray的python例子
# see https://docs.ray.io/en/latest/ray-core/tips-for-first-time.html
import ray, time
ray.init()
@ray.remote
def g(i):
return i
f = g.remote(10) # f is a future and ray invokes g asynchronouslyFuture的实现,见另外一篇论文
Ray: A Distributed Framework for Emerging AI Applications, osdi'18
挑战
- 大量的future,有些可能运行很短的时间(几个ms);
- 对象的GC
- 当运行future时候worker机器crash了
稻草人方案——集中协调者
Ray的解决方案——Ownership
论文阅读: Ray
摘要
下一代AI应用会持续与环境交互,并从这些交互中学习。Ray就是为了应对这种场景下更改性能和灵活性要求提出的。它提供了统一的接口,能够表达任务并行和基于actor的计算(由一个单一的动态执行引擎支持)。Ray 采用分布式调度器和分布式容错存储来管理系统的控制状态。在实验中,我们展示了其每秒超过 180 万个任务的扩展能力,并且在多个具有挑战性的强化学习应用中,其性能优于现有的专用系统。
1. 引言
过去20年,由于数据膨胀带来数据分析方面的需求,涌现了很多分布式计算框架,有批处理、流、图处理系统。我们称之为大数据时代。典型案例是监督学习(supervised learning),数据点带有标签,而将数据点映射到标签的的主要技术由深度神经网络提供。 这些深度网络的复杂性,催生了另一波专注于深度神经网络训练的框架热潮。这些框架通常利用老旧的专用硬件(例如 GPU 和 TPU),旨在缩短批量环境下的训练时间。例如,TensorFlow、MXNet和 PyTorch。
然而,人工智能的前景远比传统的监督学习更为广阔。新兴的人工智能(Emerging AI)应用必须越来越多地在动态环境中运行,对环境变化做出反应,并采取一系列行动以实现长期目标。它们不仅要利用收集到的数据,还要探索可能的行动空间。这些需求被纳入到了强化学习(forcement learning, RL)的范式框架。取得的进展,例如AlphGo、UAV等。
强化学习应用的核心目标是学习一种策略——从环境状态到行动选择的映射。在大规模应用中寻找有效的策略需要三个主要能力。首先,依赖模拟(simlulation)来求解策略。模拟使得探索许多不同的行动序列选择,并了解这些选择的长期后果成为可能。其次,与监督学习算法一样,强化学习算法需要进行分布式训练( distributed training),以基于通过模拟或与物理环境交互生成的数据来改进策略。第三,策略旨在为对应问题提供解决方案。
这些特性催生了新的系统需求:强化学习系统必须支持细粒度计算(例如,在与现实世界交互时以毫秒为单位渲染操作,并执行大量模拟);必须支持各种不同的时间需求和不同资源使用需求;必须支持动态执行,因为模拟结果或与环境的交互可能会改变未来的计算。因此,我们需要一个能够以毫秒级延迟每秒处理数百万个异构任务的动态计算框架。
现有的框架无法满足强化学习的新需求。 批处理并行系统,MapReduce、Apache Spark不支持细粒度模拟或策略服务。流失传输也是如此。 分布式深度学习框架(TF,MXNet)本身不支持模拟和服务。
本文我们提出了了Ray,通用的集群计算框架,支持强化学习RL的模拟、训练和服务。工作负载广泛, 可以是轻量级、无状态计算(例如模拟)、长期运行的有状态(训练)。为了实现这些需求,Ray执行了统一的接口,能够表达任务并行(task-parallel)和基于Actor的计算。Task 能够让 Ray 高效且动态平衡模拟负载,处理大量输入和状态空间(例如视频),并从故障中恢复。Actor能使Ray高效支持有状态计算,并将共享的可变状态暴露给客户端(例如参数服务器)。Ray 在一个高度可扩展且容错的动态执行引擎之上实现了 Actor 和任务抽象。
为了满足性能需求,Ray分散了现有框架通常会做集中的两个组件——任务调度器和元数据存储(通常维护计算的血统,以及数据对象目录)。这使得 Ray 能够以毫秒级延迟(每秒调度数百万个任务)。此外,Ray 为task和actor提供基于血统的容错功能,并为元数据存储提供基于复制的容错功能。
我们做出了如下贡献:
- 设计并构建了第一个统一训练、模拟和服务的分布式框架
- 为了支持这些工作负载,在执行引擎之上统一了Actor和task 的并行抽象。
- 为了实现可扩展性,提出了自下而上的分布式调度策略
- 为了实现可扩展性和容错性,我们提出了一种系统设计原则,其中控制状态存储在分片元数据存储中,而所有其他系统组件都是无状态的。
2. 动机和需求
3. 编程和计算模型
4. 架构
5. 微基准测试
论文阅读: Ownership
摘要
分布式futures接口是一种作为构建分布式应用的方式,近年来日益流行,尤其适用于处理大量数据的场景。分布式fulture是对传统RPC的扩展,它结合了fulture和分布式内存概念: 一个分布式fulture是一个引用,其最终的值可能存储在远程节点上。应用程序因此可以表达分布式计算,而无需明确指定何时或在哪 执行任务或移动数据。
然而,近年来的分布式 futures 应用越来越需要支持细粒度计算,即任务运行时间在毫秒级别。相比粗粒度任务,细粒度任务更难以在不引入高系统开销的前提下进行高效执行。在本文中,我们提出了一种支持细粒度任务的分布式 futures 系统,在保证容错能力的同时也不牺牲性能。我们的解决方案基于一个新颖的概念——ownership(所有权),该机制为每个对象分配一个在系统操作中的leader。我们证明这种去中心化架构可以实现:横向扩展能力, 每个任务约1ms的低延迟,以及快速故障恢复
1. 引言
RPC是构建分布式应用的标准方法,原因在于其通用性以及简单的语义可以带来高性能的实现。最初的 RPC 方案使用同步调用,将返回值复制回调用方(见图 2a)。近年来的一些系统。近年来一些系统扩展了 RPC,使其除了分布式通信之外,还可以由系统代表应用程序管理数据移动(data movement)和并行性(parallelism)。

数据移动。按值传递的语义要求将所有 RPC 参数直接复制进请求体发送到执行者。因此,当传输的数据很大时,性能会明显下降。在某些情况下,例如图 2a 中的场景,进程对自己先前返回的数据再次执行 RPC,复制是昂贵且不必要的。
为减少数据复制,一些 RPC 系统使用分布式内存。这使得大参数可以按引用传递(见图 2b),而小参数仍然按值传递。在最佳情况下,若按引用传递的参数已经在与执行者同一个节点上,就无需再复制(图 2b)。注意,为简化一致性模型和实现,我们将所有值设为不可变(immutable),这与传统 RPC 一致。
并行性。传统 RPC 是阻塞式的,控制流仅在收到回复后才返回给调用者(图 2a)。Futures 是扩展 RPC 以支持异步执行的流行方法,使系统可以将函数与调用者并行执行。进一步地,借助组合(composition),即将一个 future 作为另一个 RPC 的参数传入,应用程序还能表达 future RPC 之间的并行性和依赖关系。例如,在图 2c 中,add 在程序一开始就被调用,但系统只有在 a 和 b 被计算完成后才实际执行它。
分布式futures 是RPC的一种扩展,他结合了futures与分布式内存: 一个分布式futures 是一个引用,其最终值可能存储在远程节点上。这样,应用程序就能表达分布式计算,而无需明确指定 何时何地执行任务或移动数据。 这种接口在构建处理大量数据的分布式应用时越来越受欢迎。
与传统 RPC 一样,其目标之一是保持通用性。为实现这一点,系统必须尽量减少每次函数调用的开销 。例如,广泛使用的 gRPC 支持水平扩展,并具备亚毫秒级 RPC 延迟,从而能每秒执行数百万个细粒度函数(即毫秒级的“任务”)
目前,已经有一些大规模细粒度分布式 futures 应用出现,如强化学习、视频处理、模型服务。这些应用对并行性与数据移动的优化要求极高,使得分布式 futures 非常适用。不幸的是,现有的分布式 futures 系统仍局限于粗粒度任务。
我们提出了一个支持细粒度任务的分布式 futures 系统。创新在于识别并解决了在细粒度任务场景下提供容错机制所面临的挑战,并同时保持高性能。
主要挑战——分布式 futures 会在多个进程间引入共享状态。具体来说,一个对象及其元数据会被多个角色共享,包括:
- 持有该引用的进程(reference holders);
- 创建该对象的 RPC 执行者;
- 以及该对象的物理存储位置。
为了让每个持有引用的进程都能成功解引用这个值,各进程必须进行协调——这在出现故障的情况下尤其困难。而传统 RPC 则避免了这种复杂协调:数据按值传递,不存在共享状态,因此天然支持高扩展性与低延迟。例如在图 2a 中,一旦 worker 1 将 a 复制到 driver,就不再参与后续对 add 的执行;而在图 2d 中,worker 1 保存了 a,此时 worker 1 与 worker 2 必须协调,以确保 a 在 add 被执行前依然可用。此外,worker 1 还需要在 worker 2 执行完 add 并且没有其他引用时,对 a 进行垃圾回收;最后,所有进程必须协调以检测其他进程的失败并进行恢复。
以往系统普遍采用中心化 master 来存储系统状态并协调操作 [34, 37]。一种简单的容错方式是在 master 中同步记录并复制元数据,例如,在图 2d 中,在调度 add 到 worker 2 前,master 先记录相关信息,这样一旦 worker 2 失败,就能检测到 c 的失败。然而,这种方式在处理大量细粒度任务时会引入大量开销 [32, 51]。
因此,去中心化的系统状态管理对于可扩展性来说是必要的。关键问题是:如何在不增加协调复杂性的前提下实现去中心化。
本文关键洞见:Ownership 模型
我们发现:虽然分布式 futures 可以通过引用传递在多个任务间共享,但在绝大多数情况下,它们仅在调用者作用域内共享。例如在图 1 中,a_future 是被创建后传给 add 的,整个过程都在同一个作用域内。
于是我们提出了 ownership 模型:一种将系统状态分散到各个 RPC 执行节点上的方法。
具体而言:
- 任务的调用者是其返回的 future 及相关元数据的“owner”;
- 在图 2d 中,driver 是
a、b和c的所有者。
这种方式有三个优势:
- 支持水平扩展:应用可以使用嵌套任务将系统状态“分片”到多个 worker 上;
- 低延迟:由于每个 future 的元数据写操作发生在其 owner 本地,所以即使是同步写入,延迟也很低;
- 简化故障处理:每个 worker 实际上是其拥有的 futures 的“本地 master”,从而简化了恢复过程。
系统容错机制
系统保证:只要 future 的所有者还活着,任何持有该 future 引用的任务都最终可以解引用其值。因为 owner 会协调:
- 引用计数(用于内存安全);
- lineage 重构(用于失败恢复)。
当然,如果所有者失败,这还不够。
我们引入的第二个关键点是:在许多情况下,引用某个 future 的任务正是失败任务的后代。只要通过 lineage 重构恢复失败任务,其后代任务(即引用该 future 的任务)也会一并被重建。因此,我们可以让这些后代任务和其 owner 一起“命运共享”(fate sharing),这在实践中是安全的。
我们预计失败是相对少见的,因此认为:这种方法大大降低了系统开销和复杂性,其所带来的额外重执行成本是值得的。
2. 分布式Future
分布式futures的关键好处在于,系统可以代表应用透明地管理并行性和数据移动。这里,我们在表1描述API。

为了启动(spawn)一个任务,调用者会调用一个远程函数,该函数会立即返回一个 Dfut(distributed future,对应表1)。所启动的任务包括函数本身、其参数、资源需求等。 返回的 Dfut是对该任务返回值对象的引用。调用这可以通过get来对 Dfut进行解引用,这是一种阻塞调用,会返回该对象的副本。调用者也可以删除 Dfut,将其从作用域中移除,从而使系统能够回收该对象的资源。系统中所有对象都是不可变的(immutable)
通过任务调用创建 DFut后,调用者可以通过两种方式创建该Dfut的其他引用。首先,将Dfut作为参数传递给另一个任务,系统会自动对Dfut参数进行 隐式解引用。因此,该任务只有在所有上游任务都执行完成之后才会开始执行,这执行者看到的只是Dfut的值,而非本身。
3. 总览
3.1 需求
4. Ownership 设计
集群中的每个节点管理一个到多个workers(通常每个core一个),一个调度器(Scheduler)和一个对象存储(object store)(图7所示)。这些组件分别实现了:future的解析、资源管理以及分布式内存功能。
Worker的职责包括Dfut的解析,引用计数和故障处理。每个worker只执行一个任务,但它可以在任务中调用其他任务。根任务由"driver"执行。
每一个任务都有唯一的TaskID,它由父任务的ID与该父任务已调用的任务数量的哈希值组成。根任务的TaskID是随机分配的。每个任务可能返回多个对象,每个对象都会分配一个唯一的ObjectID,该ID由 TaskID与对象在返回值中的索引拼接而成。一个DFut有一个两元组表示: (ObjectID, 所有者地址Owner)
