【框架学习】Ray初探

1,706 阅读13分钟

Ray是一个支持多语言(python,java,c++)的分布式框架。其本身是辅助python在做机器学习和深度学习工作负载能够使实质性,并具有类似消息传递接口MPI的性能和细粒度。从整体的结构来看,十分类似于spark。尤其在于其使用ray.remote来对单个计算进行装饰的行为十分类似于pyspark中的装饰器。

1. 基本概念

Ray则是一种通用的集群计算框架,既支持模型的训练,又支持对环境的仿真或与环境的交互,还支持模型服务。Ray所面临的任务涵盖了从轻量级、无状态的计算任务(例如仿真)到长时间运行的、有状态的计算任务(例如训练)。为了满足这些任务的需求,Ray实现了一套统一的接口,这套接口既能表达基于任务的并行计算(task-parallel),又能表达基于行动器的并行计算(actor-based)。前者使得Ray能高效地、动态地对仿真、高维状态输入处理(如图像、视频)和错误恢复等任务进行负载均衡,后者行动器的设计使得Ray能有效地支持有状态的计算,例如模型训练、与客户端共享可变状态(如参数服务器)。Ray在一个具有高可扩展性和容错性的动态执行引擎上实现了对任务和行动器的抽象。

  • Ray使用了分布式任务调度和元数据存储器设计代替了传统并行计算框架中的集中式任务调度和元数据存储。

1.1 关键概念

1.1.1 Task

  • Tasks:任意function都可以在python workers上非同步执行,Ray让tasks在资源中特制对于资源的需求。资源需求可由cluster调度器来通过并行执行来跨cluster给task分布资源。
  • 使用方法:使用@ray.remote来给正常的function做装饰,用get来得到结果。
import ray

# 设定ray的task格式function
@ray.remote
def my_function(x, y):
    return x + y

# 执行task
futures = my_function.remote(1, 2)

# 得到结果
ray.get(futures)
  • 给remote functions传递object refs
@ray.remote
def my_function():
    return 1

@ray.remote
def function_with_an_argument(value):
    return value + 1

obj_ref1 = my_function.remote()
assert ray.get(obj_ref1) == 1

# 可以把object ref当作一个参数输入给另一个ray remote function
obj_ref2 = function_with_an_argument.remote(obj_ref1)
assert ray.get(obj_ref2) == 2
    • 在上面的代码中,由于第二个task取决于第一个的输出,则其在第一个结束后才执行。
    • 如果两个tasks被调度在不同的机器中,第一个task的输出会通过网络传递给第二个task所在的机器。
  • 在执行过程中查看taks的执行情况,使用ray.wait
ready_refs, remaining_refs = ray.wait(object_refs, num_returns=1, timeout=None)
  • 多重返回值
@ray.remote(num_returns=3)
def return_multiple():
    return 1, 2, 3

a, b, c = return_multiple.remote()
  • 取消task,使用ray.cancel
@ray.remote
def blocking_operation():
    time.sleep(10e6)

obj_ref = blocking_operation.remote()
ray.cancel(obj_ref)

from ray.exceptions import TaskCancelledError

try:
    ray.get(obj_ref)
except TaskCancelledError:
    print("Object reference was cancelled.")

1.1.2 Actor

  • Actors:这个就是把task对于function的方法扩展到class结构。actor就是一个状态worker。当actor初始化之后,一个新的worker就被创建了。actor中所有的方法都会被调度到worker中来改变worker得状态。
@ray.remote
class Counter(object):
    def __init__(self):
        self.value = 0
        
    def increment(self):
        self.value += 1
        return self.value
    
# 创建actor对象
counter = Counter.remote()
  • 资源要求
# Specify required resources for an actor.
@ray.remote(num_cpus=2, num_gpus=0.5)
class Actor(object):
    pass
  • calling:调用actor中的方法
# Call the actor.
obj_ref = counter.increment.remote()
assert ray.get(obj_ref) == 1
    • 在不同actors中调用的方法可以并行执行,在同一个actor中调用的方法只能串行执行。在同一个actor中的方法会共享状态。
# 创建十个Counter actors.
counters = [Counter.remote() for _ in range(10)]

# 对每个actor执行内部方法increment一次,并得到结果。这些都是并行的。
results = ray.get([c.increment.remote() for c in counters])
print(results)  # prints [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

# 对第一个actor执行五次,是串行执行的。
results = ray.get([counters[0].increment.remote() for _ in range(5)])
print(results)  # prints [2, 3, 4, 5, 6]
  • actor handles可以传递给其他的task
import time

@ray.remote
def f(counter):
    for _ in range(1000):
        time.sleep(0.1)
        counter.increment.remote()
    • 如果实例化一个actor,就可以把handle在多个tasks中传递。
counter = Counter.remote()

# Start some tasks that use the actor.
[f.remote(counter) for _ in range(3)]

# Print the counter value.
for _ in range(10):
    time.sleep(1)
    print(ray.get(counter.get_counter.remote()))
  • 对比actor,worker和resource
  • actor和worker区别
    • 每个Ray worker都是一个python过程。
    • 在actor和tasks中worker是不同的,ray worker要么是用来执行多个ray tasks,或者作为ray actor的表明项。
    • tasks:在ray开始,ray workers就被启动用来执行tasks(每个cpu核一个worker),如果你有16核,执行8个tasks只启动两个cpu,会以8/16的workers空闲结束。
    • actor:一个ray actor是一个ray worker。所有方法都会在同一套分配好的资源里运行。并且不同于task,在ray actor中运行python过程在actor删除后会被终止且不会被复用。
  • 使用建议
    • 除非对于每个actor都有运行要求,尽量使用task来执行ray

1.1.3 Object

  • Objects:在Ray中,task和actor都是在object中计算和创建的。object可以再Ray的cluster的任意地方存储。一般一个cluster中有一个object store,在cluster设置中,remote object可以存在于一个或者多个nodes。
  • Object ref是带有唯一ID的pointer,可以用于被引用但是看不到其值。创建方式:
    • 通过remote function calss来返回
    • put() 来创建。
y = 1
object_ref = ray.put(y)
  • 抓取object数据:用get。如果object是一个array或者array组合,get是以零拷贝的形式返回涌向object存储内存支持的数组。否则,将对象数据反序列化为python对象。
# Get the value of one object ref.
obj_ref = ray.put(1)
assert ray.get(obj_ref) == 1

# Get the values of multiple object refs in parallel.
assert ray.get([ray.put(i) for i in range(3)]) == [0, 1, 2]

# You can also set a timeout to return early from a ``get``
# that's blocking for too long.
from ray.exceptions import GetTimeoutError

@ray.remote
def long_running_function():
    time.sleep(8)

obj_ref = long_running_function.remote()
try:
    ray.get(obj_ref, timeout=4)
except GetTimeoutError:
    print("`get` timed out.")

\

  • 使用reference来传递object:ray object reference可以自由在ray应用间传递。所以他们可以作为参数送入task,actor中,或者存储在其他object中。
@ray.remote
def echo(x):
    print(x)

# Put an object in Ray's object store.
object_ref = ray.put(1)

# Pass-by-value: send the object to a task as a top-level argument.
# The object will be de-referenced, so the task only sees its value.
echo.remote(object_ref)
# -> prints "1"

# Pass-by-reference: when passed inside a Python list or other data structure,
# the object ref is preserved. The object data is not transferred to the worker
# when it is passed by reference, until ray.get() is called on the reference.
echo.remote({"obj": object_ref})
# -> prints "{"obj": ObjectRef(...)}"

# Objects can be nested within each other. Ray will keep the inner object
# alive via reference counting until all outer object references are deleted.
object_ref_2 = ray.put([object_ref])

# Examples of passing objects to actors.
actor_handle = Actor.remote(obj)  # by-value
actor_handle = Actor.remote([obj])  # by-reference
actor_handle.method.remote(obj)  # by-value
actor_handle.method.remote([obj])  # by-reference

1.1.4 Placement Groups

\

1.1.5 Environment dependencies

  • Ray的application可能会有在Ray script之外的包有依赖。比如其他的python包,特殊环境变量,从其他文件中引入。

1.2 核心API

api说明
futures = f.remote(args)远程地执行函数f。f.remote()以普通对象或future对象作为输入,返回一个或多个future对象,非阻塞执行。
objects = ray.get(futures)返回与一个或多个future对象相关联的真实值,阻塞执行
ready_futures = ray.wait(futures, k, timeout)当futures中有k个future完成时,或执行时间超过timeout时,返回futures中已经执行完的future
actor = Class.remote(args) futures = actor.method.remote(args)将一个类实例化为一个远程的行动器,并返回它的一个句柄。然后调用这个行动器的method方法,并返回一个或多个future. 两个过程均为非阻塞的。

2. 架构解释

2.1 系统架构

  • 系统构成:
    • 应用层(App Layer) :实现ray的api,作为框架前端跟用户接触;
      • Driver process:执行用户程序进程。
      • Worker process:执行Driver或其他Worker调用的任务(remote function)的无状态进程。Worker由系统层分配任务并自动启动。当声明一个remote function时,该函数将被自动发送到所有的Worker中。在同一个Worker中,任务是串行执行,Worker并不维护其任务与人物之间的局部状态,即在工作其中,一个远程函数执行完后,局部作用域的所有变量将不再能被其他任务访问。
      • Actor process:Actor在调用时只执行被调用的方法。Actor由Worker和Driver显式地进行实例化。与Worker相同的是,Actor也串行执行任务,不同的是Actor上执行的每个方法都依赖于其前面所执行方法所导致的状态。
    • 系统层(System Layer) :后端保障ray的高扩展性和容错性。由三个组件构成(GCS,DS,DOS),并且可以横向扩展并具有容错性。
      • 全局控制存储器GCS:让系统中各个组件都变得尽可能无状态,所以用来维护一些全局状态。
        • Object Table:记录每个对象存在哪些节点。
        • Task Table:记录每个任务运行在哪个节点。
        • Function Table:记录用户进程中定义的remote function。
        • Event Logs:记录任务运行日志。
      • 分布式调度器DS: 全局调度器+(节点)局部调度器。。
        • 自底向上:节点在资源够用的情况下,则会将task直接提交到local scheduler上。在超出需求的情况下,会传递到global scheduler,考虑将任务调度到远端。下图中的粗线条表示频繁发生,细线条则是稀疏发生,也表示本地调度的发生频率会大大高于远程调度。
        • 在需要全局调度的情况下,local scheduler会把task提交给global sheduler,并向GCS传递任务相关信息,将task涉及的对象和函数存入GCS的Object Table和Function Table中,然后global sheduler会从GCS中读取信息,并选择其他节点上调度这一任务。

      • 分布式对象存储器DOS:内存式存储task的输入和输出。
        • 通过内存共享机制子每个node上实现一个object store,从而使在同一个节点运行的任务之间不需要拷贝就可以共享数据。
        • 当task的输入不在本地时,则会执行前后分别将他的输入以及输出复制到本地对象存储器中。
        • 复制机制使得任务永远只会从本地object store中读取数据。消除热数据可能导致的问题。

\

2.2 组件构成

  • 一个Ray集群由一组工作节点和一个GCS实例组成。每个工作节点的物理进程为:
    • 一个或多个Worker进程,负责提交和执行。进程为无状态的remote函数,或者actor。每个辅助进程都与特定的作业相关联。默认的初始工作进程数等于机器上的CPU数。每个worker存储:
      • 所有权表。worker引用的对象系统元数据,例如用于存储引用计数和对象位置。
      • 进程内润初期,用于存储小对象。
    • RayLet,在同一集群上所有的taks之间共享。有两个组件:
      • Scheduler:负责资源管理和执行存储爱分布式obejct store中的任务参数。集群中的各个scheduler组成了Ray的分布对象存储(DOS)
      • 共享内存对象存储(Plasma对象存储)。负责储存和转移大型对象。集群中的单个对象存储包括Ray分布对象存储。
  • 每个Worker进程和raylet分配一个唯一的20字节标识符以及一个IP地址和端口。相同的地址和端口可以被后续组件使用(比如前一个worker进程死亡),但是唯一的id永远不会被重用(他们在进程死亡时被删除)。worker进程与本地ralet进程共享命运。
  • 其中一个worker节点被指定为head节点,除了以上过程外,还需要:
    • GCS
    • 驱动程序进程:一个特殊的工作进程,执行顶层应用程序(类比于python中的__main__)

2.3 连接到Ray

应用程序驱动程序可以通过以下方式之一连接到Ray:

  1. 不带参数调用ray.init()。这将启动一个嵌入式单节点Ray实例,该实例可立即供应用程序使用。
  2. 通过指定Ray.init(address=)连接到现有的Ray群集。Driver将在指定的地址连接到GCS,并查找集群其他组件的地址,例如,其本地raylet地址。Driver必须与Ray的一个现有节点位于同一位置。由于Ray的共享内存功能,这是必需的。
  3. 使用Ray客户端Ray.util.connect()从远程计算机(如笔记本电脑)进行连接。默认情况下,每个Ray集群启动时,头部节点上运行一个可以接收远程客户端连接的Ray客户端服务器。但是请注意,当客户端位于远程时,由于WAN延迟,直接从客户端运行的某些操作可能会较慢。

2.4 进程分析

求两数之和来分析Ray架构中如何执行

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