Ray 反模式详解:不要在任务中返回 ray.put() 的 ObjectRef,直接返回值更快更可靠
【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray
导读:在 Ray 中,任务(task)与 Actor 方法的返回值应该直接返回,而不是先调用ray.put()把值放入分布式对象存储、再返回生成的 ObjectRef。本指南基于 return-ray-put.rst 展开,详细解释这一反模式的三大危害(内联优化失效、引用计数协议开销、容错性下降),并给出单值、静态多值、动态多值三种场景的正确写法,帮助你写出更快、更省内存、更健壮的 Ray 应用。
反模式速览(TLDR)
核心结论一句话:避免在任务返回值上调用ray.put()并返回产生的 ObjectRef;只要有可能,就直接返回值本身。
# 反模式:先把值放进对象存储,再返回引用 @ray.remote def task_bad(): value = compute() return ray.put(value) # 推荐写法:直接返回 value @ray.remote def task_good(): value = compute() return value这条反模式涉及的核心 API 是ray.put(),其源码位于 python/ray/_private/worker.py,功能为“将一个对象存入对象存储,并返回指向该值的 ObjectRef”。Ray 官方文档在该函数的 docstring 中直接挂接了本反模式文章(见 python/ray/_private/worker.py),可见其是 Ray 使用者极易踩中的典型陷阱。
为什么是反模式:三个核心原因
1. 破坏了小对象内联返回优化
Ray 对任务返回值有一项性能优化:不超过 100KB 的小值会以内联(inline)方式直接随任务结果返回给调用方,完全不需要经过分布式对象存储。这意味着调用方可以立刻拿到结果,省去一次对象存储的写入与读取。
而ray.put()会无条件把值写入对象存储,这就让内联优化彻底失效:
import ray import numpy as np @ray.remote def task_with_single_small_return_value_bad(): small_return_value = 1 # 值被无条件写入对象存储,引用返回给调用方 small_return_value_ref = ray.put(small_return_value) return small_return_value_ref @ray.remote def task_with_single_small_return_value_good(): small_return_value = 1 # Ray 会将小值内联返回给调用方,比上一种方式更快 return small_return_value # 两种写法结果等价,但性能不同 assert ray.get(ray.get(task_with_single_small_return_value_bad.remote())) == ray.get( task_with_single_small_return_value_good.remote() )即使是大对象(如 10MB 的 numpy 数组),直接返回也优于ray.put():
@ray.remote def task_with_single_large_return_value_bad(): large_return_value = np.zeros(10 * 1024 * 1024) large_return_value_ref = ray.put(large_return_value) return large_return_value_ref @ray.remote def task_with_single_large_return_value_good(): # 大数组两种方式都会进对象存储, # 但直接返回更快且容错性更好。 large_return_value = np.zeros(10 * 1024 * 1024) return large_return_value assert np.array_equal( ray.get(ray.get(task_with_single_large_return_value_bad.remote())), ray.get(task_with_single_large_return_value_good.remote()), )注意上述两个例子中,调用方取值的写法差异:反模式需要两次ray.get()(先取 ObjectRef,再取实际值),而正确写法只需要一次ray.get()。
2. 引入额外的分布式引用计数协议开销
Ray 对象通过**分布式引用计数(distributed reference counting)**进行自动内存管理:ObjectRef 可以在任务、Actor 方法和对象之间自由传递,一旦对某个对象的所有引用被删除,其数据即被自动释放(参见 doc/source/ray-core/objects.rst)。
当你返回一个ray.put()产生的 ObjectRef 时,Ray 必须为这个额外层级的引用维护引用计数协议,涉及多轮进程间的引用传递与确认。而直接返回值只需对返回值本身做一次引用计数。多一层 ObjectRef 引用,就多一层分布式引用计数开销,在返回大量值的场景下,这种开销会被显著放大。
3. 容错性更差:返回值与 Owner 命运共享(fate sharing)
Ray 中每个对象都有其owner(所有者)——即创建该 ObjectRef 的进程。文档 doc/source/ray-core/fault_tolerance/objects.rst 明确说明:Ray 目前不支持 owner 失败恢复。当 owner 进程(worker)死亡时,Ray 会清理该对象残留的所有副本,随后尝试获取该对象的 worker 将收到OwnerDiedError异常。
- 反模式:
ray.put()发生在任务 worker 内部,因此返回值的 owner 是该worker 进程。如果 worker 在执行任务后死亡(如节点故障、被 OOM Killer 杀掉),即使任务本身已经成功返回了 ObjectRef,这个返回值仍然会丢失——因为其 owner 已死。 - 正确写法:直接返回值时,owner 是调用方进程(通常是 driver)。driver 通常比一次性执行的 worker 更长寿、更稳定,返回值不会因为 worker 的退出而丢失。
补充说明:
OwnerDiedError是ObjectLostError的一个子类,其触发条件正是“通过.remote()或ray.put()首次创建 ObjectRef 的 Python worker 已死亡”(见 doc/source/ray-core/fault_tolerance/objects.rst)。这条机制直接解释了为什么“在 worker 内 put 再返回引用”会让返回值脆弱。
正确的替代方案
方案一:单返回值——直接返回
无论值大小,只要任务是单个返回值,就直接返回它:
@ray.remote def task_with_single_large_return_value_good(): large_return_value = np.zeros(10 * 1024 * 1024) return large_return_value方案二:静态多返回值——使用 num_returns 选项
如果任务要返回多个值,且在调用任务之前就知道返回值的个数,请使用num_returns选项(Ray 默认每个任务只返回一个 ObjectRef,num_returns可配置返回多个,参见 doc/source/ray-core/tasks.rst)。
反模式(两种情况都错):
# 错误 1:返回一个装着两个 ObjectRef 的元组 @ray.remote(num_returns=1) def task_with_static_multiple_returns_bad1(): return_value_1_ref = ray.put(1) return_value_2_ref = ray.put(2) return (return_value_1_ref, return_value_2_ref) # 错误 2:num_returns=2,但每个返回值仍是 ObjectRef @ray.remote(num_returns=2) def task_with_static_multiple_returns_bad2(): return_value_1_ref = ray.put(1) return_value_2_ref = ray.put(2) return (return_value_1_ref, return_value_2_ref)正确写法(num_returns=2,直接返回实际值):
# 正确:返回两个实际值,每个都是独立的对象 @ray.remote(num_returns=2) def task_with_static_multiple_returns_good(): return_value_1 = 1 return_value_2 = 2 return (return_value_1, return_value_2) # 三个版本结果一致 assert ( ray.get(ray.get(task_with_static_multiple_returns_bad1.remote())[0]) == ray.get(ray.get(task_with_static_multiple_returns_bad2.remote()[0])) == ray.get(task_with_static_multiple_returns_good.remote()[0]) )注意num_returns=1时返回的元组是一个对象(一个装着两个 ObjectRef 的元组),而num_returns=2时返回的是两个独立对象。反模式两个版本都引入了额外的引用层级。
方案三:动态多返回值——使用动态生成器(dynamic generator)
如果任务返回值的数量在调用前不可预知,则使用 Ray 的动态生成器模式:把任务写成生成器函数,用yield逐个产出返回值,并把num_returns设为"dynamic"。其配套的生成器模式文档见 doc/source/ray-core/patterns/generators.rst。
反模式(先ray.put攒一堆引用再整体返回):
@ray.remote(num_returns=1) def task_with_dynamic_returns_bad(n): return_value_refs = [] for i in range(n): return_value_refs.append(ray.put(np.zeros(i * 1024 * 1024))) return return_value_refs正确写法(动态生成器):
@ray.remote(num_returns="dynamic") def task_with_dynamic_returns_good(n): for i in range(n): yield np.zeros(i * 1024 * 1024) assert np.array_equal( ray.get(ray.get(task_with_dynamic_returns_bad.remote(2))[0]), ray.get(next(iter(ray.get(task_with_dynamic_returns_good.remote(2))))), )动态生成器还有额外收益:worker 可以逐个产出、逐个释放返回值,避免在任务结束前把所有大返回值同时堆在堆内存中导致 OOM——这正是生成器模式的核心价值(见 doc/source/ray-core/patterns/generators.rst)。
Actor 方法同样适用
以上原则对 Actor 方法同样成立。ray.put()在 Actor 内调用时,owner 是执行该方法的 Actor worker 进程;如果该 Actor 崩溃,返回值同样会丢失。正确写法是让 Actor 方法直接返回实际值:
@ray.remote class Actor: def task_with_single_return_value_bad(self): single_return_value = np.zeros(9 * 1024 * 1024) return ray.put(single_return_value) def task_with_single_return_value_good(self): return np.zeros(9 * 1024 * 1024) actor = Actor.remote() assert np.array_equal( ray.get(ray.get(actor.task_with_single_return_value_bad.remote())), ray.get(actor.task_with_single_return_value_good.remote()), )静态多返回值的 Actor 版本使用@ray.method(num_returns=...)装饰器,规则与任务完全一致:能确定返回个数就用num_returns=N直接返回元组,不要用ray.put()包装。
判断准则与常见误区
什么时候应该用ray.put()?反模式针对的是“在任务/Actor 方法内部对返回值调用ray.put()再返回引用”这一场景。ray.put()本身并非错误——例如在 driver 中提前把大对象放入对象存储供多个任务共享引用,是合法的ray.put()用途(该场景的配套讨论可参考 doc/source/ray-core/patterns/pass-large-arg-by-value.rst)。判断核心在于:ObjectRef 是在哪个进程被创建的,返回值是否会因此与易死的 worker 绑定。
判断口诀:
| 场景 | 反模式 | 正确做法 |
|---|---|---|
| 单返回值(无论大小) | 任务内ray.put()后返回引用 | 直接返回值 |
| 返回个数调用前已知 | 返回一坨 ObjectRef(元组或列表) | num_returns=N直接返回值元组 |
| 返回个数调用前未知 | 攒 ObjectRef 列表整体返回 | num_returns="dynamic"生成器逐个yield |
| Actor 方法 | 方法内ray.put()后返回引用 | @ray.method(num_returns=...)直接返回值 |
误区澄清:ray.get(ray.get(...))这种双层取值写法本身就暴露了问题——调用方被迫先解析外层 ObjectRef、再解析内层引用,多一次ray.get()往返。若你的代码里出现了这种“双重解引用”,大概率已踩中本条反模式。
小结
在 Ray 中,任务与 Actor 方法的返回值应直接返回,而不是先ray.put()再返回 ObjectRef。三个核心理由:小值(≤100KB)内联返回优化失效、额外的分布式引用计数协议开销、返回值与ray.put()所在 worker 命运共享导致容错性下降。对静态多返回值使用num_returns,对动态多返回值使用num_returns="dynamic"生成器,即可在保证功能等价的同时获得更快的速度与更强的容错能力。
所有示例代码可在仓库中直接查看与运行:doc/source/ray-core/doc_code/anti_pattern_return_ray_put.py,其中包括任务与 Actor 两种实体的全部对比断言。
【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考