Prhub

#46862 [GLM5.2 Perf] `fused_indexer_q_rope_quant` triton kernel, 1.9% ~ 3.3% E2E Throughput improvement.

原始 PR 作者 yewentao256 合并时间 2026-06-27 13:16 文件变更 2 提交数 1 评论 2 代码增减 +167 / -0

执行摘要

融合 Q RoPE、FP8 量化与权重缩放 Triton kernel,提升 GLM-5.2 吞吐 1.9%-3.3%

PR body指出GLM-5.2模型有与DeepSeek V4(DSv4)类似的融合kernel需求。原来流程为:Q RoPE → cat → FP8 quant → q_scale fold into weights,现在在单个Triton kernel内完成所有步骤,减少launch开销和显存带宽。

值得精读该Triton kernel的融合实现,尤其是在attention量化场景下的编程技巧。对于未来支持其他模型(如DeepSeek系列)的类似优化有直接参考价值。

讨论亮点

Review中tlrmchlsmth在sparse_attn_indexer.py第93行评论:"Should this be 1e-4 to match DSv4's?",作者yewentao256回复:"We here should use 1e-10 to match the current eps with per token group quant fp8... So no behavior change"。最终确认eps使用1e-10与既有量化函数一致。

实现拆解

  1. vllm/model_executor/layers/sparse_attn_indexer.py中新增Triton JIT kernel _fused_indexer_q_rope_quant_kernel和Python wrapper函数fused_indexer_q_rope_quant。Kernel内部按token和head并行,首先根据布局(NeoX或interleaved)加载Q的rope部分,应用RoPE旋转,然后与nope部分合并计算per-token的FP8量化scale(e8m0格式,向上取整到2的幂),统一量化写入fp8输出buffer;同时将q_scale折叠到weights中(乘以softmax_scale和head_scale)。
  2. deepseek_v2.pyDeepseekV2Attention类中添加use_fused_indexer_q开关,生效条件为:CUDA平台、quant_block_size == head_dim == 128rope_dim == 64scale_fmt非空。在forward方法中新增elif分支,先通过一次GEMM获得k和weights,然后调用融合kernel得到量化后的q_fp8和带scale的weights,再对k的rope部分单独旋转后与nope部分拼接,最后调用self.indexer_op完成剩余操作。
  3. 精度和性能验证:使用GLM-5.2-FP8模型,通过lm_eval gsm8k任务验证精度(0.9439 exact_match),通过vllm bench对比main分支,显示吞吐提升1.9%-3.3%,TTFT降低约5%。
  4. 测试配套:未新增独立单元测试,但通过集成测试和benchmark验证。
文件 模块 状态 重要度
vllm/model_executor/layers/sparse_attn_indexer.py 注意力层 modified 8.18
vllm/model_executor/models/deepseek_v2.py DeepSeek 模型 modified 6.14

关键符号

_fused_indexer_q_rope_quant_kernel fused_indexer_q_rope_quant

关键源码片段

vllm/model_executor/layers/sparse_attn_indexer.py core-logic

核心变更,新增 Triton JIT kernel,实现 RoPE、FP8 量化、权重缩放融合

融合kernel:_fused_indexer_q_rope_quant_kernel

@triton.jit
def _fused_indexer_q_rope_quant_kernel(
    positions,
    q,
    q_s0,
    q_s1,
    cos_sin_cache,
    cos_sin_s0,
    q_fp8,
    q_fp8_s0,
    q_fp8_s1,
    weights,
    weights_s0,
    weights_s1,
    weights_out,
    weights_out_s0,
    weights_out_s1,
    softmax_scale,
    head_scale,
    fp8_min: tl.constexpr,
    fp8_max: tl.constexpr,
    is_neox: tl.constexpr,
):
    token = tl.program_id(0)
    head = tl.program_id(1)
    offs32 = tl.arange(0, 32)
    offs64 = tl.arange(0, 64)
​
    pos = tl.load(positions + token)
    cos = tl.load(cos_sin_cache + pos * cos_sin_s0 + offs32).to(tl.float32)
    sin = tl.load(cos_sin_cache + pos * cos_sin_s0 + 32 + offs32).to(tl.float32)
    q_base = q + token * q_s0 + head * q_s1
    out_base = q_fp8 + token * q_fp8_s0 + head * q_fp8_s1
​
    if is_neox:
        # NeoX 布局:前半部分 0-31 为 x0,后半部分 32-63 为 x1
        x0 = tl.load(q_base + offs32).to(tl.float32)
        x1 = tl.load(q_base + 32 + offs32).to(tl.float32)
    else:
        # 交错布局:x0 取偶数索引,x1 取奇数索引
        x0 = tl.load(q_base + offs32 * 2).to(tl.float32)
        x1 = tl.load(q_base + offs32 * 2 + 1).to(tl.float32)
​
    # 应用 RoPE 旋转,中间用 bfloat16 降低精度损耗
    r0 = (x0 * cos - x1 * sin).to(tl.bfloat16).to(tl.float32)
    r1 = (x1 * cos + x0 * sin).to(tl.bfloat16).to(tl.float32)
    amax = tl.maximum(tl.max(tl.abs(r0)), tl.max(tl.abs(r1)))
​
    # 处理 nope 部分(不进行旋转)
    q_nope = tl.load(q_base + 64 + offs64).to(tl.float32)
    amax = tl.maximum(amax, tl.max(tl.abs(q_nope)))
​
    # 计算量化 scale(e8m0 格式:向上取整到 2 的幂)
    scale_raw = tl.maximum(amax, 1e-10) * (1.0 / fp8_max)
    q_scale = tl.math.exp2(tl.ceil(tl.log2(scale_raw)))
​
    # 存储量化后的 Q(包含 rope 和 nope 部分)
    if is_neox:
        tl.store(out_base + offs32, tl.clamp(r0 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty))
        tl.store(out_base + 32 + offs32, tl.clamp(r1 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty))
    else:
        tl.store(out_base + offs32 * 2, tl.clamp(r0 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty))
        tl.store(out_base + offs32 * 2 + 1, tl.clamp(r1 / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty))
    tl.store(out_base + 64 + offs64, tl.clamp(q_nope / q_scale, fp8_min, fp8_max).to(q_fp8.dtype.element_ty))
​
    # 将 q_scale 折叠到权重中:weight * q_scale * softmax_scale * head_scale
    weight = tl.load(weights + token * weights_s0 + head * weights_s1).to(tl.float32)
    tl.store(weights_out + token * weights_out_s0 + head * weights_out_s1,
             weight * q_scale * softmax_scale * head_scale)

模型forward中的调用分支

        elif self.use_fused_indexer_q and q.dtype == torch.bfloat16:
            # 融合 wk + weights_proj:一次 GEMM,然后拆分
            kw, _ = self.wk_weights_proj(hidden_states)
            k = kw[:, :self.head_dim]
            weights = kw[:, self.head_dim:]
​
            k = self.k_norm(k)
            k_pe, k_nope = torch.split(
                k, [self.rope_dim, self.head_dim - self.rope_dim], dim=-1
            )
​
            # 调用融合 kernel:一次完成 Q 的 RoPE、FP8 量化和 scale 折叠
            q_fp8, weights = fused_indexer_q_rope_quant(
                positions,
                q,
                rotary_emb.cos_sin_cache,
                weights,
                self.softmax_scale,
                self.n_head ** -0.5,
                rotary_emb.is_neox_style,
            )
​
            # 对 K 的 rope 部分单独旋转(MQA 风格,unsqueeze 后旋转再 squeeze)
            q_dummy = torch.empty_like(k_pe.unsqueeze(1))
            _, k_pe = rotary_emb(positions, q_dummy, k_pe.unsqueeze(1))
            k_pe = k_pe.reshape(-1, 1, self.rope_dim)
            k = torch.cat([k_pe.squeeze(-2), k_nope], dim=-1)
​
            return self.indexer_op(hidden_states, q_fp8, k, weights)

评论区精华

eps value alignment 正确性

tlrmchlsmth 询问 scale_raw 是否应改为 1e-4 以匹配 DSv4,作者 yewentao256 回应应使用 1e-10 以匹配 per_token_group_quant_fp8 的 eps,无行为变更

结论:采用 1e-10,与既有量化函数一致 · 已解决

风险与影响

  1. 新kernel仅在特定条件启用(CUDA、head_dim=128等),其他情况回退到原路径,不会引入功能退化。
  2. 缺少单元测试,但通过精度(bench)验证,且融合逻辑与原路径数值差异极小(eps一致)。
  3. 未来模型配置变化(如head_dim非128)会导致自动回退,需确保回退路径表现正常。
  4. kernel使用Triton,对ROCm平台不生效,但已有ROCm专用路径。

直接使用GLM-5.2-FP8模型的用户可获得1.9%-3.3%吞吐提升,TTFT略降,无功能变化。代码量小,维护成本可控。该融合kernel为类似场景的优化提供参考模式。

特定形状依赖 缺少单元测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论