Prhub

#35736 [Bugfix] Fix Ray compiled-DAG SHM channel stalls by detaching zero-copy `np.ndarray` logprobs buffers

原始 PR 作者 JeanPaulShapo 合并时间 2026-04-16 23:49 文件变更 3 提交数 7 评论 10 代码增减 +109 / -2

执行摘要

修复 Ray 编译 DAG 零拷贝数组导致的通道阻塞

关联 Issue #35319 报告了多节点 PP>1 时,包含 logprobs 的请求在首次成功后会因 RAY_CGRAPH_get_timeout 崩溃。PR 作者分析指出,Ray 的编译 DAG 共享内存通道返回的零拷贝 np.ndarray(通常标记为只读)若在调度迭代中持续持有引用,会阻塞通道下游的读操作。此 PR 旨在通过拷贝只读数组来解除对 Ray SHM 缓冲区的持有,从而避免通道阻塞。

值得精读。该 PR 体现了对 Ray 底层共享内存通道模型的深入理解,修复方案精准且最小化改动。对于使用 Ray 编译 DAG 的分布式部署团队,此 PR 是必读内容。设计上选择只拷贝只读数组而非全部拷贝,兼顾了正确性与性能。

讨论亮点
  • 审阅者 kouroshHakha:指出 prompt_logprobs_dict 采用 PyTorch 张量,不受 SHM 影响,应添加注释说明。作者采纳并更新了 docstring。
  • 审阅者 kouroshHakha:建议对 cu_num_generated_tokens 跳过拷贝添加注释。作者添加了注释。
  • 审阅者 kouroshHakha:建议增加单元测试。作者创建了完整的测试文件。
  • gemini-code-assist:建议将 numpy 导入的 except Exception 改为 except ImportError。此建议未被采纳,当前写法保持。

实现拆解

  1. ray_utils.py 中新增核心函数:导入 numpy,添加 detach_zero_copy_from_model_runner_output 函数。该函数内部定义了 _copy_if_readonly 辅助函数,检查每个数组是否为只读 np.ndarray,是则拷贝,否则直接返回原引用。此函数仅处理 output.logprobs 中的四个字段:token_idslogprobsrankscu_num_generated_tokens,其中 cu_num_generated_tokens 是 Python list 所以跳过。

  2. FutureWrapper.result 中集成:修改 ray_utils.py 中的 FutureWrapper.result 方法,在 ray.get() 后(无论 aggregator 是否存在)调用 detach_zero_copy_from_model_runner_output

  3. 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 中触发分离。
  4. 添加单元测试:新建 tests/v1/executor/test_ray_utils.py,创建包含只读 numpy 数组的 ModelRunnerOutput 实例,调用 detach_zero_copy_from_model_runner_output 后验证:结果数组不再与原始数组相同,且变为可写;同时验证 prompt_logprobs_dictcu_num_generated_tokens 未被修改。

文件 模块 状态 重要度
vllm/v1/executor/ray_utils.py 执行器 modified 7.63
vllm/v1/executor/ray_executor.py 执行器 modified 5.74
tests/v1/executor/test_ray_utils.py 测试 added 6.09

关键符号

detach_zero_copy_from_model_runner_output _copy_if_readonly

关键源码片段

vllm/v1/executor/ray_utils.py core-logic

核心修复:添加 `detach_zero_copy_from_model_runner_output` 函数和辅助函数 `_copy_if_readonly`

import numpy as npdef 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
    )

评论区精华

确认 prompt_logprobs_dict 不被处理 设计

kouroshHakha 指出 prompt_logprobs_dict 使用 PyTorch 张量,不受 SHM 影响,应在代码中注释说明以避免误解。

结论:作者采纳建议,在函数 docstring 中添加注释说明不处理 prompt_logprobs_dict 的原因。 · 已解决

测试覆盖确认 测试

kouroshHakha 建议添加单元测试以确保 detach 函数功能正确。

结论:作者添加了完整的测试文件 tests/v1/executor/test_ray_utils.py。 · 已解决

风险与影响

  1. 性能开销:每次获取 logprobs 后都会进行只读检查并可能需要拷贝。由于仅拷贝只读数组,且只出现在 logprobs 路径,对大部分无 logprobs 请求无影响;对有 logprobs 的请求,拷贝成本与 logprobs 数组大小成正比,但通常较小。
  2. 错误处理:numpy 导入异常被广泛捕获(except Exception),理论上可能掩盖其他异常如 KeyboardInterrupt(但实践中可能性低)。
  3. 未来兼容性:如果 ModelRunnerOutput.logprobs 的结构发生变化(例如增加新字段),detach_zero_copy_from_model_runner_output 需要同步更新。
  4. 测试覆盖:单元测试覆盖了基本场景,但缺少多节点集成测试。由于环境限制,集成测试由 CI 覆盖(需触发完整的分布式测试流水线)。

用户:修复了多节点 PP 配置下 logprobs 请求的偶发崩溃,使该功能在实际部署中可靠可用。
系统:对执行路径添加了轻量级数组拷贝,仅当 logprobs 启用且返回只读数组时产生额外开销,不影响其他特性。
团队:核心修复代码集中在 ray_utils.py,接口清晰,易于维护。未来若需对类似零拷贝场景做更多处理,可复用 _copy_if_readonly 抽象。

分布式推理 零拷贝数组 性能开销

关联 Issue

#35319 [Bug]: Multi-Node inference with PP > 1 crashes after processing completions request with non-None `logprobs` parameter.

完整报告

参与讨论