现代 Transformer 的五大改造
第 3 课:现代 Transformer 的五大改造
2017 年的 Transformer 只是个起点。从 Llama 到 Qwen,现代大模型对原始 Transformer 做了五处关键改造——每一处都在解决一个具体的工程痛点。
② 画出 MHA → MQA(所有 Query 头共享同一份 K/V)→ GQA(把多个 Query 头分成几组、每组共享同一份 K/V)的演进链,算出 GQA 省了多少 KV Cache(把已算过的键和值存下来复用)
③ 指出 RMSNorm(砍掉减均值、只保留缩放)、SwiGLU(用门控决定信息通过多少)、Pre-Norm 各自解决了什么训练/推理瓶颈
把"我打你"和"你打我"的每个字转成向量——"我"永远是 [0.5,0.3,0.8],"打"永远是 [0.2,0.9,0.4]。三个字的 Embedding 完全相同,顺序不同但模型看到的向量集合一样。不加位置信息的话,模型分不清谁打了谁。打个比方:一副扑克牌洗过之后张数没变、点数也没变,可你再也说不出谁在谁前面——拿到的就是这副洗过的牌。所以位置编码要回答的不是这个词是什么,而是这个词排在第几位。
| 字 | d₁ | d₂ | d₃ |
|---|---|---|---|
| "我" | 0.5 | 0.3 | 0.8 |
| "打" | 0.2 | 0.9 | 0.4 |
| "你" | 0.7 | 0.1 | 0.6 |
| 位置1 | 位置2 | 位置3 | |
|---|---|---|---|
| "我打你" | [0.5,0.3,0.8] | [0.2,0.9,0.4] | [0.7,0.1,0.6] |
| "你打我" | [0.7,0.1,0.6] | [0.2,0.9,0.4] | [0.5,0.3,0.8] |
| 位置 | Sinusoidal 编码(前2维) | "我打你"每个位置 = 词向量 + 编码 |
|---|---|---|
| 位置1 | [sin(1)≈0.84, cos(1)≈0.54, 0] | 我: [0.5+0.84, 0.3+0.54, 0.8] = [1.34, 0.84, 0.8] |
| 位置2 | [sin(2)≈0.91, cos(2)≈−0.42, 0] | 打: [0.2+0.91, 0.9−0.42, 0.4] = [1.11, 0.48, 0.4] |
| 位置3 | [sin(3)≈0.14, cos(3)≈−0.99, 0] | 你: [0.7+0.14, 0.1−0.99, 0.6] = [0.84, −0.89, 0.6] |
| 场景 | 位置1的向量 | 位置3的向量 | 能否看出"距离=2"? |
|---|---|---|---|
| "我打你" | [1.34, 0.84, 0.8] | [0.84, −0.89, 0.6] | 不能——点积 1.34×0.84+0.84×(−0.89)+0.6×0.8 = 0.858 |
| "你打我" | [1.54, 0.64, 0.6] | [0.64, −0.69, 0.8] | 也不能——点积 1.5415×0.6411+0.6403×(−0.69)+0.6×0.8 = 1.026,和上面不相等 |
绝对位置编码像给每个座位贴一个 GPS 坐标(第 1 排、第 2 排…)。模型拿到的是"你在 36.5°N, 120.3°E",但它真正需要知道的是"你离我 2 米远"——相对距离。不管用 Sinusoidal 函数还是查表,只要是"绝对位置 + 加法",都绕不开这个限制。RoPE 的答案:不要 GPS 了,改成记录"你在我左边还是右边,离多远"——通过旋转操作把相对距离信息直接编码进 Attention 的点积里。
上一张卡片我们看清了绝对位置编码的死穴:只编码了"你在第几位"(绝对坐标),没编码"你离我多远"(相对距离)。RoPE 换了一种思路——不把位置信息当成"加法标签",而是把每个词的向量"旋转"一个角度,角度大小由位置决定。
核心数学:在二维平面里,把一个向量旋转 θ 角:x' = x·cosθ - y·sinθ,y' = y·cosθ + x·sinθ。关键性质:两个旋转后的向量做点积,结果里天然包含了它们的相对角度差——也就是相对位置。
打个比方:把每个词的向量想成钟表上的指针,位置决定它被拨到几点——两根指针的夹角只取决于"拨过的格数之差",而不取决于它们在表盘上的绝对位置。
注意这里的关键是「旋转」而不是「相加」:把位置向量直接加到词向量上,会同时改动这个向量的长度和方向——[0.8, 0.6] 加上位置编码 [0.84, 0.54] 后变成 [1.64, 1.14],模长从 1.00 变成约 2.00,方向也从 36.9° 偏到 34.8°,词本身的语义就被搅动了。旋转只改方向、不改长度([0.8, 0.6] 旋转 0.03 弧度后模长仍是 1.00),于是点积里就只剩「两个方向差多少」这一个变量,相对位置才干净地浮现。
| 原始向量(取前2维) | 旋转角(位置×角速度) | |
|---|---|---|
| 位置1的"打" | [0.8, 0.6] | θ₁ = 1×0.01 = 0.01 rad |
| 位置3的"打" | [0.8, 0.6] | θ₃ = 3×0.01 = 0.03 rad |
| x' = x·cosθ - y·sinθ | y' = y·cosθ + x·sinθ | |
|---|---|---|
| 位置1旋转后 | 0.8×0.99995 - 0.6×0.0100 = 0.79996 - 0.006 = 0.794 | 0.6×0.99995 + 0.8×0.0100 = 0.59997 + 0.008 = 0.608 |
| 位置3旋转后 | 0.8×0.99955 - 0.6×0.0300 = 0.79964 - 0.018 = 0.782 | 0.6×0.99955 + 0.8×0.0300 = 0.59973 + 0.024 = 0.624 |
| 对比 | 旋转后点积 | 旋转前点积 |
|---|---|---|
| 位置1的"打" × 位置3的"打" | 0.794×0.782 + 0.608×0.624 = 0.621 + 0.379 = 1.000 | 0.8×0.8 + 0.6×0.6 = 1.000 |
标签表方式的死穴是"没见过位置257的标签"。旋转是连续数学操作——cos 和 sin 对任意角度都有定义,位置 257 的旋转角 257×0.01 = 2.57 rad 仍可算出。模型学会"旋转角越大、点积衰减越多"后,相对位置性质比绝对编码更好。但这不等于"天然支持任意长度"——超出训练长度后模型从未在这些角度上训练过,实测仍会退化,长文本外推需要 RoPE scaling(位置插值 / 调大 base)。
上一张卡片我们用 RoPE 让"打"知道了自己的位置。但生成时还有一个瓶颈:GPT 逐 token 生成"我爱吃苹果",生成"果"时,需要看"我""爱""吃""苹"四个字的 Key 和 Value。每次都重算前 4 个字太慢——所以把它们的 K、V 缓存起来,这就是 KV Cache。
问题:KV Cache 占多少显存?
| 阶段 | 需要缓存多少 token 的 KV? | 显存 |
|---|---|---|
| 生成第 1 个字时 | 0 个历史的 KV | 0 |
| 生成第 512 个字时 | 511 个历史的 KV | 511 KB × 8层 = 4 MB |
| 生成第 1024 个字时 | 1023 个历史的 KV | 1023 KB × 8层 = 8 MB |
| 配置 | KV Cache 总量(1024 token) |
|---|---|
| MHA:32 头独立 K、V | 32头 × 2(K+V) × 128维 × 1024token × 32层 × 2字节 = 512 MB |
| MQA:32 头共享 1 对 K、V | 1组 × 2 × 128 × 1024 × 32 × 2 = 16 MB(省了 97%) |
| GQA:32 头分 8 组共享 KV | 8组 × 2 × 128 × 1024 × 32 × 2 = 128 MB(省了 75%) |
| 方案 | KV Cache | 质量损失 | 代表模型 |
|---|---|---|---|
| MHA(全独立) | 512 MB | 0% | 原始 Transformer |
| MQA(全共享) | 16 MB | PPL +1%(估算) | PaLM |
| GQA(8组) | 128 MB | 接近 MHA | Llama 2/3 |
不同注意力头会关注不同语义(示意:有的头偏向语法、有的偏向指代、有的偏向情感——具体分工因模型而异,来自事后探针分析,非固定编号)。全共享把所有头的视野压缩到一个 K/V 里,就像让所有医生共看同一张 X 光片——省胶片但丢诊断精度。GQA 给每组留下了独立的 K/V,保留了注意力多样性。
SwiGLU = Swish-Gated Linear Unit(Shazeer, Google, 2020):用门控替代单激活——多出一个"阀门"分支,让每一维的信息通过量由数据自己决定(它的算式、以及它为什么让 FFN 的中间维变成 11008 这种"怪数字",放在第 5 课逐层解剖时算)。GLU 门控思想源自 Dauphin et al.(2017)。
上一张卡片我们用 GQA 省了 KV Cache 的 75% 显存。现在解决两个计算瓶颈:归一化层太慢、激活函数丢信息。
| 步骤 | 操作 | 结果 |
|---|---|---|
| ① 减均值 | μ = (1.2-0.8+0.5-1.5)/4 = -0.15 | [1.2+0.15, -0.8+0.15, 0.5+0.15, -1.5+0.15] = [1.35, -0.65, 0.65, -1.35] |
| ② 除标准差 | σ = √((1.35²+0.65²+0.65²+1.35²)/4) = √(1.1225) ≈ 1.06 | [1.27, -0.61, 0.61, -1.27] |
| ③ 乘 γ | 可学习参数缩放 | 最终输出 |
| 步骤 | 操作 | 结果 |
|---|---|---|
| ① 算 RMS | RMS = √(mean(x²)) = √((1.44+0.64+0.25+2.25)/4) = √(4.58/4) = 1.07 | — |
| ② 直接除 | x / 1.07 × γ | [1.2/1.07, -0.8/1.07, 0.5/1.07, -1.5/1.07] = [1.12, -0.75, 0.47, -1.40] |
| 组件 | 作用 | 类比 |
|---|---|---|
| W_gate(门控权重) | 算出每个维度"该不该过" | 水龙头阀门 |
| Swish(x·W_gate) | Swish 门控值(Swish 即 SiLU,一种平滑激活) | 阀门开多大(关小≈0, 开大≈原值) |
| W_up·x | 信息本身 | 水管里的水流 |
| Swish(x·W_gate) × (x·W_up) | 门控值 × 信息 | 阀门控制流量 |
| 激活函数 | 验证 PPL(示意) | 说明 |
|---|---|---|
| ReLU | 9.7 | max(0,x):负值全截断,信息损失大 |
| GELU | 8.9 | GELU(x)=x·Φ(x):负值被软抑制,不再硬截断,但所有维度共用同一处理 |
| SwiGLU | 8.5 | 门控机制 Swish(x·W_gate)⊙(x·W_up):示意 PPL 低于前两者 |
保留 LayerNorm 的减均值 → 每层都要多算一次中心化(每层每步都生效,累积起来是可观的算子开销)。保留 ReLU → 负半轴信息直接清零。两者叠加:同等训练预算下,训练运行时间可降 7%~64%(RMSNorm 原文口径),困惑度也有改善——对大规模训练,这是实打实的算力与成本。
上一张卡片我们砍掉了 LayerNorm 的减均值。现在看 Norm 的位置——放在残差连接的入口还是出口?这决定了模型能叠多少层,因为 Norm 挡在残差主干上时,梯度回传到浅层会被逐层压缩。
打个比方:它就是主水管上的减压阀——装在主管道上(Post-Norm),水压(梯度)要先过阀门才到上游;改装在支路上(Pre-Norm),主管道就是一根直通管,上游水压几乎不受影响。
| 层数 | 梯度累积(示意:每层乘 0.5) |
|---|---|
| 第 1 层 | 0.5 |
| 第 4 层 | 0.5⁴ = 0.0625 |
| 第 12 层(BERT) | 0.5¹² ≈ 0.00024 |
| 第 24 层 | 0.5²⁴ ≈ 5.9 × 10⁻⁸(几乎为零) |
| 第 32 层 | 0.5³² ≈ 2.3 × 10⁻¹⁰(彻底为零,训不动) |
| 层数 | 梯度累积 |
|---|---|
| 无论多少层 | 1^N = 1(不变) |
| Post-Norm(BERT) | Pre-Norm(Llama) | |
|---|---|---|
| 公式 | Norm(x + Sublayer(x)) | x + Sublayer(Norm(x)) |
| 梯度路径 | 经过 Norm → 被压缩 | 不经过 Norm → 直达 |
| 最大深度 | ~24 层 | 80+ 层 |
| 32 层时梯度 | 0.5³² ≈ 2.3×10⁻¹⁰ | 1³² = 1 |
Pre-Norm 是工程上最简单但影响最深远的改造——只换了一行代码的位置(Norm 放前面而不是后面),就让模型层数从 20+ 突破到 80+。没有 Pre-Norm,ChatGPT 级别的模型根本堆不起来。
前面 5 张卡片我们走完了 RoPE 和 GQA 两步改造。后面三步是 RMSNorm、SwiGLU 与 Pre-Norm。这五条约束的解互不冲突,因此可以同时装进同一个 Block。打个比方:装修一套房子,水电、地板、门窗是不同工种的活,最后都装进同一个空间——但能这样拼装的根本原因,是它们各自作用在 Block 的不同环节,彼此不抢工位。把它们拼起来之后,从 2017 年的原始 Transformer 到 2024 年的 Llama,每一处改动的数值收益才能放在同一口径下算清。
| 方案 | KV 头数 | 算式(字节) | 结果 |
|---|---|---|---|
| MHA:每头独立 K、V | 32 | 32 × 2 × 128 × 1024 × 32 × 2 | 536,870,912 B = 512 MB |
| GQA:32 头分 8 组共享 KV | 8 | 8 × 2 × 128 × 1024 × 32 × 2 | 134,217,728 B = 128 MB |
| 结构 | 梯度逐层回传倍数 | 32 层后 |
|---|---|---|
| Post-Norm(原始 Transformer) | 0.5³² | ≈ 2.3 × 10⁻¹⁰(≈ 0) |
| Pre-Norm(Llama) | 1³² | 1 |
| 改造 | 量 | 口径 / 置信度 |
|---|---|---|
| GQA | KV Cache 512 MB → 128 MB(−75%) | 按 32 头 / 128 维 / 32 层 / 1024 token / fp16 算得,可复算 |
| Pre-Norm | 可堆层数 ~24 → 80+;32 层梯度 2.3×10⁻¹⁰ → 1 | 0.5 衰减为「示意」;层数上限 24 / 80+ 为经验值(估算) |
| RMSNorm + SwiGLU | 运行时间降低 7%~64%;SwiGLU 示意 PPL 8.5 vs ReLU 9.7 | 7%~64% 为 RMSNorm 论文实测口径;PPL 为示意值 |
| 改造 | 原始 Transformer (2017) | 现代 Llama (2023+) | 数值收益 |
|---|---|---|---|
| 位置编码 | Sinusoidal 绝对位置 | RoPE 旋转编码 | 编码相对距离,外推性优于绝对位置编码,但超出训练长度仍需 RoPE scaling(位置插值 / 调大 base) |
| 注意力 | MHA(全头独立 KV) | GQA(分组共享 KV) | KV Cache 512MB→128MB (-75%),质量损失很小(估算) |
| 归一化 | LayerNorm | RMSNorm | 砍掉减均值,论文实测运行时间降低 7%~64% |
| 激活函数 | ReLU | SwiGLU | 门控机制替代一刀切(示意 PPL 8.5 vs 9.7) |
| Norm 位置 | Post-Norm | Pre-Norm | 梯度不再衰减 (0.5³²→1³²),可堆 80 层 vs 24 层 |
这些改造不是"锦上添花"——GQA 没有的话 70B 模型推理根本跑不动显存,Pre-Norm 没有的话 32 层以上就训不动。它们是现代 LLM 的生存必需品,不是优化项。