Ray 架构

原论文:Ray: A Distributed Framework for Emerging AI Applications
Ray的体系结构:

  • 实现API的应用层
  • 提供高可扩展性和容错的系统层。

在这里插入图片描述

应用层

应用层由三类进程组成:

  • Driver: 执行用户程序的进程。顾名思义,所有操作都需要由主进程来驱动。
  • Worker: 被 一个 driver 或者 另一个 worker 调用的无状态进程, 用来执行 task。worker是自动启动的,由系统层分配任务。当声明一个远程函数时,该函数会自动发布给所有worker。Worker内部会串行执行任务,跨任务不维护本地状态,即在worker中,一个远程函数执行完后,其局部作用域的所有变量将不再能被其他task所访问。
  • Actor: 被调用时只执行其所暴露的方法。Actor由Worker或Driver显式地进行实例化。与Worker相同的是,Actor也会串行地执行任务,只不过每个方法都依赖于前一个方法执行所产生的状态。。

系统层

系统层由三个主要组件组成:全局控制存储 GCS(global control store)、分布式调度器(distributed scheduler)和分布式对象存储(distributed object store)。所有组件都是支持水平扩展和容错的。

Global Control Store (GCS)

全局控制库(Global Control Store,GCS)维护了系统的整个控制状态,就其核心而言,GCS是一个具有pubsub功能的键值存储。使用分片(sharding)来实现规模,而每分片(shard)链式复制(shard)来提供容错。GCS及其设计的主要起因是为每秒可以动态生成数百万个任务的系统保持容错和低延迟。

节点故障时的容错需要一个解决方案来维护血缘信息。现有的基于血缘的解决方案专注于粗粒度的并行,因此可以使用单个节点(例如 master, driver)来存储血缘,而不会影响性能。但是,这种设计对于像模拟这样的细粒度和动态工作负载来说是不可扩展的。因此,我们将持久血缘存储与其他系统组件分离,允许每个组件独立扩展。
GCS设计的初衷是让系统中的各个组件都变得尽可能地无状态,因此GCS维护了一些全局状态:

  • 对象表 (Object Table):记录每个对象存在于哪些节点
  • 任务表 (Task Table):记录每个任务运行于哪个节点
  • 函数表 (Function Table):记录用户进程中定义的远程函数
  • 事件日志 (Event Logs):记录任务运行日志

自下而上的分布式调度器

Ray设计了一个由全局调度器和每个节点本地调度器组成的两级分层调度器。为了避免全局调度程序过载,在节点上创建的任务首先被提交到节点的本地调度。除非节点过载(即,其本地任务队列超过预定义的阈值),或者它不能满足任务的要求(例如,缺少GPU)。如果本地调度器决定不在本地调度某个任务,它将其转发给全局调度器。由于调度器首先尝试在本地(即,在调度层次结构的左侧)调度任务,因此我们将其称为自底向上的调度器。

全局调度器考虑每个节点的负载和任务的约束来做出调度决策。更准确地说,全局调度器识别具有任务所请求类型的足够资源的节点集合,并在这些节点中选择提供最低估计等待时间的节点。在给定节点上,此时间是(i)任务将在该节点排队的估计时间(即,任务队列大小乘以平均任务执行)和(ii)任务远程输入的估计传输时间(即,远程输入的总大小除以平均带宽)的总和。全局调度器通过心跳获得每个节点的队列大小和节点资源可用性,以及任务输入的位置和它们的大小从GCS。此外,全局调度器使用简单的指数平均来计算平均任务执行和平均传输带宽。如果全局调度器成为瓶颈,我们可以通过GCS实例化更多共享相同信息的副本。
在这里插入图片描述

分布式对象存储

为了最小化任务延迟,Ray 实现了一个内存中的分布式存储系统来存储每个任务的输入和输出,也就是无状态计算。在每个节点上,Ray 通过共享内存实现对象存储。这允许在同一节点上运行的任务之间进行零拷贝数据共享。

如果任务的输入不是本地的,则在执行之前将输入复制到本地对象存储。此外,将任务输出写入本地对象存储。复制消除了热数据对象造成的潜在瓶颈,并最大限度地减少了任务执行时间,因为任务只从本地内存读取/写入数据。这增加了受计算限制的工作负载的吞吐量,这是许多AI应用程序共享的特性。为了实现低延迟,Ray 将对象完全保留在内存中,并根据需要使用LRU策略将它们逐出磁盘。

与现有的集群计算框架(如Spark 和Dryad )一样,对象存储仅限于不可变数据。这消除了对复杂一致性协议的需求(因为对象不更新),并简化了对容错的支持。在节点故障的情况下,Ray通过血缘重新执行来恢复任何需要的对象。GCS中存储的血缘在初始执行期间跟踪无状态task和有状态actor;Ray使用前者来重构存储中的对象。

端到端过程

现在假设有一个求两数之和的任务需要交给Ray来执行,我们来具体分析一下这一任务在Ray的架构中是如何执行的。以下以全局调度为例,因为它更具有一般性。

下图(a)描述了任务的定义、提交和执行的过程

在这里插入图片描述

  1. 【定义远程函数】位于 N1 的用户程序中定义的远程函数add被装载到GCS的函数表中,位于 N2 的工作器从GCS中读取并装载远程函数add
  2. 【提交任务】位于 N1 的用户程序向本地调度器提交add(a, b)的任务
  3. 【提交任务到全局】本地调度器将任务提交至全局调度器
  4. 【检查对象表】全局调度器从GCS中找到add任务所需的实参a, b,发现a在 N1 上,b在 N2 上(a, b 已在用户程序中事先定义)
  5. 【执行全局调度】由上一步可知,任务的输入平均地分布在两个节点,因此全局调度器随机选择一个节点进行调度,此处选择了 N2
  6. 【检查任务输入】 N2 的局部调度器检查任务所需的对象是否都在 N2 的本地对象存储器中
  7. 【查询缺失输入】 N2 的局部调度器发现任务所需的a不在 N2 中,在GCS中查找后发现a在 N1 中
  8. 【对象复制】将a从 N1 复制到 N2
  9. 【执行局部调度】在 N2 的工作器上执行add(a, b)的任务
  10. 【访问对象存储器】add(a, b)访问局部对象存储器中相应的对象

下图(b)描述了获取任务执行结果的的过程
在这里插入图片描述

  1. 【提交get请求】向本地调度器提交ray.get的请求,期望获取add任务执行的返回值
  2. 【注册回调函数】 N1 本地没有存储返回值,所以根据返回值对象的引用id_c,在GCS的对象表中查询该对象位于哪个节点,假设此时任务没有执行完成,那么对象表中找不到id_c,因此 N1 的对象存储器会注册一个回调函数,当GCS对象表中出现id_c时触发该回调,将c从对应的节点复制到 N1 上
  3. 【任务执行完毕】 N2 上的add任务执行完成,返回值c被存储到 N2 的对象存储器中
  4. 【将对象同步到GCS】 N2 将c及其引用id_c存入GCS的对象表中
  5. 【触发回调函数】第2步中注册的回调函数被触发
  6. 【执行回调函数】将c从 N2 复制到 N1
  7. 【返回用户程序】将c返回给用户程序,任务结束
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值