Skip to content

2. 核心思想:三大原语 + 所有权模型 ​

本章拆解 Ray 的设计内核:task / actor / object 三原语、动态任务图、所有权(ownership)+ 溢回(spillback)调度如何替代 OSDI'18 论文的全局调度器,以及它与其他系统的边界。


2.1 三大原语 ​

Ray 把"分布式 Python"浓缩为三个一等公民:

原语一:Task(无状态远程函数) ​

python
@ray.remote
def square(x):
    return x * x

ref = square.remote(3)   # 立即返回 ObjectRef,不阻塞
print(ray.get(ref))      # 10 —— 此时才真正取回
  • @ray.remote 把普通函数包成远程函数;.remote(...) 立即返回 ObjectRef(future),调用方不阻塞。
  • 默认资源:1 CPU。可声明 @ray.remote(num_cpus=2, num_gpus=1)。
  • 无状态 + 可重放:任务失败可被重新调度执行(默认非 actor 任务重试 3 次)。
  • 支持生成器返回(num_returns="streaming"),用于流式大输出。

原语二:Actor(有状态长进程) ​

python
@ray.remote
class Counter:
    def __init__(self):
        self.n = 0
    def inc(self):
        self.n += 1
        return self.n

c = Counter.remote()      # 创建 actor(一个独立进程,状态常驻)
print(ray.get(c.inc.remote()))   # 1
print(ray.get(c.inc.remote()))   # 2  —— 状态被保持
  • @ray.remote 作用在类上 → actor。actor 是一个长寿命进程,方法调用串行(默认)作用于其内部状态。
  • 默认:0 CPU 用于执行、1 CPU 用于放置(所以 --num-cpus=0 的节点会拒绝默认 actor——必须显式声明资源)。
  • 可配 max_restarts(actor 死后重启几次)、max_task_retries(在途方法重试几次)、max_concurrency(并发度,默认 1,见 §2.5 坑)。

为什么需要 actor? 这是 Ray 区别于 Spark 的关键。RL 需要"一个仿真器进程、一个环境实例、一个策略服务"这种可寻址、可变、长寿命的 worker——纯无状态 task 表达不了。actor 补上了这一刀。

原语三:Object(共享内存对象 + ObjectRef) ​

python
ref = ray.put(big_array)      # 显式放进 Plasma 对象存储
# 或隐式:task 返回值自动落 Plasma
  • 每个 ObjectRef 指向 Plasma 对象存储里的一份对象。Plasma 基于 Apache Arrow,同节点 worker 间零拷贝读(mmap /dev/shm)。
  • 大小规则(源码 ray_constants.py):可用 RAM 的 30%(DEFAULT_OBJECT_STORE_MEMORY_PROPORTION=0.30),Linux/云 200GB 硬上限(DEFAULT_OBJECT_STORE_MAX_MEMORY_BYTES),macOS 仅 2GB(MAC_DEGRADED_PERF_MMAP_SIZE_LIMIT,避免性能退化),最小 75MB。Linux /dev/shm:min(200GB, 0.95×/dev/shm)。
  • 小对象(≤100 KiB)走 in-band:max_direct_call_object_size = 100 KiB(ray_config_def.h),直接内联在任务回复里、存在 owner 进程内对象存储,owner 死则丢失、不可重建。每个 task 的 RPC 内联上限 10MB(task_rpc_inlined_bytes_limit)。

2.2 动态任务图(Dynamic Task Graph) ​

Ray 程序在运行时构建一张动态有向图:

  • 节点 = task / actor 方法调用;边 = 数据依赖(ObjectRef)。
  • 图是运行时生成/修改的(例如根据 task 返回值决定下一步调谁),不是像 Spark 那样静态定义后再执行。
python
@ray.remote
def step(x): return x + 1

# fan-out:100 个任务,依赖同一个 ref
refs = [step.remote(ref) for _ in range(100)]
results = ray.get(refs)

这与 Spark 的"先 build lineage 再 materialize action"形成对照——Ray 的图可以边跑边长,这是 RL/Agent 这种"下一步依赖上一步结果"工作负载的关键。


2.3 所有权模型:替代论文里的全局调度器 ​

这是理解现代 Ray(v2 白皮书)最关键的概念,也是它与 OSDI'18 论文最大的架构差异。

论文里的旧设计(2018) ​

  • 全局调度器(global scheduler,在 GCS 内)+ 每节点本地调度器(raylet)。
  • GCS 持有任务表 + lineage 表,集中调度。

问题:GCS 成为吞吐与延迟瓶颈,不符合"亚毫秒派发、百万任务/秒"目标。

现代设计:所有权(Ownership) ​

核心转变:"谁提交任务,谁拥有它"。任务不再进 GCS,而是由**提交它的 worker(owner)**持有其 spec、返回值引用与 lineage。

  • 每个任务/对象有一个唯一 owner(提交它的 CoreWorker)。owner 维护该任务的状态、返回值 ObjectRef 的引用计数、以及 lineage。
  • 所有权不可转移:ObjectRef 终生与 owner 命运绑定——owner 死,即使物理副本还在,ray.get 也会抛 OwnerDiedError。
  • 量化(白皮书):任务提交延迟 ~1 RTT、<200 µs;单客户端吞吐 ~10k tasks/s,集群线性到百万级;每个 ObjectRef 元数据约 3 KB(CALLER_MEMORY_USAGE_PER_OBJECT_REF=3000)。
  • Lineage 也不再在 GCS:每个 worker 自缓存 lineage,上限 max_lineage_bytes = 1 GiB。

溢回调度(Spillback) ​

没有全局调度器后,任务如何找到合适的节点?靠两跳溢回:

owner 的本地 raylet A  →  (A 拒绝本地放行,返回一个更合适的远端 raylet B 的地址)  →  owner 直接向 B 请求 worker
  • owner 优先选本地 raylet(默认)、或数据局部性最优的节点(参数对象最多的节点)、或node affinity 指定的节点。
  • 若该 raylet 当前资源不足,它不会排队死等,而是返回一个溢回提示——告诉 owner"去 raylet B 试试"。这就是旧"转发给全局调度器"的现代等价物。

默认调度策略:HYBRID(不是 SPREAD) ​

常见误解:Ray 默认把任务摊开(spread)。错。默认是 HYBRID(白皮书 "Default Hybrid Policy"):

  1. 在 top-k 节点组(默认 20% 节点)内,先偏向已有本任务/actor 的节点(为局部性打包);
  2. 再偏向低利用率节点(负载均衡);
  3. top-k 组内随机选。

切换阈值 RAY_scheduler_spread_threshold = 0.5:关键资源利用率 < 50% 时倾向打包(局部性),> 50% 时倾向摊开(负载均衡)。(调度文档)

工程含义:默认 HYBRID 对"局部性 + 资源均衡"做了平衡;但对冷启动/缩容不友好(打包会让部分节点满、部分空)。要友好的缩容,用 placement group 的 PACK 策略把活儿压到少数节点(见 第 8 章 §8.x)。


2.4 资源模型 ​

资源粒度说明
CPU固定点(kResourceUnitScaling=10000)默认 task=1、actor=0(执行)/1(放置)
GPU整数分配 GPU 自动设 CUDA_VISIBLE_DEVICES
内存仅准入控制运行时不强制——超了靠对象存储溢写/killer(见下)
自定义任意键值"GPU": 2、"memory": 1e9、"node:...": 1
  • 内存是准入而非执行强制:Ray 用内存配额做调度准入,但不在运行时限制进程内存。这是对象存储 OOM 与 ray_memory_manager_worker_eviction_total killer 存在的根本原因(内存管理文档)。

Placement Group(放置组) ​

需要"一组资源原子地放在一起"时(如多 GPU all-reduce),用 placement group:

  • 原子创建:bundles 全部就位才算成功;任一 bundle 放不下则全部不占资源(all-or-nothing)。
  • bundle 必须能装进单节点(不跨节点切分一个 bundle)。
  • 四策略:PACK(默认,压少数节点)、STRICT_PACK(严格同节点)、SPREAD(散开)、STRICT_SPREAD(每节点最多一个)。
  • 创建走 GCS 协调的两阶段提交(跨 raylet 预留)。

2.5 与相邻系统的边界 ​

系统模型为何不是 Ray
Spark批/微批 RDD、静态 lineage无原生 actor;粗粒度;无 sub-ms 延迟;服务是附加(Structured Streaming)非统一
Dask动态任务图 + pandas/numpy 扩展图相似,但无原生 actor、无 Serve/RL/Tune 库
MPI / PyTorch DDP静态 SPMD,训练专用紧耦合训练强,但无动态图、无服务、无 RL 循环、无长作业容错。Ray 与 DDP 互补:Ray Train 包装 DDP/FSDP/DeepSpeed
Kubernetes Jobs容器编排无 ML 语义:无共享对象存储、无任务图、无 actor 生命周期。KubeRay 把 Ray 跑在 K8s 上
Modal无服务器 Python(专有云)不同抽象(云上函数),厂商锁定;无开源集群运行时

2025–2026 新边界:Agent 编排层的"挤压"与 Ray 的应对 ​

随着 Agent 工作流兴起,LangGraph(Agent 图编排 + HITL)、Temporal(持久化工作流执行)、Modal(serverless GPU)常被拿来与 Ray 比较。但业界 2026 年的共识是:它们与 Ray 处在不同抽象层,更多是互补/堆叠关系而非替代——典型组合是 LangGraph 负责 Agent 推理与工具路由、Temporal 负责跨小时/天的耐久编排、Ray 负责底层大规模训练/推理/数据计算(GMI Cloud 2026 对比、LangChain 官方对比)。

系统抽象层与 Ray 的关系
LangGraphAgent 推理/工具调用图Agent 编排层;重计算(批量 rollout、训练、批量推理)下沉到 Ray。Ray 是多 Agent/RL 底座(见 verl/OpenRLHF)
Temporal持久化工作流(durable execution)业务流程/长事务编排;payload 约 2MB 限制不适合大对象——正好是 Ray 对象存储的场景
Modalserverless 函数 + 托管 GPU与 Ray Serve/Anyscale 在"免运维 GPU 计算"上有竞争;但 Modal 是闭源专有云,无开源集群运行时

Ray 的应对(2025–2026):① 加入 PyTorch Foundation,与 vLLM 绑定为"AI 计算栈"事实底座;② Ray Serve LLM 2.59 GA + SGLang 双引擎,正面占领 LLM serving;③ 2.58 推出 gVisor 沙箱(experimental)吸收"不可信 Agent 代码执行"的新需求;④ Ray Workflows 弃用后,长任务 DAG 场景由 Compiled Graph + 外部编排器承接。

Ray 的独特赌注不变:一个底座跑训练 + 服务 + 数据 + RL,全部是普通 Python。


2.6 容错思想(概览,细节见 第 8 章) ​

Ray 的容错分五层:任务、actor、对象、节点、GCS。核心机制是 lineage 重建:

  • 对象的 lineage 存在 owner worker;对象丢失时,沿 lineage 重新执行生成它的任务。
  • 重建前提:对象必须由 task 生成、owner 存活、任务可重放。
  • 关键例外:
    • ray.put 的对象不可重建——它与 owner 命运绑定,owner 死 → OwnerDiedError。
    • 小对象(≤100 KiB)随 owner 而亡(存在 owner 进程内,无物理副本)。
    • 默认非 actor 任务重试 3 次、actor 任务不可重试(除非设 max_task_retries + max_restarts)。
    • actor 任务默认不可重建(actor 有状态,重放可能重复副作用)。

这把"哪些可丢、哪些不可丢"讲得非常清楚——是设计分布式 AI 系统时值得借鉴的语义分层。


下一章 架构设计 画出 Ray 的组件分层与控制面/数据面。

Released under CC-BY-SA-4.0 License.