
假设你有 800 组模型参数需要评估
每组参数运行 2 分钟,一个进程,需要 26 个小时
如果一个机器上有 16 个 CPU 核心,理想情况下,似乎两个小时就能干完
但引入多进程之后,新的问题会接踵而至,比如
800 个任务应该如何分配给进程
某个进程崩了之后,失败的任务应该怎么分配
一台机器不够用,怎么扩展到多台机器
一部分任务需要CPU,一部分任务又需要GPU,应该怎么管理资源
有些任务状态需要长期保存,又要怎么处理
当任务开始涉及调度、资源、状态和失败恢复时,单纯的多进程就有些不够用了。Ray 就是为这类问题设计的。
我们先这样理解 Ray,就是你告诉它要运行什么、依赖什么、需要多少资源,Ray 负责把这些计算安排到合适的进程和机器上。
我们先看一个简单的任务,处理 8 个独立数据的分片,每个分片消耗一秒
defprocess_shard(shard_id: int) -> dict:
"""模拟一个耗时 1 秒、彼此独立的数据分片处理任务。"""
time.sleep(1)
return {"shard_id": shard_id, "pid": os.getpid()}这个 sleep(1) 是对读取文件、调用模型的一个简化模拟
这个版本很直接,循环调用,得到一个结果后,再处理下一个分片
8 个任务依次执行,最终运行时间是 8.03 秒
把它改成 Ray 版本,代码如下
@ray.remote
defprocess_shard(shard_id):
"""模拟一个耗时 1 秒、彼此独立的数据分片处理任务。"""
time.sleep(1)
return {"shard_id": shard_id, "pid": os.getpid()}
result_refs = [process_shard.remote(i) for i inrange(8)]
results = ray.get(result_refs)这里主要加了三个内容,添加 @ray.remote 装饰器,把函数声明为 Ray 的 Remote 函数;调用变成了 process_shard.remote 不会等函数执行完成;使用 ray.get 获取结果
在 4 个 CPU 资源下,8个任务会分批执行,最终运行时间是 2.07 秒

▲ 8 个独立任务的串行与 Ray 并行执行对比
这个例子是把独立任务并行执行,当计算从一个进程扩展到多个进程,从一个机器扩展到多台机器以后,Ray 都可以使用同一种方式描述任务、状态、数据和资源。
这就是 Ray 的核心设计理念。
一开始接触 Ray 的时候,容易被各种组件和上层库淹没
但实际上,不需要一开始就记住 Raylet、GCS、Plasma,也不用研究 Ray Data、Train 这些
真正理解 Ray,可以先从这四个方面开始

▲ Ray Runtime 管理的计算、数据、资源和失败
任务并行起来以后,这些任务应该怎么表示?
Ray 提供了两种核心的计算单元,Task 和 Actor
Task 来自函数,适合无状态计算。例如:
Actor 来自类,适合需要保存状态的计算。例如:
如果每个任务都是独立的,可以使用 Task。
但如果模型加载一次需要几十秒,我们不希望每个任务都重新加载模型,这时就需要 Actor。
实用判断:不需要状态时优先考虑 Task;确实需要跨调用保存状态时,再考虑 Actor。
比如前面 800 组模型参数评估,每个评估都是一个单独的任务,因此用 Task 会更合适
任务拆出来了,上下游的数据怎么传?
Ray Task 提交后返回的不是计算结果,而是 ObjectRef。
ObjectRef 不只是一个"以后再取结果"的占位符,它还可以用来连接上下游任务。
data_ref = load_data.remote()
transformed_ref = transform.remote(data_ref)
total_ref = summarize.remote(transformed_ref)注意,中间没有执行 ray.get(data_ref)。
Driver 不需要先把数据取回来,再手动提交下一个任务。它可以直接把 ObjectRef 传给下游任务,由 Ray 等待依赖就绪并安排执行。
这也是为什么理解 ObjectRef 比记住一堆 API 更重要。
一个完整的计算任务,往往还会被拆成多个步骤,而不同步骤需要的资源可能完全不同。
数据预处理主要消耗 CPU,模型训练需要 GPU,某些任务还只能运行在安装了特定软件或者挂载了特定数据盘的节点上。
Ray 允许 Task 和 Actor 声明逻辑资源需求:
@ray.remote(num_cpus=2, num_gpus=1)
deftrain_model(config):
...Ray 调度器会根据这些请求寻找能够运行任务的节点。
但要注意,"声明了 2 个 CPU"不等于操作系统一定会把进程严格限制在两个 CPU 上。Ray 资源首先是一种调度和准入机制,不应该直接理解成操作系统级资源隔离。
资源调度的具体规则,我们会在后续文章单独展开。
只要进入多进程和多节点环境,失败就不再是意外,而是分布式运行模型必须要考虑的部分。
Worker 可能退出,Actor 可能崩溃,节点可能失联,对象也可能随着所在节点消失。
Ray 为 Task、Actor、Object 和节点提供了相应的故障处理机制,但它并不承诺所有工作都自动恢复,更不意味着所有业务都天然获得 exactly-once 语义。
例如,一个任务在写完数据库后、返回结果前进程崩溃。系统如果重试任务,数据库写入就可能发生第二次。
因此,只要任务带有外部副作用,就仍然需要考虑幂等、事务或者去重。
首先这个问题的答案是——不一定,并行和分布式都有成本:
任务需要被提交和调度
参数和结果可能需要序列化
Worker 进程需要启动
数据可能需要复制或者跨节点传输
Driver 还要维护任务和对象引用
如果一个普通函数只需要几微秒,却把它拆成几千个远程 Task,调度开销很可能比函数本身还大。
一般来说,单个任务耗时越长、任务数量越多、资源需求越复杂,Ray 的价值越明显。
反过来,如果任务本身很短,或者单机进程池已经足够,引入 Ray 反而可能增加复杂度。
那怎么判断你该不该用 Ray 呢?可以参考下面的判断流程:

▲ 是否应该使用 Ray 的判断流程
Ray 真正提供的是一套统一的分布式计算抽象
如果你的问题还没有超出单机简单并发的范围,先使用更轻量的工具完全没有问题。
如果你的任务正在从一个进程走向多个进程、从一台机器走向多台机器,同时还要处理状态、数据、异构资源和失败,那么 Ray 才真正值得进入候选方案。
— 完 —