执行摘要
- 一句话:融合Q RoPE、FP8量化与权重缩放Triton kernel,提升GLM-5.2吞吐1.9%-3.3%
- 推荐动作:值得精读该Triton kernel的融合实现,尤其是在attention量化场景下的编程技巧。对于未来支持其他模型(如DeepSeek系列)的类似优化有直接参考价值。
功能与动机
PR body指出GLM-5.2模型有与DeepSeek V4(DSv4)类似的融合kernel需求。原来流程为:Q RoPE → cat → FP8 quant → q_scale fold into weights,现在在单个Triton kernel内完成所有步骤,减少launch开销和显存带宽。
实现拆解
- 在
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)。
- 在
deepseek_v2.py的DeepseekV2Attention类中添加use_fused_indexer_q开关,生效条件为:CUDA平台、quant_block_size == head_dim == 128、rope_dim == 64、scale_fmt非空。在forward方法中新增elif分支,先通过一次GEMM获得k和weights,然后调用融合kernel得到量化后的q_fp8和带scale的weights,再对k的rope部分单独旋转后与nope部分拼接,最后调用self.indexer_op完成剩余操作。
- 精度和性能验证:使用GLM-5.2-FP8模型,通过lm_eval gsm8k任务验证精度(0.9439 exact_match),通过vllm bench对比main分支,显示吞吐提升1.9%-3.3%,TTFT降低约5%。
- 测试配套:未新增独立单元测试,但通过集成测试和benchmark验证。
关键文件:
vllm/model_executor/layers/sparse_attn_indexer.py(模块 注意力层;类别 source;类型 core-logic;符号 _fused_indexer_q_rope_quant_kernel, fused_indexer_q_rope_quant): 核心变更,新增Triton JIT kernel,实现RoPE、FP8量化、权重缩放融合
vllm/model_executor/models/deepseek_v2.py(模块 DeepSeek模型;类别 source;类型 core-logic): 修改模型forward分支,调用融合kernel,添加条件开关
关键符号:_fused_indexer_q_rope_quant_kernel, fused_indexer_q_rope_quant
关键源码片段
vllm/model_executor/layers/sparse_attn_indexer.py
核心变更,新增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)
评论区精华
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与既有量化函数一致。
- eps value alignment (correctness): 采用1e-10,与既有量化函数一致
风险与影响
- 风险:
- 新kernel仅在特定条件启用(CUDA、head_dim=128等),其他情况回退到原路径,不会引入功能退化。
- 缺少单元测试,但通过精度(bench)验证,且融合逻辑与原路径数值差异极小(eps一致)。
- 未来模型配置变化(如head_dim非128)会导致自动回退,需确保回退路径表现正常。
- kernel使用Triton,对ROCm平台不生效,但已有ROCm专用路径。
- 影响:直接使用GLM-5.2-FP8模型的用户可获得1.9%-3.3%吞吐提升,TTFT略降,无功能变化。代码量小,维护成本可控。该融合kernel为类似场景的优化提供参考模式。
- 风险标记:特定形状依赖, 缺少单元测试
关联脉络
- PR #46808 [GLM-5] Add DSV3.2/GLM5 to
vllm/models/: 为GLM模型添加了基础模型实现,本PR在该模型上进一步做性能优化
参与讨论