Prhub

#53540 [ROCm][Perf] Fuse SWA q/kv RMSNorm and q FP8 group quant for DeepSeek-V4

原始 PR 作者 shen-shanshan 合并时间 2026-08-28 00:37 文件变更 4 提交数 2 评论 10 代码增减 +333 / -12

执行摘要

融合 DSV4 q/kv RMSNorm 与 FP8 量化,ROCm 热路径 2.9 倍提速

PR body 指出每个 decode step 的 SWA token 插入管线在 ROCm 上要执行三步:Triton fused_q_kv_rmsnorm 归一化、aiter per-1x128 动态量化 bf16 q-lora(喂给 block-scaled FP8 wq_b GEMM)、以及 indexer 的 wq_b 对共享 q-lora 的第二次相同量化('the same quant a second time in the indexer's wq_b')。fusion 后量化直接从 fp32 累加器读取,'single rounding, equivalent-or-better precision'。实测热路径从 3 次 launch(14µs 277ns)降为 1 次(4µs 919ns,2.90x)。

值得精读。核心看两点:一是如何用『类型门控 + cached_property』在共享代码路径上做平台特化且保证 fallback;二是 pre-quantized activation 直接驱动 block-scaled GEMM 的数据契约设计,避免 apply_weights 二次量化造成的双重舍入。配套测试中的 ULP 对比与 strided view 用例也很有参考价值。

讨论亮点

Review 中唯一的技术交锋集中在 vllm/models/deepseek_v4/attention.py 的 _wq_b_proj:tjtanaa 指出 'This function seems to be duplicated.'。作者解释有两个 _wq_b_proj(SWA 的 wq_b 与 indexer 的 wq_b),融合内核启用后两侧 linear 都必须跳过输入量化,因此重复是刻意设计。tjtanaa 还要求性能数据,作者补充了 kernel profile 前后对比与端到端 A/B benchmark,并获 AndreasKaratzas 批准合并。

实现拆解

本 PR 通过 4 个步骤完成融合,核心思路是把「归一化 + 量化」下沉到单个 AITER 内核,并把量化结果作为数据契约在 attention 与 indexer 之间共享。

  1. 注册融合 custom op:在 vllm/_aiter_ops.py 中新增 _fused_mla_dual_rms_norm_group_quant_impl 与配套的 _fake 实现,在 register_ops_once() 中注册名为 fused_mla_dual_rms_norm_group_quant 的 op,并新增静态访问器 fused_qk_rmsnorm_group_quant。real 实现调用 aiter.ops.fused_qk_rmsnorm_group_quant,一次 pass 完成 q 的 RMSNorm + FP8 group quant(输出 (M, N/128) 的 fp32 scales)与 kv 的 RMSNorm;fake 实现仅分配相同形状的占位张量,保证元设备/编译路径可用。

  2. 重构共享注意力路径:在 vllm/models/deepseek_v4/attention.py 中把 forward 里原先内联的 split + fused_q_kv_rmsnorm 抽成 _split_qkv_and_norm(共享路径返回 qr_scale=None),并把 qr_scale 作为新参数贯穿 _prepare_and_attn_eager / _prepare_and_attn,同时传给 indexer 调用;wq_b 投影改为 _wq_b_proj,当 qr_scale 为 None 时等价于原 self.wq_b(qr),从而保证 CUDA/XPU 路径行为完全不变。

  3. ROCm 特化与条件门控:在 vllm/models/deepseek_v4/amd/rocm.py 中覆写 _split_qkv_and_norm,并用 functools.cached_property 实现 _wq_b_uses_aiter_block_scaled——仅当 aiter 的 fp8 linear 开关开启、且 attention 的 wq_b 与 indexer 的 wq_b 都是 Fp8BlockScaledMMLinearKernel 时才启用融合路径,否则回退共享实现。启用时返回预量化的 fp8 qr 与 qr_scale,_wq_b_proj 侧通过新增的 apply_pre_quantized_block_scaled_mm 直连 linear 的 block-scaled GEMM,绕过 apply_weights 的二次量化;该函数仅对 column/replicated 类 linear 有效(输出即本地 TP 分片,无需 all-reduce)。

  4. 测试配套:在 tests/kernels/core/test_rocm_aiter_ops.py 中新增两个测试:test_rocm_aiter_mla_dual_rmsnorm_group_quant_vs_sequential 验证融合 op 与 rms_norm -> group_fp8_quant 序列在反量化域的误差预算(M=1 覆盖单 token decode,kv 侧约束 1 个 bf16 ULP);test_rocm_aiter_mla_dual_rmsnorm_group_quant_strided_split_view 验证 DeepSeek-V4 实际产生的 split-view(strided)输入与 contiguous 输入结果一致,并引入 fp8_ulp_distance 工具。

文件 模块 状态 重要度
vllm/models/deepseek_v4/amd/rocm.py ROCm 后端 modified 8.02
vllm/_aiter_ops.py 算子注册 modified 7.81
vllm/models/deepseek_v4/attention.py 注意力层 modified 7.43
tests/kernels/core/test_rocm_aiter_ops.py 内核测试 modified 6.42

关键符号

_fused_mla_dual_rms_norm_group_quant_impl _fused_mla_dual_rms_norm_group_quant_fake fused_qk_rmsnorm_group_quant _split_qkv_and_norm _wq_b_proj _wq_b_uses_aiter_block_scaled apply_pre_quantized_block_scaled_mm test_rocm_aiter_mla_dual_rmsnorm_group_quant_vs_sequential test_rocm_aiter_mla_dual_rmsnorm_group_quant_strided_split_view

关键源码片段

vllm/models/deepseek_v4/amd/rocm.py data-contract

ROCm 特化主路径:覆写 `_split_qkv_and_norm`,新增 `_wq_b_uses_aiter_block_scaled` 条件门控与 `apply_pre_quantized_block_scaled_mm` 预量化 GEMM 入口,是本次融合能否启用的决策点。

# vllm/models/deepseek_v4/amd/rocm.py
# 对预量化激活执行 block-scaled fp8 GEMM:融合内核写出 fp8 qr 与 per-1x128 scales,
# 本函数用它们直接驱动 linear 的 block-scaled GEMM,绕过 apply_weights 对 fp8 输入的重复量化。
# 仅适用于 wq_b 这类 column/replicated linear:其输出就是本地 TP 分片,无需 all-reduce。
def apply_pre_quantized_block_scaled_mm(
    linear: torch.nn.Module,
    x_fp8: torch.Tensor,
    x_scale: torch.Tensor,
) -> torch.Tensor:
    from vllm.model_executor.kernels.linear.scaled_mm.BlockScaledMMLinearKernel import (
        FP8BlockParams,
    )
​
    # 从 linear 的量化参数中取权重 block scale(兼容 weight_scale_inv 表示)。
    params = FP8BlockParams.from_layer(linear)
    weight_scale = (
        params.weight_scale
        if params.weight_scale_inv is None
        else params.weight_scale_inv
    )
    kernel = linear.quant_method.fp8_linear
    out = kernel.apply_block_scaled_mm(
        A=x_fp8, B=params.weight, As=x_scale, Bs=weight_scale
    )
    return out.to(dtype=kernel.config.out_dtype)
​
​
# 两个 wq_b GEMM 都走 aiter block-scaled fp8 内核时才启用融合路径。
# 模型构建后 linear kernel 与 aiter 开关即固定,故在首个 forward 时求值并缓存。
# 若 quant 或 GEMM 与 aiter 实现不一致,则回退共享路径,保证正确性优先。
@functools.cached_property
def _wq_b_uses_aiter_block_scaled(self) -> bool:
    from vllm._aiter_ops import rocm_aiter_ops
    from vllm.model_executor.kernels.linear.scaled_mm import (
        Fp8BlockScaledMMLinearKernel,
    )
​
    if not rocm_aiter_ops.is_linear_fp8_enabled():
        return False
​
    # attention 与 indexer 两侧的 wq_b 必须都是 block-scaled kernel,
    # 否则预量化 (qr_fp8, qr_scale) 契约不成立。
    linears = [self.wq_b]
    if self.indexer is not None:
        linears.append(self.indexer.wq_b)
    for linear in linears:
        kernel = getattr(getattr(linear, "quant_method", None), "fp8_linear", None)
        if not isinstance(kernel, Fp8BlockScaledMMLinearKernel):
            return False
    return True
vllm/_aiter_ops.py core-logic

新增 `fused_mla_dual_rms_norm_group_quant` custom op 的 real/fake 实现与注册,是融合内核进入 vLLM 算子体系的入口。

# vllm/_aiter_ops.py
# AITER 融合 MLA q/kv RMSNorm 与 q 的 FP8 分组量化。
# 支撑 DeepSeek-V4 ROCm attention 路径的 custom op fused_mla_dual_rms_norm_group_quant:
# q latent 直接从 fp32 归一化累加器量化(跳过 bf16 中间表示,单次舍入),
# kv latent 只做 RMSNorm 到 bf16,供 fused insert 内核使用。
def _fused_mla_dual_rms_norm_group_quant_impl(
    q: torch.Tensor,
    q_weight: torch.Tensor,
    kv: torch.Tensor,
    kv_weight: torch.Tensor,
    q_epsilon: float,
    kv_epsilon: float,
    group_size: int,
    transpose_scale: bool,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    from aiter.ops.fused_qk_rmsnorm_group_quant import (
        fused_qk_rmsnorm_group_quant,
    )
​
    mq, nq = q.shape
    q_out = torch.empty((mq, nq), dtype=FP8_DTYPE, device=q.device)
    q_scale = torch.empty((mq, nq // group_size), dtype=torch.float32, device=q.device)
    kv_normed = torch.empty(kv.shape, dtype=kv.dtype, device=kv.device)
​
    # split 视图可直接传入(单元内 stride),内核自行处理 strided 输入。
    fused_qk_rmsnorm_group_quant(
        q_out_quantized=q_out,
        q_out_scale=q_scale,
        q=q,
        q_weight=q_weight,
        q_epsilon=q_epsilon,
        k_out=kv_normed,
        k=kv,
        k_weight=kv_weight,
        k_epsilon=kv_epsilon,
        group_size=group_size,
        transpose_scale=transpose_scale,
    )
    return q_out, q_scale, kv_normed
​
​
# fake 实现:仅供元设备 / 编译追踪使用,形状与 dtype 与真实实现一致。
def _fused_mla_dual_rms_norm_group_quant_fake(
    q: torch.Tensor,
    q_weight: torch.Tensor,
    kv: torch.Tensor,
    kv_weight: torch.Tensor,
    q_epsilon: float,
    kv_epsilon: float,
    group_size: int,
    transpose_scale: bool,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    mq, nq = q.shape
    q_out = torch.empty((mq, nq), dtype=FP8_DTYPE, device=q.device)
    q_scale = torch.empty((mq, nq // group_size), dtype=torch.float32, device=q.device)
    kv_normed = torch.empty(kv.shape, dtype=kv.dtype, device=kv.device)
    return q_out, q_scale, kv_normed
vllm/models/deepseek_v4/attention.py data-contract

共享注意力路径的数据契约调整:抽出 `_split_qkv_and_norm` 与 `_wq_b_proj`,`qr_scale` 贯穿至 indexer,是所有平台共用的基础改动。

# vllm/models/deepseek_v4/attention.py
# 拆分 fused q-lora / kv 投影并对两半分别做 RMSNorm。
# qr_scale 在共享路径上恒为 None;ROCm 子类覆写本方法后,
# 返回预量化的 fp8 qr 及其 per-1x128 fp32 scales,供下游 wq_b 投影直接消费。
def _split_qkv_and_norm(
    self, qr_kv: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor]:
    qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
    qr, kv = fused_q_kv_rmsnorm(
        qr,
        kv,
        self.q_norm.weight.data,
        self.kv_norm.weight.data,
        self.eps,
    )
    return qr, None, kv
​
​
# 将 q-lora 输入投影到 query heads。
# qr_scale 仅由 ROCm 融合路径设置:此时 qr 已是 fp8 量化且带 per-1x128 scales,
# 必须绕过 linear 内部的再次量化(apply_weights 会把 fp8 输入再量化一次)。
def _wq_b_proj(
    self, qr: torch.Tensor, qr_scale: torch.Tensor | None = None
) -> torch.Tensor:
    if qr_scale is None:
        # 共享路径:维持原有行为,由 linear 自己完成量化。
        return self.wq_b(qr)
    # ROCm 融合路径:直连 block-scaled GEMM,跳过 apply_weights。
    return apply_pre_quantized_block_scaled_mm(self.wq_b, qr, qr_scale)

评论区精华

_wq_b_proj 方法重复问题 设计

tjtanaa 在 attention.py 的 diff 上指出 'This function seems to be duplicated.',质疑新加的 `_wq_b_proj` 与已有逻辑重复。

结论:作者回应:SWA 的 `wq_b` 与 indexer 的 `wq_b` 是两个独立 linear,融合内核启用后两侧都必须跳过输入量化,因此各自需要该分支,属于刻意设计。 · 已解决

性能数据补充 question

tjtanaa 要求附加性能数据('can you attach some perf figures ?'),作者随后补充了 kernel profile 前后对比与端到端 A/B benchmark,并附上 profile traces。

结论:数据补充后获 reviewer 认可,并由 AndreasKaratzas 批准合并。 · 已解决

风险与影响

主要风险集中在数值与兼容性上。第一,量化源从 bf16 中间值变为 fp32 累加器直接量化,属单次舍入,理论精度更优,但输入分布存在微小漂移,GSM8K 实测 0.9431 达标,其他数据集未被覆盖。第二,attention.py 的 forward / _prepare_and_attn 签名变化影响所有平台,qr_scale=None 分支提供保底,但若 ROCm 子类返回 fp8 qr 而调用链某处遗漏传 qr_scale,会导致 apply_weights 对 fp8 输入二次量化,属隐性正确性风险;该风险由 _wq_b_uses_aiter_block_scaled 门控与贯穿式参数缓解,但测试仅覆盖 op 层面而非完整 attention 集成。第三,AITER 内核版本演进可能改变数值行为,ULP 约束测试在 aiter 不可用时直接 skip,CI 覆盖有限。第四,fused_qk_rmsnorm_group_quant 无零行保护,依赖调用方保证 num_tokens > 0。第五,benchmark 中并发 1 时 TTFT 恶化 10.29%(462.05ms vs 418.93ms),作者未解释原因,建议关注低并发场景的调度开销变化。

用户侧:在 AMD MI350X/MI355X 等支持 AITER FP8 的 ROCm 环境上,DeepSeek-V4 SWA 解码热路径 kernel 级提速约 2.90x,端到端吞吐提升 0.5%~0.8%、TPOT 下降约 0.5%~1%,长上下文场景(indexer 参与)额外省去第三次量化 launch;非 ROCm、非 AITER 或 kernel 类型不符时按门控回退旧路径,行为不变。系统侧:_aiter_ops.py 增加了新的 custom op 注册模板,attention.py 的数据契约扩展(qr_scale 贯穿)为后续 MLA 类模型的预量化 GEMM 接入提供了通用接口。团队侧:该模式可复用于其他 ROCm 后端的归一化+量化融合优化。

ROCm/AITER 特定路径 条件门控依赖 kernel 类型 共享 attention 签名变更 单次舍入精度行为漂移 低并发 TTFT 上升 10%

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论