Prhub

#49757 [BugFix] Stop dummy runs from writing mamba state through stale block-table rows

原始 PR 作者 njhill 合并时间 2026-07-29 09:09 文件变更 5 提交数 2 评论 3 代码增减 +86 / -2

执行摘要

修复 dummy run 通过陈旧 block table 行写入 mamba 状态

在混合模型 P/D 分离和 DP+EP 解码场景下,虚拟运行批次通过 mamba/GDN/KDA 路径写入状态时可能使用陈旧 block id,导致罕见的第一个 token 损坏。修复此问题以提升混合模型部署的稳定性。

建议深入阅读此 PR,它展示了如何定位和修复数据损坏的典型思路:从源头和重放路径双重修复。对于涉及 cudagraph 和 speculative decoding 的开发者尤其有价值。后续可关注 follow-up:将零填充 gating 在混合模型上以减少不必要的开销。

讨论亮点

WoosukKwon 在 V2 模型运行器的变更上评论「I kinda understand the intent here, but I don't feel this is the right solution」(我大致理解意图,但感觉这不是正确的解决方案)。majunze2001 回复「This did fix the corruption but I didn't find the root cause. I'll take another look into this」(这确实修复了损坏,但我没有找到根本原因,我会再研究)。讨论未就根因达成一致,但 PR 仍被批准合并。

实现拆解

  1. 清除 vacated block-table 行:在 vllm/v1/worker/block_table.pyBlockTable.move_row() 中,将源行的 block ids 和 num_blocks_per_row 置零,阻止 condense 后 vacated 行残留陈旧 id。
  2. 返回零填充 dummy block tables:在 vllm/v1/worker/gpu/block_table.pyBlockTables.get_dummy_block_tables() 中,返回 zero_() 后的持久张量切片,保持地址不变同时确保 dummy run 不会使用前一次的真实 block id。
  3. 重新注入 FULL cudagraph 元数据缓冲区:在 V1 模型运行器 vllm/v1/worker/gpu_model_runner.py_dummy_run() 中,当 cudagraph_runtime_mode == FULL 时传递 for_cudagraph_capture=True;在 V2 模型运行器 vllm/v1/worker/gpu/model_runner.pyexecute_model() 中,当 dummy run 且 cg_mode 为 FULL 时传递 for_capture=True。这确保零 block tables 被传播到 cudagraph 重放的元数据缓冲区。
  4. 测试配套:在 tests/v1/worker/test_gpu_block_table.py 中新增两个测试,分别验证 move_row 清除 vacated 行,以及 get_dummy_block_tables 返回零行并保持持久地址。
文件 模块 状态 重要度
vllm/v1/worker/gpu/block_table.py GPU 块表 modified 5.17
vllm/v1/worker/block_table.py CPU 块表 modified 4.94
vllm/v1/worker/gpu_model_runner.py V1 运行器 modified 5.8
vllm/v1/worker/gpu/model_runner.py V2 运行器 modified 5.46
tests/v1/worker/test_gpu_block_table.py GPU 块表测试 modified 6.45

关键符号

BlockTable.move_row BlockTables.get_dummy_block_tables _dummy_run (V1) execute_model (V2) test_v1_block_table_move_row_clears_vacated_row test_get_dummy_block_tables_returns_zeroed_rows

关键源码片段

vllm/v1/worker/gpu_model_runner.py data-contract

V1 模型运行器,修改 `_dummy_run` 中 `for_cudagraph_capture` 参数,确保 FULL cudagraph 重放时重新注入零 block tables 到注意力元数据。

                attn_metadata, _ = self._build_attention_metadata(
                    num_tokens=num_tokens_unpadded,
                    num_tokens_padded=num_tokens_padded if pad_attn else None,
                    num_reqs=num_reqs_padded,
                    max_query_len=max_query_len,
                    ubatch_slices=(ubatch_slices_padded if pad_attn else ubatch_slices),
                    # FULL replay reads capture-time metadata buffers. Re-stage them
                    # from the zeroed dummy block tables instead of retaining state
                    # indices from the previous real batch.
                    for_cudagraph_capture=(
                        is_graph_capturing
                        or cudagraph_runtime_mode == CUDAGraphMode.FULL
                    ),
                    slot_mappings=slot_mappings_by_group,
                    use_spec_decode=self.speculative_config is not None,
                )
vllm/v1/worker/gpu/model_runner.py data-contract

V2 模型运行器,传递 `for_capture` 参数到 `prepare_attn`,确保 FULL 模式下 dummy run 的注意力元数据基于零 block tables 构建。

            attn_metadata = self.model_state.prepare_attn(
                input_batch,
                batch_desc.cg_mode,
                block_tables,
                slot_mappings,
                self.attn_groups,
                self.kv_cache_config,
                # FULL replay reads capture-time metadata buffers. Re-stage them
                # from the zeroed dummy block tables instead of retaining state
                # indices from the previous real batch.
                for_capture=dummy_run and batch_desc.cg_mode == CUDAGraphMode.FULL,
            )
tests/v1/worker/test_gpu_block_table.py test-coverage

新增两个测试验证 `move_row` 清除 vacated 行和 `get_dummy_block_tables` 返回零行并保持持久地址,覆盖修复的两种场景。

def test_v1_block_table_move_row_clears_vacated_row():
    """condense() moves the last row into a freed slot; the vacated row must
    not keep stale block ids. Padded dummy-run batches dereference stale rows
    as mamba state slots (bypassing the NULL_BLOCK_ID fill of real decode
    padding) and write state in place there — corrupting the blocks' new
    owner once they are reallocated, e.g. to an in-flight NIXL load."""
    from vllm.v1.worker.block_table import BlockTable
    block_table = BlockTable(
        block_size=16,
        max_num_reqs=4,
        max_num_blocks_per_req=8,
        max_num_batched_tokens=64,
        pin_memory=False,
        device=torch.device("cuda"),
        kernel_block_size=16,
        cp_kv_cache_interleave_size=1,
    )
    block_table.add_row([7, 8, 9], row_idx=0)
    block_table.add_row([4, 5], row_idx=1)
    block_table.move_row(1, 0)
    assert block_table.block_table.np[0, :2].tolist() == [4, 5]
    assert block_table.num_blocks_per_row[0] == 2
    # The vacated source row routes to the reserved null block.
    assert block_table.num_blocks_per_row[1] == 0
    assert (block_table.block_table.np[1] == 0).all()
​
​
def test_get_dummy_block_tables_returns_zeroed_rows():
    """Dummy runs bypass the gather, so the persistent input_block_tables
    hold the previous real step's rows. Mamba/GDN metadata routes in-place
    state writes through block_table[:, 0] (dummy slot mappings are
    PAD-filled, state indices are not), so stale rows would direct dummy
    state writes at freed — possibly reallocated — blocks.
    get_dummy_block_tables must hand out zeroed (null block) rows while
    preserving the persistent storage address for CUDA graphs."""
    device = torch.device("cuda")
    block_tables = BlockTables(
        block_sizes=[16],
        max_num_reqs=4,
        max_num_batched_tokens=64,
        max_num_blocks_per_group=[8],
        device=device,
        kernel_block_sizes=[16],
    )
    # Simulate a real step: stage a request's blocks and gather them into
    # the persistent input block tables.
    block_tables.append_block_ids(req_index=0, new_block_ids=([1, 2],), overwrite=True)
    block_tables.apply_staged_writes()
    idx_mapping = torch.zeros(1, dtype=torch.int32, device=device)
    block_tables.gather_block_tables(idx_mapping, num_reqs_padded=1)
    torch.accelerator.synchronize()
    assert block_tables.input_block_tables[0][0, 0].item() == 1
    dummy = block_tables.get_dummy_block_tables(num_reqs=1)
    torch.accelerator.synchronize()
    assert (dummy[0] == 0).all()
    # CUDA graph invariant: same persistent tensor, not a fresh allocation.
    assert dummy[0].data_ptr() == block_tables.input_block_tables[0].data_ptr()

评论区精华

for_capture 方案的正确性 设计

WoosukKwon 评论「I kinda understand the intent here, but I don't feel this is the right solution」;majunze2001 回复「This did fix the corruption but I didn't find the root cause. I'll take another look into this」

结论:根因未完全明确,方案被批准但设计正确性留待后续调查。 · unresolved

风险与影响

风险较低,但值得关注:

  • 核心路径变更:修改了 block_table.py 和模型运行器中的关键逻辑,可能影响其他依赖 get_dummy_block_tablesfor_cudagraph_capture 的模块。
  • 无条件零填充:当前对所有模型(包括非混合模型)都进行零填充,虽性能开销极小,但可通过 gate 优化。
  • 设计正确性存疑:WoosukKwon 认为 V2 中的解决方案不彻底,未来版本可能需重新审视根因并调整。

正面影响:修复了混合模型 (Mamba/GDN/KDA) 在 P/D 分离和 DP+EP 解码下的罕见数据损坏,提升系统稳定性。负面影响:对非混合模型无功能影响,但引入极轻微的性能开销(每次 dummy run 对 block table 行进行零填充)。团队需关注后续根因分析的进展以决定是否需要优化。

核心路径变更 混合模型特定

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论