Prhub

#48261 [BugFix][ModelRunner V2] Fix stale attn metadata in speculator prefill cudagraph capture

原始 PR 作者 njhill 合并时间 2026-07-14 00:39 文件变更 7 提交数 2 评论 4 代码增减 +72 / -108

执行摘要

修复投机解码预填充 CG 捕获的陈旧注意力元数据

PR body 指出:投机解码的预填充 FULL CUDA Graph 捕获复用了目标模型捕获时构建的注意力状态,但这些元数据对象是 per-builder 持久缓冲区的视图(如 FlashAttention 的 AOT scheduler_metadata、dummy query_start_loc/seq_lens),每个后续构建都会覆盖缓冲区。当投机解码捕获其预填充图时,缓冲区内容与目标上次捕获的 batch descriptor 匹配,而不是正在捕获的 descriptor,导致内核在捕获期间使用了错误的启动元数据。在低 SM 设备(如 H200 MIG 16-SM 切片)上,FA3 split-KV combine 内核 (flash_fwd_combine) 会读取越界,导致非法内存访问崩溃。该问题也导致 CI 测试失败。

该 PR 值得精读,特别是如何通过共享目标模型的持久缓冲区地址来避免运行时注意力元数据重建的设计决策。合并后的 SpeculatorCudaGraphManager 简化了捕获逻辑,同时也展示了 FULL CUDA Graph 捕获中元数据一致性的重要性。

讨论亮点

评论者 @TheEpicDolphin 提出担忧:投机解码预填充现在使用与目标模型不同的注意力元数据,是否需要在每次解码步骤前重建注意力元数据以避免 FULL CUDA Graph 下的陈旧数据?@njhill 回应:捕获时通过 set_attn 传递目标模型的 input_buffersattn_groups,并调用 prepare_inputs_to_capture 构建元数据,这确保了捕获的图与运行时刷新的同一持久缓冲区地址对应,因此运行时无需额外重建。@TheEpicDolphin 表示理解并认可修复方案。WoosukKwon 最终批准合并。

实现拆解

  1. 修改 BaseSpeculator.capture 签名:移除了 attn_states 参数,改为无参方法(参见 speculator.py)。
  2. DraftModelSpeculator.set_attn 中新增 target_input_bufferstarget_attn_groups 参数,保存目标模型运行器的缓冲区和注意力组引用,用于后续捕获时构建一致的注意力元数据。
  3. 合并 PrefillSpeculatorCudaGraphManagerDecodeSpeculatorCudaGraphManager 为统一的 SpeculatorCudaGraphManagerautoregressive/cudagraph_utils.py)。新类的 capture 方法不再接收预先构建的注意力状态,而是调用 prepare_inputs_to_capture 为每个预热和捕获 pass 构建新鲜的注意力元数据,元数据通过目标模型的缓冲区和构建器生成,确保捕获的图烘焙运行时刷新的同一持久地址。
  4. 移除 AttentionStatePair 类及相关协议修改(cudagraph_utils.py):删除了 AttentionStatePair NamedTuple,并将 CreateForwardFn 的返回类型从 tuple[Callable, AttentionState] 简化为 Callable,因为不再需要区分 warmup 和 captured 状态对。
  5. 更新所有调用点:AutoRegressiveSpeculator.captureDFlashSpeculator.capture 改为无参调用,并传递必要的参数给新的 SpeculatorCudaGraphManager.capturemodel_runner.py 中不再将 attn_states 传递给 speculator.capture
文件 模块 状态 重要度
vllm/v1/worker/gpu/spec_decode/autoregressive/cudagraph_utils.py 投机解码 CG modified 8.25
vllm/v1/worker/gpu/cudagraph_utils.py CG 工具 modified 7.0
vllm/v1/worker/gpu/spec_decode/speculator.py 投机解码基类 modified 6.73
vllm/v1/worker/gpu/spec_decode/autoregressive/speculator.py 自回归投机 modified 6.68
vllm/v1/worker/gpu/spec_decode/dflash/speculator.py DFlash 投机 modified 6.59
vllm/v1/worker/gpu/model_runner.py 模型运行器 modified 5.91
vllm/v1/worker/gpu/spec_decode/dflash/cudagraph.py DFlash CG modified 5.21

关键符号

SpeculatorCudaGraphManager.capture PrefillSpeculatorCudaGraphManager.capture DecodeSpeculatorCudaGraphManager.capture create_forward_fn BaseSpeculator.capture DraftModelSpeculator.set_attn AutoRegressiveSpeculator.capture DFlashSpeculator.capture AttentionStatePair

关键源码片段

vllm/v1/worker/gpu/spec_decode/autoregressive/cudagraph_utils.py core-logic

核心变更文件,合并了两个 CUDA Graph 管理器为统一的 SpeculatorCudaGraphManager,重构了 capture 逻辑以构建新鲜注意力元数据。

class SpeculatorCudaGraphManager(CudaGraphManager):
    """CudaGraphManager for draft prefill and decode.
    Builds fresh dummy inputs and attention metadata for every warmup and
    capture pass so that buffer contents match the batch descriptor
    being captured, avoiding stale metadata reuse.
    """
​
    def capture(
        self,
        forward_fn: Callable,
        model_state: ModelState,
        input_buffers: InputBuffers,
        block_tables: BlockTables,
        attn_groups: list[list[AttentionGroup]],
        kv_cache_config: KVCacheConfig,
        progress_bar_desc: str = "Capturing CUDA graphs",
    ) -> None:
        def create_forward_fn(
            desc: BatchExecutionDescriptor,
            warmup: bool,
        ) -> Callable[[CUDAGraphMode], None]:
            num_tokens = desc.num_tokens
            num_reqs = desc.num_reqs or min(num_tokens, self.max_num_reqs)
            num_tokens_across_dp = (
                torch.full((self.dp_size,), num_tokens, dtype=torch.int32, device="cpu")
                if self.dp_size > 1
                else None
            )
            # 通过目标模型的构建器构建新鲜的注意力元数据
            # 确保捕获的图烘焙运行时刷新的持久缓冲区地址
            attn_metadata, slot_mappings = prepare_inputs_to_capture(
                num_reqs,
                num_tokens,
                model_state,
                input_buffers,
                block_tables,
                attn_groups,
                kv_cache_config,
                skip_attn=(desc.cg_mode == CUDAGraphMode.PIECEWISE),
            )
​
            return lambda cg_mode: forward_fn(
                num_reqs,
                num_tokens,
                attn_metadata,
                slot_mappings,
                num_tokens_across_dp,
                cg_mode,
            )
​
        super().capture(create_forward_fn, progress_bar_desc)
vllm/v1/worker/gpu/cudagraph_utils.py core-logic

移除了 AttentionStatePair 类,简化了 CreateForwardFn 协议,不再返回 AttentionState。

# 移除后的 CreateForwardFn 协议——不再需要返回 AttentionState
class CreateForwardFn(Protocol):
    """Factory that prepares inputs (OUTSIDE the graph) and returns a
    forward_fn. Called with warmup=True for the warmup pass and warmup=False
    for the captured pass."""
​
    def __call__(
        self,
        desc: BatchExecutionDescriptor,
        warmup: bool,
    ) -> Callable[[CUDAGraphMode], None]: ...# CudaGraphManager.capture 方法也不再返回 AttentionStatePair
@torch.inference_mode()
def capture(
    self,
    create_forward_fn: CreateForwardFn,
    progress_bar_desc: str = "Capturing CUDA graphs",
) -> None:
    # 不再维护 attn_states 字典
    ...

评论区精华

Speculator prefill attention metadata freshness in FULL cudagraph 正确性

@TheEpicDolphin 担心投机解码预填充使用独立注意力元数据后,需要在每步解码前重建元数据以避免陈旧数据。@njhill 解释捕获时通过目标模型输入缓冲区和注意力组构建元数据,确保捕获的图与运行时刷新的持久缓冲区地址对应,无需运行时重建。@TheEpicDolphin 理解并认可。

结论:问题澄清,修复方案被接受,无需额外运行时重建。 · 已解决

风险与影响

  • 捕获路径变更:合并后的 SpeculatorCudaGraphManager 同时用于预填充和解码,可能引入解码捕获的回归。但 PR 验证了在 H200 MIG 和全 H200 上通过 eagle correctness 测试。
  • 共享缓冲区依赖:构建元数据时依赖目标模型的缓冲区和注意力组,如果这些组件在运行时发生变化,可能导致捕获的图不一致。但捕获是一次性的,运行时准备阶段会刷新缓冲区。
  • 性能影响:捕获阶段每次预热都构建新元数据,这会增加捕获时间,但捕获完成后无运行时开销。
  • 代码兼容性:移除了 AttentionStatePair 和相关接口,可能影响外部自定义 speculator 实现(如果有)。但该 API 是内部使用,且所有调用点已更新。

对用户:无 API 变更,修复了在特定 GPU 配置(如 H200 MIG)上投机解码时的崩溃,提升稳定性。
对系统:V2 模型运行器的投机解码预填充 CUDA Graph 捕获现在更可靠,减少 CI 失败。
对团队:代码结构更清晰,预填充和解码段使用同一管理器,移除了冗余的 AttentionStatePair 机制,降低了维护成本。

核心路径变更 共享缓冲区依赖 CUDA Graph 兼容性

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论