Prhub

#50176 [4/N][warmup][DSv4] Migrate common attention kernels

原始 PR 作者 LopezCastroRoberto 合并时间 2026-09-08 22:25 文件变更 19 提交数 4 评论 48 代码增减 +2978 / -1272

执行摘要

DSv4 公共注意力内核迁入共享 warmup 契约,避免运行时 JIT

追踪 issue #49349(Zero JIT compilation during runtime),目标是让模型在启用 --jit-monitor-mode error 时也能无缝运行,从而在 CI 中可靠检测任何未被跟踪的运行时编译内核。PR body 明确本 PR 是 DSv4 去 JIT 化系列的一部分:迁移 fused Q/K 归一化、MTP 输入归一化、缓存压缩/反量化、partial-state、索引器和稀疏 attention 内核,并定义 kernel-owned 编译键与 AST 追踪的 warmup 空间,杜绝首次请求时的 Triton 编译。

值得精读。这是 vLLM 运行时零 JIT 编译计划中承上启下的核心 PR:它展示了如何用紧凑的类封装把 Triton 的隐式 JIT 缓存键显式化,并通过 _trace_dispatch 的 AST 追踪自动展开 warmup 空间。重点关注三处设计:一是 CompileKey 字段的选取原则(只纳入影响编译的 constexpr/指针类型/strides),二是 warmup_inputs 必须与运行时 launch 形状严格一致(dtype、stride、do_not_specialize),三是 kernel_launcher 声明式 launch 与输出分配的解耦。这些模式对任何需要消除运行时编译的 Triton 内核开发都有直接借鉴价值。

讨论亮点

本轮 review 的质量很高,机械检查和人工审查互补,暴露了 warmup 键与运行时编译键对齐的多个隐蔽问题。

  • 启动崩溃(LucasWilkinson):FusedMTPInputRMSNormKernel.__call__ 内的 assert、is_contiguous、torch.empty_like 会直接运行在 TritonWarmupTensor 上,导致 AttributeError: 'TritonWarmupTensor' object has no attribute 'ndim',只要 DSv4 MTP 开启 warmup 就启动崩溃。作者通过 kernel_launcher 支持输出描述符、将断言与分配移出被 trace 的路径解决。
  • dtype 不匹配导致 warmup 无效:Lucas 指出 MTP、QKVRMSNorm、fused_indexer_q 的 warmup 输入用 fp32 权重,而运行时 RMSNorm 权重与 weights_proj 输出都是模型 dtype bf16;Triton 缓存键包含指针元素类型,导致 warm 变体不可达,首次请求仍然 JIT。作者逐一改为 model_config.dtype。
  • cache_utils 覆盖错位(Lucas):max_blocks_per_seq、block_table_stride 应从原始 raw block size(256)推导,而非压缩后的 64/2;has_gather_lens 配对相反;C4A/C128A 的动态 topk 宽度没有任何 warmup 键,DeepSeek-V4-Pro(index_topk=1024)完全没被覆盖。作者全部修复并扩展为从 hf_config 推导。
  • save_partial_states 只覆盖主压缩器:基于全局 head_dim=512 推导键,漏掉了每个 cr=4 层的 indexer 压缩器(head_dim=128)。作者改为在 register_warmup() 时携带实例自己的 head_dim 与 compress_ratio。
  • 测试策略(Lucas):不建议写专用 warmup 单元测试,应直接对已清理的模型设置 --jit-monitor-mode error 作为回归闸门;作者相应移除了专用单元测试。
  • profile 命名(zyongye):建议 Triton 内核保留原始内核名而不是统一叫 kernel,否则 profiling trace 中无法区分内核。该建议在合并时尚未落实到所有内核,属于可跟进事项。

实现拆解

按“基础设施 → 内核迁移 → 模型层注册 → 测试验证”四步完成,核心是把每个 Triton 内核包装成拥有自描述编译键的 VllmTritonJitKernel 子类。

  1. 扩展 warmup 基础设施。vllm/model_executor/warmup/jit_warmup.py 的 _trace_dispatch 新增 predicate-only 输入过滤:当 _when 谓词引用了既不在 dispatch 输入、也不在 CompileKey 字段中的名称时(如 max_tokens),该名称只参与展开与谓词匹配,不再被传入 compile_key(),避免 CompileKey 收到多余字段。jit_warmup_triton_helper.py 扩展 LaunchSpec 为 (grid, launch_kwargs) 或 (grid, launch_kwargs, outputs) 三元素形式,grid=None 表示空批次跳过 launch 但仍返回声明的输出;kernel_launcher 据此在包装 __call__ 时返回 outputs。launch() 同时支持 _warming_compile_key 下的 launch_pdl 透传和自定义 kernel 选择,为 FP8/MXFP4 双变体内核铺路。
  2. 逐个迁移内核。每个内核类都遵循相同模式:@dataclass(frozen=True) CompileKey 声明影响 Triton 编译的维度;dispatch(**runtime_fields) 把运行时参数规约成 CompileKey(含 do_not_specialize 与 constexpr 推导);get_warmup_keys 用 _trace_dispatch 以 AST 追踪展开 warmup 空间,通常依据 hf_config、cache_config、平台能力生成;warmup_inputs 用 TritonWarmupTensor 构造与运行时等价的张量形状/strides;__call__ 以 @kernel_launcher 声明式返回 grid 与 launch kwargs。涉及文件包括 sparse_mla.py(BuildC128ATopkMetadataKernel)、common/ops/cache_utils.py(反量化、全局 topk、FlashInfer 混合稀疏索引等多个内核)、fused_indexer_q.py、fused_mtp_input_rmsnorm.py(含新增 MTPSharedHeadRMSNormKernel)、fused_inv_rope_fp8_quant.py、fused_compress_quant_cache.py、common/ops/fused_qk_rmsnorm.py、save_partial_states.py。fused_qk_rmsnorm 还顺带将输出张量改为显式 packed 分配,避免单 token 时保留父张量行 stride 导致下游弃用 cuBLAS。
  3. 模型层注册 warmup。vllm/models/deepseek_v4/attention.py 的注意力层 __init__ 按后端平台和 has_cutedsl() 条件注册 _FUSED_Q_KV_RMSNORM_KERNEL、反逆 RoPE 量化、C128A 元数据、topk 索引与反量化内核;compressor.py 注册 _SAVE_PARTIAL_STATES_KERNEL 与 _FUSED_KV_COMPRESS_NORM_ROPE_INSERT_INDEXER_TRITON_KERNEL;AMD/NVIDIA/XPU 的 mtp.py 统一改用 _FUSED_MTP_INPUT_RMSNORM_KERNEL 并注册相关 RMSNorm 内核。注册均受 vllm_config.kernel_config.enable_jit_warmup 门控,平台分支(CUDA/ROCm/XPU、FNUZ、cutedsl)在注册处显式区分。
  4. 测试与验证配套。tests/model_executor/test_jit_warmup.py 新增 test_forwarded_compile_key_fields_exclude_predicate_only_inputs,验证 _when 谓词专用输入不会流入 CompileKey;tests/kernels/core/test_fused_q_kv_rmsnorm.py 改用公开包装 fused_q_kv_rmsnorm 调用。根据合并者 LucasWilkinson 的意见,删除了专用 warmup 单元测试,改用 --jit-monitor-mode error 在模型级验证覆盖。
文件 模块 状态 重要度
vllm/models/deepseek_v4/sparse_mla.py 稀疏 MLA modified 9.03
vllm/models/deepseek_v4/common/ops/cache_utils.py 缓存内核 modified 7.72
vllm/models/deepseek_v4/common/ops/fused_mtp_input_rmsnorm.py MTP 归一化 modified 7.58
vllm/models/common/ops/fused_qk_rmsnorm.py QKV 归一化 modified 7.43
vllm/models/deepseek_v4/attention.py 注意力层 modified 7.16
vllm/model_executor/warmup/jit_warmup_triton_helper.py 暖启框架 modified 7.09
tests/model_executor/test_jit_warmup.py 暖启测试 modified 6.36

关键符号

BuildC128ATopkMetadataKernel.dispatch BuildC128ATopkMetadataKernel.get_warmup_keys BuildC128ATopkMetadataKernel.warmup_inputs BuildC128ATopkMetadataKernel.__call__ DequantizeAndGatherKCacheKernel.dispatch DequantizeAndGatherKCacheKernel.get_warmup_keys FusedIndexerQRopeQuantTritonKernel.dispatch FusedIndexerQRopeQuantTritonKernel.get_warmup_keys FusedMTPInputRMSNormKernel.dispatch FusedMTPInputRMSNormKernel.get_warmup_keys FusedMTPInputRMSNormKernel.__call__ MTPSharedHeadRMSNormKernel.dispatch FusedInvRopeFP8QuantKernel.dispatch FusedInvRopeFP8QuantKernel.get_warmup_keys FusedKVCompressNormRopeInsertIndexerTritonKernel.dispatch FusedKVCompressNormRopeInsertIndexerTritonKernel.get_warmup_keys FusedQKVRMSNormKernel.dispatch FusedQKVRMSNormKernel.get_warmup_keys FusedQKVRMSNormKernel.__call__ SavePartialStatesKernel.get_warmup_keys VllmTritonJitKernel.compile VllmTritonJitKernel.launch kernel_launcher

关键源码片段

vllm/model_executor/warmup/jit_warmup_triton_helper.py data-contract

warmup 基础设施的关键扩展:LaunchSpec 升级为可携带 outputs 的三元组并支持 grid=None,kernel_launcher 返回声明输出,launch 支持自定义 kernel 与 launch_pdl 透传;MTP 内核的启动崩溃修复依赖此契约升级。

# vllm/model_executor/warmup/jit_warmup_triton_helper.py 的关键契约升级
​
# LaunchSpec 现在可以是 (grid, launch_kwargs) 或 (grid, launch_kwargs, outputs):
# - 自分配输出的内核返回三元组,装饰器会把 outputs 回传给调用方;
# - grid=None 表示空批次跳过 launch(例如 num_tokens == 0),但仍返回 outputs。
LaunchSpec = (
    tuple[tuple[int, ...] | None, dict[str, Any]]
    | tuple[tuple[int, ...] | None, dict[str, Any], Any]
)
​
​
def kernel_launcher(call_fn: Callable[..., LaunchSpec]) -> Callable[..., Any]:
    """把 __call__ 的声明式返回值转成真正的 Triton launch。"""
    signature = inspect.signature(call_fn)
​
    @wraps(call_fn)
    def wrapper(self: VllmTritonJitKernel[Any], *args: Any, **kwargs: Any) -> Any:
        spec = call_fn(self, *args, **kwargs)
        if len(spec) == 3:
            grid, launch_kwargs, outputs = spec
        else:
            grid, launch_kwargs = spec
            outputs = None
        # 把绑定后的参数表(含默认值)作为 inputs 传给 launch,
        # launch 内部会按 kernel 的 arg_names 做 *_ptr 后缀映射。
        bound = signature.bind(self, *args, **kwargs)
        bound.apply_defaults()
        inputs = {
            name: value for name, value in bound.arguments.items() if name != "self"
        }
        self.launch(grid, inputs, **launch_kwargs)
        return outputs
​
    return wrapper
​
​
class VllmTritonJitKernel(VllmJitKernel[CompileKeyT], Generic[CompileKeyT]):
    def compile(self, compile_key: CompileKeyT) -> None:
        # warmup 时把 compile_key 记录到 _warming_compile_key,
        # 供 launch() 在 warmup 分支里把 key 中的 launch_pdl 等字段注入内核。
        inputs = self.warmup_inputs(compile_key)
        self._warming = True
        self._warming_compile_key = compile_key
        try:
            cast(Callable[..., None], self)(**inputs)
        finally:
            self._warming = False
            self._warming_compile_key = None
​
    def launch(self, grid, inputs, /, *, kernel=None, **kwargs):
        kernel = kernel if kernel is not None else self.kernel
        runtime_launcher = kwargs.pop("_runtime_launcher", None)
        runtime_launcher_arg_count = kwargs.pop("_runtime_launcher_arg_count", 0)
        # 把 inputs 中与内核形参匹配的名字映射到 kernel 参数名;
        # 形如 x 的 Tensor 会按需要映射为 x_ptr。
        for name, value in inputs.items():
            target = name if name in self._kernel_arg_names else f"{name}_ptr"
            if target in self._kernel_arg_names and target not in kwargs:
                kwargs[target] = value
        if self._warming:
            # warmup 期:使用 Triton 的 warmup()(compile-only,不真正 launch),
            # 并把 CompileKey 中的 launch_pdl 字段透传,确保编译键一致。
            if (
                self._warming_compile_key is not None
                and "launch_pdl" in kwargs
                and hasattr(self._warming_compile_key, "launch_pdl")
            ):
                kwargs["launch_pdl"] = self._warming_compile_key.launch_pdl
            warmup = getattr(kernel, "warmup", None)
            assert warmup is not None
            return warmup(grid=(1,), **kwargs)
        if grid is None:
            return None
        if runtime_launcher is not None:
            regular_args = [
                kwargs.pop(name)
                for name in self._kernel_arg_names[:runtime_launcher_arg_count]
            ]
            return runtime_launcher(kernel, grid, *regular_args, **kwargs)
        return kernel[grid](**kwargs)

评论区精华

MTP RMSNorm 在 warmup 期启动崩溃 正确性

LucasWilkinson 指出 `FusedMTPInputRMSNormKernel.__call__` 内的 assert、`is_contiguous` 与 `torch.empty_like` 会作用在 `TritonWarmupTensor` 上,报 `AttributeError: 'TritonWarmupTensor' object has no attribute 'ndim'`,导致启用 warmup 时引擎直接崩溃;且 `JitWarmupRegistry.warmup()` 无异常处理。

结论:作者通过升级 `kernel_launcher` 支持显式 `_outputs` 输出描述符,把断言与分配移出被 trace 的 `__call__` 路径,并处理空批次(grid=None)返回输出。 · 已解决

warmup 权重 dtype 与运行时不一致 正确性

LucasWilkinson 验证 MTP、QKVRMSNorm、fused_indexer_q 的 warmup 输入把 RMSNorm 权重和 index_weights 做成 fp32,而运行时是 bf16;Triton 编译键按指针元素类型区分,导致 warm 变体不可达,首次请求仍 JIT,warmup 白做。

结论:作者逐一将 warmup 输入改为 `model_config.dtype`(运行时模型 dtype),保证 warmup 与真实 launch 的指针类型一致。 · 已解决

cache_utils 的 block_table 宽度与 has_gather_lens 配对错误 正确性

LucasWilkinson 指出 `max_blocks_per_seq`/`block_table_stride` 用压缩后的 block_size(64/2)推导,而运行时 block_table 是 raw block size 256 宽的表;同时 `has_gather_lens` 配对反了,压缩路径(gather_lens=None)组合没有被 warm。

结论:作者改为从 raw KV-cache block size 256 推导表宽,压缩路径与 SWA 路径分别使用 `has_gather_lens=False/True`。 · 已解决

C4A/C128A 动态 topk 宽度的 warmup 覆盖缺口 性能

LucasWilkinson 发现组合列表只覆盖 Flash 的 `(4, 512, True, False)`,DeepSeek-V4-Pro 的 index_topk=1024 未覆盖;纯 decode 批次的 top_k=0 是独立编译变体;C128A 使用动态 `active_topk_width`,而只 warm 了一个宽度。

结论:作者改为从 `hf_config.index_topk` 推导 C4A 键,并覆盖 prefill/decode 与动态 C128 宽度(powers of two 封顶)。 · 已解决

save_partial_states 未覆盖 indexer 压缩器实例 正确性

LucasWilkinson 指出 `SavePartialStatesKernel.get_warmup_keys` 从 `hf_config.head_dim`(512)推导,但 indexer 压缩器(head_dim=128)也会创建同名内核实例,其 HEAD_SIZE/STATE_WIDTH 组合从未被 warm。

结论:作者让每个压缩器实例在 `register_warmup()` 时携带自己的 `head_dim` 与 `compress_ratio`,保证两种实例都被覆盖。 · 已解决

warmup 单元测试的去留 测试

LucasWilkinson 建议不要写特定 warmup 单元测试,而是对已清理的模型直接开启 `jit_monitor_mode=error` 作为回归闸门,更接近真实运行路径。

结论:作者删除了专用单元测试(commit 'remove unit test'),后续验证交给 `--jit-monitor-mode error`。 · 已解决

Triton 内核在 profiling trace 中统一显示为 kernel style

zyongye 提出:Triton kernel 应保留原始内核名而非包装类里的 `kernel` 方法名,否则 profiling trace 只显示 `kernel`,无法区分具体内核;cutedsl 路径没问题。

结论:该改进作为 nit 提出,合并前未看到进一步修改,属于可跟进事项。 · unresolved

gfx942 FNUZ 变体未 warm 性能

CodeRabbit 发现 `DequantizeAndGatherKCacheKernel` 的 warmup 只生成 `use_fnuz=False`,而 SWA 路径在 gfx942 上会传 `use_fnuz=True`(constexpr 独立编译键),首次 forward 仍会编译。

结论:review 时提出建议 `use_fnuz=(False, current_platform.is_fp8_fnuz())`,作者未明确回复,合并时此点仍可能遗留。 · unresolved

风险与影响

  1. 启动期回归:迁移后的 __call__ 统一走 kernel_launcher,任何参数名映射错误都会在冷启动时报错;本 PR 中 MTP RMSNorm 的启动崩溃即为例证,说明该路径仍需更细的 CI 覆盖。
  2. warmup 键覆盖不全会退化为首次请求 JIT:cache_utils.py 的 use_fnuz 变体(gfx942 SWA 路径)在 review 时仍未被 warm,C128A 动态宽度虽已修复但仍依赖 active_topk_width 的枚举完整性;attention.py 的注册条件穿插 current_platform.is_cuda()、has_cutedsl()、backend_name 多重分支,遗漏任一组合都会造成编译 stall。
  3. 平台兼容性:fused_indexer_q.py 在 XPU/无 cutedsl 条件下才注册,ROCm 走 FNUZ 路径;若平台判断与运行时 kernel 选择不一致,warmup 键不可达,属于白编译 + 运行时 JIT 双损失。
  4. 性能影响面:fused_qk_rmsnorm.py 改变了输出分配方式与 num_warps 选择(block_size ≥ 2048 时 8 warps),数值上保持 fp32 全精度,但改动被 3 个平台共享,需关注吞吐回归。
  5. 测试缺口:专用 warmup 单元测试被删除后,回归主要依赖 --jit-monitor-mode error,该模式尚未在本 PR 合入时为 DSv4 开启 CI 强制检查,覆盖风险被后置。

对 DeepSeek-V4 全系(含 V4.1、与 Kimi-K3 共享的公共 ops)的用户而言,开启 enable_jit_warmup 后首次请求不再出现 Triton 编译停顿,CUDA graph 捕获和弹性调度更稳定;对 vLLM 内核开发团队而言,本 PR 确立了 Triton 内核迁移的标准样板(CompileKey/dispatch/warmup_inputs/_trace_dispatch),后续 Gemma4、GLM-5.2/5.3、gpt-oss 的去 JIT 化可直接复用同一模式。影响程度高:19 个文件、跨 8 个内核模块与三平台 MTP 路径,且基础设施契约(LaunchSpec)被后续内核迁移依赖,属于 vLLM 'Zero JIT compilation during runtime' 里程碑的关键基石。

核心路径变更 跨平台分支多 warmup 键覆盖风险 启动期曾现崩溃 测试覆盖依赖后续

关联 Issue

#49349 Zero JIT compilation during runtime

完整报告

参与讨论