Prhub

#56452 [Bugfix] Fix DeepGEMM FP8 warmup coverage

原始 PR 作者 ayush1399 合并时间 2026-09-13 02:20 文件变更 4 提交数 2 评论 5 代码增减 +140 / -99

执行摘要

修复 DeepGEMM FP8 warmup 漏覆盖,消除请求时 10-20 秒停顿

PR body 明确指出:DeepGEMM warmup 漏掉了 compressed-tensors 和 ModelOpt block-FP8 层(包括 block-FP8 ParallelLMHead 层),也排除了输出维度 N 可被 64 整除但不可被 128 整除的合法权重形状;漏掉的层导致请求期间内核 JIT 编译,在 Gemma 运行中产生 10-20 秒停顿。PR 中给出了实测对比:main 分支 0 层预热、51 个 JIT 缓存条目,而本 PR 预热 240/240 层、缓存 72 个条目;关闭 warmup 时 463 个 prompt 尺寸扫描发现三个请求时编译停顿(12.5s、9.7s、20.0s)。

值得精读。核心价值不在修复本身,而在“kernel 自注册 provider”这一设计模式——它把 warmup 从“猜测 kernel 行为”变成“kernel 声明自己的行为”,显著降低了两者耦合。关注点:一是 FP8BlockParams 的 scale 统一抽象是否可推广到其他量化 kernel;二是测试文件粒度争议的最终取舍,可以作为团队 CI 资源策略的参考案例;三是 marlin 与 DeepGEMM 组合的覆盖仍待后续补全。

讨论亮点

Review 中仅有一轮实质讨论:LucasWilkinson 对新增测试文件的粒度提出质疑,认为可能 over-specified、浪费 CI 资源,并征询 mgoin 的意见;ayush1399 回应理解 CI 资源顾虑,表示愿意裁剪测试或直接删除整个测试文件,等待 mgoin 表态。该线程没有在评论中明确收尾,但 PR 已被合并,测试文件最终保留。

实现拆解

实现按四个步骤展开:

  1. 确立 kernel 注册式 provider 契约(vllm/model_executor/kernels/linear/scaled_mm/deep_gemm.py):DeepGemmFp8BlockScaledMMKernel.process_weights_after_loading 在完成权重格式转换后,把 self 写入 layer.deep_gemm_warmup_provider,并新增 get_deep_gemm_warmup_weights(layer) 统一返回 (weight, block_scale)。关键约束是 is_bmm=True 的层不注册——因为 BMM 层运行时走 fp8_einsum,不会经过本 kernel,预热的 kernel 永远不会被用到。

  2. 统一 block scale 属性查找(vllm/model_executor/kernels/linear/scaled_mm/BlockScaledMMLinearKernel.py):在 FP8BlockParams 上新增 block_scale_attr 与 block_scale 两个属性,封装“Fp8LinearMethod 注册为 weight_scale_inv、compressed-tensors 注册为 weight_scale”的差异;process_weights_after_loading 与 apply_weights 都改为走 params.block_scale。这消除了此前散落在多个文件里的 if weight_scale_inv is None else ... 判断,并对 scale 缺失的情况加断言,把静默错误变成显式失败。

  3. warmup 消费端改为读取注册标记(vllm/model_executor/warmup/deep_gemm_warmup.py):删除 _extract_data_from_linear_base_module、_is_deep_gemm_backed_kernel、_fp8_linear_may_use_deep_gemm 三个基于“量化方法 + 属性探测”推断 kernel 的函数,替换为 _deep_gemm_linear_data——只检查 layer 上是否存在 deep_gemm_warmup_provider 且 weight_block_size == get_mk_alignment_for_contiguous_layout(),再回调 provider 取数。deepgemm_fp8_gemm_nt_warmup 与 _count_warmup_iterations 同步改为遍历注册标记。原实现要求 w.shape[0] % 128 == 0 and w.shape[1] % 128 == 0,新实现不再自己算对齐,交给 kernel 的运行时选择逻辑,天然支持 N=576 这类仅 64 对齐的形状。

  4. 测试配套(新增 tests/model_executor/test_deep_gemm_warmup.py):用 SimpleNamespace 构造桩 layer,覆盖两种 scale 属性名(weight_scale / weight_scale_inv)、N=576(64 对齐但非 128 对齐)、未注册/block size 不匹配的跳过逻辑,以及 kernel 注册时对 BMM 层的条件判断。warmup 测试在 CPU 与 H100 上各 6 个全部通过;compressed-tensors 的 [marlin] 用例因验证环境工具链不匹配(驱动支持 CUDA 13.0,扩展用 13.3 编译)报 cudaErrorUnsupportedPtxVersion,属环境问题而非代码缺陷。

文件 模块 状态 重要度
vllm/model_executor/warmup/deep_gemm_warmup.py 预热模块 modified 8.5
vllm/model_executor/kernels/linear/scaled_mm/deep_gemm.py 线性内核 modified 7.1
vllm/model_executor/kernels/linear/scaled_mm/BlockScaledMMLinearKernel.py 线性内核 modified 7.18
tests/model_executor/test_deep_gemm_warmup.py 预热模块 added 7.33

关键符号

_deep_gemm_linear_data deepgemm_fp8_gemm_nt_warmup _count_warmup_iterations process_weights_after_loading get_deep_gemm_warmup_weights block_scale_attr block_scale

关键源码片段

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

warmup 主路径从属性探测改为消费 kernel 注册的 provider,删除 3 个推断函数并新增 _deep_gemm_linear_data,是本次修复的核心消费端。

# vllm/model_executor/warmup/deep_gemm_warmup.py
​
def _deep_gemm_linear_data(module: torch.nn.Module):
    """从 layer 上读取 warmup 数据,前提是 kernel 已在权重加载时注册 provider。
​
    相比旧实现(从 quant_method 属性反推 kernel 类型),这里直接信任
    ``process_weights_after_loading`` 盖的戳,因此能覆盖 compressed-tensors、
    ModelOpt、ParallelLMHead 等所有经过 kernel 处理的层,不再关心量化方法细节。
    """
    provider = getattr(module, "deep_gemm_warmup_provider", None)
    if not isinstance(provider, DeepGemmFp8BlockScaledMMKernel):
        return None
    # block size 不匹配时,即便注册了也不走 DeepGEMM 主路径,跳过预热
    if module.weight_block_size != get_mk_alignment_for_contiguous_layout():
        return None
    return provider.get_deep_gemm_warmup_weights(module)
​
​
def deepgemm_fp8_gemm_nt_warmup(model, max_tokens, pbar=None):
    """遍历模型所有层,只对注册过 provider 的层执行 JIT 预热。
​
    旧代码用列表推导先过滤再取数,这里改为单次遍历加 walrus 判空,
    逻辑更直接,且天然支持 N=576(仅 64 对齐)这类此前被误排除的形状。
    """
    for m in model.modules():
        if (data := _deep_gemm_linear_data(m)) is not None:
            w, ws = data
            _deepgemm_fp8_gemm_nt_warmup(w=w, ws=ws, max_tokens=max_tokens, pbar=pbar)
vllm/model_executor/kernels/linear/scaled_mm/deep_gemm.py data-contract

DeepGEMM kernel 在 process_weights_after_loading 中注册自身为 warmup provider,并新增 get_deep_gemm_warmup_weights 统一供数接口,是本次修复的注册端。

# vllm/model_executor/kernels/linear/scaled_mm/deep_gemm.py
​
def process_weights_after_loading(self, layer):
    """权重加载后处理:转换权重布局,并把自身注册为 warmup provider。
​
    BMM 层运行时会绕过本 kernel(走 fp8_einsum),因此不注册,
    避免为不会用到的路径做无谓的 JIT 预热。
    """
    super().process_weights_after_loading(layer)
    params = self._get_layer_params(layer)
    assert layer.weight_block_size is not None
​
    is_bmm = getattr(layer, "is_bmm", False)
​
    if self.is_deep_gemm_supported:
        dg_weight, dg_weight_scale = deepgemm_post_process_fp8_weight_block(
            wq=params.weight,
            ws=params.block_scale, # 经 FP8BlockParams 统一解析 scale 属性
            quant_block_shape=tuple(layer.weight_block_size),
            use_e8m0=self.use_deep_gemm_e8m0,
            is_bmm=is_bmm,
            bmm_batch_size=getattr(layer, "bmm_batch_size", 0),
        )
        replace_parameter(layer, params.WEIGHT, dg_weight)
        replace_parameter(layer, params.block_scale_attr, dg_weight_scale)
​
    # bmm 层绕过本 kernel,跳过注册;其余层在权重加载阶段盖戳,
    # 后续 warmup 无需再推断 kernel 选择逻辑。
    if not is_bmm:
        layer.deep_gemm_warmup_provider = self
​
​
def get_deep_gemm_warmup_weights(self, layer):
    """供 warmup 阶段读取权重与 scale,数据访问契约统一由 kernel 侧提供。"""
    params = self._get_layer_params(layer)
    return params.weight, params.block_scale

评论区精华

新增测试文件是否过度细化、浪费 CI 资源 测试

LucasWilkinson 在 tests/model_executor/test_deep_gemm_warmup.py 第 1 行评论:"I think this might be a bit overly specified, i think we should remove it to avoid wasting CI resources, thoughts @mgoin?"。ayush1399 回应:"yeah I see the concern on ci resources. happy to push a commit that trims it or remove the entire test file but will wait for @mgoin to chime in"。

结论:作者愿意裁剪或删除测试文件,等待 mgoin 表态;线程未在评论中明确收尾,PR 已带着测试文件合并。 · unresolved

风险与影响

  1. 依赖 kernel 注册时序:新机制要求每个 DeepGEMM 支持层都必须经过 process_weights_after_loading。若未来出现绕过该流程的权重加载路径(如自定义加载器、子类覆写),provider 不会注册,warmup 会静默跳过——表现等同于修复前的旧行为,属于可接受的降级而非错误。
  2. block_scale 断言行为变更:FP8BlockParams.block_scale 对缺失 scale 直接 assert,此前的代码在 scale 为 None 时会以更隐晦的方式继续执行;对于某个同时把 weight_scale 与 weight_scale_inv 都设为 None 的量化配置,现在会在加载阶段显式失败,这是有意收紧但需要知晓的兼容性变化。
  3. marlin 路径未完全验证:旧 _fp8_linear_may_use_deep_gemm 中有 not use_marlin 的检查;新逻辑完全由 kernel 注册决定。若 marlin 与 DeepGEMM 共享同一 kernel 类的注册路径,warmup 行为可能变化。PR 验证中 [marlin] 压缩张量测试因工具链不匹配失败,该组合的覆盖仍不完整。
  4. warmup 启动成本:240 层预热耗时 84 秒,属于一次性启动开销,但多模型、多尺寸下会线性放大,新增 kernel 注册后该成本会继续上升。

对用户:修复了 block-FP8 量化模型(如 Gemma-4-31B-it-FP8-block)首请求 10-20 秒 JIT 停顿,这是直接可见的延迟改善;同时覆盖了 ParallelLMHead 等此前漏掉的层,减少了线上运行时编译风险。对系统:warmup 数据访问从“warmup 层反向探测 quant_method 属性”改为“kernel 主动注册 provider”,解耦了 warmup 与 kernel 选择逻辑,后续新增 DeepGEMM 支持的 kernel 只需在 process_weights_after_loading 中盖戳即可被自动预热。对团队:FP8BlockParams.block_scale 成为 block-FP8 系列 kernel 的统一 scale 入口,多个 kernel 文件中的重复分支被收敛。

warmup 依赖 kernel 注册时序 block_scale 断言行为变更 marlin 路径覆盖未验证 CI 测试粒度争议未收尾

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论