← 返回列表

RMSNorm:归一化只需要缩放,不需要中心化

2023 年之后发布的开源模型——Llama、Qwen、DeepSeek、Gemma——在归一化层上做出了同一个选择:RMSNorm。它不是更好的 LayerNorm,而是更小的 LayerNorm:删掉均值、删掉偏置,剩下的部分就足够了。

这篇文章回答三个问题:删掉的到底是什么、为什么敢删、以及两行代码怎么把它写出来。

前置知识:公式里的每个符号

后面两个公式一共出现五个希腊字母,先一次说清——每一个都不复杂,复杂的是它们组合出来的效果。

符号读作是什么怎么来的
μ\mu缪 (mu)均值(mean)μ=mean(x)\mu = \text{mean}(x),向量所有元素的平均数
σ2\sigma^2西格玛平方 (sigma)方差(variance)σ2=mean((x−μ)2)\sigma^2 = \text{mean}\left((x - \mu)^2\right),每个元素离均值有多远,再取平均
ϵ\epsilon伊普西龙 (epsilon)一个极小的数,如 1e-5直接写死的常数,防止除以 0
γ\gamma伽马 (gamma)可学习的缩放参数神经网络训练时自己学出来的,初始值全是 1
β\beta贝塔 (beta)可学习的偏移参数也是学出来的,初始值全是 0

记忆方式:μ\mu 和 σ2\sigma^2 是统计量(从数据算出来),γ\gamma 和 β\beta 是参数(训练学出来),ϵ\epsilon 是工程保底(写死的)。RMSNorm 删掉的全部内容,就是这张表里的一半。

删掉的到底是什么

原始 Transformer 用 LayerNorm 处理深层网络的激活漂移:

LayerNorm(x)=x−μσ2+ϵ⋅γ+β\text{LayerNorm}(x) = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta

RMSNorm 把它砍成:

RMSNorm(x)=xRMS(x)⋅γ,RMS(x)=1d∑i=1dxi2+ϵ\text{RMSNorm}(x) = \frac{x}{\text{RMS}(x)} \cdot \gamma, \qquad \text{RMS}(x) = \sqrt{\frac{1}{d}\sum_{i=1}^{d} x_i^2 + \epsilon}

被删除的三个组件:μ\mu(一次全量归约)、σ2\sigma^2(依赖 μ\mu 的第二次归约)、β\beta(一份参数加一次加法)。保留的 RMS 不做中心化,直接对平方取均值再开方。

LayerNorm 与 RMSNorm 的计算流程对比

用一个四维向量手算一遍,差异比公式更直观。x=[3.0, 1.0, −2.0, 4.0]x = [3.0,\ 1.0,\ -2.0,\ 4.0]:

LayerNorm:  μ=1.50  σ=2.29  →  [ 0.65, -0.22, -1.53,  1.09]
RMSNorm:    RMS=2.74         →  [ 1.10,  0.37, -0.73,  1.46]

两个输出的区别在分布形状:LayerNorm 强制零均值(正负各半),RMSNorm 只管缩放、不管中心(整体偏正)。这个差异就是被删掉的中心化——问题在于,它重要吗?

为什么敢删

RMSNorm 论文(Zhang & Sennrich, 2019)的核心贡献不是这个层本身,而是一个消融结论:把 LayerNorm 的 re-centering(μ\mu 和 β\beta 部分)整个移除,各项任务的性能基本不动。

这个结果指向对归一化机制的重新理解:归一化的有效性几乎全部来自缩放不变性,中心化是可以免费丢弃的部分。 深层网络的真正问题是激活方差逐层漂移——把每层输入拉回稳定尺度,梯度就稳了。至于输出的中心是不是零,模型并不在乎。

删除后的收益可以列得很具体:

  • 少一次完整的数据依赖归约(σ2\sigma^2 依赖 μ\mu,删 μ\mu 连带删两次归约);
  • 参数量减半(只有 γ\gamma);
  • fp16/bf16 训练下数值路径更短,更稳。

「效果不掉 + 更快 + 更简单」三者同向,这种机会在深度学习里不多。所以 2023 年后的模型几乎清一色切换,这不是巧合,是证据驱动的一致选择。

两行实现

公式到代码几乎是直译:

import torch
import torch.nn as nn

class RMSNorm(nn.Module):
    def __init__(self, dim: int, eps: float = 1e-5):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim))   # γ,初始全 1

    def forward(self, x):                             # x: (B, S, d)
        return x * torch.rsqrt(
            x.pow(2).mean(-1, keepdim=True) + self.eps
        ) * self.weight

公式与代码的逐行对照

三个实现细节决定了这段代码的正确性:

keepdim=True 是广播的关键。 xx 的形状是 (B,S,d)(B, S, d),在最后一维归约后得到 (B,S,1)(B, S, 1) 而不是 (B,S)(B, S)——后者无法广播回原形状。keepdim 保留的那个长度为 1 的维度,让每个 token 用自己的 RMS 归一化自己,一行完成。

rsqrt 不是一个写法偏好。 它是融合算子,一次 kernel 完成 ⋅\sqrt{\cdot} 和取倒数;分开写 1/torch.sqrt(...) 是两次 kernel、两次潜在精度损失。fp16 下这个差别真实存在。

γ 初始化为全 1 意味着训练开始时 RMSNorm 的输出就是干净的归一化结果,没有任何初始扰动——这是它训练稳定性的一个组成部分。

前向计算中张量形状的完整流转:

RMSNorm 前向数据流

验证三个性质

代码写完不算完,跑一遍确认它真的具备(和不具备)哪些性质:

torch.manual_seed(0)
x = torch.randn(2, 16, 256) * 3 + 1     # 故意构造非零均值

norm = RMSNorm(256)

# ① 缩放不变性:输入整体乘 k,输出不变
torch.allclose(norm(x), norm(x * 7), atol=1e-5)   # True

# ② 输出 RMS 被锁定在 γ(初始 γ=1)
norm(x).pow(2).mean(-1).sqrt().mean()             # ≈ 1.0

# ③ 平移敏感:整体加常数,输出会变
torch.allclose(norm(x), norm(x + 5), atol=1e-5)   # False

① 是归一化的本职:无论输入尺度怎样漂移,输出不变——这就是「缩放不变性」的直接体现,也是深层网络训练稳定的来源。② 说明输出尺度被严格锁定,激活不会逐层膨胀。③ 是它没有解决的问题:中心化被删掉了,平移会穿透到输出。实践里这不是问题——残差流里「所有维度同加一个常数」的扰动模式很少见,后续层和 γ\gamma 会把它吸收掉。

pre-norm:位置和层本身一样重要

RMSNorm 生效有一个前提:放在正确的位置。Llama 系列全部采用 pre-norm——归一化放在子层之前,残差从外面绕过:

x = x + attn(rmsnorm(x))    # 先归一化,再进注意力,输出加回残差流
x = x + ffn(rmsnorm(x))

对比原始 Transformer 的 post-norm(norm(x + sublayer(x))):pre-norm 的残差通路是干净的,梯度沿加法分支直通底层,不穿过任何归一化的摩擦。几十层的模型能稳定训练,这一行顺序的贡献不小于归一化本身。

代价是残差流的尺度没有被管理,所以 Llama 在最后一个解码层之后补了一个 final RMSNorm 再接 lm_head——手写实现时最容易漏的就是这个结尾。

顺带一提,Llama 全面去掉 Linear 层的 bias 也是同一个思想:RMSNorm 的 γ\gamma 是乘性缩放,已经覆盖了「调整每维幅度」的需求;β\beta 这样的加性自由度在大规模下收益趋近于零,还会在长训练里单调漂移、干扰 INT8/FP8 量化。乘性缩放优于加性偏置——Qwen3 删掉 QKV-bias 换 QK-Norm,是同一判断的第二次出现。

总结

  • RMSNorm = LayerNorm − 中心化:不减均值、不除标准差,只除 RMS、只乘 γ;
  • 敢删的依据是消融实验:归一化的价值在缩放不变性,re-centering 是可丢弃的装饰;
  • 两行核心代码,三个细节:keepdim 广播、rsqrt 融合、γ 初始化全 1;
  • pre-norm + final norm 才是完整用法;
  • 未解决的:平移会穿透输出——实践中无关紧要,但值得知道。

延伸

两个顺藤摸瓜的方向:把 RMSNorm 换回 LayerNorm 训一个小模型,看训练曲线到底差多少(消融结论的自己复现版);以及 FlashRMSNorm 这类 fused kernel 在这两行代码之上还优化了什么(归约与缩放的融合,省一次显存读写)。