Prhub

#40749 [Bugfix] Skip PP sampled-token receive on last rank during async scheduling

原始 PR 作者 wi-adam 合并时间 2026-05-06 13:31 文件变更 2 提交数 7 评论 7 代码增减 +54 / -1

执行摘要

跳过 PP 最后一 rank 的采样 token 接收

在 RDNA4/R9700 部署上使用 Gemma 4 31B FP8 和 PP=2 时,sample_tokens() 会在最后一个 rank 调用 _pp_receive_prev_sampled_token_ids_to_input_batch(),该方法内部有 assert not pp.is_last_rank,导致 HTTP 500 错误。后续在 NVIDIA RTX 3060 上也独立复现了同一问题(#41612)。需要确保只有非最后一个 PP rank 执行接收操作。

值得精读,虽然改动很小,但体现了一个边界条件错误的发现和验证过程。新增的测试是良好的回归保障,设计数据驱动的参数化测试值得借鉴。

讨论亮点

讨论中最核心的是关于路径是否可达的争议。njhill 最初认为最后 rank 不会进入该路径,但作者提供了复现步骤,并且 he-yufeng 在 #41612 中给出了第二个独立复现(NVIDIA 平台),证实了该路径确实可达。njhill 随后认可修复。另一个讨论是 njhill 建议将条件简化为 not is_last_rank,作者已采纳。

实现拆解

  1. 定位问题:在 vllm/v1/worker/gpu_model_runner.pysample_tokens 方法中,当 execute_model_state is None(即空执行状态)且启用了异步调度时,原条件 self.use_async_scheduling and get_pp_group().world_size > 1 会令所有 PP rank 都尝试接收采样 token。但只有非最后 rank 才应接收,最后 rank 是广播者。
  2. 修改条件:将条件改为 self.use_async_scheduling and not get_pp_group().is_last_rank,直接跳过最后 rank,不再需要先检查 world_size > 1,因为 is_last_rank 本身就蕴含了非单 rank 的情况(单 rank 时 is_last_rankTrue,也会被跳过,这与旧行为一致)。
  3. 添加单元测试:在 tests/v1/worker/test_gpu_model_runner.py 中新增两个测试:第一个参数化测试验证不同 world_sizeis_last_rank 组合下接收函数的调用次数;第二个验证当 use_async_scheduling=False 时完全不进行 PP 组查询。
文件 模块 状态 重要度
vllm/v1/worker/gpu_model_runner.py worker 层 modified 5.1
tests/v1/worker/test_gpu_model_runner.py 测试 modified 6.26

关键符号

sample_tokens test_sample_tokens_receives_pp_sampled_ids_only_on_non_last_rank test_sample_tokens_skips_pp_group_lookup_without_async_scheduling

关键源码片段

tests/v1/worker/test_gpu_model_runner.py test-coverage

新增回归测试,覆盖 rank 选择契约和异步调度禁用路径。

@pytest.mark.skip_global_cleanup
@pytest.mark.parametrize(
    ("world_size", "is_last_rank", "expected_calls"),
    [(1, True, 0), (2, True, 0), (2, False, 1)],
)
def test_sample_tokens_receives_pp_sampled_ids_only_on_non_last_rank(
    monkeypatch: pytest.MonkeyPatch,
    world_size: int,
    is_last_rank: bool,
    expected_calls: int,
):
    # 创建 runner 实例并模拟空执行状态(execute_model_state = None)
    runner = GPUModelRunner.__new__(GPUModelRunner)
    runner.execute_model_state = None
    runner.kv_connector_output = None
    runner.use_async_scheduling = True
    receive_calls = 0
​
    # 定义一个简单的接收函数来记录调用次数
    def receive_prev_sampled_token_ids():
        nonlocal receive_calls
        receive_calls += 1
​
    runner._pp_receive_prev_sampled_token_ids_to_input_batch = receive_prev_sampled_token_ids
    # 使用 monkeypatch 替换 get_pp_group 返回模拟的 PP 组
    monkeypatch.setattr(
        gpu_model_runner_module,
        "get_pp_group",
        lambda: SimpleNamespace(world_size=world_size, is_last_rank=is_last_rank),
    )
​
    # 调用 sample_tokens,期望返回 None
    assert GPUModelRunner.sample_tokens(runner, None) is None
    # 验证接收调用次数等于预期
    assert receive_calls == expected_calls
​
​
@pytest.mark.skip_global_cleanup
def test_sample_tokens_skips_pp_group_lookup_without_async_scheduling(
    monkeypatch: pytest.MonkeyPatch,
):
    # 创建 runner 实例,关闭异步调度
    runner = GPUModelRunner.__new__(GPUModelRunner)
    runner.execute_model_state = None
    runner.kv_connector_output = None
    runner.use_async_scheduling = False
​
    # 将 get_pp_group 设为 pytest.fail,确保不会被调用
    monkeypatch.setattr(
        gpu_model_runner_module,
        "get_pp_group",
        pytest.fail,
    )
​
    # 调用 sample_tokens,验证不会调用 get_pp_group
    assert GPUModelRunner.sample_tokens(runner, None) is None

评论区精华

简化条件为 is_last_rank 取反 设计

njhill 建议将 `if self.use_async_scheduling and get_pp_group().world_size > 1:` 简化为 `if self.use_async_scheduling and not get_pp_group().is_last_rank:`,同时保留 world_size 检查的隐含逻辑。

结论:作者接受建议并更新代码。 · 已解决

确认最后 PP rank 确实会进入空执行路径 正确性

njhill 最初质疑最后 rank 是否会到达 `_pp_receive_prev_sampled_token_ids_to_input_batch`,作者提供了在 RDNA4 上的复现,随后 he-yufeng 在 #41612 中给出第二个独立复现(NVIDIA),证实该路径确实可达。

结论:njhill 认可了修复的必要性并批准 PR。 · 已解决

风险与影响

风险较低:改动仅一行,且位于核心采样路径。如果 pp_groupis_last_rank 实现在某些并行拓扑下不准确,可能导致非最后 rank 错误跳过接收。但 is_last_rank 是 PP 组的标准属性,风险很小。单 rank 场景下 is_last_rank=True,不会执行接收,行为保持不变。新增测试覆盖了主要组合,有助于防止回归。

直接修复了 PP 配合异步调度时最后一个 rank 崩溃的问题。所有使用 V1 引擎、流水线并行并启用异步调度的用户均受益。工作组和 CI 需确保新测试通过。对单 rank 无影响。

PP 最后 rank 空执行路径可达 异步调度与 PP 交互 断言可能在特定配置下触发

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论