深蓝黑背景封面。左侧两座并排的六层结构塔对比同一个 Transformer 块:左塔 Post-LN 自下而上为「输入 x、自注意力、+残差→LN、FFN、+残差→LN、输出」,两个含 LN 的层以紫色描边高亮,表示归一化在残差相加之后;右塔 Pre-LN 自下而上为「输入 x、LN→自注意力、+残差、LN→FFN、+残差、输出」,两个 LN 层以青色描边高亮,表示归一化挪到子层之前。塔上方标注「归一化只差一格位置,训练行为完全不同」。右侧大标题「LayerNorm 放前面还是放后面」,副标题「Pre-LN 与 Post-LN 差在哪」,并附注「一个结构变量,决定深层模型能不能稳定训练」。左上标注「面试经典 · 归一化」,右上圆角胶囊标注「Day 10 · 11-27」。 图 1:本文封面,同一个 Transformer 块的两种 LN 位置。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。Pre-LN 与 Post-LN 的差别,字面看只是把 LayerNorm 挪了一格,但这一格决定了深层 Transformer 能不能稳定训练——它是理解现代大模型结构的一把钥匙。

先把两种结构的公式钉死。

一个 Transformer 子层(自注意力或 FFN)配残差连接,两种摆法:

  • Post-LN:输出 = LN(x + Sublayer(x))。先做残差相加,再过归一化。2017 年的 Transformer 原论文(arXiv:1706.03762)用的就是这个结构。
  • Pre-LN:输出 = x + Sublayer(LN(x))。先归一化,再进子层,残差相加时不碰 LN。

差别只有一行,但反向传播走的是完全不同的路。

Post-LN 的问题:梯度每层都要穿过 LN。

Post-LN 结构里,反向梯度从输出往输入传,每过一层都要穿过一次 LayerNorm 的雅可比矩阵。2020 年的论文《On Layer Normalization in the Transformer Architecture》(arXiv:2002.04745)用平均场理论证明了:初始化时 Post-LN 靠近输出层的参数梯度期望很大,直接上大学习率就会训练发散。这就是早期 Transformer 训练必须用学习率 warm-up(先小步预热再爬坡)的原因——不是玄学,是在躲初始化阶段的梯度尖峰。层数越深,问题越严重,所以原始 Post-LN 结构很难直接堆到几十上百层。

Pre-LN 的关键:残差变成恒等高速通道。

Pre-LN 把 LN 挪进残差分支内部,主干上只剩纯粹的加法:输出 = x + 分支(x)。反向传播时,梯度可以沿着这条「恒等通道」从输出层直达输入层,不被任何归一化缩放。同一篇论文证明 Pre-LN 初始化时梯度表现良好,因此可以干脆去掉 warm-up 直接训,论文实验里省去 warm-up 的 Pre-LN 在多种任务上达到了与基线相当的效果,还省下了大量调参时间。层数堆到上百层依然稳定——这就是今天主流开源大模型几乎清一色 Pre-LN(或其变体)的原因。

那 Post-LN 为什么还有人惦记:效果上限略高。

稳定是有代价的。Pre-LN 的恒等通道让每层的实际「有效深度」打了折扣——直观理解是深层分支的贡献容易稀释在残差流里,经验上同等规模下 Pre-LN 收敛后的最终效果常常略逊于训得动的 Post-LN。于是这道题的标准结论是:Post-LN 效果上限高但训练难(要 warm-up、深度受限),Pre-LN 稳定、好堆层、训练省心;大模型时代深度和稳定性压倒一切,选择了 Pre-LN。后续还有 Sandwich-LN、DeepNorm 等变体试图两边都要,属于这条线的延伸。

用代码看梯度行为差多少。

下面这段 Node.js 22.15.0 程序(node ln.js)搭了两座简化残差塔(tanh 子层 + 固定权重),用有限差分数值测量「输出对输入的梯度范数」,对比两种结构随深度增加的表现:

const D = 4;
let s = 7;
const rand = () => (s = (s * 1103515245 + 12345) % 2147483648) / 2147483648;
function ln(v) {           // 简化 LayerNorm(无仿射参数)
  const m = v.reduce((a, b) => a + b) / v.length;
  const sd = Math.sqrt(v.reduce((a, b) => a + (b - m) ** 2, 0) / v.length + 1e-5);
  return v.map(x => (x - m) / sd);
}
const W = Array.from({length: D}, () => Array.from({length: D}, () => rand() - 0.5));
const sublayer = v => W.map(r => Math.tanh(r.reduce((a, x, j) => a + x * v[j], 0)));
function stack(x, L, pre) {
  let v = [...x];
  for (let i = 0; i < L; i++) {
    v = pre ? v.map((a, j) => a + sublayer(ln(v))[j])   // Pre-LN:残差是恒等通道
            : ln(v.map((a, j) => a + sublayer(v)[j]));  // Post-LN:每层穿过 LN
  }
  return v;
}
function gradNorm(x, L, pre) {
  const f = xx => stack(xx, L, pre).reduce((a, b) => a + b * b, 0);
  const eps = 1e-4;
  let g = 0;
  for (let j = 0; j < D; j++) {
    const xp = [...x]; xp[j] += eps;
    const xm = [...x]; xm[j] -= eps;
    g += ((f(xp) - f(xm)) / (2 * eps)) ** 2;
  }
  return Math.sqrt(g);
}
const x = [0.5, -0.3, 0.8, 0.1];
for (const L of [1, 6, 12, 24]) {
  console.log(`堆叠 ${String(L).padStart(2)} 层:输入侧梯度范数 Post-LN = ${gradNorm(x, L, false).toFixed(4)},Pre-LN = ${gradNorm(x, L, true).toFixed(4)}`);
}

实测输出:

堆叠  1 层:输入侧梯度范数 Post-LN = 0.0047,Pre-LN = 3.5273
堆叠  6 层:输入侧梯度范数 Post-LN = 0.0000,Pre-LN = 28.4210
堆叠 12 层:输入侧梯度范数 Post-LN = 0.0000,Pre-LN = 55.5273
堆叠 24 层:输入侧梯度范数 Post-LN = 0.0000,Pre-LN = 109.4523

Post-LN 的输入侧梯度只堆了几层就衰减到近乎消失(显示为 0.0000),浅层参数几乎收不到学习信号;Pre-LN 的梯度沿恒等通道直达输入,24 层依然保持健康的量级。玩具实验不能复刻论文里的全部结论,但「梯度要不要逐层穿 LN」造成的方向性差异一目了然。

深蓝黑背景概念图。左右两栏各画一座简化结构塔并叠加反向梯度箭头。左栏 Post-LN(紫色描边):自下而上为输入 x、自注意力、Add+LN、FFN、Add+LN、输出;反向梯度箭头必须逐层穿过每个 LN,箭头旁标注「梯度每层都要过 LN,深层易消失或爆炸」,下方注「需要学习率 warm-up 才敢开大;代价是训练难,回报是收敛后效果上限略高」。右栏 Pre-LN(青色描边):自下而上为输入 x、LN→自注意力、+残差、LN→FFN、+残差、输出;一条青色粗箭头沿残差边从输出直通输入,标注「残差是恒等高速通道,梯度直达浅层」,下方注「去掉 warm-up 也能训,百层深度堆得上去,主流大模型标配」。底部总结条:「一句话:Post-LN 效果上限高但难训,Pre-LN 稳定好堆层 —— 大模型时代选择了稳定」。 图 2:两种结构的梯度路径对比。

面试现场的答题结构。

先写两行结构公式,指明差别只在 LN 与残差相加的先后;再讲反向传播:Post-LN 梯度逐层穿 LN、初始化时输出侧梯度大、需要 warm-up、深度受限;Pre-LN 残差是恒等通道、可去 warm-up、可堆百层;最后补 trade-off:Post-LN 效果上限略高,Pre-LN 胜在稳定,主流大模型选了后者。你训练时有没有被梯度炸过?当时是调 warm-up 还是改结构解决的?欢迎聊聊。