执行摘要
- 一句话:修复Ray compiled DAG在logprobs请求下SHM通道阻塞,通过复制只读numpy数组脱离零拷贝缓冲区。
- 推荐动作:该PR值得精读,特别是
detach_zero_copy_from_model_runner_output函数的实现,展示了如何安全地处理Ray SHM中的零拷贝对象。关注注释中关于prompt_logprobs_dict和cu_num_generated_tokens的设计决策,以及如何平衡性能与稳定性。对于涉及Ray或分布式执行的开发者,此修复提供了重要的技术洞察。
功能与动机
Issue #35319 报告了在PP>1的多节点推理中,带有logprobs的请求初始成功但后续崩溃,出现RayChannelTimeoutError: Timed out acquiring the read lock.。Ray通道API明确警告,如果零拷贝反序列化对象(如np.ndarray)仍在作用域内,后续read()调用可能阻塞。vLLM的ModelRunnerOutput.logprobs可能包含由Ray SHM支持的numpy数组,这些数组通常标记为只读,保持其引用会阻塞通道并导致超时。因此,需要复制这些数组以脱离Ray缓冲区。
实现拆解
- 新增核心函数:在
vllm/v1/executor/ray_utils.py中,添加detach_zero_copy_from_model_runner_output函数及其辅助函数_copy_if_readonly。该函数检查ModelRunnerOutput.logprobs中的numpy数组是否为只读,如果是则复制它们,从而脱离Ray SHM缓冲区。注释解释了为何跳过prompt_logprobs_dict(由PyTorch张量支持)和cu_num_generated_tokens(普通Python列表)。
- 修改FutureWrapper:在同一文件中,更新
FutureWrapper.result方法,在ray.get()后调用detach_zero_copy_from_model_runner_output,确保单工作器和多工作器场景下输出都已脱离缓冲区。
- 集成到执行器:在
vllm/v1/executor/ray_executor.py中,导入新函数并在阻塞模式下获取输出后立即调用它,覆盖无连接器和有连接器的情况。
- 添加测试配套:新增
tests/v1/executor/test_ray_utils.py文件,包含_make_readonly辅助函数和test_detach_zero_copy_from_model_runner_output_copies_only_numpy_views测试,验证只读numpy数组被正确复制,而其他部分保持不变。
关键文件:
vllm/v1/executor/ray_utils.py(模块 执行器;类别 source;类型 core-logic;符号 detach_zero_copy_from_model_runner_output, _copy_if_readonly): 核心修复逻辑文件,新增了脱离零拷贝缓冲区的函数并修改了FutureWrapper,直接解决通道阻塞问题。
tests/v1/executor/test_ray_utils.py(模块 测试;类别 test;类型 test-coverage;符号 _make_readonly, test_detach_zero_copy_from_model_runner_output_copies_only_numpy_views): 新增测试文件,验证脱离零拷贝函数的正确性,确保只复制只读numpy数组且其他部分不变。
vllm/v1/executor/ray_executor.py(模块 执行器;类别 source;类型 entrypoint): 集成修复到Ray执行器,在阻塞模式下获取输出后立即调用脱离函数,确保实际执行中缓冲区的及时释放。
关键符号:detach_zero_copy_from_model_runner_output, _copy_if_readonly, FutureWrapper.result, test_detach_zero_copy_from_model_runner_output_copies_only_numpy_views
关键源码片段
vllm/v1/executor/ray_utils.py
核心修复逻辑文件,新增了脱离零拷贝缓冲区的函数并修改了FutureWrapper,直接解决通道阻塞问题。
import numpy as np # 新增导入,用于处理numpy数组
def detach_zero_copy_from_model_runner_output(output: "ModelRunnerOutput") -> None:
"""从ModelRunnerOutput中脱离Ray SHM通道的零拷贝缓冲区。
Ray编译DAG的SHM通道可能返回零拷贝对象(如`np.ndarray`),
这些对象由Ray的共享内存对象存储支持。Ray通道文档明确警告,
如果此类对象仍在作用域内,后续读取可能阻塞。
vLLM可以在`ModelRunnerOutput.logprobs`中返回numpy支持的logprobs。
如果这些数组由Ray SHM支持(通常是只读的),在调度器迭代中保持它们
会导致通道阻塞并最终触发`RAY_CGRAPH_get_timeout`。
复制只读numpy数组,使返回的输出不再引用Ray的共享内存缓冲区。
我们故意不处理`prompt_logprobs_dict`:这些条目是`LogprobsTensors`,
由PyTorch拥有的CPU张量支持,而不是从Ray通道解码的NumPy视图。
"""
if output.logprobs is None:
return # 如果logprobs为None,无需处理
token_ids, logprobs, ranks, cu_num_generated_tokens = output.logprobs
def _copy_if_readonly(arr):
# 辅助函数:仅当arr是只读numpy数组时才复制,避免不必要的开销
if isinstance(arr, np.ndarray) and not arr.flags.writeable:
return arr.copy()
return arr
# `cu_num_generated_tokens`已经是普通的Python列表(或None),
# 所以它永远不会别名Ray SHM缓冲区,可以直接重用。
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 # 如果没有数组被复制,则提前返回,避免不必要的对象重建
output.logprobs = type(output.logprobs)(
token_ids_c, logprobs_c, ranks_c, cu_num_generated_tokens
) # 重建logprobs元组,仅替换被复制的数组
评论区精华
风险与影响
- 风险:- 内存开销增加:复制只读numpy数组会引入额外内存分配,但仅限于logprobs中的数组,且通常较小,影响可控。
- 性能影响:如果误判可写数组为只读并进行复制,可能导致不必要的性能下降,但通过
flags.writeable检查可最小化。
- Ray依赖:修复依赖于Ray通道行为,若Ray API变更可能失效,但当前基于Ray文档警告,风险较低。
- 测试覆盖:新增测试确保了核心逻辑正确,但未覆盖所有边缘情况(如不同数据类型),但现有测试足以验证主要场景。
- 影响:- 用户影响:使用Ray compiled DAG进行多节点流水线并行且开启logprobs的用户将不再遇到通道超时崩溃,提升了系统稳定性和可用性。
- 系统影响:修复针对特定崩溃场景,不会影响其他功能;内存开销轻微,但避免了更严重的阻塞问题。
- 团队影响:为Ray集成中的零拷贝问题提供了解决方案,可作为类似问题的参考;代码注释增强了可维护性。
- 风险标记:内存开销增加, Ray集成依赖, 核心路径变更
关联脉络
- PR #39990 Fix #33773: Replace unconditional pandas import with PlaceholderModule: 同为bugfix PR,涉及依赖管理和导入优化,展示了vLLM中对可选依赖的处理模式,可作参考。
- PR #40011 [Bugfix] Fix LLM priority normalization for single-string prompts: 同为v1标签的bugfix,修复前端逻辑错误,体现了项目对稳定性的持续改进。
参与讨论