【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
标题
226、【AI】【模型部署】基座模型研究:RMSNorm 与残差(归一化为什么必要)
背景
上篇 blog
【AI】【模型部署】基座模型研究:从文字到向量(分词器与嵌入层)
讲清了基座的入口:Qwen2Tokenizer用 BPE 把文本切成子词(hello是 1 个 token、模型部署是 2 个),vocab.json151643 条加特殊 token 凑成vocab_size=151936;嵌入层是一张(151936, 896)的查表,把 id 变成 896 维向量,并与lm_head共享权重。向量从这里进入 24 层Qwen2DecoderLayer。224 拆出的骨架里,每层第一件事是"归一化 → 子层 → 残差",本篇就从这个骨架的第一个零件读起:Qwen2RMSNorm
模型部署
224 的Qwen2DecoderLayer.forward里,self.input_layernorm出现在注意力之前、self.post_attention_layernorm出现在 MLP 之前——归一化是每层的"开场动作"。它到底做了什么,看源码最直接。
🧩源码:RMSNorm(Root Mean Square Normalization,均方根归一化) 只有几行
Qwen2RMSNorm(modeling_qwen2.py:238-255)的核心就三步(实测打印源码):
classQwen2RMSNorm(nn.Module):def__init__(self,hidden_size,eps:float=1e-6):self.weight=nn.Parameter(torch.ones(hidden_size))self.variance_epsilon=epsdefforward(self,hidden_states):variance=hidden_states.to(torch.float32).pow(2).mean(-1,keepdim=True)hidden_states=hidden_states*torch.rsqrt(variance+self.variance_epsilon)returnself.weight*hidden_states拆开看:算平方均值 → 开根号取倒数 → 乘回去,最后再乘一个可学习权重。没有减均值这一步——这正是它和LayerNorm(Layer Normalization,层归一化) 的最大区别。
🧩公式与手写验证
把源码翻译成公式:对向量x,先算variance = mean(x²),输出weight · x / sqrt(variance + eps)。用一句 Python 手写一遍,和模型里的层对比(实测):
ln=model.model.layers[0].input_layernorm# Qwen2RMSNorm(896)x=torch.randn(2,5,896)ref=ln(x)defmy_rms_norm(x,weight,eps=1e-6):var=x.float().pow(2).mean(-1,keepdim=True)returnweight*(x.float()*torch.rsqrt(var+eps)).to(x.dtype)mine=my_rms_norm(x,ln.weight,ln.variance_epsilon)print("最大误差:",(ref-mine).abs().max().item())实测最大误差 0.0——手写实现与 transformers 完全一致。eps=1e-6是防止方差为 0 时除零的保险项,来自 config 的rms_norm_eps。
🧩和 LayerNorm 差在哪:实测对比
RMSNorm只按"平方均值"缩放,LayerNorm还会先减去均值再缩放。差异用数据说话(实测):
lnorm=nn.LayerNorm(896)a,b=ln(x),lnorm(x)print("RMSNorm 输出均值:",a.mean().item())print("LayerNorm 输出均值:",b.mean().item())print("两者差异最大:",(a-b).abs().max().item())实测:RMSNorm输出均值约0.000298(没有归零),LayerNorm约3.4e-9(被减均值拉到 0 附近),两者逐元素最大差异3.748。可见 RMSNorm 保留了向量的"整体偏移",只控制"尺度"。
为什么这样反而好:省掉"减均值"就省了跨特征的均值计算与一次减法,在超大模型上更省算力;而实践表明只做尺度归一化已经足够稳定训练,这就是它被 Qwen、Llama 等采用的原因。
| 维度 | RMSNorm | LayerNorm |
|---|---|---|
| 减均值 | 否,只按平方均值缩放 | 是,先中心化再缩放 |
| 计算量 | 更少 | 多一步均值与减法 |
| 可学习参数 | 一组weight | weight+bias |
| 代表采用 | Qwen、Llama | 早期 Transformer、GPT-2 |
🧩两个实现细节:为什么要转 float32、eps 防什么
源码里有一行容易被略过:hidden_states.to(torch.float32)——先升到 fp32 算方差与开方,再转回原精度:
input_dtype=hidden_states.dtype hidden_states=hidden_states.to(torch.float32)# 升精度再算variance=hidden_states.pow(2).mean(-1,keepdim=True)hidden_states=hidden_states*torch.rsqrt(variance+self.variance_epsilon)returnself.weight*hidden_states.to(input_dtype)# 转回原精度原因:模型权重是 bf16(config 里torch_dtype=bfloat16),而rsqrt对精度敏感,用 bf16 直接算容易放大误差;放进 fp32 里算、再转回去,是数值稳定的常规做法。至于eps(variance_epsilon=1e-6)——它加在方差里,防止"某段向量恰好全 0 → 方差为 0 → 除零",加一个极小数让rsqrt永远有定义。
🧩在层里的位置:pre-norm + 残差
224 读过的Qwen2DecoderLayer(modeling_qwen2.py:279-297)把归一化放在子层之前(pre-norm),子层输出再与输入相加(残差):
x ──input_layernorm──> 注意力 ──┐ │ │ └────────── 残差相加 <─────────┘ ──post_attention_layernorm──> MLP ──┐ │ │ └──────────── 残差相加 <─────────────────┘归一化让每层输入保持稳定尺度,残差让梯度能直通深网络——两者配合,24 层才堆得起来。
为什么是 pre-norm(先归一化再进子层):如果反过来(post-norm,先子层再归一化),深层网络里梯度要穿过一次次归一化,训练早期容易不稳、需要小心调 warmup;pre-norm 让残差那条"直通路径"始终干净,梯度可以无衰减地回传,这是深层 Transformer 能稳定训练的关键改动之一。Qwen2 的Qwen2DecoderLayer用的正是 pre-norm。
🧩顺带算一笔:归一化层有多少参数
每层有两个 RMSNorm(input_layernorm与post_attention_layernorm),每个只有一组 896 维weight;24 层共 48 个,参数合计48 × 896 = 43,008——相比 MLP 的千万级几乎可以忽略。但它不是常量:weight是可学习参数,训练时会调整这个"缩放系数",让网络自己决定每层的归一化强度。
| 组件(单层) | 参数量 | 占比 |
|---|---|---|
| 两个 RMSNorm | 2 × 896 = 1,792 | 极小 |
| 注意力(含 GQA) | 1,836,160 | 中等 |
| SwiGLU MLP | 13,074,432 | 大头 |
这也解释了 224 打印结构时看到的顺序:
input_layernorm、self_attn、post_attention_layernorm、mlp——归一化夹在两个"重"子层前后,本身很轻,却决定了整个网络能不能训得动。
两个归一化的weight初始化为全 1,训练前等价于"不做额外缩放",训练中再由梯度学习调整。
下一篇从注意力的位置信息讲起:RoPE。
📌一句话记忆
Qwen2RMSNorm只做三件事:算mean(x²)、乘rsqrt(variance+eps)、再乘可学习权重(不减均值);手写实现与源码输出误差为 0,与LayerNorm的差异(输出均值 0.0003 vs 3.4e-9)正体现"只归尺度、不归中心";它被放在每层子层之前(pre-norm)并与残差配合,是 24 层能稳定堆叠的基础。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog
【AI】【模型部署】基座模型研究:RoPE 旋转位置编码