Ray 反模式详解:不要在任务中返回 ray.put() 的 ObjectRef,直接返回值更快更可靠
2026/9/20 7:26:22 网站建设 项目流程

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 的退出而丢失。

补充说明:OwnerDiedErrorObjectLostError的一个子类,其触发条件正是“通过.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询