从0训练LLM · 现代大模型体系 阅读+实验 ≈ 40 分钟 页内实验 ×3
C3 注意力:一个 Token 怎么从别的 Token 那里取信息 第 10 课

KV 还能再压吗?

MLA:只存一个潜向量
GQA 一路减 KV 头,尽头是掉质量的 MQA。MLA 换了个维度:不存 K、V,只存一个 512 维的低秩潜向量,用的时候再展开;RoPE 那部分因为会破坏「把展开矩阵吸收进 Q」,要单独存 64 维。每 token 每层 576 个数。DeepSeek 从 67B 到 V2,KV 从 389,120 字节降到 69,120 字节——官方的「−93.3%」还包含了 KV 量化。

本课一问:KV 还能再压吗?

这一课先不碰:把序列本身压短、只读一部分缓存的稀疏注意力,以及不存 KV 的线性注意力(C11);把缓存量化到更低精度(C9、C13)。本课讲到 MLA 为止。

从已有出发

从已有出发 · 累计数据流 点已学环节看形状与参数
解码层

缓存 = 层数 × 每层每 token 存的条目 × 字节 × 上下文长度。上一课动的是条目里的「KV 头数」:从每个 Q 头一份,减到几个 Q 头共用一份。这一课沿着 DeepSeek 的真实演进走一遍,看看还能从哪里下刀。下面所有数字都由组件从 frontier-models.json 里的官方 config 现算。

先自己设计:继续减 KV 头

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案继续减 KV 头(一路走到 MQA)

KV 头数是一个整数,从 8 减到 4、2、1,每次最多砍一半;到了 1 就没法再减了,而质量在这条路的末端已经开始吃亏。要继续压,得换一个维度。

KV 压缩的演进 · 每 token 每一步砍掉哪一维(官方 config 现算)
每 token 的 KV 缓存字节(对数刻度,同一刻度;悬停或点击一行看对应的卡)
🎯

试三件事:① 沿着阶梯从左往右看每一张卡的「砍掉了哪个维度」:先是头数,再是「每层每 token 存什么」;② 注意 V2 与 V3 是两张卡:同样是 576 个数,层数不同(60 与 61)所以字节不同——旧稿把它们合成一行是错的;③ 打开「−93.3% 是相对谁」面板,看同一个 MLA 为什么能被说成降了 82.2%、93.3% 或 98.2%。

改进:MLA 怎么存、怎么用

MLA 的核心是四个矩阵乘法。点每一项看它是什么、为什么在那(旧版课程的解说在这里补上了漏掉的 RoPE 一项,并纠正了 −93.3% 的基线):

MLA:缓存的不是 K、V,是 c 和 k^R · DeepSeek-V2 式 (9)–(17)
= ; ktC = , vtC = ; ktR =
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
对比每层每 token 的缓存元素数(DeepSeek-V2 表 1 的口径):MHA 2·n_h·d_h;GQA 2·g·d_h;MLA d_c + d_R。代价是打分时要多做上投影(或把它吸收进 Q 之后,在 512 维上打分):拿计算换显存。

真实形状(GLM-5.3 的 config;它和 DeepSeek-V3 一样是 latent 512 + RoPE 64,只是 K 的非位置部分每头 192 维、V 每头 256 维):

MLA 的压缩与展开(GLM-5.3 真实形状)
cKV = h·WDKV  kC = cKV·WUK  vC = cKV·WUV  kR = RoPE(h·WKR)
输入参数(可学习)输出中间结果
边长按对数比例缩放,块边上的数字是真实值;同一维度符号的边在全图等长。
维度含义 真实规模 小规模
T 序列长度(token 数) 5 5
d 隐藏维度(hidden_size) 6144 8
dc KV 潜向量维度(kv_lora_rank),每层每 token 缓存它 512 2
dR 解耦 RoPE 维度(qk_rope_head_dim),所有头共用,也缓存 64 1
nh·dnope 头数 × 每头不带 RoPE 的 K 维 = 64 × 192(num_attention_heads × qk_nope_head_dim) 12,288 6
nh·dv 头数 × 每头 V 维 = 64 × 256(num_attention_heads × v_head_dim) 16,384 6
数字来自 frontier-models.json 的 GLM-5.3 config(zai-org/GLM-5.3,2026-09-26 获取);每 token 每层缓存 dc + dR = 512 + 64 = 576 个数,× 78 层 × 2 B(BF16)= 89,856 B/token。展开后的 kC 每 token 12,288 个数、vC 16,384 个数,都不进缓存。「小规模」只为看清形状关系,数值不来自任何模型。

「−93.3%」到底是相对谁。DeepSeek-V2 报告说它「相对 DeepSeek 67B 把 KV 缓存降了 93.3%」 。这句话有两个容易被略掉的前提:基线是 67B(本身已经是 GQA-8),不是同配置的 MHA;并且部署时还把 KV 量化到了平均约 6 bit。只看 BF16,V2 相对 67B 降的是 82.2%;按 V2 论文表 1 的口径和「同配置 MHA」比,降的是 98.2%——上面组件的面板把三个数都现算了一遍。

⚠ 常见误解

⚠

「MLA 只是换了个名字的 GQA。」不是。GQA 仍然存 K、V,只是少存几份(条目按 g × dh 记账);MLA 不存 K、V,存的是能生成全部头的 K、V 的一个潜向量,加一小段位置。DeepSeek-V2 报告里的换算是:MLA 的缓存量相当于只有 2.25 组的 GQA ,但每个头仍有自己独立展开的 K、V。报告还自称它的效果强于 MHA——这是官方自报的实验结论 。

⚠

「DeepSeek-V2 的 KV −93.3%,说明 MLA 相对同配置 MHA 降了 93.3%。」见上:基线是 DeepSeek 67B(GQA-8),还含了约 6 bit 的 KV 量化。三种口径(82.2% / 93.3% / 98.2%)各有前提,引用时要带上。

注意力的演进,你在这里

注意力的演进:每一代都被上一代的某个真数字逼出来(本课所在的一代已高亮;另一条线可展开)
① 训练时:一个 token 怎么从别的 token 那里取信息
  1. 起点 RNN:一个接一个地读 注意力之前的主流
    解决了能读任意长的序列,参数与长度无关;手里只带一个固定大小的状态 h。
    卡在ht 要等 ht−1 算完:T = 4,096 时每层 4,096 步不能并行,第 1 个 token 的信息要经过 4,095 次更新才到最后;翻译时整句还得挤进一个定长向量——Bahdanau 2014 说的瓶颈。
    展开岔路 B1
  2. 第一代 Bahdanau 加性注意力 Bahdanau 2014
    解决了解码器每一步回头给源句的每个位置打分、加权取回,不再只靠那一个定长向量:score = vT·tanh(W1q + W2k)。
    卡在每一对都要单独过 tanh,交不给矩阵乘单元;朴素实现要先落一个 T×T×da 的中间张量:T = 4,096、da = 256 时单头单层 8.0 GiB(BF16),点积只留 T×T 个分数,32 MiB。它还长在 RNN 翻译模型里,串行没解决。
    展开第 2 课
  3. 第二代 ① 自注意力:Q/K/V 三种投影 + 点积 Vaswani 2017 §3.2
    解决了直接比 embedding 时「给了」最像的是它自己(余弦 1.000),小明、苹果、小王排在最后;投影成 Q、K 再点积,它自己排到最后、前三名是苹果、小王、小明,全部配对一次矩阵乘 QKT;取回用单独的 V——两个人名的 K 余弦 0.995,取回 K 分不清是谁。
    卡在不缩放:锚点每头 256 维,点积标准差 √256 = 16,同一行分差十几二十是常态(e20 ≈ 4.9 亿),softmax 几乎独占,梯度传不回 WQ、WK。
  4. 第二代 ② softmax 与 ÷√d:缩放点积 Vaswani 2017 式 (1)
    解决了softmax 把分数变成正的、和为 1 的权重,只看分差(按占比分,对话样本的 10、3、−4 会给「汽车」−44.4%);除以 √dk = 16,点积方差回到 1。
    卡在训练时整句一次喂进去:不加掩码,「给了」一行有 32.5% 的注意力直接落在它要预测的答案「小王」上,loss 低得虚假。
    展开第 3 课
  5. 第二代 ③ 因果掩码 Vaswani 2017 §3.2.3
    解决了softmax 之前把右上三角置为 −∞:未来的权重恰为 0,一次前向做完 4 道「只看前缀」的题(逐个前缀喂要 5 次前向、15 个位置)。
    卡在一次 softmax 只有一份 100%:「给了」要同时找「谁给的」和「给了什么」,只能在苹果 56.5% 与小明 39.4% 之间劈开,取回两个 V 的混合。
    展开第 4 课
  6. 回扣 C2 RoPE 只转 Q、K Su 2021;锚点部分旋转
    解决了旋转后 q·k 只依赖距离 j − i;V 不参与打分,不转(转了会把绝对位置混进取回的内容)。锚点每头 256 维里只转 64 维。
    卡在旋转矩阵随位置变,夹在展开矩阵和缓存之间就没法事先并进 Q:MLA 因此把 RoPE 单独拆出 64 维另存(DeepSeek-V2 config,第 10 课)。
  7. 第二代 ④ 多头注意力 Vaswani 2017 §3.2.2
    解决了同一组投影按列切成几个头,各做各的 softmax,参数与乘法次数不变:头 A 把 89.5% 给「小明」、头 B 把 77.0% 给「苹果」;锚点 24 个头 × 256 维。
    卡在生成时一次只出一个 token:每一步把整段重新喂进去,提示 2 个、再生成 3 个就要算 9 份 K/V,其中 5 份是上一步刚算过的;K/V 次数随长度平方增长。
    展开第 7 课
  8. 旁支 attention sink 与它的出口 Xiao 2023 → Gated Attention 2025、DeepSeek-V4
    解决了标准 softmax 逼每行分完 100%:「苹果」一行 3 个分数只差零点几(0.04、0.36、0.00),照样分成 30% / 41% / 29%;分母加可学习的 sink logit,或在输出上乘门,这个头就能「这次不取」——锚点开着输出门(attn_output_gate = true)。
    卡在出口的效果只有实验证据:Gated Attention 原文说门控「缓解」sink,不是消除;DeepSeek-V4 没给 sink logit 的单独消融。
    展开岔路 B4
② 生成时:K、V 怎么越存越少
  1. 第三代 KV cache 自回归生成的标准做法
    解决了因果掩码保证旧 token 的 K、V 永不再变,存下来反复用:同一个例子只算 4 份,输出逐位相同(项目测试核对)。
    卡在缓存随上下文线性增长:若 24 个 Q 头各存一份(MHA),锚点形状下每 token 16 层 × 2 × 24 × 256 × 2 B = 384 KiB,原生 262,144 上下文 96 GiB——一条请求,还没算权重。
    展开第 8 课
  2. 第四代 MQA:所有头共用一份 K、V Shazeer 2019
    解决了缓存直接除以头数:锚点形状下每 token 16 KiB,262,144 上下文 4 GiB。
    卡在质量会掉、训练可能不稳:GQA 论文表 1(T5-XXL,七个任务平均)MHA 47.2 → MQA 46.6。
    展开第 9 课
  3. 第五代 GQA:分组共用 K、V Ainslie 2023;锚点 4 组
    解决了24 个 Q 头分 4 组、每组 6 个共用 1 个 KV 头(组数 = KV 头数):每 token 64 KiB、262,144 上下文 16 GiB;T5-XXL 上 GQA-8 平均 47.1、推理 0.28 s,质量接近 MHA(47.2、1.51 s),速度接近 MQA(0.24 s)。
    卡在再减 KV 头就回到 MQA:DeepSeek LLM 67B 已是 GQA-8,每 token 仍要 389,120 B(每头一份的 MHA 是 3,112,960 B),1M token 单条请求 380 GiB。
    展开第 9 课
  4. 第六代 MLA:只存一个低秩潜向量 DeepSeek-V2 2024 → V3、GLM-5.3 你在这里
    解决了不存 K、V,每层每 token 只存 512 维潜向量 + 64 维 RoPE = 576 个数,用时再展开:DeepSeek-V2(60 层)每 token 69,120 B、V3(61 层)70,272 B;相对 67B 按 BF16 降 82.2%,官方「−93.3%」还含约 6 bit 的 KV 量化。
    卡在仍随长度线性增长,打分仍是 T²:GLM-5.3 同是 576 个数、78 层,每 token 89,856 B,原生 1,048,576 上下文时单条请求只算潜向量就要 87.75 GiB。
  5. 交接 C11 线性注意力 / 稀疏注意力 锚点另外 48 层;DeepSeek-V4.1
    解决了换掉「按长度存」本身:锚点 64 层里 48 层不存 KV,只存固定大小的状态(FP32 共 144 MiB),与上下文长度无关(这些层是什么、怎么更新,C11 第 4 课揭晓);DeepSeek-V4.1 叠加压缩、稀疏与低精度,官方报告全局 KV 890 B/token。
    卡在固定状态装不下任意多对键值,锚点仍留 16 层全注意力;稀疏注意力每步只读挑出来的一部分,挑本身也有代价——各自省了什么、没省什么,C11 讲。
数字口径:锚点 Qwen3.8-27B 与 DeepSeek LLM 67B / V2 / V3、GLM-5.3 的字节数由 anchor-model.json / frontier-models.json 的官方 config 在构建时现算(BF16 每元素 2 字节);「给了」一行的余弦、权重、各头注意力与 sink 那一行,由本章 teaching-world.json 现算——那是手工构造的教学向量,不是模型学到的,与各课组件同一份数据;T = 4,096 是第 2 课与岔路 B1 取的示例长度;T5-XXL 分数出自 GQA 论文表 1,「−93.3%」出自 DeepSeek-V2 报告,890 B 出自 DeepSeek-V4.1 模型卡(均为官方自报)。

证据三问

  1. 它解决什么问题?在不减少头数、不牺牲每个头独立 K/V 的前提下,把每层每 token 的缓存从「2 × 头数 × 每头维数」降到「潜向量 + 一小段 RoPE」。
  2. 数学上为什么可能有效?缓存字节是计数 ;WUK 可以并进 Q 是矩阵乘法结合律 ;RoPE 会让吸收失效是因为旋转矩阵随位置变化、矩阵乘法不可交换 。「K、V 的有效信息是低秩的」是理论动机 。
  3. 实验上为什么选了它?DeepSeek-V2 报告它比 MHA 更强、KV 小得多 ;之后 DeepSeek-V3、GLM-5 系列、Kimi 的全局注意力层都采用了 MLA(见 frontier-models.json 与调研档案 R2、R3)。

一句话

MLA 不存 K、V,只存一个低秩潜向量;RoPE 会破坏「把展开矩阵并进 Q」,所以位置那部分单独存一小段。

下一个缺口

到这里,「注意力」这一环讲完了:每个 token 按自己的 Q,从前面的 token 那里按权重取回 V,多个头各取各的,WO 把它们混在一起。可这些都只是在搬运和混合已有的信息(第 5 课:加权平均造不出新方向)。取回来之后,每个 token 自己还要做什么加工?→ C4:一个 Transformer 块——FFN、残差、归一化、门控。

在进入 C4 之前,推荐先去 实验课 看一眼我们自己训练的模型,它的注意力到底长什么样。

你可能会问

这一课还没有收到真实提问;你的提问会记到 05-问答记录/C3-L9.md。

🔍 KV 压到 576 个数之后,还能再压吗?

还能,但要换维度。前面两步砍的是「每层每 token 存什么」;接下来可以砍「序列有多长」(把几个 token 的 KV 压成一条、只读挑出来的一部分)、「多少层存」(几层共用一份)、以及「每个数几个比特」(FP8、FP4)。DeepSeek-V4.1 把这些叠在一起,官方报告全局 KV 只有 890 字节/token——上面组件的阶梯末尾有一张虚线卡。那是 C11 的故事:稀疏注意力省了什么、没省什么,以及锚点 Qwen3.8 那 48 层不存 KV 的注意力是什么。

🔍 MLA 省下了显存,是不是也省了计算?

没有,反而多了一点。训练和 prefill 时,每个头的 K、V 仍要从 c 展开出来参与打分;decode 时可以把 WUK 并进 Q、在 512 维的 c 上直接打分,省掉展开,但每个头的查询维度也跟着变成了 512 + 64。DeepSeek 的选择是用这点计算换大幅减少的显存与读取量——decode 的瓶颈在读数据,这笔交易划算。