原论文开篇就说"Transformer 每步的开销随当前序列长度平方增长"——这句话其实不严谨。每步解码的真实成本是 \(O(N)\)(有 KV Cache 时),\(O(N^2)\) 的是训练期的总序列开销。作者一开始被这个表述绕晕了,随后反应过来:2020 年时 FlashAttention 还不存在(2022 年才发布),训练普遍会把完整的 \(N \times N\) 矩阵物化到显存,而参考实现经常连 KV Cache 都不用、每步重算全部历史。线性注意力的历史意义要结合当时的工程水平来看。
22,580:从 GPT-2 到 Kimi K3
大模型架构七年进化全解
两万两千五百八十——这是能塞进一个 Kimi K3(2026)里的 GPT-2(2019)的数量。七年,参数规模膨胀了 22,580 倍。但这真的只是"规模"的故事吗?本文从零基础所需的五块数学积木讲起,沿架构演化主线走完从 GPT-2 到 Kimi K3 的每一步,公式逐项推导、参数逐笔对账。
两万两千五百八十。
这是 GPT-2(2019 年,约 1.24 亿参数)能被装进 Kimi K3(2026 年,2.8 万亿参数)的次数。七年时间,我们把模型做大了 22,580 倍。但故事真的只是"变大"吗?
在这篇工作日志式的长文里,我们会一步一步追溯这些年真正发生的事——你会发现变的东西很多,没变的东西也不少。原文作者沿着"线性注意力 → DeltaNet → Gated DeltaNet → Kimi Delta Attention"这条线性化主线走到了 Kimi K3;本文在完整保留这条主线(含全部代码与推导)的基础上,额外补上了零基础友好的数学学前班、手算示例、参数账本,以及注意力家族(MQA/GQA/MLA)、状态空间模型(Mamba)、MoE 稀疏化、训练与推理优化方法四条支线。读完后,一个几乎不懂 LLM 的人也能搭建起完整的知识地图——而且是真的会算的那种。
全文分四种内容:学前班(01)给零基础读者补齐全部数学前提,每块积木都手算一遍;主线章节(02、04–06、08、09、11、12)翻译并详解原文的架构演化链条;补课章节(03、07、10、13)是新增的背景知识;14、15 与附录 A/B 是时间线、总结与完整证明。所有代码均可直接运行验证,所有公式都给出直觉解释或逐步推导。如果你已经熟悉 Transformer 基础,可以从第 02 章直接开始。
01学前班:五块数学积木
LLM 论文里的公式看起来很多,真正反复使用的积木只有五块:向量、点积、矩阵乘法、Softmax、对数。下面每一块都手算一遍——手算过一遍的东西,才是真正学会的东西。
1.1 积木一到五,各手算一遍
积木一:向量不是"一个意思",而是一组坐标
为什么一个 token 要变成几千个数?因为单个数值很难同时表达"词性、语义、语气、实体、位置"等大量属性。模型给每个 token 一个 \(d\) 维向量;每个维度不一定有能用人话命名的含义,信息通常分布在许多维的组合方向上。
"猫" → \(x = [2,\ 1]\);"狗" → \(y = [1.8,\ 1.1]\);"税率" → \(z = [-1,\ 2]\)。
玩具例子里,"猫"和"狗"的方向比较接近;真实 embedding 可能是 768、4096 或 7168 维。
积木二:点积回答"两个方向有多匹配"
两个同维向量逐项相乘再相加,得到一个标量:\(q \cdot k = \sum_j q_j k_j\)。
- 对应位置相乘\(2 \times 3 = 6\),\((-1) \times 4 = -4\)。
- 把乘积相加\(6 + (-4) = 2\),所以 \(q \cdot k = 2\)。
- 几何解释\(q \cdot k = \|q\|\,\|k\|\cos\theta\)。长度固定时,方向越接近点积越大;方向相反点积为负。
把当前 token 的需求编码为 query \(q\),把每个历史 token 可被检索的标签编码为 key \(k\)。点积越大,就把对应 value 读得越多。注意:这是模型训练出来的匹配空间,不是人手规定的关键词匹配。
积木三:矩阵乘法就是"成批点积"
如果 \(A\) 是 \(m \times n\),\(B\) 是 \(n \times p\)(中间的 \(n\) 必须相同),结果 \(AB\) 是 \(m \times p\)。结果第 \((i,j)\) 项,就是 A 的第 i 行与 B 的第 j 列做点积。
检查左上角:\(1\times2 + 2\times0 + 0\times3 = 2\)。右下角:\((-1)\times1 + 3\times4 + 1\times(-2) = 9\)。
积木四:Softmax 把任意分数变成概率
点积可能是负数,也没有固定总和。Softmax 先取指数让每项为正,再除以总和,让全部权重加起来等于 1:\(p_i = e^{z_i} / \sum_j e^{z_j}\)。
- 取指数\(e^2 \approx 7.389\),\(e^1 \approx 2.718\),\(e^0 = 1\)。
- 求和\(Z = 7.389 + 2.718 + 1 = 11.107\)。
- 各自除以 Z\(p \approx [0.665,\ 0.245,\ 0.090]\),相加正好约等于 1。
数值稳定技巧:\(\text{softmax}(z) = \text{softmax}(z - \max(z))\)。给每个分数同时减 2,变成 \([0,-1,-2]\),概率不变,却避免计算溢出的大指数。FlashAttention 正是把这个技巧推广到分块计算(见 3.4 节与附录 A 的完整证明)。
积木五:对数把"概率很小"变成"惩罚很大"
若正确答案概率是 \(p\),交叉熵损失是 \(-\log p\)。预测越自信且正确,损失越接近 0;正确答案概率越小,损失越大:
| 正确答案概率 p | 损失 −ln p | 直觉 |
|---|---|---|
| 0.90 | 0.105 | 很有把握地答对,惩罚小 |
| 0.50 | 0.693 | 像抛硬币 |
| 0.10 | 2.303 | 把正确答案压得很低,惩罚大 |
| 0.001 | 6.908 | 极其自信地忽略正确答案 |
设 logits 是 \(z\)、Softmax 概率是 \(p\)、正确类别为 \(y\)。合在一起求导,会得到非常干净的结果:
\(\mathbb{1}[i=y]\) 表示:如果 i 是正确答案就取 1,否则取 0。沿用上面的 \(p=[0.665, 0.245, 0.090]\),假设第二类才正确,则梯度为 \([0.665, -0.755, 0.090]\)。梯度下降会压低第一、三类分数,提高第二类分数。它不是一句"模型知道错了",而是每个 logit 都收到一个可计算的改动方向。这个简洁的梯度是整个 LLM 训练大厦的地基。
1.2 "预测下一个 token"到底在优化什么
GPT 类模型的训练目标极朴素:给定前面的 token,提高真实下一个 token 的概率。复杂能力并不是另写了一个"理解世界"的损失函数,而是在压缩海量序列规律时逐渐形成的。
先把一句话错开一格
〈BOS〉 → 我 → 爱 → 猫
每个位置只看左边
我 → 爱 → 猫 → 〈EOS〉
训练时,四个位置可以一起算损失(这就是 GPU 并行训练的由来);但每个位置只能看自己和左边,不能偷看右边目标。这个约束由第 02 章的"因果掩码"强制执行。
第一行用条件概率链式法则把整段文本的概率拆成每一步;第二行取负对数,把"最大化乘积"变成"最小化求和",更适合数值计算。
它当然会记住一部分高频或重复内容,但参数容量、数据规模与训练过程迫使模型大量复用规律。想准确补全"巴黎是法国的___",需要实体关系;想补全代码,需要语法和变量依赖;想补全证明,需要推理模式。下一个 token 预测是目标,不代表内部只能学一个词级查表。
更准确地说,模型学习的是一个条件概率分布。生成时,我们再从这个分布里贪心取最大值或按温度采样。
从 token id 到概率:完整流水线
整数
token → 向量
跨 token 混合
每个 token 内变换
向量 → 概率
一张总地图:模型每层只做两类混合——注意力类模块在 token 之间搬运信息(跨位置),MLP 类模块在每个 token 内部变换信息(位置内)。本文后面所有的架构创新,都是在替换这两个格子里的东西。
# 省略 batch 维;T=序列长度,d=隐藏维,V=词表大小
X = Embedding[token_ids] # [T] → [T, d]
for layer in layers:
X = X + Attention(RMSNorm(X)) # 跨 token 混合
X = X + MLP(RMSNorm(X)) # 每个 token 内变换
logits = RMSNorm(X) @ W_vocab.T # [T,d]×[d,V] → [T,V]
prob = softmax(logits, axis=-1) # 每行成为词表概率
loss = -mean(log(prob[真实下一个token]))
不是。它首先是一张可训练的表 \(E \in \mathbb{R}^{V \times d}\)。token id 是 17,就取第 17 行。若把 id 写成只有一位为 1 的 one-hot 向量,查表等价于矩阵乘法——只是工程上没必要真的构造巨大的 one-hot。
残差连接与归一化:深网络的稳定器
若新分支 \(F\) 暂时没学好,模型至少能沿着 "+x" 的直通路径传递信息与梯度;深层网络因此更容易训练。这就是著名的 Pre-Norm 残差结构。
\(\text{RMS}(x) = \sqrt{\frac{1}{d}\sum_j x_j^2 + \epsilon}\),\(\quad \text{RMSNorm}(x) = g \odot x / \text{RMS}(x)\)
\(\text{RMS} = \sqrt{(3^2 + 4^2)/2} = \sqrt{12.5} \approx 3.536\),所以归一化后约为 \([0.849,\ 1.131]\)。它没有把向量变成同一个方向,只是把整体尺度拉回稳定范围;可训练缩放 \(g\) 再决定每一维需要多大。
MLP:两次(或三次)线性变换
原始 GPT-2 使用两层 MLP:先从 \(d\) 扩到 \(d_{ff}\),过 GELU,再投回 \(d\)。现代模型多用 SwiGLU 三路结构:
\(\odot\) 是逐元素乘法。gate 分支决定哪些扩展维度通过,up 分支提供内容,down 再压回模型宽度。若忽略 bias,三张权重分别含 \(d \times d_{ff}\)、\(d \times d_{ff}\)、\(d_{ff} \times d\) 个参数,总计约 \(3 d d_{ff}\)——这条公式后面会直接解释 Kimi K3 的万亿参数从哪里来(见 11.2 节的完整对账)。
- 五块积木:向量(坐标)、点积(匹配度)、矩阵乘法(成批点积)、Softmax(归一化权重)、−log(损失)。
- 训练目标 = 逐位置最大化真实下一 token 的概率;Softmax+交叉熵的梯度是 \(p - \mathbb{1}[y]\),极其干净。
- 总地图:每层 = 一次跨 token 混合(注意力)+ 一次 token 内变换(MLP),残差与归一化保驾护航。
02起点:解剖 GPT-2
要理解后面所有的改进,必须先把基准模型拆到骨头。GPT-2 是一个典型的 decoder-only(仅解码器)架构——今天几乎所有主流大模型(GPT、Llama、Qwen、DeepSeek、Kimi)都属于这个家族。它的完整前向传播用不到 15 行代码就能写完:
def forward(self, idx, pos): # idx: (b, t) token id;pos: (t,)
tok_emb = self.transformer.wte(idx) # (b, t) → (b, t, n_embd)
pos_emb = self.transformer.wpe(pos) # (t,) → (t, n_embd)
x = self.transformer.drop(tok_emb + pos_emb) # (b, t, n_embd)
for block in self.transformer.h:
x = block(x) # 12 个 Transformer 块,形状不变 (b, t, n_embd)
x = self.transformer.ln_f(x) # 最终 LayerNorm,(b, t, n_embd)
logits = self.lm_head(x) # (b,t,n_embd)×(n_embd,V) → (b, t, vocab_size)
return logits
2.1 从嵌入到输出:一次完整前向
输入文本先被 tokenizer 切成 token 序列(GPT-2 用 BPE,词表约 50,257 个)。每个 token 查表得到一个 768 维向量(token embedding),再加上它所在位置的向量(position embedding)——注意,GPT-2 的位置编码是可学习的绝对位置嵌入,它和 token 嵌入一样是一张待训练的查找表。这一点后来被彻底改变(见 3.2 节)。
嵌入相加后,数据依次流过 12 个结构完全相同的 Transformer 块。每个块放大看是这样的:
class Block(nn.Module):
def __init__(self, config):
super().__init__()
self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
self.attn = CausalSelfAttention(config)
self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
self.mlp = MLP(config)
def forward(self, x): # x: (b, t, n_embd)
x = x + self.attn(self.ln_1(x)) # 注意力子层 + 残差连接 → (b, t, n_embd)
x = x + self.mlp(self.ln_2(x)) # MLP 子层(内部升至 4×n_embd 再降回)→ (b, t, n_embd)
return x # (b, t, n_embd)
注意 x = x + f(x) 这种写法。它让每一层只需学习"对输入的修正量"而非完整的新表示,梯度可以沿恒等路径无损回传,使得几十上百层的深网络可以稳定训练。2017 年原始 Transformer 把 LayerNorm 放在子层之后(Post-LN),GPT-2 开始把它移到子层之前(Pre-LN),深模型训练稳定性大幅改善——这个顺序延续至今。而残差本身在第 12 章(AttnRes)还会被再次革命。
2.2 手算一次因果注意力
每个块的核心是因果自注意力(Causal Self-Attention)。它的代码同样短得惊人:
B, T, C = x.size() # batch、序列长度、嵌入维度;x: (B, T, C)
# 一次线性投影同时算出 q, k, v,再拆成多头
q, k, v = self.c_attn(x).split(self.n_embd, dim=2) # (B,T,C)→(B,T,3C),拆成三份各 (B, T, C)
k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs),hs = C/nh 为头维
# 注意力分数:每个 query 与所有 key 的点积,除以 √d_head
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1))) # (B,nh,T,hs)×(B,nh,hs,T) → (B, nh, T, T)
att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf')) # 因果掩码,(B, nh, T, T)
att = F.softmax(att, dim=-1) # (B, nh, T, T),每行和为 1
y = att @ v # (B,nh,T,T)×(B,nh,T,hs) → (B, nh, T, hs)
y = y.transpose(1, 2).contiguous().view(B, T, C) # 拼回头部 → (B, T, C)
y = self.resid_dropout(self.c_proj(y)) # 输出投影 → (B, T, C)
return y
浓缩成公式就是:
与其背公式,不如真正手算一次。设投影后已得到三枚 token 的 Q/K/V(二维单头),我们只求第 3 个 token 的输出,即取 \(q_3 = [0,1]\) 与三个 key 分别点积:
| 位置 | q(只用 q₃) | k | v | q₃·k / √2 |
|---|---|---|---|---|
| 1 | [1, 0] | [1, 0] | [1, 0] | 0/√2 = 0 |
| 2 | [1, 1] | [1, 1] | [0, 2] | 1/√2 ≈ 0.707 |
| 3 | [0, 1] | [0, 1] | [1, 1] | 1/√2 ≈ 0.707 |
- 缩放分数\(s = [0,\ 0.707,\ 0.707]\)。
- Softmax指数约为 \([1,\ 2.028,\ 2.028]\),总和 \(5.056\),权重约 \(a = [0.198,\ 0.401,\ 0.401]\)。
- 加权 value\(o_3 = 0.198[1,0] + 0.401[0,2] + 0.401[1,1]\)。
- 逐维相加\(o_3 \approx [0.599,\ 1.203]\)。
注意力输出不是"选中一个词",而通常是多个 value 的连续加权混合。权重由当前 query 决定,因此同一段历史对不同查询会返回不同结果。
因果掩码:训练时也不能偷看未来
对第 \(t\) 行,只允许看 \(1 \dots t\)。未来位置的分数加 \(-\infty\),经过 Softmax 后权重变成 0:
这使训练能并行计算全部位置,同时保持与逐 token 生成相同的信息约束。并行的是矩阵运算,不是让早期位置看到未来——这是"自回归生成"的数学保证,也是 decoder-only 与 BERT 类 encoder 的分水岭。
不是。假设 q、k 各维独立、均值 0、方差 1,那么乘积 \(q_j k_j\) 的方差约为 1;d 项相加后,点积方差约为 \(d\),标准差约为 \(\sqrt{d}\)。维度越大,未缩放的 logits 越极端,Softmax 越容易饱和成接近 one-hot,梯度随之消失。除以 \(\sqrt{d}\) 后方差回到约 1,不同头维度下数值尺度都稳定。
多头不是重复算同一件事
每颗头有不同投影矩阵,因而可以学习不同匹配子空间(语法、指代、位置邻近等)。若总隐藏维 \(d = h \cdot d_h\),拼接后仍回到 d 维,再经 \(W_O\) 混合各头。头的数量与组织方式将在第 03 章成为主角。
二次复杂度从哪里出现
\(QK^\top\) 生成 \(T \times T\) 分数矩阵:T 个 query 每个都和 T 个 key 比较,每次点积成本约 d,所以粗略计算量为 \(O(T^2 d)\),注意力权重显式存储为 \(O(T^2)\)。这张表感受一下"二次"的恐怖:
| 序列长度 T | 注意力分数个数 T² | 相对 4K |
|---|---|---|
| 4,096 | 16,777,216 | 1× |
| 32,768 | 1,073,741,824 | 64× |
| 1,048,576 | 1,099,511,627,776 | 65,536× |
FlashAttention(3.4 节)能避免把完整 T×T 矩阵写回显存,让计算更快、更省内存——但它计算的仍是精确 Softmax 注意力,算术复杂度仍是二次。而第 04 章的线性注意力则真正改写了数学结构。两条路线一个改工程、一个改数学,请勿混淆。
2.3 KV Cache:第一个工程妥协
最后一个 Transformer 块输出隐藏状态矩阵后,语言模型头(LM head)把它映射成词表大小的 logits。自回归解码时,每一步只需要最后一个位置的 logits 来采样下一个 token。
这就暴露了 decoder-only 生成的一个天然低效:模型为每一个输入位置都计算了完整表示,但每个解码步只消费最后一个位置的 logits。如果不做任何缓存,生成下一个 token 时所有这些计算都会被原样重做一遍。
KV Cache 来自一个朴素观察:把新生成的 token 接回输入后,之前所有 token 的 key 和 value 投影一字不差——因为因果掩码保证历史 token 的表示不受未来影响。那么把它们的 K、V 向量存下来,下一步只需为新 token 计算 QKV,再让它与缓存中全部历史 K、V 做注意力即可。计算量从每步 \(O(N^2)\) 降到 \(O(N)\)。
这块存储就是 KV Cache。它保留之前 \(N-1\) 个 token 的 K/V 向量,大小为 \(2 \times N \times n_{layer} \times n_{head} \times d_{head} \times 2\) 字节(FP16),随序列长度线性增长,很快就会大到把推理变成显存带宽瓶颈而非计算瓶颈。请记住这句话——后面从 GQA 到 MLA 到线性注意力,几乎整条演化线都是在跟"KV Cache 太大"这件事作斗争。
这是一个特别容易混淆的点:KV Cache 是"流水账",只追加,从不更新。生成第 1 个 token 时缓存里有 1 对 K/V;生成第 100 个时,里面是 99 对,一对都不少;再生成一个,就追加成 100 对。没有任何一对被覆盖或合并。
为什么不能合并?因为下一步注意力要做的是:"用我的新 query,逐个和历史上每一个 key 单独算点积,决定读哪个 token 的 value"。如果第 37 个和第 58 个 token 的 K/V 被合并了,模型就再也无法区分它们——而"每个 token 保留独立槽位、随时精确调取",正是标准注意力质量的根基。
算一笔账:GPT-2 里每个历史 token 要存 12 层 × 12 头 × 2(K、V)× 64 维 × 2 字节 ≈ 36 KB。序列每变长 1 个 token 就多吃 36 KB——是每多一个 token 多一份,不是总共一份。换成现代大模型(80 层、128 维头),每 token 约 0.3 MB,一个 128K 长上下文请求的缓存就要 40 GB 上下——这就是"显存爆炸"。
真正有"更新"语义的,是第 04 章线性注意力的状态 \(S\)(S = S + k @ v):所有历史被揉进一个固定大小的矩阵,旧信息会被新信息覆盖、干扰——那是"白板"。两种记忆方式的对抗就是本文主线:
| 标准注意力 KV Cache(流水账) | 线性注意力状态 S(白板) | |
|---|---|---|
| 结构 | 每个 token 一个独立槽位(列表) | 所有 token 共用一个固定矩阵 |
| 操作 | 只追加,永不覆盖 | 只更新(累加/改写),永不增长 |
| 检索 | 精确(能单独找到第 37 个 token) | 模糊(历史互相混叠) |
| 显存 | 随序列线性增长,会爆炸 | 恒定,极小 |
| 代表 | GPT-2、Llama(配 GQA/MLA 压缩) | 线性注意力、DeltaNet、KDA(第 04–09 章) |
从 MQA/GQA(少存几份)到 MLA(压缩每份的大小)再到线性注意力(干脆不存列表),七年来的架构创新,全都是在这张表的两端之间找平衡点。
2.4 参数账本:124M 怎么算出来
"参数"就是训练时会更新、推理时要读取的数字。看清每张权重矩阵的形状,参数量不再是厂商给出的神秘总数,而是一笔可以复核的加法账。Dense Transformer 一共四本账:
| 模块 | 主要矩阵 | 忽略 bias 后的参数量 | 随什么增长 |
|---|---|---|---|
| 词嵌入 | \(E \in \mathbb{R}^{V \times d}\) | \(Vd\) | 词表 V、宽度 d |
| 注意力 | \(W_Q, W_K, W_V, W_O\) 各约 d×d | \(4d^2\) | 宽度的平方 |
| GPT-2 式 MLP | d→d_ff→d | \(2dd_{ff}\) | 宽度×中间维 |
| SwiGLU MLP | gate、up、down 三张 | \(3dd_{ff}\) | 宽度×中间维 |
| 归一化 | 每维 scale(部分有 bias) | \(O(d)\) | 相对很小 |
因为主要矩阵两边都含 d。\(d \times d\) 在 d 翻倍后变成 \((2d)^2 = 4d^2\);如果层数也翻倍,主干参数大致变成 8 倍。参数扩展看的是矩阵面积,不是向量长度。
GPT-2 的配置:约 5 万词表、12 个块、12 个头、768 维嵌入:
vocab_size: int = 50304 # GPT-2 词表 50257,向上对齐到 64 的倍数以利计算
n_layer: int = 12
n_head: int = 12
n_embd: int = 768
- Token embedding\(50{,}257 \times 768 = 38{,}597{,}376\)。
- Position embedding\(1{,}024 \times 768 = 786{,}432\)。
- 单层注意力权重\(4 \times 768^2 = 2{,}359{,}296\)。
- 单层 MLP 权重\(2 \times 768 \times 3{,}072 = 4{,}718{,}592\)。
- 12 层主权重\(12 \times (2{,}359{,}296 + 4{,}718{,}592) = 84{,}934{,}656\)。
- 合计\(38.60\text{M} + 0.79\text{M} + 84.93\text{M} \approx 124.3\text{M}\),加上 bias 与 LayerNorm 约 0.12M,总计 ≈ 124.44M。输出词表投影与 token embedding 共享权重,所以不再另加 38.6M。
合计约 1.24 亿参数。而 Kimi K3 有 2.8 万亿参数——一个 K3 里大约装着 22,580 个 GPT-2。
- GPT-2 = token/位置嵌入 + 12×(Pre-LN 注意力块 + Pre-LN MLP 块) + LM head,共 1.24 亿参数。
- 自注意力 = 可微分查表:Q 匹配 K,加权取出 V;手算过一遍 3-token 示例就不再神秘。
- KV Cache 用显存换时间,但随序列线性增长——它是后续七年大量架构创新的"靶子"。
- 参数四本账:嵌入 \(Vd\)、注意力 \(4d^2\)、MLP \(2dd_{ff}\) 或 \(3dd_{ff}\)、归一化 \(O(d)\)。
03补课 I:注意力的家族演化
原文的主线从"线性注意力"直接开讲,但在那之前,工业界其实先走了另一条路:不改注意力的数学,只改它的存储与工程实现。这条支线孕育了 GQA、RoPE、RMSNorm、FlashAttention 等今天所有主流模型的标配,必须补上。
3.1 MHA → MQA → GQA → MLA:一场针对 KV Cache 的围剿
GPT-2 用的是多头注意力(MHA, Multi-Head Attention):12 个头,每个头有自己独立的 Q、K、V。问题在于 KV Cache——每个头都要为每个历史 token 存一份 K 和 V。12 层 × 12 头 × 2(K和V)× 64 维 × 每 token,长序列下显存爆炸。
演化链条清晰得像教科书:
- MQA(Multi-Query Attention, Shazeer 2019):所有 query 头共享同一份 K 和 V。KV Cache 直接除以头数(12 倍压缩),解码吞吐大幅提升,但质量有可感知的损失。
- GQA(Grouped-Query Attention, 2023,Llama 2 推广):折中方案——把 Q 头分成若干组,每组共享一份 K/V。Llama 2 70B 用 \(h=64\) 个 Q 头配 \(g=8\) 组 KV,缓存压缩 8 倍,质量几乎无损。GQA 此后成为 2023–2024 年开源模型的默认配置(Llama、Qwen、Mistral)。
- MLA(Multi-head Latent Attention, DeepSeek-V2, 2024):换了个思路——不共享 K/V,而是把所有头的 K/V 联合低秩压缩成一个小的潜向量(latent vector)存起来,用时再解压。压缩率比 GQA 更高,质量反而更好。这是 DeepSeek-V2/V3 与 Kimi 系模型的核心技术,第 10.2 节会详细拆解。
| 机制 | 提出 | 每 token KV 缓存量 | 质量 | 代表模型 |
|---|---|---|---|---|
| MHA | 2017 | \(2 \cdot n_{head} \cdot d_h\) | 基准 | GPT-2/3、原始 Transformer |
| MQA | 2019 | \(2 \cdot d_h\)(÷头数) | 略降 | Falcon、PaLM 早期 |
| GQA | 2023 | \(2 \cdot n_{group} \cdot d_h\) | ≈MHA | Llama 2/3QwenMistral |
| MLA | 2024 | \(d_c + d_h^R\)(潜维度,压缩 90%+) | 反超 MHA | DeepSeek-V2/V3Kimi K2/K3 |
3.2 位置编码:RoPE 的胜利
GPT-2 用可学习绝对位置嵌入,有两个硬伤:训练时见过的最长序列就是能力上限,且位置信息与内容信息纠缠在一起。演化路径:
- 正弦位置编码(2017 原始 Transformer):固定公式生成,无需学习,可外推性稍好,但依然脆弱。
- RoPE(Rotary Position Embedding,苏剑林 2021):把位置编码成对 Q/K 向量的二维子空间旋转——位置 \(m\) 的 query 旋转 \(m\theta\) 角。Llama 全系、Qwen、DeepSeek、Kimi 全部使用 RoPE,是近年少数"零争议"的技术共识。
- 长度外推补丁:RoPE 直接外推到训练未见的长度会失效,于是有了位置插值(Position Interpolation)、NTK-aware 缩放、YaRN 等一系列"续长"技术,让 4K 训练的模型能用到 128K 甚至更长。
- ALiBi(2022):不给词嵌入加位置,而是直接给注意力分数加一个与距离成正比的惩罚项,外推性极佳(Bloom 采用)。
- NoPE / 因果位置涌现:另一个有趣发现是,decoder-only 模型即使不加任何位置编码也能学到一定位置感;而第 04 章要讲的线性注意力/RNN 类架构,位置感天然内建于递归结构中。
目标:找到一个位置编码方案,使 \(q_m\) 和 \(k_n\) 的内积仅依赖于相对位置 \(m-n\):
\[ \langle f_q(x_m, m),\ f_k(x_n, n) \rangle = g(x_m, x_n,\ m - n) \]二维情形用旋转矩阵实现:
\[ R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}, \qquad f_{\{q,k\}}(x, pos) = R(pos \cdot \theta)\, x \]关键恒等式:旋转矩阵满足 \(R(m\theta)^\top R(n\theta) = R((n-m)\theta)\),于是
\[ \big(R(m\theta)q\big)^\top \big(R(n\theta)k\big) = q^\top R\big((n-m)\theta\big)\, k \]点积自然只依赖相对距离 \(n-m\)。推广到 d 维(偶数),把维度两两分组,每组分配自己的旋转频率 \(\theta_i = 10000^{-2i/d}\):高频组管近距离结构,低频组管远距离结构——位置信息直接编码在向量旋转中,无需任何额外的位置嵌入向量相加,且注意力随距离自然衰减。
3.3 归一化与激活:安静的地基
这些改动不性感,但每一个都省下了真金白银:
标准 LayerNorm:
\[ \text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{\sigma} + \beta, \quad \mu = \tfrac{1}{d}\textstyle\sum_i x_i, \quad \sigma = \sqrt{\tfrac{1}{d}\sum_i (x_i - \mu)^2} \]RMSNorm 去掉均值中心化(re-centering),只做缩放:
\[ \text{RMSNorm}(x) = \gamma \cdot \frac{x}{\sqrt{\tfrac{1}{d}\sum_i x_i^2 + \epsilon}} \]为什么可以去掉均值?实验表明 re-centering 对性能影响甚微,但省掉它能减少约 10–20% 的归一化计算量。归一化在 Transformer 每层出现两次,这个节省非常可观——Llama 之后 RMSNorm 成为事实标配。
| 组件 | 旧 | 新 | 为什么换 |
|---|---|---|---|
| 归一化 | LayerNorm(减均值除方差) | RMSNorm(只除均方根) | 去掉均值平移,计算少约 10–20%,效果无损 |
| 归一化位置 | Post-LN | Pre-LN | 深层训练稳定,免去复杂 warmup;GPT-2 开创 |
| 稳定性补丁 | — | QK-Norm、z-loss、sandwich norm | 防止注意力 logits 与残差流数值爆炸(第 12 章 AttnRes 与此呼应) |
| FFN 激活 | ReLU / GELU | SwiGLU:\((xW_1 \odot \text{SiLU}(xW_g))W_2\) | 门控线性单元,同参数下效果更优;隐藏维取 \( \frac{8}{3}d \) 补齐参数量 |
| FFN 结构 | 升维→激活→降维 | 门控三路 | PaLM/Llama 确立;第 11 章的 SiTU 是这个家族的远亲 |
3.4 FlashAttention:不改数学,改世界
2022 年 Tri Dao 的 FlashAttention 可能是对工业界影响最大的单篇论文。它没有改变注意力的任何数学定义,输出与标准实现逐位等价——它改的是计算在 GPU 上的组织方式。
关键洞察:标准实现的瓶颈不是 FLOPs,而是显存读写(IO)。朴素实现把 \(N \times N\) 的注意力分数矩阵完整物化到 HBM(高带宽显存),再读出来做 softmax、再写回、再读出乘 V——序列 8K 时这块中间矩阵就有数 GB。GPU 的 SRAM(片上缓存)快一个数量级但极小(每 SM 约 200KB)。FlashAttention 的做法:
- 分块(Tiling):把 Q/K/V 切成能塞进 SRAM 的小块;
- 在线 softmax(Online Softmax):边扫 K/V 块边维护运行中的最大值与归一化因子,一遍扫描完成精确 softmax,永不物化 \(N \times N\) 矩阵;
- 重计算:反向传播不存注意力矩阵,用到时重新算一遍——多花 FLOPs 省 IO,反而更快。
标准 Softmax 需要遍历向量两次(一次求最大值保数值稳定,一次求指数和)。向量太大装不进 SRAM 时,每次都得去 HBM 读。在线 Softmax 用一次遍历维护两个运行统计量 \((m, \ell)\):
- 初始化\(m_0 = -\infty\),\(\ell_0 = 0\)。
- 对每个 \(x_i\)更新运行最大值:\(m_i = \max(m_{i-1},\ x_i)\)。
- 重缩放累加\(\ell_i = \ell_{i-1} \cdot e^{m_{i-1} - m_i} + e^{x_i - m_i}\)。
- 最终输出\(o_i = e^{x_i - m_n} / \ell_n\)。
直觉:\(\ell_i\) 始终等于 \(\sum_{j=1}^{i} e^{x_j - m_i}\)——每个新元素到来时,旧的指数和乘上修正因子 \(e^{m_{i-1}-m_i}\) 被"换底",新指数直接加入。结果与两遍扫描逐位等价,却只需一遍。完整的归纳法证明见附录 A。
后续 FlashAttention-2(更好的并行划分)与 FlashAttention-3(针对 Hopper GPU 的异步流水线)继续压榨硬件。记住一个连锁反应:FlashAttention 让"全长注意力"在工程上变得便宜,一度压制了线性注意力的生存空间——直到长序列 + 高并发解码场景下 KV Cache 的带宽瓶颈再次把线性化路线请回牌桌。这正是第 04 章的历史背景。
- KV Cache 是注意力的阿喀琉斯之踵:MHA→MQA→GQA→MLA 是一条"缓存压缩"演化线。
- RoPE 用旋转矩阵让点积只含相对位置;RMSNorm + SwiGLU 是 2023 年后所有开源模型的隐形标配。
- FlashAttention = Tiling + 在线 Softmax + 重计算:不改数学却改变了整个行业的成本曲线。
04线性注意力:把 RNN 请回来
现在回到原文主线。第一条试图从数学上掀掉 \(O(N^2)\) 桌子的路线,是 2020 年的线性注意力(Linear Attention)。
Softmax 注意力在 \(q \cdot k\) 点积之后施加非线性(指数函数),这把每个 query 和每个 key 牢牢耦合在一起——你必须先算出完整的 \(N \times N\) 分数矩阵才能归一化。线性注意力换了个顺序:先对 q 和 k 各自施加一个特征映射(如 ELU+1),再让它们相乘:
这个调换的魔力在于结合律:\(\phi(q)\cdot(\phi(k)^\top v)\) 可以先算 \(\phi(k)^\top v\)——而所有历史 token 的 \(\phi(k_j)^\top v_j\) 可以累加成一个固定大小的 \(D \times D\) 矩阵 \(S\)。于是整件事变成了一个 RNN:每来一个 token,就往状态 \(S\) 里加一点东西;读出时只需一次 \(q \cdot S\) 矩阵向量乘。因果版本可以写成递推:
def forward(self, x, mask=None, cache=None):
# x: (b, t, d)
b, t, d = x.shape
d_head = d // self.num_heads # 头维 dh = d/h
qkv = self.qkv_proj(x) # (b,t,d) → (b, t, 3d)
q = qkv[:, :, :d].view(b,t,h,d_head).transpose(1,2) # (b, h, t, dh)
k = qkv[:, :, d:2*d].view(b,t,h,d_head).transpose(1,2) # (b, h, t, dh)
v = qkv[:, :, 2*d:].view(b,t,h,d_head).transpose(1,2) # (b, h, t, dh)
k = F.elu(k) + 1 # 特征映射:保证非负,(b, h, t, dh)
k = k.transpose(-1,-2) # (b, h, dh, t)
q = F.elu(q) + 1 # (b, h, t, dh)
S, z = cache if cache is not None else (0.0, 0.0) # S: (b,h,dh,dh);z: (b,h,dh,1)
S = S + k @ v # (b,h,dh,t)×(b,h,t,dh) → (b,h,dh,dh),固定 D×D 不随序列增长
z = z + k # (b,h,dh,1),归一化项累加
o = q @ S # (b,h,t,dh)×(b,h,dh,dh) → (b,h,t,dh),一次矩阵向量乘读出
denom = q @ z # (b,h,t,1)
o_scaled = o / denom # (b,h,t,dh)
...
cache = (S, z) # 形状恒定,不随 t 增长
return o_proj, cache # o_proj: (b, t, d)
三个层面都线性:训练总开销从 \(O(N^2 d)\) 降到 \(O(N d^2)\)(对 N 线性);解码每步只读写固定大小的状态,与已生成长度无关(O(1));状态不随序列增长(O(1) 显存)。代价是引入了第二个维度 d²——当 \(N \gg d\) 时大赚,N 短时反而不划算。
代价是什么?表达力。把注意力拆解为三步看最清楚:
- 让 qk 分数非负——softmax 用指数函数,线性注意力用 ELU+1;
- 除以分数总和归一化;
- 对 value 做加权平均。
三步都还在,但第一步的"非负化"函数从指数换成了 ELU+1——这是对 softmax 核的一个低表达力近似。指数函数能制造尖锐的、赢者通吃的注意力分布(精准检索单个 token),ELU+1 造出来的分布则更平、更模糊。实际精度损失取决于架构和任务,但在需要"从长上下文中精确捞出某个细节"的任务上,朴素线性注意力的劣势是实打实的。
还有一个更本质的问题藏在"固定大小状态"里:KV Cache 为每个历史 token 保留独立槽位(无损但无限增长);而 \(D \times D\) 状态把所有历史压缩进固定矩阵(有界但必然互相干扰)。这就像把无数张卡片不断塞进一个固定大小的抽屉——塞得越多,卡片之间的字越糊。下一章的 DeltaNet,就是来解决"抽屉塞满之后怎么办"的。
- 线性注意力 = 把 softmax 换成特征映射,利用结合律把历史折叠成固定 D×D 状态。
- 收益:解码每步 O(1) 读写、状态不随序列增长;代价:注意力分布变钝,精确检索能力下降。
- 它把 Transformer 重新接回了 RNN 的血脉——此后所有线性化工作都在修这条血脉的两个缺陷:精度与记忆管理。
05DeltaNet:会改错的白板
有限容量的缓存,必然面临覆盖与干扰。线性注意力里,第 \(i-1\) 个 token 的状态没有自己的槽位——它被加进了同一个 \(D \times D\) 矩阵。新 query 再想精确读回某个早期 token 的独立表示,已经做不到了。
这个"加法"既是效率的来源,也是灾难的来源:用累加而非拼接来更新缓存,缓存才不至于 O(N) 膨胀;但也正是累加让信息互相污染。Schlag 等人在《Linear Transformers Are Secretly Fast Weight Programmers》里把问题说得极为透彻:
"当序列长度超过存储容量,模型就进入了'超容量'运行状态。要在此状态下正常工作,模型应该学会与记忆内容动态交互,有选择地决定保留哪些键值关联、删除哪些。纯粹累加的指令不适合这个目的……向有限记忆无休止地添加新关联, inevitably 会触及极限。"
5.1 先理解:一块联想记忆白板
先看单个关联是怎么存取的。写入 \(S = k^\top v\)(外积);用同一个 key 读出:\(k(k^\top v) = (kk^\top)v = \|k\|^2 v\)。也就是说,读出的正好是 value 乘以 key 的模长平方——只要把 k 归一化到单位长度(或读出后除以模长),就能无损取回 v。
Q 在这里是一个可学习的指针:\(W_q\) 和 \(W_k\) 读取同一条残差流,某个事实的 query 会指向该事实当初被写入的 key 方向。这套机制与老世纪的 Hopfield 网络一脉相承——Transformer 的注意力本来就是一种现代联想记忆。
5.2 Delta 规则:先擦除,再写入
Delta 规则(Widrow-Hoff 规则的现代化身)的更新逻辑:
- 先用当前 key 去读缓存:\(v_{old} = k_i S\),看看这个位置现在存着什么;
- 算出增量(delta):想存的新值与旧值之差,乘上写入强度 \(\beta\):\(u_i = \beta_i (v_i - v_{old})\);
- 把增量写回:\(S \leftarrow S + k_i^\top u_i\)。
def forward(self, x, mask=None, cache=None):
# x: (b, t, d)
b, t, d = x.shape
...
q = F.normalize(F.silu(q), dim=-1) # (b,h,t,dh),单位长度
k = F.normalize(F.silu(k), dim=-1) # (b,h,t,dh),归一化:保证读出无损
beta = torch.sigmoid(self.w_beta(x)).view(b, 1, t, 1) # (b,1,t,1),新增:逐 token 写入强度
S = cache if cache is not None else 0.0 # (b, h, dh, dh)
v_old = k @ S # (b,h,t,dh)×(b,h,dh,dh) → (b,h,t,dh),读出这个 key 位置上的旧信息
u = beta * (v - v_old) # (b,h,t,dh),delta:只写"真正新"的部分
S = S + k.transpose(-1, -2) @ u # (b,h,dh,t)×(b,h,t,dh) → (b,h,dh,dh),与之前相同的外积写入
o = q @ S # (b,h,t,dh)×(b,h,dh,dh) → (b,h,t,dh),读出(不再需要归一化分母)
o = o.transpose(1, 2).contiguous().view(b, t, d) # (b, t, d)
return self.o_proj(o), S # (b,t,d);cache S: (b,h,dh,dh)
旧信息被定向移除,新信息写在原处——缓存从"只增不减的流水账"变成了"可以修改的白板"。\(\beta\) 由输入决定(sigmoid 门控),模型自己学会每个 token 该用多大力气改写记忆。
这个视角下,状态矩阵 \(S\) 本身是一个"快速权重"网络——普通权重靠梯度下降缓慢更新(训练期),而 \(S\) 在前向传播中被输入数据实时编程(推理期)。DeltaNet 让模型学会给自己写程序:根据当前内容,精确修改自己的记忆矩阵。这是 1990 年代 Schmidhuber 一脉思想的现代复兴,也是"in-context learning 即隐式梯度下降"理论家族的成员。
- 线性注意力的状态是联想记忆;纯累加写入导致超容量后信息混叠。
- Delta 规则 = 读出旧值 → 算差值 → 写回增量,实现定点修改而非盲目叠加。
- 写入强度 β 由数据驱动,模型自学"何时改、改多重"。
06并行化 DeltaNet:分块的魔术
"这是全文最难的一节,我花了大约七个小时才建立起可用的理解,所以我从实现出发来构建解释。"——本章我们保留这个路线:先看清问题,再看数学重参数化,最后落到代码。一句话概括:DeltaNet 把一阶线性递归的转移矩阵写成了广义 Householder 变换的乘积,从而能做分块并行的前向传播,把线性时间训练变得硬件友好。
6.1 问题:prefill 阶段的顺序依赖
推理时逐 token 递归很自然,但训练/预填充(prefill)时如果把 T 个 token 挨个过一遍 delta 规则,GPU 只能干瞪眼:
S = torch.zeros(b, h, dh, dh) if cache is None else cache # (b,h,dh,dh)
outs = []
for i in range(t): # 纯顺序循环,T 次小矩阵运算
k_i = k[:, :, i:i+1] # (b,h,1,dh)
v_i = v[:, :, i:i+1] # (b,h,1,dh)
b_i = beta[:, :, i:i+1] # (b,1,1,1),标量写入强度
v_old = k_i @ S # (b,h,1,dh)×(b,h,dh,dh) → (b,h,1,dh)
u_i = b_i * (v_i - v_old) # (b,h,1,dh)
S = S + k_i.transpose(-1, -2) @ u_i # (b,h,dh,1)×(b,h,1,dh) → (b,h,dh,dh),写入
outs.append(q[:, :, i:i+1] @ S) # (b,h,1,dh)
o = torch.cat(outs, dim=2) # (b,h,t,dh)
这与标准注意力不同:delta 规则在每个 key 上都要先做一次"读出—修正",第 \(i\) 步依赖第 \(i-1\) 步修正完的状态,通向并行矩阵乘法的路径一点也不显然。其实哪怕没有 delta 修正,朴素的线性注意力 prefill 也同样是顺序的(\(S\) 逐步累加)。
6.2 分块:两种结合顺序的缝合
出路是分块(chunking)。把序列切成若干长度为 C 的块(实践中 C 常取 64 或 128,因为张量核指令在这个粒度上效率最高,如 NVIDIA 的 UMMA):
- 块内:按 \(q(k^\top v)\) 的顺序算——先算分数再做掩码加权,即"真注意力"模式,块内复杂度 \(O(C^2)\);
- 块间:按 \((k^\top v)q\) 的顺序走——每块结束把整个块的贡献折叠进状态 \(S\),下一块用一次矩阵乘法从状态读出,即"递归"模式。
S = torch.zeros(b, h, dh, dh) if cache is None else cache # (b,h,dh,dh)
outs = []
for i in range(t // C): # 只循环 t/C 次,每次是大矩阵乘
q_c = q[:, :, i*C:(i+1)*C] # (b,h,C,dh)
k_c = k[:, :, i*C:(i+1)*C] # (b,h,C,dh)
v_c = v[:, :, i*C:(i+1)*C] # (b,h,C,dh)
o_prev = q_c @ S # (b,h,C,dh)×(b,h,dh,dh) → (b,h,C,dh),这一块之前所有历史的贡献
attn = (q_c @ k_c.transpose(-1,-2)).tril() # (b,h,C,C),块内掩码注意力
o_curr = attn @ v_c # (b,h,C,C)×(b,h,C,dh) → (b,h,C,dh)
o = o_prev + o_curr # (b,h,C,dh)
S_new = k_c.transpose(-1,-2) @ v_c # (b,h,dh,C)×(b,h,C,dh) → (b,h,dh,dh),整块折叠进状态
S = S + S_new # (b,h,dh,dh)
outs.append(o)
o = torch.cat(outs, dim=2) # (b,h,t,dh)
成本的分解非常优雅:固定部分 \(2Ld^2\)(状态维护,与 C 无关)+ 增长部分 \(2LCd\)(对角线上的分数矩阵)。C = L 时退化为标准注意力(第二项变成 \(2L^2d\),二次);C = 1 时就是纯线性注意力。中间的 C 用一点点块内计算换取硬件利用率——纯 FLOPs 上 C=1 最便宜,但墙钟时间未必:GPU 只有在工作能映射到大块矩阵乘时才跑得快。
6.3 把 delta 修正塞进分块:WY 重参数化
把同样的分块思路直接套到 delta 规则上会遇到障碍:每个 key 的修正量依赖当时的状态,而分块并行要求块内所有修正一次性算完:
v_old = k_i @ S # (…,1,dh)×(…,dh,dh) → (…,1,dh)
u_i = b_i * (v_i - v_old) # (…,1,dh),每个 u_i 都依赖前面所有修正后的 S
我们需要按顺序经过每一个中间状态,才能算出该减去的量——不做数学重参数化就没法并行。作者们的做法是把递推
展开后发现:\(S_T\) 可以写成初始状态左乘一串 \((I - \beta k k^\top)\) 的乘积,再加上各 \(v k^\top\) 项的加权和——这正是 WY 表示(数值线性代数中 Householder 变换乘积的紧凑形式)。修正量 \(u\) 可以通过对一个 C×C 下三角矩阵做前代法求逆一次性解出。从递推到展开式的完整四步推导见附录 B;这里直接落到代码——分块代码一次算出整块 C 个 delta:
def chunk_delta_rule_forward(Q, K, V, beta, C):
# Q,K,V: (L, d);beta: (L,);C: 块长
L, d = Q.shape
# 分块
Q, K, V = map(lambda x: x.reshape(-1, C, d), [Q, K, V]) # 各 (L/C, C, d)
beta = beta.reshape(-1, C) # (L/C, C)
K_beta = K * beta.unsqueeze(-1) # (L/C, C, d)
V_beta = V * beta.unsqueeze(-1) # (L/C, C, d)
# 向量化前代法:求下三角矩阵 (I + 严格下三角) 的逆
T = -(K_beta @ K.t()).tril(-1) # (L/C, C, C),严格下三角
for i in range(1, C):
T[i, :i] = T[i, :i] + (T[i, :, None] * T[:, :i]).sum(-2)
T += torch.eye(C) # (L/C, C, C),下三角、对角为 1
W = T @ K_beta # (L/C,C,C)×(L/C,C,d) → (L/C, C, d),修正后的 key
U = T @ V_beta # (L/C, C, d),修正后的 value(含全部 delta)
# 分块并行主循环
S = torch.zeros(d, d) # (d, d)
O = torch.empty_like(V) # (L/C, C, d)
for i in range(L // C):
q_i, k_i, w_i = Q[i], K[i], W[i] # 各 (C, d)
u_i = U[i] - w_i @ S # (C,d) − (C,d)×(d,d) → (C, d),整块的修正量一次算完
o_inter = q_i @ S # (C, d),历史贡献(递归模式)
A_i = (q_i @ k_i.t()).tril() # (C, C),块内注意力分数
o_intra = A_i @ u_i # (C,C)×(C,d) → (C, d),块内贡献(注意力模式)
S += k_i.t() @ u_i # (d,C)×(C,d) → (d, d),更新状态
O[i] = o_intra + o_inter # (C, d)
return O.reshape(L, d) # (L, d)
至此我们来到第一个里程碑式的对比——标准多头注意力 Transformer vs DeltaNet Transformer:
- 分块 = 块内注意力(精确、二次)+ 块间递归(压缩、线性),C 是精度和效率的旋钮。
- WY 重参数化把"逐个修正"变成"整块求解",delta 规则由此可以吃满张量核。
- 注意全文的反复主题:数学等价变换换硬件友好性——与 FlashAttention 异曲同工。
07补课 II:状态空间模型与 Mamba
下一章要讲的 Gated DeltaNet 里有一半血统来自 Mamba。所以在讲"遗忘门"之前,必须先认识线性注意力的平行宇宙——状态空间模型(State Space Models, SSM)。
7.1 从控制论到深度学习:SSM 的数学基础
状态空间模型是控制论的老古董:一个连续时间的线性时不变系统
其中 \(x(t)\) 是输入信号(LLM 中是 token 嵌入的一个维度),\(h(t) \in \mathbb{R}^N\) 是隐状态,\(A\) 是状态转移矩阵,\(B\)、\(C\) 是输入/输出投影。要处理离散的 token 序列,需要用零阶保持(Zero-Order Hold)离散化:
离散化后的递推就是一个线性 RNN——每步成本与序列位置无关,这就是 SSM 实现 \(O(L)\) 总复杂度(而非 \(O(L^2)\))的原因。它和线性注意力的 \(S_t = S_{t-1} + k_t^\top v_t\) 神似并非巧合:两者都是"用一个固定大小的隐状态压缩全部历史",只是方言不同。
SSM 家族的关键节点:
- S4(Gu et al., 2021):第一个能在超长序列上打赢 Transformer 的 SSM。核心是用 HiPPO 矩阵初始化 \(A\)(一种数学上最优的"历史压缩基"),并用对角化 + 卷积技巧实现并行训练。但 S4 的转移矩阵是固定的——不随输入变化,如同一台不会根据内容调整的记忆机器。
- Mamba(Gu & Dao, 2023):让 \(B\)、\(C\) 和离散化步长 \(\Delta\) 全部变成输入的函数——"选择性(selective)"。模型可以按内容决定这一步是认真记还是忽略。代价是失去了卷积并行形式,于是配套发明了硬件感知的并行扫描(parallel scan)算法,在 SRAM 里完成递推。Mamba 在语言建模上第一次以线性时间架构逼平同尺寸 Transformer。
- Mamba-2 / SSD(2024):揭示了 SSM 与线性注意力的严格对偶——标量门控的 SSM 就是"带遗忘门的线性注意力"。这一洞见统一了两个平行宇宙,也直接铺平了 Gated DeltaNet 的道路(下一章)。
经典 SSM 的参数 \(A, B, C, \Delta\) 是时不变的——对所有 token 一视同仁,无法根据内容决定记住什么、忽略什么。Mamba 让 \(B, C, \Delta\) 依赖输入:
\[ B_t = s_B(x_t), \quad C_t = s_C(x_t), \quad \Delta_t = \text{softplus}\big(s_\Delta(x_t)\big) \]其中 \(s_B, s_C, s_\Delta\) 是可学习的线性投影。效果是:\(\Delta_t\) 大 → 更关注当前输入("忘记"历史);\(\Delta_t\) 小 → 更多依赖历史状态。但由于参数变成输入依赖,递推不再是时不变的,无法直接用卷积并行——Mamba 为此设计了专门的硬件感知选择性扫描算法。
把 SSM 的递推展开成"卷积形式":
\[ y_t = \sum_{s=0}^{t} C_t\, \bar{A}^{\,t-s}\, \bar{B}_s\, x_s \]对比线性注意力的展开 \(o_t = \sum_s \phi(q_t)\, \phi(k_s)\, v_s\)——\(\bar{A}^{t-s}\) 扮演的正是"随距离衰减的注意力分数"的角色。Mamba-2 利用这个对偶性设计了结构化矩阵乘法(SSD),让 SSM 也能像注意力一样用分块矩阵乘高效实现。这个对偶是第 08 章的钥匙:SSM 的衰减门 + DeltaNet 的精准写入,可以在同一个数学框架里合体。
7.2 同一条赛道上的其他选手
| 架构 | 年份 | 核心思想 | 一句话评价 |
|---|---|---|---|
| RWKV | 2023 | 把 RNN 重构成可并行训练的形式:token-shift + 指数衰减加权的"无 softmax 注意力" | 开源社区宠儿,证明 RNN 能训到 14B 级 |
| RetNet | 2023 | 微软提出 retention 机制:训练并行 / 推理递归 / 分块递归三种形式等价 | 与线性注意力思路高度同构 |
| Hyena | 2023 | 用超长卷积 + 数据控制门控替代注意力 | 长卷积路线的代表,后被 SSM 吸收 |
| S4 / S4D | 2021+ | HiPPO 初始化 + 对角化 SSM | 理论奠基者,工程上被 Mamba 接棒 |
| Mamba / Mamba-2 | 2023/24 | 输入依赖的选择机制 + 硬件感知扫描;SSD 对偶理论 | 线性化路线的另一面旗帜 |
| xLSTM | 2024 | 现代化 LSTM:指数门控 + 矩阵记忆(mLSTM) | LSTM 发明者的"复仇",与 DeltaNet 思想相通 |
1990 年代,RNN(LSTM)被 Transformer 革了命;2020 年代,研究者们前赴后继地把 RNN 请回来,只是为了干掉 Transformer 的 \(O(N^2)\) 和 KV Cache。但请注意结局:最终胜出的不是任何"纯种"架构,而是混血——下一章起你会看到,所有前沿模型都是"线性递归记忆 + 周期性全注意力"的混合体。
08Gated DeltaNet:学会遗忘
回到主线。DeltaNet 给了我们精确修改缓存的能力:每来一个携带新事实的 key,就能读出该位置旧信息并定向替换。但它有一个盲区——
它只能遗忘"有替代品的关联"。换话题时想清空一批旧记忆?想让不重要的事实随时间自然衰减来腾地方?Delta 规则做不到:它的橡皮擦一次只能擦一个 key 对准的那一小块。
反过来,如果我们做的是纯累加线性注意力,加"遗忘"反而简单——只需要一个控制遗忘的标量参数:
S_old = cache # (b,h,dh,dh)
S_new = k @ v # (b,h,dh,t)×(b,h,t,dh) → (b,h,dh,dh)
# cache = S_old + S_new # 原来的纯累加
cache = alpha * S_old + S_new # alpha: (b,1,t,1) 标量门 → (b,h,dh,dh),先衰减旧状态再全强度写入
这正是 Mamba-2 的贡献:先衰减旧缓存,再全量写入新内容,防止状态无界膨胀。但统一衰减有个明显缺陷——不区分关联的重要性。模型若想忘掉某个特定关联,所有关联都得跟着等比例变旧。而 Delta 规则恰好相反:能定点更新单个事实,却无法让其余事实批量衰减。
Gated Delta 规则 = Mamba 的门控 × Delta 规则的精准。引入参数 \(\alpha_t\):\(\alpha = 1\) 时退化为纯 Delta 规则;\(\alpha = 0\) 时清空记忆。更新式:
实现沿用第 06 章的 WY 重参数化,数学几乎不变,只多了一个 0 到 1 之间、由数据决定的标量来控制上一状态的衰减。难点仍在工程:分块并行形式里需要处理累积衰减——
走到这里,线性化模型已经集齐了记忆管理的完整操作集:写入(外积累加,线性注意力)→ 定点修改(delta 规则)→ 整体衰减(门控)。这三者恰好对应人类记忆的"记住、纠正、淡忘"。还缺最后一块拼图——精细化的遗忘粒度,那是下一章 KDA 的事。
- Delta 规则会改错但不会淡忘;Mamba 会淡忘但不会改错——Gated DeltaNet 两者兼得。
- α 与 β 都由数据驱动:模型在上下文中实时决定记忆策略。
- 累积衰减 = 乘法版前缀和,是分块并行化的主要工程障碍。
09KDA 与 Kimi Linear
到这一阶段,研究者们开始尝试混合架构——在同一个模型里交错使用多种注意力形式,比如 Gated DeltaNet 与 Mamba 混搭,或线性层与全注意力层混搭。2025 年的 Kimi Linear 把这条路推到了一个标志性结论:
在严格受控对比(同数据、同算力)下,Kimi Linear 全面超过全注意力基线——不是"接近",是反超;同时解码吞吐最高提升 6 倍,KV Cache 减少最多 75%。它把自己定位为"即插即用的架构替代品",而非长序列专用补丁。
9.1 KDA:细粒度门控
Kimi Delta Attention(KDA)对 Gated DeltaNet 的关键改进,是把遗忘从标量升级为逐通道向量:不再用一个 α 统一衰减整个状态矩阵,而是为每个特征通道学习独立的衰减率。
更新规则骨架不变,但代码里多了一次 reshape:
alpha.reshape(nb, C, d) 这一行——它承载了论文最重要的贡献:对记忆衰减的逐通道精细控制。细粒度门控不是简单地"加参数"。它有明确的数学职能:给容量有限的记忆体装上分级回收制度——每个通道独立的衰减率,等价于让模型在特征空间里做软性的记忆分片管理。这正是第 05 章"超容量混叠"问题的工程化解法。
9.2 Kimi Linear 的完整配方
与 DeltaNet Transformer 并排对比,Kimi Linear 引入了三大变化:
- 混合架构:交错插入 MLA(Multi-head Latent Attention)层——3 层 KDA 配 1 层全注意力;
- MoE 替换 MLP:前馈网络换成混合专家层;
- α 投影扩容:通过 alpha 投影给 DeltaNet 机制增加容量(即上面的细粒度门控)。
MLA 和 MoE 的细节放在第 10 章展开。这里先强调原文的一个重要论点:这不是盲目堆料。每一处新增容量都有具体的数学目的——逐通道 α 给的是记忆衰减的精细控制;周期性 MLA 补的是线性状态必然丢失的精确检索;MoE 加的是知识容量而不加每 token 的计算量。
Scaling law 依然有效,但容量必须加在正确的位置、以系统能消化的形式。这条演化线上的每个架构,都是在为前一个系统的某个具体缺陷定向扩容。
- KDA = Gated DeltaNet + 逐通道遗忘门,记忆的每个维度有独立半衰期。
- Kimi Linear = 3:1 混合 KDA/MLA + MoE,受控对比下反超全注意力,解码快 6 倍。
- "混合"成为主旋律:线性递归负责效率,周期注意力负责保真。
10补课 III:MoE 与 MLA 深解
Kimi Linear 和 Kimi K3 的两根支柱——MoE 与 MLA——在原文里只是一笔带过。但它们是 2024–2026 年大模型军备竞赛的主战场(DeepSeek 的崛起几乎全靠这两样),值得各自一节。
10.1 MoE:稀疏化的艺术
回顾第 02 章的参数量分解:GPT-2 里约三分之二的参数在 MLP。MLP 被认为是模型的"知识仓库"(注意力负责路由信息,MLP 负责存储事实)。于是有个诱人的想法:把 MLP 复制很多份,但每个 token 只用其中几份——参数量(知识容量)暴涨,每 token 计算量几乎不变。这就是混合专家(Mixture-of-Experts)。
关键设计点与演化史:
- 起源:Shazeer 等人 2017 年的 Sparsely-Gated MoE(LSTM 时代!),2021 年 Google 的 Switch Transformer(简化为 Top-1 路由)与 GShard 把 MoE 带上万卡规模。
- 负载均衡:路由器容易"嫌贫爱富"——少数专家被疯狂选择、其余饿死,训练崩溃。经典解法是辅助损失(auxiliary load-balancing loss)强制均匀,但它会伤害模型质量;DeepSeek-V3 开创了无辅助损失路线:给每个专家一个动态偏置项,只影响路由选择、不进入梯度,均衡与质量兼得。
- 细粒度 + 共享专家(DeepSeekMoE, 2024):把专家切得更小、数量更多(提升组合灵活性),并设置若干共享专家处理所有 token 的通用知识,路由专家只学专精知识——缓解知识冗余。Kimi K3 的 898 专家(2 共享 + 896 路由)正是这一思想的超大号版本。
- 开源引爆点:Mixtral 8×7B(2023)证明中型 MoE 也能吊打同档稠密模型;此后 DeepSeek-V3(671B/激活 37B)、Qwen3-MoE、Kimi K2(1T/激活 32B)把 MoE 变成前沿模型默认形态。
- 代价:显存必须装下全部专家(参数不减),训练通信复杂(专家并行 AllToAll),小 batch 推理时专家利用率低。MoE 省的是 FLOPs,不是显存。
10.2 MLA:把 KV Cache 压成精华
第 3.1 节说过 MLA 是 KV Cache 围剿战的最新武器,现在拆开它。GQA 的思路是"让 Q 头共享 K/V";MLA(DeepSeek-V2, 2024)的思路更激进:把所有头的 K、V 联合低秩压缩成一个潜向量 \(c_t\),只缓存它,用时再解压出各头的 K/V。
标准 MHA:\(K = XW_K,\ V = XW_V\),需要缓存完整的 K、V。MLA 引入低维潜变量:
\[ c_t^{KV} = W^{DKV}\, h_t \;\in\; \mathbb{R}^{d_c} \qquad\text{(只缓存它和 RoPE 分量)} \] \[ k_t = W^{UK}\, c_t^{KV}, \qquad v_t = W^{UV}\, c_t^{KV} \]典型配置:\(d_c = 512\),而标准 MHA 每 token 的 KV 缓存维度可能是 \(64 \times 128 = 8192\)——压缩比约 16 倍。
矩阵吸收(absorption)是最精妙的一步:解压矩阵 \(W^{UK}\) 可以在推理前预先乘进 query 投影,\(W^{UV}\) 可以乘进输出投影——推理时根本不需要真的解压出 K/V,直接在压缩空间里算注意力。
解耦 RoPE(decoupled RoPE):RoPE 的旋转与"内容压缩"格格不入(旋转后的向量无法做低秩分解),所以 MLA 把位置信息拆到独立的少量维度(如 64 维)单独施加 RoPE,缓存里多存这一小段 \(k^R\),主压缩体保持纯粹的内容表示。
再叠加多头潜注意力对"头间冗余"的利用,MLA 做到了一个反常的结果:缓存比 GQA 小得多,效果却比 MHA 还好。
回到 Kimi Linear 的 3:1 混合:KDA 层状态固定、不产生 KV Cache;少数 MLA 层产生的缓存又极小——整个模型的长上下文推理成本被压到极低。这就是为什么"线性化 + MLA"成为 2025–2026 混合架构的黄金组合。第 11 章的 Gated MLA 还会再进化一步。
- MoE:用路由把"参数量"与"计算量"解耦——加知识不加 FLOPs;核心难题是负载均衡。
- MLA:低秩压缩 + 矩阵吸收 + 解耦 RoPE,缓存缩小 90%+ 而质量反超 MHA。
- MoE 省训练/推理算力,MLA 省推理显存带宽——两者分别打击大模型成本的两条腿。
11Kimi K3:两万倍的终点(暂时)
所有铺垫到此汇合。Kimi K3 的语言骨干就是放大版的 Kimi Linear:包含 23 个四层大循环(macrocycle)——每个大循环里 3 层用 Kimi Delta Attention,第 4 层用 MLA;第 1 层用稠密前馈网络,其余全部换成潜空间 MoE。
相比 Kimi Linear,改动清单初看不起眼:
- 规模大幅增长(至 2.8T 参数)
- 每 12 层一次 Blockwise AttnRes(第 12 章详解)
- MLA 的 query LoRA 与输出门控
- 潜空间 MoE
- SiTU 激活函数
- Gated MLA
分工原则一如既往:KDA 提供恒定状态的递归记忆,周期性 MLA 层保留对上下文的全量 softmax 检索。一张简图统领全模型:
11.1 三个直接的变化:Gated MLA、潜空间 MoE、SiTU
Gated MLA:决定 MLA 检索出的特征有多少能进入残差流——用输入投影出一个门,与注意力输出逐元素相乘。本质是给"精确检索通道"也装上水龙头:检索虽准,但该放多少信息进主干,由上下文决定。
潜空间 MoE:传统 MoE 里,路由器用点积相似度把每个 token 发给若干专家。Kimi K3 共 898 个专家:2 个共享专家处理所有 token,剩余 896 个中由路由器为每个 token 选出 16 个。K3 的进一步创新是让专家在压缩的潜空间中工作:
SiTU 激活:K3 还更换了专家内的激活函数。不再走"升维投影 → SiLU → 与门逐元素乘 → 降维"的老路,而是用 SiTU:
d = x.shape[-1] // 2 # x: (..., 2d),前半 gate、后半 up
gate = x[..., :d].to(torch.float32) # (..., d)
up = x[..., d:].to(torch.float32) # (..., d)
situ_a = self.beta * torch.tanh(gate / self.beta) * torch.sigmoid(gate) # (..., d)
if self.linear_beta is not None:
up = self.linear_beta * torch.tanh(up / self.linear_beta) # (..., d)
return (situ_a * up).to(x.dtype) # (..., d)
注意 tanh 项里有个可学习的 \(\beta\):门控的饱和区间变成了可学习的,模型可以按层调节激活的"软硬"程度。
这揭示了一个推理侧反复出现的难题:如果没有融合 kernel,新激活函数比原路径慢近 3 倍——数学上免费的东西,放到内存层级里全是读写。一个对冲优化是专家在压缩潜空间工作,前向快得多、FLOPs 几乎减半。架构创新必须与 kernel 工程协同落地,这是原文作者(Baseten 推理工程师)反复暗示的行业真相。
剩下的改动:query LoRA、输出门控、AttnRes
MLA 的 query 侧加 LoRA 低秩适配、输出加门控,以及每 12 层一次的 Blockwise Attention Residuals。AttnRes 只带来约 2% 的推理延迟,但换来两个重要收益:对早期表示的选择性检索(缓解残差稀释与隐藏状态数值增长),以及 1.25 倍的计算优势(等效算力下更强)。
最后一点值得停下来体会:AttnRes 和 MLA 从不同方向解决同一个底层限制。KDA 层状态恒定,必然丢弃信息;MLA 从序列维度(token 上下文)做无损检索补位,AttnRes 从深度维度(早期层的表示)做选择性检索补位。一个补横向记忆,一个补纵向记忆。
11.2 账本:1T 怎么变成 2.8T
第 02 章我们算了 GPT-2 的 124M,现在用同一套方法给 K3 对账。先回答一个常见的困惑:K3 的层数并没有比 K2 多几倍,参数怎么从 1T 涨到了 2.8T?答案藏在 MoE 的三项乘积里:
| 项目 | Kimi K2 | Kimi K3 | 对总参数的作用 |
|---|---|---|---|
| 总层数 | 61(1 个 dense 层) | 93(1 个 dense 层) | MoE 层实际 60 → 92,约 1.53× |
| 路由专家 / 层 | 384 | 896 | 约 2.33× |
| 专家输入维 | 7168 | LatentMoE 维度 3584 | K3 单专家输入更窄 |
| 专家中间维 | 2048 | 3072 | K3 中间维更宽 |
| 每 token 选中 | 8 / 384 + 1 共享 | 16 / 896 + 2 共享 | 影响激活计算,不直接改变总参数 |
| 官方总 / 激活 | 1T / 32B | 2.8T / 104B | 总参数与单 token 路径是两套口径 |
单专家(gate、up、down 三张矩阵):\(P_{expert,K2} \approx 3 \times 7{,}168 \times 2{,}048 = 44{,}040{,}192 \approx 44.04\text{M}\)
60 个 MoE 层 × 每层 384 个路由专家:
\[ 60 \times 384 \times 44{,}040{,}192 = 1{,}014{,}686{,}023{,}680 \approx 1.015\text{T} \]这是按维度估算的主导项——官方 1T 口径与之吻合。
单专家(潜空间输入 3584、中间维 3072):\(P_{expert,K3} \approx 3 \times 3{,}584 \times 3{,}072 = 33{,}030{,}144 \approx 33.03\text{M}\)
92 个 MoE 层 × 每层 896 个路由专家:
\[ 92 \times 896 \times 33{,}030{,}144 = 2{,}722{,}740{,}830{,}208 \approx 2.723\text{T} \]再加共享专家、KDA/MLA 投影、embedding、dense 层与视觉编码器等,官方总量为 2.8T。
三项因子复核:\((92/60) \times (896/384) \times (33.03/44.04) \approx 1.533 \times 2.333 \times 0.750 \approx 2.68\)。所以答案不是"层数变多",而是 MoE 层数 × 专家密度 × 单专家大小三项一起变——单专家虽然缩小了 25%,网格的扩大远远盖过了它。(注:此为按公开维度做的主导项近似,具体 checkpoint 还涉及实现细节与计入口径。)
| 维度 | GPT-2 (2019) | Kimi K3 (2026) |
|---|---|---|
| 参数量 | 1.24 亿 | 2.8 万亿(MoE,激活约 104B) |
| 层数 | 12 层均质块 | 23 大循环 × (3 KDA + 1 MLA),异质层 |
| 注意力 | MHA + KV Cache(O(N) 增长) | KDA 恒定状态 + Gated MLA 潜缓存 |
| FFN | 稠密 2 层 MLP | 潜空间 MoE:898 专家,2 共享 + 16 路由 |
| 激活 | GELU | SiTU(可学习饱和区) |
| 位置编码 | 可学习绝对位置嵌入 | RoPE 家族 + 递归内禀位置感 |
| 残差 | 恒等累加 | 恒等累加 + 每 12 层 AttnRes 选择性混合 |
- K3 = Kimi Linear 的定向扩容:3:1 KDA/MLA 大循环 + 潜空间 MoE + Gated MLA + SiTU。
- 2.8T ≈ 92 MoE 层 × 896 专家 × 33M/专家 + 其余部件;激活参数仅约 104B。
- 每个改动都对应一个具体缺陷:检索精度、记忆粒度、算力效率、数值稳定。
12AttnRes:跨深度的注意力
(原文此节致谢 @chloey3k 的协助。)每次前向,输入穿过一叠层——每层是一个注意力块(KDA 或 MLA)加一个 MLP/MoE 块。传统上,每层的输入都是原始嵌入与之前所有层输出的等权求和:
其中 \(h_i\) 是第 i 层输入,\(h_1\) 是当前 token 的嵌入,\(f_i(h_i)\) 是第 i 层输出。问题在于:没有选择性。不同类型的层收到同一份"大杂烩"状态,哪怕它们想加权不同的历史;且由于递推是纯加法的,越靠后的层越要学出更大的输出才能影响累积残差——这既造成"残差稀释"(早期信息被淹没),也导致隐藏状态数值持续增长、训练不稳。
AttnRes 的做法:不再一视同仁,而是给求和式每一项乘上专门权重:
每个权重 \(\alpha_i\) 由 query-key 点积算出:query 按层学习,key 和 value 来自更早的残差流状态;分数归一化到和为 1,再加权组合。模型因此不必只条件于紧邻的前一层——每层都能用学出来的 query,从整个深度历史中检索对当前计算最有用的表示。这是把"注意力"从序列维度(token 之间)推广到了深度维度(层与层之间)。
在块粒度上应用同一思想:一个"块"是 12 个解码层内注意力与 MLP 输出的逐元素累加,作为单个深度表示存下来,供后续 AttnRes 混合。每层都做残差注意力太贵,只在固定块边界做能以很小代价捕获大部分收益——K3 里每个边界相隔 12 层,23 个四层大循环共产生 8 个 AttnRes 块,并提升了推理速度。
下面这段可能是 block_attn_res 函数里最重要的部分:
V = torch.stack(blocks + [partial_block]) # [N+1, B, T, D] 全部历史块
K = norm(V) # [N+1, B, T, D],归一化后用于打分
logits = torch.einsum('d, n b t d -> n b t', proj.weight.squeeze(), K) # [N+1, B, T],每个历史块一个分数
h = torch.einsum('n b t, n b t d -> b t d', logits.softmax(0), V) # softmax 沿块维 → 加权和 (B, T, D)
return h # (B, T, D)
四行代码:堆叠所有历史块 → 归一化后用一个学习向量打分 → 在"块"这个维度上做 softmax → 加权混合。注意 softmax 是沿第 0 维(块维度)做的——竞争的对象不是 token,而是深度。
回望全文,注意力机制发生了三次"迁徙":① token 之间(2017,标准注意力);② 记忆状态与查询之间(2020+,线性注意力/DeltaNet,注意力退化为状态读写);③ 层与层之间(2026,AttnRes)。同一个"查询-匹配-加权"原语,在序列、记忆、深度三个维度上各开了一次花。
- 残差流的纯加法递推导致稀释与数值膨胀;AttnRes 给每层配上对深度历史的检索能力。
- 块粒度(每 12 层一次)是收益与成本的平衡点;约 2% 延迟换 1.25× 等效算力。
- MLA 补序列维度的精确检索,AttnRes 补深度维度的精确检索——两者共同兜底 KDA 的信息损失。
13补课 IV:改变战局的优化方法
架构只是故事的一半。过去七年里,真正让 LLM 从实验室走向亿万用户的,还有一批不改变模型形态、却改变成本曲线和能力上限的方法。按训练、推理、对齐三条线梳理。
13.1 训练侧:把每一度电花在刀刃上
Scaling Laws 与 Chinchilla(2020–2022)
OpenAI 2020 年的 scaling law 论文证明:损失随参数、数据、算力呈平滑幂律下降——能力可以预测,这给了行业"敢烧钱"的底气。但 DeepMind 2022 年的 Chinchilla 纠正了一个广泛错误:此前模型普遍"太大、喂太少";最优配方是每个参数配约 20 个 token。700 亿的 Chinchilla 吊打 2800 亿的 Gopher。此后 Llama 时代干脆"超量喂食"(小模型吃几万亿 token),因为推理成本才是长期大头。这条思想脉络一直延续到今天:先定推理预算,再倒推训练配方。
优化器:Adam → Muon
AdamW 统治了八年。2024 年 Keller Jordan 的 Muon(用于 NanoGPT 竞速)以"对隐藏层权重矩阵做 Newton-Schulz 正交化更新"的思路,在多项基准上把样本效率提升约 2 倍;Moonshot 将其扩展到大模型训练(Kimi K2 使用带 QK-clip 的 MuonClip),Muon 由此进入一线武器库。embedding 与输出头等仍用 Adam——混合优化器成为新常态。
数值精度与系统
- BF16 混合精度(2020+):训练标配;FP16 的溢出毛病被 BF16 的大动态范围治好。
- FP8 训练(DeepSeek-V3, 2024):首次在超大规模上验证 FP8 混合精度可行,配合逐块缩放,训练成本再砍一刀。
- 并行策略:数据/张量/流水/专家/序列并行的组合学(Megatron、ZeRO、FSDP)——万卡集群的有效算力利用率(MFU)从 30% 一路卷到 50%+。
- 数据配方:去重、质量过滤、领域配比、退火(annealing)、合成数据——业内共识是"数据质量与配比的贡献不低于架构",只是各家秘方极少公开。
13.2 推理侧:吞吐量的战争
训练一次性花钱,推理性命攸关地天天花钱。这一战线的主角:
| 方法 | 年份 | 一句话原理 | 影响 |
|---|---|---|---|
| 连续批处理 | 2022+ | 不等整批跑完,token 级动态插拔请求 | GPU 利用率数倍提升,所有推理框架标配 |
| PagedAttention / vLLM | 2023 | 像操作系统管理虚拟内存一样分页管理 KV Cache,消除碎片与预留浪费 | 吞吐 2–4×,开源推理的事实标准 |
| 投机解码 | 2023 | 小模型起草多 token,大模型一次并行验证,数学上无损 | 解码提速 2–3×;EAGLE/Medusa 等变体落地广泛 |
| 权重量化 GPTQ/AWQ | 2023 | 权重压到 4bit 甚至更低,激活保持高精度 | 消费级显卡跑 70B 成为可能 |
| KV Cache 量化 | 2024+ | 缓存压到 8bit/4bit | 长上下文服务成本大降,与 MLA 叠加使用 |
| 前缀缓存 | 2023+ | 相同前缀(系统提示等)的 KV 复用 | 多轮对话与 Agent 场景的隐形加速器 |
| 蒸馏 | 经典/2023+ | 大模型当老师教小模型(含推理链蒸馏) | R1 蒸馏系列让小模型获得推理能力 |
| 分离式架构 PD 分离 | 2024+ | Prefill(算力密集)与 Decode(带宽密集)拆到不同硬件池 | 大规模推理服务的新范式 |
13.3 对齐与推理:RL 的第二春
RLHF 时代(2022–2023)
InstructGPT/ChatGPT 的三段式——监督微调(SFT)→ 训练奖励模型(RM)→ PPO 强化学习——让模型从"会续写"变成"会听话",是 LLM 产品化的关键一跃。但 PPO 训练又贵又脆,于是 DPO(2023)用闭式解直接对偏好数据做分类式优化,免去了奖励模型与在线 RL,一度成为开源社区标配。
推理模型时代(2024– )
OpenAI o1 揭示了新 scaling 维度:测试时算力(test-time compute)——让模型生成长思维链(CoT)再作答,数学/代码能力陡增。DeepSeek-R1(2025)把路径开源化:用 GRPO(组相对策略优化,免去 value 网络)对"可验证奖励"(数学答案对不对、代码过不过测试)直接做大规模 RL,涌现出自我反思与长链推理——RLVR(可验证奖励强化学习)成为后训练的新主菜。此后 Kimi k1.5、Qwen3 等全部跟进,"预训练 scaling + RL scaling + 测试时 scaling"三条曲线并行。
参数高效微调
LoRA(2021)冻结主干、只训低秩旁路矩阵,把微调成本砍掉几个数量级;QLoRA(2023)叠加 4bit 量化,让单卡微调 65B 成为现实。LoRA 的思想还以"MLA query LoRA"等形式回流进主干架构(见第 11 章)——低秩,是贯穿本书的隐藏主题之一。
架构(KDA/MLA/MoE)决定单位能力的理论成本;训练方法(scaling law/Muon/FP8)决定抵达能力的实际开销;推理优化(vLLM/投机解码/量化)决定能力的边际售价。七年 22,580 倍的规模扩张背后,是这三条曲线同时被压低——缺任何一条,今天的模型经济学都不成立。
14全景时间线 2017–2026
| 年份 | 架构线 | 方法线 | 标志性事件 |
|---|---|---|---|
| 2017 | Transformer(MHA、正弦位置、Post-LN) | Sparsely-Gated MoE | 《Attention Is All You Need》 |
| 2018–19 | BERT、GPT-2(Pre-LN、decoder-only 确立) | MQA | "大模型"概念萌芽 |
| 2020 | GPT-3 175B;线性注意力 | Scaling Laws | 规模即能力时代开启 |
| 2021 | RoPE、S4、DeltaNet(FWP) | Switch Transformer;LoRA | 线性化与稀疏化两条暗线铺开 |
| 2022 | — | Chinchilla;FlashAttention;RLHF | ChatGPT 引爆世界 |
| 2023 | GQA、Mamba、RWKV、RetNet | DPO;vLLM;投机解码;QLoRA | 开源模型大爆发(Llama) |
| 2024 | MLA、Gated DeltaNet、Mamba-2、xLSTM | Muon;FP8 训练;o1(测试时 scaling) | DeepSeek-V2 重新定义性价比 |
| 2025 | KDA / Kimi Linear(反超全注意力) | GRPO/RLVR;R1;混合架构主流化 | 推理模型元年 |
| 2026 | Kimi K3:KDA+Gated MLA+潜空间 MoE+AttnRes | — | 22,580 × GPT-2 |
15总结:规模之外
至此,从 GPT-2 到 Kimi K3 的旅程走完了。
核心变化从来不是规模本身。每一步架构演进,改变的都是三件事之一:模型存储什么、如何更新这些状态、以及如何检索那些固定大小状态留不住的信息。
Kimi K3 把四种机制缝合为一个系统:恒定状态的递归记忆(KDA)、周期性的 softmax 精确检索(Gated MLA)、稀疏的专家容量(潜空间 MoE)、跨深度的选择性残差访问(AttnRes)。每一份新增的容量都被花在有明确职能的地方。
一个固定容量的联想记忆(维度固定)必然需要驱逐策略——纯加法的线性写入在容量耗尽后只会不断叠加干扰。因此可学习的选择机制(门控、路由、衰减)是必需品;而注意力,依然是人类已发明的最有效的"选择性读取"机制。
最后,给几乎零基础起步、一路读到这里的你一张最小知识地图:
- 骨架没变:嵌入 → N × (注意力 + FFN + 残差) → LM head。七年只动了内脏。
- 主线矛盾:KV Cache(无损但无限)vs 固定状态(有界但有损)。全部线性化工作都在逼近"固定状态做出无损检索的效果",最终答案是混合。
- 记忆三操作:写入(外积)→ 定点修改(Delta 规则)→ 衰减(门控);KDA 把衰减细化到逐通道。
- 效率三杠杆:MoE(稀疏参数)、MLA(压缩缓存)、FlashAttention/分块/WY(IO 与并行)。
- 能力三曲线:预训练 scaling、RL scaling(GRPO/RLVR)、测试时 scaling。
下一次当你看到"某某新模型发布"时,试着问自己三个问题:它的记忆存在哪?它如何决定遗忘?它的容量加在了什么地方?——答案大概率就藏在这篇文章讲过的某条演化线上。
A附录 A:Softmax 数值稳定性与 Online Softmax 证明
A.1 为什么需要减去最大值
直接计算 \(e^{x_i}\) 的问题是:当 \(x_i\) 较大时,\(e^{x_i}\) 会溢出 FP16/FP32 的表示范围(FP16 在 \(x > 11\) 左右就溢出了)。设 \(m = \max(x_1, \ldots, x_n)\),数值稳定的 Softmax 为:
为什么等价?分子分母同除 \(e^m\):
减去 \(m\) 后,最大指数为 \(e^0 = 1\),不会溢出;最小的指数 \(e^{x_{\min} - m} \approx 0\),不影响求和。
A.2 Online Softmax 的正确性证明(归纳法)
归纳基础:\(i = 0\) 时,\(\ell_0 = 0\),成立。
归纳步骤:假设 \(\ell_{i-1} = \sum_{j=1}^{i-1} e^{x_j - m_{i-1}}\)。当 \(m_i > m_{i-1}\)(即 \(x_i\) 是新的最大值):
\[ \begin{aligned} \ell_i &= \ell_{i-1} \cdot e^{m_{i-1} - m_i} + e^{x_i - m_i} \\ &= \sum_{j=1}^{i-1} e^{x_j - m_{i-1}} \cdot e^{m_{i-1} - m_i} + e^{x_i - m_i} \\ &= \sum_{j=1}^{i-1} e^{x_j - m_i} + e^{x_i - m_i} = \sum_{j=1}^{i} e^{x_j - m_i} \end{aligned} \]当 \(m_i = m_{i-1}\) 时,\(e^{m_{i-1} - m_i} = 1\),同样成立。证毕。 ∎
这个"换底"技巧意味着:无论把输入切成多少块、按什么顺序扫描,只要每块到来时重缩放一次累加器,最终结果就是精确 Softmax——FlashAttention 的分块正确性由此而来。注意力输出还维护一个类似的加权威组 \(acc_i = acc_{i-1} \cdot e^{m_{i-1}-m_i} + e^{x_i - m_i} v_i\),原理完全相同。
B附录 B:Delta 规则重参数化推导
DeltaNet 的循环更新:
- 展开括号 \[ S_t = S_{t-1} + \beta_t k_t^\top v_t - \beta_t k_t^\top k_t S_{t-1} \]
- 提取 \(S_{t-1}\) \[ S_t = S_{t-1}\big(I - \beta_t k_t^\top k_t\big) + \beta_t k_t^\top v_t \] 注意 \(k_t \in \mathbb{R}^{1 \times d_k}\),所以 \(k_t^\top k_t \in \mathbb{R}^{d_k \times d_k}\) 是一个 rank-1 矩阵。
- 展开递推 \[ \begin{aligned} S_1 &= S_0(I - \beta_1 k_1^\top k_1) + \beta_1 k_1^\top v_1 \\ S_2 &= S_1(I - \beta_2 k_2^\top k_2) + \beta_2 k_2^\top v_2 \\ &= S_0(I - \beta_1 k_1^\top k_1)(I - \beta_2 k_2^\top k_2) + \beta_1 k_1^\top v_1 (I - \beta_2 k_2^\top k_2) + \beta_2 k_2^\top v_2 \end{aligned} \]
- 一般形式 \[ S_C = S_0 \cdot \prod_{t=1}^{C} \big(I - \beta_t k_t^\top k_t\big) + \sum_{t=1}^{C} \beta_t k_t^\top v_t \prod_{s=t+1}^{C} \big(I - \beta_s k_s^\top k_s\big) \] 这个展开式说明:块内所有 C 个 token 的状态更新可以通过前缀乘积一次性计算。实现中通过下三角矩阵 \(T\)(其中 \(T_{ij} = -\beta_i\, k_i k_j^\top\),\(i > j\))和向量化的前代法(forward substitution)高效算出整块的修正量。
Householder 反射矩阵的形式是 \(H = I - 2\, vv^\top / \|v\|^2\)。DeltaNet 中的 \(I - \beta k k^\top\) 是一个广义 Householder 变换——\(\beta\) 不是固定的 \(2/\|k\|^2\),而是数据依赖的。这类变换在数值线性代数中有极好的性质(当 \(\beta = 2/\|k\|^2\) 时是正交变换,天然数值稳定),这也是为什么 DeltaNet 的递推可以展开成紧凑的 WY 表示——它本来就是一串"半个" Householder 反射的乘积。
这个推导还揭示了一个优美的几何画面:每一步 delta 更新,都是让状态矩阵在一个由当前 key 张成的方向上做一次"受控反射"——擦除该方向上的旧投影,写入新值。记忆的编辑,原来是几何变换的复合。