Prhub

#47198 [Perf] Remove redundant op for GLM 5.2

原始 PR 作者 yewentao256 合并时间 2026-07-05 01:25 文件变更 1 提交数 4 评论 0 代码增减 +16 / -20

执行摘要

消除 GLM 5.2 稀疏注意力中冗余算子

作为 Issue #46654(GLM 5.2 性能优化)的一部分,本 PR 旨在移除稀疏注意力计算中的冗余算子,减少小张量操作开销。PR body 明确说明“All changes identical to previous implementation”,确保数值一致性。

值得合并。改动小、无风险,且是 Issue #46654 性能优化工作中清除冗余算子的典型步骤。对维护者而言,这类零碎优化在长期可积累显著的端到端加速。

实现拆解

  1. 预计算缩放因子:在 __init__ 中添加 self.n_head_scale = self.n_head**-0.5,避免每次 forward 调用时重复计算指数和除法。该值在推理过程中恒定。

  2. 消除 k_pe 冗余维度操作:在 fused_indexer_q 分支中,将原先的 k_pe.unsqueeze(1), k_pe.unsqueeze(1) 传给 rotary_embreshape(-1, 1, self.rope_dim)squeeze(-2) 合并为:先 unsqueeze(1) 传给 rotary_emb,再 reshape(-1, self.rope_dim) 直接得到 MQA 形状 [num_tokens, rope_dim]。类似地,在 else 分支中,直接 reshape(-1, self.rope_dim) 并拼接,省去中间 1 维度操作。

  3. 简化 q_scale 维度:将 q_scale.view(-1, self.n_head, 1) 改为 q_scale.view(-1, self.n_head),因为后续 weights 计算中会做乘法,广播机制能自动处理最后一维。

  4. 合并 weights 的乘法和维度操作:原先的 weights.unsqueeze(-1) * q_scale * self.softmax_scale * self.n_head**-0.5squeeze(-1) 改为直接 weights * q_scale * self.softmax_scale * self.n_head_scale,利用广播消除中间张量操作。

文件 模块 状态 重要度
vllm/model_executor/models/deepseek_v2.py 模型执行 modified 6.09

关键符号

DeepseekAttention.__init__ DeepseekAttention.forward

关键源码片段

vllm/model_executor/models/deepseek_v2.py core-logic

唯一修改的文件,包含 DeepseekAttention 类的优化,涉及稀疏注意力前向传播中多个冗余操作的移除。

class DeepseekAttention(nn.Module):
    def __init__(self, ...):
        ...
        self.is_inplace_rope = is_inplace_rope
        # 预计算缩放因子,避免每次前向都重复计算 self.n_head ** -0.5
        self.n_head_scale = self.n_head ** -0.5
        self.use_fused_indexer_q = (
            current_platform.is_cuda()
            and self.quant_block_size == self.head_dim
            and self.head_dim == 128
            and self.rope_dim == 64
            and self.scale_fmt is not None
        )
​
    def forward(self, ...):
        ...
        # fused_indexer_q 分支:消除 k_pe 上额外的 unsqueeze/squeeze 操作
        if current_platform.is_rocm() and self.is_inplace_rope:
            ...
        elif self.use_fused_indexer_q and q.dtype == torch.bfloat16:
            ...
            # 原实现是多次 unsqueeze 再 reshape,改为更直接的 reshape
            k_pe = k_pe.unsqueeze(1) # 仍保持 3D 供 rotary_emb 消费
            q_dummy = torch.empty_like(k_pe)
            _, k_pe = rotary_emb(positions, q_dummy, k_pe)
            k_pe = k_pe.reshape(-1, self.rope_dim) # 直接展平最后一维
            k = torch.cat([k_pe, k_nope], dim=-1) # 拼接得到 [num_tokens, head_dim]
            return self.indexer_op(hidden_states, q_fp8, k, weights)
        else:
            ...
            q_pe, k_pe = rotary_emb(positions, q_pe, k_pe.unsqueeze(1))
            q_pe = q_pe.reshape(-1, self.n_head, self.rope_dim)
            k_pe = k_pe.reshape(-1, self.rope_dim) # 直接展平,省去中间 1 维度
            q = torch.cat([q_pe, q_nope], dim=-1)
            k = torch.cat([k_pe, k_nope], dim=-1) # 拼接结果与之前 squeeze 后一致
        # 量化分支:简化 q_scale 的 shape,利用广播机制
        q_fp8, q_scale = per_token_group_quant_fp8(q, ...)
        q_fp8 = q_fp8.view(-1, self.n_head, self.head_dim)
        q_scale = q_scale.view(-1, self.n_head) # 去掉 expand 到 head_dim 维的 view
        # 合并 weights 的乘法和 squeeze,直接使用预计算的 n_head_scale
        weights = weights * q_scale * self.softmax_scale * self.n_head_scale
        return self.indexer_op(hidden_states, q_fp8, k, weights)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

本 PR 只调整了张量维度操作和预计算常量,没有修改算法逻辑或控制流,数值等价性在 PR body 中声明。主要风险是 fused_indexer_q 分支中 k_pe 的维度操作变化可能影响后续 rotary_emb 的调用,因为 rotary_emb 可能依赖输入形状。但作者通过精简 unsqueeze 次数并保持 rotary_emb 调用时 k_pe 仍为 3D,确保了兼容性。此外,else 分支中去掉了 squeeze(-2),直接拼 [num_tokens, rope_dim]k_pe,需要确认 k_nope 的形状也为 [num_tokens, head_dim-rope_dim](保持不变),拼接结果维度正确。整体风险较低。

本 PR 是纯性能优化,不影响功能或接口。目标用户是使用 GLM 5.2 模型(DeepseekV2 架构变体)且启用稀疏 MLP 注意力(use_fused_indexer_q)的用户。预计推理速度提升微小但稳定,因为去除了几个小张量操作。由于改动仅在一处文件中,影响范围限于 DeepseekAttention 类。

关联 Issue

#46654 [Feature]: GLM 5.2 Performance Optimization

完整报告

参与讨论