ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Ray 反模式详解:不要在任务中返回 ray.put() 的 ObjectRef,直接返回值更快更可靠

Ray 反模式详解:不要在任务中返回 ray.put() 的 ObjectRef,直接返回值更快更可靠 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 sharingRay 中每个对象都有其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 默认每个任务只返回一个 ObjectRefnum_returns可配置返回多个参见 doc/source/ray-core/tasks.rst。反模式两种情况都错# 错误 1返回一个装着两个 ObjectRef 的元组 ray.remote(num_returns1) 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) # 错误 2num_returns2但每个返回值仍是 ObjectRef ray.remote(num_returns2) 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_returns2直接返回实际值# 正确返回两个实际值每个都是独立的对象 ray.remote(num_returns2) 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_returns1时返回的元组是一个对象一个装着两个 ObjectRef 的元组而num_returns2时返回的是两个独立对象。反模式两个版本都引入了额外的引用层级。方案三动态多返回值——使用动态生成器dynamic generator如果任务返回值的数量在调用前不可预知则使用 Ray 的动态生成器模式把任务写成生成器函数用yield逐个产出返回值并把num_returns设为dynamic。其配套的生成器模式文档见 doc/source/ray-core/patterns/generators.rst。反模式先ray.put攒一堆引用再整体返回ray.remote(num_returns1) 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_returnsdynamic) 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_returnsN直接返回元组不要用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_returnsN直接返回值元组返回个数调用前未知攒 ObjectRef 列表整体返回num_returnsdynamic生成器逐个yieldActor 方法方法内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_returnsdynamic生成器即可在保证功能等价的同时获得更快的速度与更强的容错能力。所有示例代码可在仓库中直接查看与运行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),仅供参考
返回列表