Prhub

#49750 [Perf] RMSNorm uncontiguous support, 1.2~3.1x kernel performance improvement

原始 PR 作者 yewentao256 合并时间 2026-07-30 04:45 文件变更 9 提交数 2 评论 3 代码增减 +31 / -29

执行摘要

RMSNorm 内核支持非连续张量,性能提升 1.2-3.1 倍

这是 Issue #46654(GLM5.2 性能优化)的一部分。作者 @tlrmchlsmth 发现大量模型在 fused_add_rms_norm 前做了不必要的 .contiguous(),这些操作本可通过内核直接支持非连续输入来消除。

值得精读。改动虽小但收益显著,且 review 中关于 aliasing 的讨论清晰展示了内核设计中“所有权/semantics”的权衡。可作为性能优化 PR 的典范。

讨论亮点

mgoin 在 csrc/libtorch_stable/layernorm_kernels.cu:319 提出疑问:“既然调用端去掉了 .contiguous(),是否应在内核内部做条件拷贝确保安全?如 if (residual.stride(-1) != 1) residual = residual.contiguous()”。
yewentao256 回应:“条件拷贝对于非修改的 rms_norm 是安全的,但 fused_add_rms_norm 会原地更新 residual。如果让 residual 重新绑定到临时拷贝,则调用方的原始 tensor 不会被修改,违背了操作符的别名语义。所以不能添加条件拷贝,必须保持对外要求 stride(-1)==1。”
mgoin 认可后予以批准。

实现拆解

步骤 1:放宽 C++ 内核输入要求
在 csrc/libtorch_stable/layernorm_kernels.cu 中,将 STD_TORCH_CHECK(residual.is_contiguous()) 改为 STD_TORCH_CHECK(residual.stride(-1) == 1),允许最后一维连续但前面维度不连续的输入。同时调整内部偏移计算以适配非连续 stride。

步骤 2:移除模型层冗余 .contiguous()
在 vllm/model_executor/models/ 下的 6 个模型文件中,删除了调用 fused_add_rms_norm 或 RMSNorm 前的 .contiguous() 调用,包括:

  • hunyuan_v1.py:HunYuanAttention 和 HunYuanCrossAttention 的 qk-norm
  • deepseek_v2.py:DeepseekV2DecoderLayer 中 all-gather 后的 residual 处理
  • step3p5.py:Step3P5Attention 的 qk-norm
  • apertus.py:ApertusAttention 的 qk-norm
  • lfm2.py 和 lfm2_moe.py:q 和 k 的 view 后去 contiguous

步骤 3:更新单元测试
在 tests/kernels/core/test_layernorm.py 中调整测试用例,确保新条件 stride(-1)==1 被正确验证,并添加对非连续输入的支持(通过 view 而非 contiguous 来构造)。
所有模型代码改动仅涉及删除 .contiguous(),不改变语义,但需确认调用处的 tensor 确实满足 stride 要求。
配置、文档、部署无变更。

文件 模块 状态 重要度
csrc/libtorch_stable/layernorm_kernels.cu 层归一化内核 modified 4.31
vllm/model_executor/models/hunyuan_v1.py 模型代码 modified 5.51
vllm/model_executor/models/deepseek_v2.py 模型代码 modified 5.48
vllm/model_executor/models/step3p5.py 模型代码 modified 5.28
tests/kernels/core/test_layernorm.py 单元测试 modified 4.57

关键符号

fused_add_rms_norm HunYuanAttention.forward HunYuanCrossAttention.forward DeepseekV2DecoderLayer.forward Step3P5Attention.forward ApertusAttention.forward test_input_perturbations_have_expected_behavior

关键源码片段

vllm/model_executor/models/hunyuan_v1.py data-contract

代表性模型改动,展示 HunYuanAttention 和 HunYuanCrossAttention 中移除冗余 .contiguous() 的典型模式。

def forward(self, positions, hidden_states, kv_states=None):
    qkv, _ = self.qkv_proj(hidden_states)
    q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
    q, k = self.rotary_emb(positions, q, k)
    ori_k = k
    if self.use_qk_norm:
        # 不再需要 .contiguous(),因为 fused_add_rms_norm 内核现在支持
        # stride(-1)==1 的输入。 view 产生的 tensor 自动满足最后一维连续,
        # 因此可直接传入。
        q = self.query_layernorm(
            q.view(-1, self.num_heads, self.head_dim),
        )
        k = self.key_layernorm(
            k.view(-1, self.num_kv_heads, self.head_dim),
        )
    attn_output = self.attn(q, k, v)
    attn_output = attn_output.view(q.shape[0], -1)
    output, _ = self.o_proj(attn_output)
    return output, (ori_k, v)

评论区精华

Conditional contiguous in fused_add_rms_norm kernel 设计

mgoin: 是否应在内核内做条件 contiguous 拷贝以保证安全性?yewentao256: 不能,因为 fused_add_rms_norm 会原地修改 residual,条件拷贝将导致 aliasing 语义不符。

结论:决定不添加条件拷贝,保持对外要求 stride(-1)==1。 · 已解决

风险与影响

回归风险:内核断言从 is_contiguous() 收紧到 stride(-1)==1,若任何路径传递了 stride(-1)!=1 的残差张量,将触发断言失败。但通过审核所有调用点,确认它们移除 .contiguous() 后仍满足 stride(-1)==1(均为 split 或 view 结果)。风险较低。
兼容性:fused_add_rms_norm 的 C 接口未变,仅加强文档。向上兼容。
性能反向:非极少数情况外无负面性能影响;对于调用方传入的已是 contiguous 的残差,性能不变;对于非 contiguous 输入,避免了额外拷贝,提升显著。
无安全风险。

用户:使用 DeepSeek、Hunyuan、Apertus、LFM、MiniCPM 等模型的用户获得免费加速,最大 3.15x。无需配置变更。
系统:内核改动可能影响所有使用 fused_add_rms_norm 的路径,但经过静态审查和测试覆盖。团队无需额外维护成本。
团队:此优化可推广到其他可能保留 .contiguous() 的模型 PR 中。作为性能优化的范例,有助于培养“移除冗余、利用内核能力”的意识。

内核边界条件变化 多处模型调用耦合

关联 Issue

#46654 [Feature]: GLM 5.2 Performance Optimization

完整报告

参与讨论