01 · 基础:MHA → MQA → GQA → MLA¶
本篇是 sparse 与 linear 两条路线之前的基础。读这一篇之前,最好已经熟悉标准 scaled dot-product attention 的公式与 shape,并且知道 decode 阶段每生成一个 token 都要把整个 KV cache 从 HBM 读一遍——这一点在 Attention 机制 第 1 节已经讲过。本篇沿用的统一记号见 Attention 机制 §2。
这一族机制有一个共同特点:不改 mask、不改递归,只改「一个 token 在 KV cache 里占多少个数」。所以整篇文章只有一条主线——每 token 每层的 KV cache 元素数,从 MHA 的 \(2 h d_h\) 一路压到 MLA 的 \(d_c + d_h^R\)。以 DeepSeek-V3 的量级为例,这个数字是从 32768 压到 576,相差 56.89 倍。
论文:MHA arXiv:1706.03762、MQA arXiv:1911.02150、GQA arXiv:2305.13245、MLA arXiv:2405.04434(DeepSeek-V2)与 arXiv:2412.19437(V3)。
1. MHA:定义与 KV cache 的来源¶
DeepSeek-V2 §2.1.1 的记法最适合往后衔接(\(h_t \in \mathbb{R}^d\) 是第 \(t\) 个 token 进 attention 前的 hidden state,\(n_h\) 个 head,每 head \(d_h\) 维):
把 \(q_t, k_t, v_t\) 切成 \(n_h\) 份,得到 \(q_{t,i}, k_{t,i}, v_{t,i} \in \mathbb{R}^{d_h}\),然后:
KV cache 从哪来:decode 第 \(t\) 步只算一个新 query,但 softmax 要遍历 \(j \le t\) 的全部 key/value。重算 \(k_{j,i}, v_{j,i}\) 需要重跑整个前缀,所以缓存它们。原文明确写 "MHA needs to cache 2 n_h d_h l elements for each token"(\(l\) = 层数)⇒
这个数字有多严重,可以用一个例子说明:Llama-3-70B(\(n_h=64\)、\(d_h=128\)、80 层,实际用的是 GQA-8)如果换成纯 MHA,128K 上下文下的 KV cache 会是 2·64·128·80·131072·2B = 320 GiB,已经远超单卡容量。
2. MQA:只留一份 K/V¶
Shazeer 2019 §3 的原话是这样写的:"Multi-query attention is identical except that the different heads share a single set of keys and values. The code … is identical to … multi-head attention, except that we remove the letter 'h' from the tf.einsum equations where it represents the 'heads' dimension of K, V, P_k, or P_v."
共享的是 K/V,Q 和 O 的投影仍然是 per-head:
| tensor | MHA | MQA |
|---|---|---|
| \(P_q\) | \([h, d, k]\) | \([h, d, k]\)(不变) |
| \(P_k\) | \([h, d, k]\) | \([d, k]\) |
| \(P_v\) | \([h, d, v]\) | \([d, v]\) |
| \(P_o\) | \([h, d, v]\) | \([h, d, v]\)(不变) |
| logits einsum | bhnk,bhmk->bhnm |
bhnk,bmk->bhnm |
cache prev_K |
\([b, h, m, k]\) | \([b, m, k]\) |
arithmetic-intensity 分析¶
原文用三段分析给出了访存量 / 算术运算量的比值(\(m=n\)、\(k=v=d/h\)、\(n \le d\)):
| 场景 | 算术运算 | 访存量 | 访存/算术 |
|---|---|---|---|
| batched MHA(训练/prefill) | \(\Theta(bnd^2)\) | \(O(bnd + bhn^2 + d^2)\) | \(O(1/k + 1/(bn))\) |
| incremental MHA(decode,跨 \(n\) 步) | \(\Theta(bnd^2)\) | \(\Theta(bn^2d + nd^2)\) | \(\Theta(n/d + 1/b)\) |
| incremental MQA | \(\Theta(bnd^2)\) | \(\Theta(bnd + bn^2k + nd^2)\) | \(\Theta(1/d + n/(dh) + 1/b)\) |
原文的论证链是这样展开的:"When n ≈ d or b ≈ 1, the ratio is close to 1, causing memory bandwidth to be a major performance bottleneck… The 1/b term is the easier one — we can just use a larger batch size… Reducing the n/d term is harder. This term is related to the expense of reloading at each step the K and V tensors … which have size bhmk = bn²." 由此得到的结论是:"We have reduced the offensive n/d by a factor of h."
这个论证背后的硬件前提原文也写明了:"modern GPU/TPU hardware, where the computational capacity can be two orders of magnitude higher than the memory bandwidth."
这正是 Attention 机制 §1 那个式子最早的出处。\(n/d\) 项是 decode 阶段全部痛苦的根源,它正比于「每步要重新读的 KV cache 大小」除以「每步能做的算力」。可以说,整个基础机制这条线的历史,就是在不断压这一项。
代价:质量与稳定性¶
省下带宽不是没有代价的。Table 1(WMT14 EN-DE,211M 参数,为对齐参数量把 d_ff 从 4096 加宽到 5440)给出了质量对比:
| Attention | d_ff |
ln(PPL) dev | BLEU test (beam 1 / 4) |
|---|---|---|---|
| multi-head | 4096 | 1.424 | 27.7 / 28.4 |
| multi-query | 5440 | 1.439 | 27.5 / 28.5 |
| multi-head, \(h=1, d_k=128\) | 6784 | 1.518 | —— |
| multi-head, \(h=2, d_k=64\) | 6784 | 1.480 | 26.8 / 27.9 |
| multi-head, \(h=8, d_k=16\) | 6784 | 1.513 | —— |
这里最值得记住的一点是:MQA 只掉了 0.015 的 ln-PPL,而「降低 head 数 \(h\)」或「降低每 head 维度 \(d_k\)」这两种替代性的省法却要掉 0.05–0.09,差了整整一个量级。换来的解码速度也很可观(TPUv2 μs/token):MHA 是 1.7 + 46,MQA 是 1.5 + 3.8,decoder 部分快了 12 倍。
但还有一个更隐蔽的代价,写在 GQA 论文的 Appendix A 里,很多人读论文时会漏掉:"multi-query attention can lead to training instability during fine-tuning, in particular combined with long input tasks… pre-training suffered from frequent loss spikes and the final models diverged immediately when fine-tuning on long-input tasks." 也就是说,MQA 在长输入任务上微调时可能直接训崩——这也是后面 GQA 要解决的问题之一。
3. GQA:分组共享与 mean-pool uptraining¶

图:GQA 论文的核心对照图(Ainslie et al. 2023, Fig 2;arXiv:2305.13245)。三种方案的唯一差别就是 KV head 的份数:MHA 留 \(h\) 份、GQA 留 \(g\) 份、MQA 只留 1 份。Q 的 head 数和 \(W^O\) 在三者中完全相同,这也正是为什么 GQA 可以从一个 MHA checkpoint「改造」而来,而不需要从头训练。
原文给出的定义是:"Grouped-query attention divides query heads into G groups, each of which shares a single key head and value head. GQA-g refers to grouped-query with G groups. GQA-1 … is equivalent to MQA, while GQA-h … is equivalent to MHA."
三者其实可以用同一段代码表达,靠的是 repeat_interleave 把 \(h_{\mathrm{kv}}\) 份 KV 广播到 \(h\) 个 query head:
import math, torch
def gqa(q, k, v, causal=True):
"""q: [B, Hq, T, D]; k, v: [B, Hkv, T, D]. Hq % Hkv == 0.
Hkv == Hq -> MHA; Hkv == 1 -> MQA; otherwise GQA-Hkv."""
g = q.shape[1] // k.shape[1]
k, v = k.repeat_interleave(g, dim=1), v.repeat_interleave(g, dim=1)
s = q @ k.transpose(-1, -2) / math.sqrt(q.shape[-1])
if causal:
T = q.shape[-2]
s = s.masked_fill(torch.ones(T, T, dtype=torch.bool, device=q.device).triu(1), float('-inf'))
return s.softmax(-1) @ v
需要注意的是,
repeat_interleave会真的拷贝张量,而不是返回一个 view,所以在 kernel 外面这样写,相当于白白多读了 \(g\) 倍的 KV 带宽。真实的 kernel 用的是pack_gqa:把共享同一个 KV head 的 \(g\) 个 Q head 打包进一次 KV 读取,让 KV 只从 HBM 读一次,具体见 04 · FA4(CuTeDSL)与工程接口 §5。这个「一个 KV entry 被多少 query 共享」的量,正是后面 NSA/DSA 设计的核心约束,会在04§5 再次出现。
Uptraining:把 MHA checkpoint 改造成 GQA¶
GQA 真正的工程贡献,其实不是这个架构本身,而是它不需要从头训练——已有的 MHA checkpoint 可以直接改造。做法分两步(原文逐字):
- Checkpoint conversion:"The projection matrices for key and value heads are mean pooled into single projection matrices, which we find works better than selecting a single key and value head or randomly initializing new key and value heads from scratch." 对 GQA:"we construct each group key and value head by mean-pooling all the original heads within that group."
- 继续预训练 \(\alpha\) 比例的原始训练步数(注意:是 steps / compute,不是 tokens)。
def mha_to_gqa(Wk, Wv, n_h, g):
"""Wk, Wv: [n_h * d_h, d]. 返回 mean-pool 到 g 组后的投影。"""
d = Wk.shape[-1]
per = n_h // g # 每组多少个原 head
Wk_g = Wk.view(g, per, -1, d).mean(dim=1) # 组内平均
Wv_g = Wv.view(g, per, -1, d).mean(dim=1)
return Wk_g.reshape(-1, d), Wv_g.reshape(-1, d)
具体数字上,\(\alpha = 0.05\) 大约需要 600 TPUv3 chip-days;论文写道"Both MQA and GQA gain from 5% uptraining with diminishing returns from 10%",也就是超过 10% 之后收益就不明显了。消融实验里三种初始化方式的效果排序是 Mean > First > Random。
其中最重要的一句对比,也是决定了工业界为什么最终选 GQA 而不是 MQA 的关键:
"GQA already achieves reasonable performance after conversion while MQA requires uptraining to be useful." 以及 "Uptrained multi-query attention models are more stable but still display high variance… Uptrained grouped-query attention models, however, appear to be stable."
也就是说,GQA 转换完不训练也能用,MQA 必须靠 uptraining 才行;而且即使经过 uptraining,MQA 的稳定性还是不如 GQA。下面是 T5 上的结果(单位是 s/sample per TPUv4 chip):
| Model | T_infer |
Average |
|---|---|---|
| MHA-Large | 0.37 | 46.0 |
| MHA-XXL | 1.51 | 47.2 |
| MQA-XXL (5% uptrained) | 0.24 | 46.6 |
| GQA-8-XXL (5% uptrained) | 0.28 | 47.1 |
GQA 随模型规模变大的优势¶
论文给出了三条论据,说明为什么模型越大越适合 GQA,都值得记住。
第一条是关于压缩比例的:"larger models generally scale the number of heads, such that multi-query attention represents a more aggressive cut in both memory bandwidth and capacity. GQA lets us keep the same proportional decrease … as model size increases." 也就是说,随着模型变大、head 数变多,MQA 相当于砍得越来越狠,而 GQA 可以保持固定的压缩比例。
第二条是关于带宽敞口的:"larger models suffer relatively less from memory bandwidth overhead from attention, as the KV-cache scales with model dimension while model FLOPs and parameters scale with the square of model dimension." KV cache 只随模型维度线性增长,而 FLOPs 和参数量是平方增长,所以模型越大,attention 的带宽压力相对越轻。
第三条是 TP 场景下的工程理由,也是很多人容易忽略的一点:"standard sharding for large models replicates the single key and value head by the number of model partitions; GQA removes the waste from such partitioning." 这里说的是 MQA 在张量并行(TP)下的隐藏成本:\(h_{\mathrm{kv}}=1\) 时每个 TP rank 都得存一份完整的 KV,而 \(h_{\mathrm{kv}} = \mathrm{TP}\) 时正好每个 rank 分到一份,不会有冗余。这也解释了为什么 \(g=8\) 这个配置出现得如此频繁(可以对照 02 · 整个 Transformer block 的切分方式 里 attention 部分的切分方式)。
此外还有一条附带的说明:encoder 的 self-attention 不需要用 MQA/GQA 省带宽,因为"encoder representations are computed in parallel, and memory bandwidth is therefore generally not the primary bottleneck"——encoder 是并行计算的,带宽通常不是瓶颈。
论文选 8 组的理由写在 Fig 6 附近:"Going from 1 (MQA) to 8 groups adds modest inference overhead, with increasing cost to adding more groups. We selected 8 groups as a favorable middle ground." 也就是从 1 组加到 8 组,推理开销增加得还算温和,再往上加代价就变大了,8 是一个折中点。
各模型的分组数 g¶
下表全部来自 HF 的 config.json,可以看到 \(g=8\) 确实是最常见的选择:
| Model | hidden | layers | \(n_h\) (Q) | \(n_{\mathrm{kv}}\)(\(g\)) | \(d_h\) | Q:KV |
|---|---|---|---|---|---|---|
| Llama-3-70B | 8192 | 80 | 64 | 8 | 128 | 8:1 |
| Llama-3-8B | 4096 | 32 | 32 | 8 | 128 | 4:1 |
| Llama-4-Scout | 5120 | 48 | 40 | 8 | 128 | 5:1 |
| Mistral-7B-v0.1 | 4096 | 32 | 32 | 8 | 128 | 4:1 |
| Qwen2.5-72B / Qwen3-32B | 8192 / 5120 | 80 / 64 | 64 | 8 | 128 | 8:1 |
| Qwen3-235B-A22B | 4096 | 94 | 64 | 4 | 128 | 16:1 |
| Qwen3-Next-80B-A3B | 2048 | 48(12 attn) | 16 | 2 | 256 | 8:1 |
| Gemma-2/3-27B | 4608 / 5376 | 46 / 62 | 32 | 16 | 128 | 2:1 |
| gpt-oss-120b | 2880 | 36 | 64 | 8 | 64 | 8:1 |
| GLM-4.6 | 5120 | 92 | 96 | 8 | 128 | 12:1 |
| MiniMax-M2 | 3072 | 62 | 48 | 8 | 128 | 6:1 |
| OLMo-2-13B | 5120 | 40 | 40 | 40 | 128 | MHA |
4. MLA:把 KV 压成一个 latent¶
前面 MHA→MQA→GQA 这条线,本质上都是在「留几份 K/V head」这一个维度上做取舍。MLA 换了一个维度来解决这个问题。

图:DeepSeek-V2 的核心对照图(DeepSeek-AI 2024, Fig 3;arXiv:2405.04434)。MHA/GQA/MQA 都在「留几份 K/V head」这一个维度上做取舍,而 MLA 换了维度:缓存一个低秩 latent(图中橙色),K 和 V 都从它上投影得到。图右下角还标出了 decoupled RoPE 的那一小段 \(k^R\),它是唯一另外需要缓存的东西。
MLA 的出发点是:GQA/MQA 都是在 head 维度上做硬性丢弃,但实际上 K/V 之间、head 之间存在大量冗余,不必靠丢弃来省空间,而是可以做低秩联合压缩。
4.1 KV 联合压缩(V2 Eq. 9–11)¶
其中 \(c_t^{KV}\) 是唯一需要缓存的主体。
4.2 Query 也做低秩(V2 Eq. 12–13)¶
原文点明了这样做的动机,这句话很容易被误读成「省 KV cache」,需要格外留意:
"in order to reduce the activation memory during training, we also perform low-rank compression for the queries, even if it cannot reduce the KV cache."
原因很简单:query 本来就不进 cache(decode 每步只算一个新 query),所以压缩它跟 KV cache 无关,纯粹是为了省训练期的 activation 内存。
4.3 Decoupled RoPE¶
这是 MLA 设计里唯一不那么优雅的部分,也是最值得花时间理解的部分。 原文(§2.1.3)是这样解释的:
"RoPE is position-sensitive for both keys and queries. If we apply RoPE for the keys
k_t^C,W^UKin Equation 10 will be coupled with a position-sensitive RoPE matrix. In this way,W^UKcannot be absorbed intoW^Qany more during inference, since a RoPE matrix related to the currently generating token will lie betweenW^QandW^UKand matrix multiplication does not obey a commutative law. As a result, we must recompute the keys for all the prefix tokens during inference."
用代数说清楚。无 RoPE 时打分是:
中间那坨是常量矩阵,可以离线乘好——于是 \(c_j^{KV}\) 直接充当 key,\(W^{UK}\) 根本不用在推理时出现。加了 RoPE 之后:
问题就在这里:\(R_{j-t}\) 卡在两个投影中间,而它每一步都在变化,这意味着必须为每个前缀 token 现算 \(k_j^C\),\(d_c\) 带来的压缩优势就全部作废了。
修法(V2 Eq. 14–19):额外拉出一小段专门承载 RoPE,其中 query 侧 per-head、key 侧跨 head 共享(MQA 式):
这样一来,需要缓存的东西只剩两样:\(c_t^{KV}\)(\(d_c\) 维)加上 \(k_t^R\)(\(d_h^R\) 维)。
注意这里没有因子 2——latent 同时充当 K 和 V。
这里 \(q^R\) 是 per-head 的,而 \(k^R\) 是单头共享的,这个不对称正是理解「MLA 吸收后就是 MQA」这句话的钥匙,§4.5 会展开讲。
4.4 矩阵吸收¶
Appendix C 结尾有一句话点出了矩阵吸收的原理:"due to the associative law of matrix multiplication, we can absorb W^UK into W^UQ, and W^UV into W^O. Therefore, we do not need to compute keys and values out for each query." 也就是说,靠矩阵乘法的结合律,可以把两个投影矩阵吸收进别的矩阵里,从而避免每次都显式算出 key 和 value。本节把这个恒等式完整写出来,并用代码验证它。
拆开打分的两项:
吸收一(\(W^{UK}\) → query 侧):
这就推出了吸收一的结论:key 就是 latent 本身。
吸收二(\(W^{UV}\) → \(W^O\)):
同理,吸收二得到的结论是:value 也是 latent 本身。
这两条路径——直接还原 K/V 再算 attention,与吸收后直接用 latent 打分——理论上必须给出逐位相同的结果。下面这段代码把两条都实现出来并做对比,本篇所有数值都来自 float64 精度下的实测:
import math, torch
def mla_naive(h, W, pos):
"""MHA mode: 从 latent 还原 k^C / v^C,跑标准 MHA。训练 / prefill 用这条。"""
T, nh, dh, dr = h.shape[0], W['nh'], W['dh'], W['dr']
cQ, cKV = h @ W['Wdq'].T, h @ W['Wdkv'].T
kR = rope(h @ W['Wkr'].T, pos) # [T, dr] 共享
qC = (cQ @ W['Wuq'].T).view(T, nh, dh)
qR = rope((cQ @ W['Wqr'].T).view(T, nh, dr).transpose(0, 1), pos).transpose(0, 1)
kC = (cKV @ W['Wuk'].T).view(T, nh, dh) # ← 物化出来
vC = (cKV @ W['Wuv'].T).view(T, nh, dh) # ← 物化出来
s = (torch.einsum('ihd,jhd->hij', qC, kC) +
torch.einsum('ihr,jr->hij', qR, kR)) / math.sqrt(dh + dr)
a = s.masked_fill(causal_mask(T), float('-inf')).softmax(-1)
o = torch.einsum('hij,jhd->ihd', a, vC)
return o.reshape(T, nh * dh) @ W['Wo'].T
def mla_absorbed(h, W, pos):
"""MQA mode: W^UK 吸进 query、W^UV 吸进 W^O。每个缓存 token 只读 c^KV 与 k^R。decode 用这条。"""
T, nh, dh, dr, dc = h.shape[0], W['nh'], W['dh'], W['dr'], W['dc']
cQ, cKV = h @ W['Wdq'].T, h @ W['Wdkv'].T
kR = rope(h @ W['Wkr'].T, pos)
qC = (cQ @ W['Wuq'].T).view(T, nh, dh)
qR = rope((cQ @ W['Wqr'].T).view(T, nh, dr).transpose(0, 1), pos).transpose(0, 1)
Wuk_h, Wuv_h = W['Wuk'].view(nh, dh, dc), W['Wuv'].view(nh, dh, dc)
Wo_h = W['Wo'].view(-1, nh, dh)
q_tilde = torch.einsum('ihd,hdc->ihc', qC, Wuk_h) # 吸收一:q̃ ∈ ℝ^{d_c}
s = (torch.einsum('ihc,jc->hij', q_tilde, cKV) + # key = latent 本身
torch.einsum('ihr,jr->hij', qR, kR)) / math.sqrt(dh + dr)
a = s.masked_fill(causal_mask(T), float('-inf')).softmax(-1)
o_tilde = torch.einsum('hij,jc->ihc', a, cKV) # value = latent 本身
Wo_tilde = torch.einsum('dhe,hec->dhc', Wo_h, Wuv_h) # 吸收二
return torch.einsum('ihc,dhc->id', o_tilde, Wo_tilde)
# 实测:rel_err(mla_absorbed, mla_naive) = 5.5e-16 ← 就是浮点噪声,两条路径数学上恒等
其中 rope 用论文 Eq. 34 的 interleaved 排布(\(x_{2i}\) 与 \(x_{2i+1}\) 配对):
def rope(x, pos, base=10000.0):
"""x: [..., T, dr], dr 偶数。返回旋转后的 x。"""
half = x.shape[-1] // 2
freq = base ** (-torch.arange(half, dtype=x.dtype, device=x.device) / half)
ang = pos[:, None].to(x.dtype) * freq[None, :] # [T, half]
cos = ang.cos().repeat_interleave(2, -1)
sin = ang.sin().repeat_interleave(2, -1)
x_rot = torch.stack([-x[..., 1::2], x[..., 0::2]], dim=-1).flatten(-2)
return x * cos + x_rot * sin
⚠️ HF 的 Llama 实现用的是 half-split 排布(\(x_i\) 配 \(x_{i + d_r/2}\))。两者数学等价(只差一个维度置换),但权重不能混用。见
02§2。⚠️ 工程上不要真的把 \(\tilde{W} = (W^{UK}_i)^{\top} W^{UQ}_i\) 物化。DeepSeek-V3 每层 \(W^{UQ}\) 是
128·128×1536 = 25.2M、\(W^{UK}\) 是128·128×512 = 8.4M,合计 33.6M;而 \(\tilde{W}\) 是128×512×1536 = 100.7M,大了 3 倍。vLLM/SGLang 的做法是两次 GEMM:先 \(q^C_{t,i} = W^{UQ}_i c_t^Q\),再 per-head bmm \(\tilde{q}_{t,i} = (W^{UK}_i)^{\top} q^C_{t,i}\)。
4.5 MHA mode 与 MQA mode¶
吸收之后,每个 query head 拿一个 \((d_c + d_h^R)\) 维 query,去和全体 head 共享的唯一一个 \((d_c + d_h^R)\) 维 key(就是 \([c_j^{KV} ; k_j^R]\))打分,value 是共享的 \(d_c\) 维 \(c_j^{KV}\)——按定义这就是 MQA(\(n_{\mathrm{kv}} = 1\), \(d_{\mathrm{qk}} = 576\), \(d_v = 512\))。
DeepSeek-V3.2 Appendix A 把这件事说成了一个显式的运行模式选择(Figure 7 caption 逐字):"For DeepSeek-V3.1-Terminus, the MHA mode is used for training and prefilling, while the MQA mode is used for decoding."
| MHA mode(unabsorbed) | MQA mode(absorbed) | |
|---|---|---|
| 用在 | training / prefill(compute-bound) | decode(memory-bound) |
| 做什么 | 从 \(c^{KV}\) 还原 \(k^C, v^C \in \mathbb{R}^{n_h d_h}\),跑标准 FlashAttention | \(W^{UK}\) 吸进 query,\(W^{UV}\) 吸进 \(W^O\) |
| KV heads | \(n_h\) = 128 | 1 |
| per-head QK 维 | \(d_h + d_h^R\) = 192 | \(d_c + d_h^R\) = 576 |
| per-head V 维 | \(d_h\) = 128 | \(d_c\) = 512 |
| 每 token 每层需读的量 | 还原后 \(2 n_h d_h = 32768\) 元素 | 576 元素(56.9× 更少) |
需要两种模式的原因是:prefill 时算力是瓶颈,MHA mode 的 per-head 维只有 192(对比 576),core-attention FLOPs 小 3.4 倍,而 KV 还原的上投影是一次性成本;decode 时带宽是瓶颈,MQA mode 只读 576 个数。同一组权重按阶段换算法,这是 MLA 设计上最精彩的地方。
4.6 具体数字与验算¶
V2 Table 1(原文逐字):
| Attention | KV Cache per Token (# Element) | Capability |
|---|---|---|
| MHA | \(2 n_h d_h l\) | Strong |
| GQA | \(2 n_g d_h l\) | Moderate |
| MQA | \(2 d_h l\) | Weak |
| MLA (Ours) | \((d_c + d_h^R)\, l \approx (9/2)\, d_h l\) | Stronger |
caption:"For DeepSeek-V2, d_c is set to 4 d_h and d_h^R is set to d_h/2. So, its KV cache is equal to GQA with only 2.25 groups, but its performance is stronger than MHA."
自己验算一遍(\(d_h = 128\)):\(d_c = 512 = 4 d_h\) ✓,\(d_h^R = 64 = d_h/2\) ✓,\(d_c + d_h^R = 576 = 4.5\, d_h = (9/2)\, d_h\) ✓(这是精确相等而非近似——论文写 ≈ 只是因为 \(d_c = 4 d_h\) 是设计选择而非恒等式)。等效组数 \(2 n_g d_h = (9/2)\, d_h \Rightarrow n_g = 9/4 = 2.25\) ✓。MHA 与 MLA 的元素比为 \(32768/576 = 56.89\) ✓。
| DeepSeek-V2 | DeepSeek-V3 / R1 | Kimi K2 | |
|---|---|---|---|
| layers | 60 | 61 | 61 |
| \(d\) | 5120 | 7168 | 7168 |
| \(n_h\) | 128 | 128 | 64 |
\(d_h\) (qk_nope_head_dim) |
128 | 128 | 128 |
\(d_c\) (kv_lora_rank) |
512 | 512 | 512 |
\(d_c'\) (q_lora_rank) |
1536 | 1536 | 1536 |
\(d_h^R\) (qk_rope_head_dim) |
64 | 64 | 64 |
⚠️ 三个易错点:其一,V2 是 \(d=5120\)、60 层,\(d=7168\)、61 层是 V3;其二,per-head QK 维是 192(128 维 NoPE 加 64 维 RoPE),而 V 是 128,所以 softmax scale 是 \(1/\sqrt{192}\) 而不是 \(1/\sqrt{128}\);其三,训练稳定性需要额外的 norm——V2 §3.1.2:"we employ additional RMS Norm layers after the compressed latent vectors, and multiply additional scaling factors at the width bottlenecks"。写 PyTorch 时别忘了
kv_a_layernorm与q_a_layernorm。
4.7 MLA 的质量对比¶
MLA 不只是更省,质量上确实优于 MHA。Appendix D.2 Table 9(MoE,同架构只换 attention):
| Benchmark | Small MoE + MHA | Small MoE + MLA | Large MoE + MHA | Large MoE + MLA |
|---|---|---|---|---|
| # Total Params | 15.8B | 15.7B | 250.8B | 247.4B |
| KV Cache/Token (#Elem) | 110.6K | 15.6K | 860.2K | 34.6K |
| BBH (3-shot) | 37.9 | 39.0 | 46.6 | 50.7 |
| MMLU (5-shot) | 48.7 | 50.0 | 57.5 | 59.0 |
| CMMLU (5-shot) | 52.3 | 53.4 | 60.7 | 62.5 |
原文:"MLA requires a significantly smaller amount of KV cache (14% for small MoE models and 4% for large MoE models) than MHA."(验算:15.6/110.6 = 14.1%、34.6/860.2 = 4.02% ✓)
配上 Appendix D.1 的 7B dense 对照(MQA 37.9 / GQA-8 41.2 / MHA 45.2 on MMLU),完整的结论链是:
MLA 在质量和 cache 两条轴上同时占优,这是它被 Kimi K2、Kimi Linear 等一路继承的原因。
摘要里的 "reduces the KV cache by 93.3%" 我复现不出来。按元素算(DeepSeek-67B 本身已是 GQA-8:
2·8·128·95 = 243,200;V2 MLA:576·60 = 34,560)是减少 85.8%;最可能的解释是它按部署字节算,V2 §3.2.3 还做了「KV cache 量化到平均 6 bit」(34560·6/8 = 25,920 Bvs243200·2 = 486,400 B→ 94.7%,接近但仍不精确)。文档里只引论文原话并给出元素级复现,不宣称能推出 93.3%。
5. GQA 到 MLA 的转换¶
既然 MLA 更好,能不能把已有的 GQA 模型改造过去?有两篇同期工作:
- TransMLA(arXiv:2502.07864):先证明「同等 KV cache 开销下 MLA 的表达力严格强于 GQA」,然后给出 training-free 转换。核心难点和 DeepSeek 自己遇到的一样——RoPE 阻断 Absorb。它用了两个技术:RoRoPE(对 key 输出做 PCA,把旋转跨 RoPE 两端施加,把所有 head 的主成分集中到第一个 head 的维度上;不变性条件是旋转矩阵只在所有 head 的同一维度内旋转、且 RoPE 实部虚部同样旋转)和 FreqFold(利用相邻 RoPE 维度频率相似来提升集中效率)。数字:LLaMA-2 压缩 68.75% 的 KV cache,6 个 benchmark 只掉 1.65%(training-free);压 93% 再训 6B tokens 基本恢复;vLLM 上最高加速 10.6 倍。
- MHA2MLA(arXiv:2502.14837):partial-RoPE(按对 attention score 的贡献做 contribution-aware top-k 选维,把贡献小的维度的 RoPE 去掉)加 joint SVD(对 \(W_{k,\mathrm{nope}}\) 与 \(W_v\) 合并分解,
SVD_joint优于分别做的SVD_split),只用 0.6%–1% 的训练数据。
partial-RoPE 这个想法后来被独立地大规模采用(Qwen3-Next 0.25、GLM-4.6 0.5、MiniMax 0.5、DeepSeek-V4 的 64 维),见 02 §3。
6. 小结¶
| 机制 | 每 token 每层元素 | 共享/压缩了什么 | 代价 |
|---|---|---|---|
| MHA | \(2 n_h d_h\) | —— | cache 巨大 |
| MQA | \(2 d_h\) | 全部 head 共享一份 K/V | 质量掉、微调不稳定、TP 下要复制 |
| GQA(\(g\)) | \(2 g d_h\) | 每 \(n_h / g\) 个 head 共享一份 | 可从 MHA mean-pool uptraining(5% steps),\(g=8\) 恰好对齐 TP |
| MLA | \(d_c + d_h^R\) | K/V 联合低秩压缩成一个 latent | 需要 decoupled RoPE 这条「补丁」;prefill/decode 要跑两种模式 |
| MLA + NoPE | \(d_c\) | 连补丁都不要了 | 位置信息得由别人提供 → 见 09、11 |
最后一行是一个伏笔:Kimi Linear 让 MLA 层全部 NoPE,把位置编码的责任整个交给 KDA 层,于是 MLA 在推理时退化成纯 MQA,长上下文训练也不再需要 YaRN 调参。这是「基础」这条线和「linear」那条线的交汇点。
下一篇:02 · 位置编码与数值稳定 —— 位置编码和数值稳定看起来不是「attention 机制」,但 RoPE 能不能被吸收、NoPE 能不能用、logit 会不会失控,恰恰决定了上面每一个机制在真实模型里能否落地。