Skip to content

约 11 分钟阅读

06. 多头注意力与 KV 头:MHA、MQA、GQA、MLA ​

上一篇用 4 个 token、每头 2 维的小向量手算了一次注意力:“申请”拿自己的 Q 去和前面每个 token 的 K 比分数,softmax 之后按权重把 V 加起来。真实模型不是只算一次,Llama-3.1-8B 每层同时算 32 份,这就是多头注意力。这一篇回答三个问题:为什么要拆成多个头;为什么 K、V 的头可以比 Q 少,少了省什么、丢什么;DeepSeek 的 MLA 怎样把 K、V 压成一个小向量再存。读完你应该能从任意模型的 config.json 判断它用的是哪种注意力,并算出每个 token 的 KV Cache 字节数。

一个头只能给出一种权重分布 ​

回到资料助手的问题“年假怎么申请?”。模型处理到“申请”时,至少有两件事要从上下文里取:申请的对象是“年假”,句子的形式是“怎么……”提问。一次注意力只产生一组权重,权重和为 1。如果它把 0.6 给“年假”、0.3 给“怎么”,取回来的就是两者 V 向量按 6:3 混合的结果,两种信息搅在了一起,后面的层很难再把它们分开。

多头注意力(Multi-Head Attention,MHA)的做法是把同一个 4096 维向量切成 32 段,每段 128 维,各自独立算一次注意力。每一段叫一个头(head):有自己的 Q、K、V,有自己的 softmax,也就有自己的一组权重。一个头可以几乎全看“年假”,另一个头可以几乎全看“怎么”,最后 32 个头的结果拼回 4096 维,再由一个输出矩阵混合起来。

text
Q = x · W_q      [n, 4096] → [n, 4096] → 看成 [n, 32 个头, 128]
K = x · W_k      [n, 4096] → [n, 1024] → 看成 [n, 8 个头, 128]
V = x · W_v      [n, 4096] → [n, 1024] → 看成 [n, 8 个头, 128]

第 j 个头:head_j = softmax(Q_j · K_g(j)ᵀ / √128 + 因果掩码) · V_g(j)     [n, 128]
输出:     o = concat(head_0, …, head_31) · W_o                         [n, 4096]

n 是 token 数,W_q、W_k、W_v、W_o 是这一层的四个投影矩阵(Transformers 里叫 q_proj、k_proj、v_proj、o_proj)。√128 ≈ 11.3,用来把点积缩回合适的范围,理由见上一篇。g(j) 表示第 j 个 Q 头用哪一组 K、V:在 Llama-3.1-8B 里 K、V 只有 8 个头,g(j) = ⌊j / 4⌋,每 4 个 Q 头共用一组。这一点后面细讲。

拆成多头并不增加计算。一个 4096 维的大头做点积要 4096 次乘加,32 个 128 维的小头合计也是 4096 次;q_proj 不管拆不拆都是 4096×4096 的矩阵。多头只是把“一次 4096 维的比较”换成“32 次 128 维的比较”,换来 32 组相互独立的权重。

Llama-3.1-8B 一层注意力中,32 个 Q 头每 4 个共用一组 K、V,各头结果拼接后经 o_proj 输出

图里颜色相同的 4 个 Q 头读同一组 K、V,但它们的 Q 不同,算出来的权重分布也不同。所以“共用 KV”不等于“这 4 个头看的东西一样”,它们是在同一份被查询的内容上提不同的问题。

每个头学到了什么 ​

头的分工不是人设计的,训练只规定“把 32 个头的结果拼起来后要有助于预测下一个 token”,每个头学成什么样由数据决定。可解释性研究看到了一些规律,但都有边界:

  • 在机器翻译模型里,有些头稳定地看前一个或后一个位置,有些头对应句法关系,有些头专门看罕见词;剪掉其余大部分头,质量下降不多(Voita 等,2019)。
  • 在 BERT 和翻译模型上,测试时去掉相当一部分头,指标几乎不变,有的层甚至只留一个头也可以(Michel 等,2019)。
  • 在语言模型里能找到 induction head:看到“A B … A”时,去找上一次 A 后面跟着什么,并把 B 抄过来。这类头和模型的上下文学习能力出现的时间点吻合(Olsson 等,2022)。

这些结论多来自中小模型和特定任务,说明头之间有分工、也有大量冗余,不能推出“第 5 层第 3 个头负责主语”这种可以直接查表的结论。同一个头在不同输入上可能做不同的事,一种功能也可能分散在多个头里。

冗余带来了一个直接的工程问题:既然很多头可以去掉,K、V 是不是不需要每个头一份?

KV Cache 只和 KV 头有关 ​

推理时每生成一个 token,新 token 只需要自己的 Q,但要读所有历史 token 的 K 和 V,所以历史的 K、V 被存成 KV Cache(原理见推理系列第 03 篇)。每个 token 要存多少,取决于层数、KV 头数和每头维度,与 Q 头数无关:

text
每 token KV 字节数 = 2(K 和 V)× 层数 × KV 头数 × 每头维度 × 每个数的字节数

Llama-3.1-8B 实际(8 个 KV 头):2 × 32 × 8  × 128 × 2 = 131,072 字节 = 128 KiB
如果每个 Q 头一组 KV(32 个):  2 × 32 × 32 × 128 × 2 = 524,288 字节 = 512 KiB

32,768 个 token 的上下文,按 MHA 存要 16 GiB,比 8B 模型 15 GiB 的 BF16 权重还多;按实际的 8 个 KV 头存是 4 GiB。整本员工手册放进上下文、几十个员工同时提问时,这个差距决定一张卡能服务几个人(显存的完整账见推理系列第 04 篇)。

除了占地方,KV 还要每步读一遍。Decode 阶段每生成一个 token,32 层的全部历史 K、V 都要从显存读到计算单元,读取时间往往比计算时间长(推理系列第 06 篇)。少存一个 KV 头,就是每步少搬一份数据。

MQA 与 GQA:让多个 Q 头共用 K、V ​

多查询注意力(Multi-Query Attention,MQA)走到了极端:Q 仍有很多头,K、V 只保留一个头,所有 Q 头共用(Shazeer,2019)。KV Cache 直接降到 MHA 的 1/头数。Falcon-7B 就是这样:71 个 Q 头、1 个 KV 头、每头 64 维,32 层每 token 只要 8 KiB。

代价是质量。所有头只能在同一份 K、V 上提问,被查询的内容只剩一种“写法”,模型容量下降;把训练好的 MHA 模型改成 MQA 还容易训练不稳定(Ainslie 等,2023)。分组查询注意力(Grouped-Query Attention,GQA)取中间:把 Q 头分成若干组,每组共用一个 KV 头。8 组时 KV 是 MHA 的 1/4、MQA 的 8 倍。GQA 论文报告,用原预训练约 5% 的算力就能把一个 MHA 检查点改训成 GQA,质量接近 MHA、解码速度接近 MQA。Llama 2 论文在 30B 规模上对比过三种结构:GQA 在多数任务上与 MHA 相当,平均好于 MQA,于是 34B 和 70B 用了 GQA;到 Llama 3,8B 也改用 8 个 KV 头。

GQA 在代码里很简单。Transformers 的 LlamaAttention 先按 8 个 KV 头算出 K、V 并写进缓存,计算注意力前用 repeat_kv 把每个 KV 头复制 4 份,对齐 32 个 Q 头:

python
# 简化自 transformers/models/llama/modeling_llama.py 的 LlamaAttention
q = q_proj(x).view(n, 32, 128)          # 32 个 Q 头
k = k_proj(x).view(n, 8, 128)           # 8 个 KV 头
v = v_proj(x).view(n, 8, 128)
q, k = apply_rope(q, k, pos)            # 位置编码,见第 09 篇
k, v = cache.update(k, v)               # 缓存里存的是 8 个头
k = k.repeat_interleave(4, dim=1)       # repeat_kv:复制成 32 个头参与计算
v = v.repeat_interleave(4, dim=1)
out = softmax(q @ k.T / 128**0.5 + causal_mask) @ v   # 逐头计算
return o_proj(out.reshape(n, 4096))

复制只发生在计算时,缓存里始终只有 8 份。高效的注意力算子会直接按组读 KV,连复制都省掉。

GQA 也有不适用的地方。KV 头太少会和张量并行冲突:把一层注意力切到 16 张卡上,每张卡要分到至少一个 KV 头,8 个 KV 头不够分,推理引擎只好在多张卡上复制同一个 KV 头,每张卡的 KV 显存不再按卡数下降(多卡切分见推理系列第 16 篇)。另外,KV 头的数目是预训练时定下的,对现成的 MHA 模型直接合并 KV 头而不再训练,质量会明显下降;要改,就得像 GQA 论文那样再训一段。

每 token KV 的对比 ​

把几种结构放在一起,按字节等比例画出来:

BF16 下每 token 的 KV Cache:Llama-3.1-8B 若用 MHA 为 512 KiB,实际 GQA 为 128 KiB,MQA 为 16 KiB;DeepSeek-V3 的 MLA 为 68.6 KiB

最下面一行值得多看一眼:DeepSeek-V3 是 6710 亿参数的模型,每层 128 个注意力头,每 token 的 KV 却只有 68.6 KiB,比 80 亿参数、8 个 KV 头的 Llama-3.1-8B 还少一半。它没有减少头数,用的是另一种办法。

MLA:缓存一个压缩向量,用时再展开 ​

GQA 省 KV 的方式是让头共用,代价是头之间的 K、V 不再独立。DeepSeek-V2 提出的多头潜在注意力(Multi-head Latent Attention,MLA)换了个思路:每个头仍有自己的 K、V,但它们都由同一个低维向量算出来,缓存里只存这个低维向量。

低秩压缩是核心。以 DeepSeek-V3 的配置为例(kv_lora_rank 512,128 个头,每头 K 的“内容部分” 128 维、V 128 维):

text
压缩(下投影):c_kv = RMSNorm(h · W_DKV)        [n, 7168] → [n, 512]      ← 存进缓存
展开(上投影):k_nope = c_kv · W_UK              [n, 512] → [n, 128 头, 128]
                v      = c_kv · W_UV              [n, 512] → [n, 128 头, 128]

如果直接按 MHA 存 128 个头的 K、V,每层每 token 要 128 × (128 + 128) = 32,768 个数;MLA 只存 512 个。“低秩”指 128 个头的 K、V 拼起来虽然有 32,768 维,但都是同一个 512 维向量的线性变换,自由度只有 512。这相当于假设各个头需要的 K、V 信息高度重叠,可以用一个共享的 512 维摘要表示。Q 也做了类似的压缩(7168 → 1536 → 每头 192 维),那是为了省训练时的激活显存,与 KV Cache 无关。

解耦 RoPE 解决位置编码的问题。RoPE 按 token 的位置旋转 Q 和 K(第 09 篇讲),旋转角度和位置有关。如果对展开后的 k_nope 做 RoPE,就必须先展开再旋转,缓存压缩向量的意义打了折扣,下一节的矩阵吸收也做不了。MLA 的做法是把位置信息单独拿出来:每个头的 Q 额外有 64 维 q_rope,K 额外有 64 维 k_rope,只有这两部分做 RoPE;k_rope 由输入直接算出、所有头共用一份,也存进缓存。所以 DeepSeek-V3 每层每 token 存 512 + 64 = 576 个数:

text
每头分数 = q_nope · k_nope + q_rope · k_rope      (128 维 + 64 维,再除以 √192)
每 token KV = 61 层 × 576 × 2 字节 = 70,272 字节 ≈ 68.6 KiB
若按 128 头 MHA 存:61 × 128 × (192 + 128) × 2 = 4,997,120 字节 ≈ 4.77 MiB,是 MLA 的 71 倍

576 个数相当于 2.25 个 128 维 KV 头的 K 和 V,缓存量接近一个只有 2—3 组的 GQA,但 128 个头的 K、V 各不相同。

DeepSeek-V3 的 MLA:KV 路径把 7168 维压到 512 维 c_kv 和 64 维 k_rope 存进缓存,用时经 kv_b_proj 展开;Q 路径压到 1536 维再展开成每头 192 维

矩阵吸收让推理时连展开都省掉。看一个头内容部分的分数:

text
q_nope · k_nope = q_nope · (c_kv · W_UK) = (q_nope · W_UKᵀ) · c_kv

右边先把 Q 乘上 W_UK 的转置,得到一个 512 维的向量,再直接和缓存里的 c_kv 点积。V 一侧同理:先在 512 维上按注意力权重加权求和,最后再乘 W_UV(也可以并进 o_proj)。这样 Decode 时 128 个头都在同一份 576 维缓存上计算,访存模式像 MQA,每个头的 K、V 却仍是各自的。DeepSeek 官方推理代码 inference/model.py 的默认实现 attn_impl = "absorb" 就是这样算的。代价是每个头的点积从 192 维变成 576 维,计算更多;Decode 本来受显存带宽限制,多出的计算基本被空闲算力吸收。有的推理引擎在 Prefill 阶段仍走展开路径,在 Decode 阶段走吸收路径。

读 Transformers 源码时要注意:modeling_deepseek_v3.py 的 DeepseekV3Attention 已经把压缩后的 c_kv 和 k_rope 写进缓存,但计算前调用 expand_kv 展开成 128 个头,没有做矩阵吸收。它的缓存大小是对的,计算方式是“参考实现”。

MLA 的问题也很具体:结构比 GQA 复杂,位置编码要单独拆一路,上下文扩展方法要跟着改;算子需要专门实现,早期很多推理引擎不支持或很慢;它解决的是 KV 大小,不改变注意力随上下文平方增长的计算量。质量方面,DeepSeek-V2 报告里的对比实验显示 MLA 不差于甚至好于 MHA,这是作者自己的结果,其他模型改用 MLA 时仍要用自己的评测确认。

主流模型选了哪一种 ​

模型注意力Q 头 / KV 头每头维度(K / V)层数每 token KV(BF16)
Llama-2-7BMHA32 / 3212832512 KiB
Falcon-7BMQA71 / 164328 KiB
Llama-3.1-8BGQA32 / 812832128 KiB
Llama-3.3-70BGQA64 / 812880320 KiB
Qwen2.5-0.5BGQA14 / 2642412 KiB
Qwen3-8BGQA32 / 812836144 KiB
gpt-oss-20bGQA64 / 8642448 KiB(未计滑动窗口)
DeepSeek-V3MLA128 头,缓存 512 + 64192 / 1286168.6 KiB
Kimi K2MLA64 头,缓存 512 + 64192 / 1286168.6 KiB
GLM-5MLA64 头,缓存 512 + 64256 / 2567887.75 KiB

表中数据来自各模型 2026 年 10 月时 Hugging Face 上的 config.json。看配置时:num_key_value_heads 等于 num_attention_heads 是 MHA,等于 1 是 MQA(老模型可能写成 multi_query: true),介于两者之间是 GQA;出现 kv_lora_rank 和 qk_rope_head_dim 就是 MLA,每层缓存 kv_lora_rank + qk_rope_head_dim 个数,此时 num_key_value_heads 不再表示缓存的头数。gpt-oss 一半的层只看最近 128 个 token,这些层的 KV 不随上下文增长,滑动窗口在第 12 篇讲;DeepSeek 系列的完整结构见第 17 篇。

趋势很清楚:中小规模的稠密模型基本停在 GQA,KV 头数 2—8;参数很大、要服务长上下文的 MoE 模型越来越多地采用 MLA。纯 MQA 在新的主流模型里已经少见。

多头注意力用 32 组独立的权重分布让一个位置同时取几种信息,计算量与单头相同;KV Cache 只取决于 KV 头的数目和维度,GQA 用“多个 Q 头共用一组 KV”换来 4 倍的节省且质量损失很小,MQA 省得更多但质量下降明显,MLA 则缓存一个低秩压缩向量、单独处理 RoPE,并在推理时把展开矩阵吸收进 Q 和输出。注意力把上下文的信息搬到了每个位置,下一篇看搬来之后每个位置自己做什么:07. FFN:前馈网络做了什么,也就是占了 8B 模型七成参数的那一部分。

别急,先让缓存热一下。