执行摘要
- 一句话:修复 Ray 编译 DAG 零拷贝数组导致的通道阻塞
- 推荐动作:值得精读。该 PR 体现了对 Ray 底层共享内存通道模型的深入理解,修复方案精准且最小化改动。对于使用 Ray 编译 DAG 的分布式部署团队,此 PR 是必读内容。设计上选择只拷贝只读数组而非全部拷贝,兼顾了正确性与性能。
功能与动机
关联 Issue #35319 报告了多节点 PP>1 时,包含 logprobs 的请求在首次成功后会因 RAY_CGRAPH_get_timeout 崩溃。PR 作者分析指出,Ray 的编译 DAG 共享内存通道返回的零拷贝 np.ndarray(通常标记为只读)若在调度迭代中持续持有引用,会阻塞通道下游的读操作。此 PR 旨在通过拷贝只读数组来解除对 Ray SHM 缓冲区的持有,从而避免通道阻塞。
实现拆解
-
在 ray_utils.py 中新增核心函数:导入 numpy,添加 detach_zero_copy_from_model_runner_output 函数。该函数内部定义了 _copy_if_readonly 辅助函数,检查每个数组是否为只读 np.ndarray,是则拷贝,否则直接返回原引用。此函数仅处理 output.logprobs 中的四个字段:token_ids、logprobs、ranks、cu_num_generated_tokens,其中 cu_num_generated_tokens 是 Python list 所以跳过。
-
在 FutureWrapper.result 中集成:修改 ray_utils.py 中的 FutureWrapper.result 方法,在 ray.get() 后(无论 aggregator 是否存在)调用 detach_zero_copy_from_model_runner_output。
-
在 ray_executor.py 中集成:导入新函数,在 _execute_dag 的同步返回路径中(非 non_block 路径)调用:
- 单 worker 时:
output = refs[0].get(); detach_zero_copy_from_model_runner_output(output); return output
- 多 worker 且连接器存在时:
outputs = ray.get(refs); for output in outputs: detach_zero_copy_from_model_runner_output(output); return aggregator.aggregate(outputs)
- 异步
non_block 路径保持原样,因为最终会在 FutureWrapper.result 中触发分离。
-
添加单元测试:新建 tests/v1/executor/test_ray_utils.py,创建包含只读 numpy 数组的 ModelRunnerOutput 实例,调用 detach_zero_copy_from_model_runner_output 后验证:结果数组不再与原始数组相同,且变为可写;同时验证 prompt_logprobs_dict 和 cu_num_generated_tokens 未被修改。
关键文件:
vllm/v1/executor/ray_utils.py(模块 执行器;类别 source;类型 core-logic;符号 detach_zero_copy_from_model_runner_output, _copy_if_readonly): 核心修复:添加 detach_zero_copy_from_model_runner_output 函数和辅助函数 _copy_if_readonly
vllm/v1/executor/ray_executor.py(模块 执行器;类别 source;类型 core-logic;符号 _execute_dag, FutureWrapper): 在 _execute_dag 的同步路径中调用 detach,确保输出脱离 SHM
tests/v1/executor/test_ray_utils.py(模块 测试;类别 test;类型 test-coverage;符号 _make_readonly, test_detach_zero_copy_from_model_runner_output_copies_only_numpy_views): 新增单元测试验证 detach 函数行为
关键符号:detach_zero_copy_from_model_runner_output, _copy_if_readonly
关键源码片段
vllm/v1/executor/ray_utils.py
核心修复:添加 detach_zero_copy_from_model_runner_output 函数和辅助函数 _copy_if_readonly
import numpy as np
def detach_zero_copy_from_model_runner_output(output: 'ModelRunnerOutput') -> None:
# Detach Ray SHM-channel zero-copy buffers from a ModelRunnerOutput in-place.
# Ray compiled DAG SHM channels may return zero-copy objects (e.g. `np.ndarray`)
# backed by Ray shared-memory object store. Retaining them across scheduler
# iterations can stall the channel. Copy read-only numpy arrays to break the
# reference.
if output.logprobs is None:
return
token_ids, logprobs, ranks, cu_num_generated_tokens = output.logprobs
def _copy_if_readonly(arr):
# 只读的 numpy 数组通常是 Ray SHM 零拷贝引用,必须拷贝
if isinstance(arr, np.ndarray) and not arr.flags.writeable:
return arr.copy()
return arr
# cu_num_generated_tokens 是 Python list,无需拷贝
token_ids_c = _copy_if_readonly(token_ids)
logprobs_c = _copy_if_readonly(logprobs)
ranks_c = _copy_if_readonly(ranks)
if token_ids_c is token_ids and logprobs_c is logprobs and ranks_c is ranks:
return # 所有数组均为可写,无需更新
# 替换原 tuple,注意保持相同类型
output.logprobs = type(output.logprobs)(
token_ids_c, logprobs_c, ranks_c, cu_num_generated_tokens
)
评论区精华
风险与影响
-
风险:
- 性能开销:每次获取 logprobs 后都会进行只读检查并可能需要拷贝。由于仅拷贝只读数组,且只出现在 logprobs 路径,对大部分无 logprobs 请求无影响;对有 logprobs 的请求,拷贝成本与 logprobs 数组大小成正比,但通常较小。
- 错误处理:numpy 导入异常被广泛捕获(
except Exception),理论上可能掩盖其他异常如 KeyboardInterrupt(但实践中可能性低)。
- 未来兼容性:如果
ModelRunnerOutput.logprobs 的结构发生变化(例如增加新字段),detach_zero_copy_from_model_runner_output 需要同步更新。
- 测试覆盖:单元测试覆盖了基本场景,但缺少多节点集成测试。由于环境限制,集成测试由 CI 覆盖(需触发完整的分布式测试流水线)。
- 影响:用户:修复了多节点 PP 配置下 logprobs 请求的偶发崩溃,使该功能在实际部署中可靠可用。
系统:对执行路径添加了轻量级数组拷贝,仅当 logprobs 启用且返回只读数组时产生额外开销,不影响其他特性。
团队:核心修复代码集中在 ray_utils.py,接口清晰,易于维护。未来若需对类似零拷贝场景做更多处理,可复用 _copy_if_readonly 抽象。
-
风险标记:分布式推理, 零拷贝数组, 性能开销
关联脉络
参与讨论