MLA · DeepSeek-V2 的注意力机制
原文
Paper · DeepSeek-V2 · 2024

把 KV cache 联合压成一个 latent 向量 · MLA 是怎么做到的

推理时的 KV cache 已经是大模型长上下文部署的硬约束。MLA(Multi-head Latent Attention)用一招"低秩联合压缩 + 矩阵吸收",把 cache 缩到 GQA 2.25 组的水平,效果反而比 MHA 更好。

📄 DeepSeek-V2 · arXiv 2405.04434 📅 2024 / 5 🔗 arxiv.org/abs/2405.04434 💻 GitHub
TL;DR · 30 秒拿到核心思想

要解决的问题是:自回归推理时每生成一个 token 都要把所有历史 token 的 K 和 V从显存读一遍,KV cache 同时吃掉显存容量和带宽,长上下文 + 大 batch 直接撞墙。

MQA / GQA 用「让多个 query head 共享同一组 K/V」来缩 cache,但共享是粗暴的有损压缩,效果会掉。MLA 不走共享,而是把 K 和 V 一起低秩联合压缩成一个 d_c 维 latent 向量缓存起来;再借助矩阵乘法的结合律把上投影矩阵吸收进 W^Q 和 W^O,推理时根本不用还原出 K/V。RoPE 因为破坏可交换性需要单独处理:拆出一条「带 RoPE 的小 head」并行计算,再拼回去。

93.3%
vs DeepSeek 67B(MHA),KV cache 总量减少
5.76×
最大生成吞吐量提升(同硬件、同序列长度)
>MHA
在 MoE 场景下 BBH/MMLU/CMMLU 都略胜 MHA
01 · 问题

瓶颈不在算力,而在「KV cache 的搬运」

Transformer 推理时为了不重算前面 token 的 K 和 V,会把它们逐层缓存下来。这块缓存体量随上下文长度线性增长、随 batch 线性增长,最终撞到的不只是显存装得下装不下,还撞到「每个 step 都要把它从显存读一遍」的带宽。

自回归生成的运作方式很简单:模型每生成一个 token,都要让这个新 token 的 query 与所有历史 token的 key、value 做注意力。如果每生成一步都重新算一遍前面所有 token 的 K/V,复杂度是 O(n²),根本没法用。所以工程上一定要把历史的 K 和 V 缓存下来,只为新 token 算 K/V 然后追加进去——这就是 KV cache。

问题在于这块 cache 的体量:每层 2 × nh × dh 个元素,乘上层数 l,再乘上序列长度,再乘上 batch。以 DeepSeek 67B(标准 MHA)为例,单 token 单层 cache 是几 KB;推到 128K context、batch=8,一个请求就要吞几十 GB——大头反而不是模型权重。

更要命的是:每个 decoding step 都要把整个 KV cache 从显存里读一遍。decoding 是 memory-bound 的,不是 compute-bound 的。这意味着你哪怕用 H100 也喂不饱算力,瓶颈在显存带宽(HBM bandwidth)上。所以「缩小 KV cache」不只是省显存,更是直接降低单 step 延迟、提升 throughput 的杠杆。

▸ 推理为什么是 memory-bound
DECODE 一个 TOKEN 的三件事
每生成一个 token,cache 都要被全量搬运一次

Decoding 时新 token 的 query 是单条向量,K/V 矩阵是历史所有 token——这是个又长又瘦的 GEMV,HBM 带宽喂不动算力,绝大部分时间花在搬 cache 上。

STEP 1
算新 token 的 Q/K/V
输入 hidden state h_t → 三组 projection 算出 q_tk_tv_t。计算量小、一次搞定。
STEP 2 · 瓶颈
从 HBM 读全量 KV cache
显存带宽决速步。要把过去所有 token 的 K、V 从 HBM 搬到 SRAM 才能算 attention score。
STEP 3
算 attention + FFN,写回新 KV
softmax(QKᵀ)V,输出经 FFN 得到下一层 hidden;同时把新 token 的 K/V 追加到 cache。
KV cache 大小(per token, per layer · MHA)
cache = 2 · n_h · d_h · sizeof(dtype)
n_hhead 数(number of attention heads)。多头机制把 attention 切成 n_h 份并行做。LLaMA-3 70B 用 64,DeepSeek-V2 用 128。
d_h每个 head 的维度。通常 d_h = d / n_h(d 是 hidden size),常见取值 64 或 128。
l层数。每一层的 attention 都要独立维护一份 KV cache。
×2K 和 V 各存一份,所以前面带了一个 2。
n_h = 128, d_h = 128, fp16 为例:单 token 单层 = 64 KB;推满 60 层 + 128K 序列长度 + batch=4,仅 KV 就 ≈ 1.9 TB。这就是为什么大家拼命要压 cache。
02 · MLA 之前

从 MHA 到 GQA · 业界一直在「让多个 query head 共享同一组 KV」

MLA 之前主流的 cache 压缩思路只有一条:既然每个 head 都各自有 K 和 V 太贵,那就让多个 query head 共享少量 K/V head。MQA 是极端共享(全部共享一组),GQA 是折中(按组共享)。但本质上都是在「质量 vs cache」之间做线性 trade-off。

原始的 MHA(Multi-Head Attention)给每个 head 单独算一组 K 和 V。这是 quality 的天花板,也是 cache 的"丑数字"。

2019 年 Noam Shazeer 提出 MQA(Multi-Query Attention):让所有 head 共享同一组 K/V,cache 一下子缩小 nh 倍。代价是质量肉眼可见地下降,且训练不稳定。

2023 年 Google 提出 GQA(Grouped-Query Attention):把 head 分成 ng 组,每组共享一组 K/V。本质是 MHA 和 MQA 中间的插值,绝大部分 LLaMA-2/3 系列、Qwen、Mistral 都在用。它解决了 MQA 质量太差的问题,但 KV cache 的下限是 GQA-1(即 MQA),上限是 GQA-H(即 MHA),它没有打破"共享 K/V"这个范式

DeepSeek 的论文里做过一个直观对比:在 7B dense 上对齐参数量后,MMLU 分别是 MQA 37.9 / GQA 41.2 / MHA 45.2 ——共享得越狠,掉得越多。MLA 想问的是:能不能既不共享、又把 cache 缩到 GQA 的量级,同时质量持平甚至超过 MHA?

▸ 四种注意力机制并排看 K/V 的"分布密度"
MHA · MQA · GQA · MLA · 简化对比
所有 query head 都要算 attention,但 K/V 谁出钱、出多少?

下面四张小图都画 4 个 query head(紫色)。差别只在于 K/V(青色 / 橙色)有几组、来源是什么。MLA 不再像前三种那样"按 head 摊分",而是先压成一个 latent 再 up-project。

MHA Vaswani · 2017
每个 head 独立的 K 和 V。质量最强,cache 最重。
4 Q · 4 K · 4 V Q heads K V
cache/token2·n_h·d_h·l
质量★★★★
问题cache 过大
MQA Shazeer · 2019
所有 query 共享同一对 K/V。cache 最小,质量掉得明显。
4 Q · 1 K · 1 V shared K shared V
cache/token2·d_h·l
质量★★
问题掉点 / 不稳
GQA Google · 2023
分组共享 K/V。LLaMA-2/3、Mistral、Qwen 主流方案。
4 Q · 2 K · 2 V (G=2) 2× K 2× V
cache/token2·n_g·d_h·l
质量★★★
本质插值 MHA↔MQA
MLA DeepSeek · 2024
不共享!把 K/V 联合压成一个 latent 向量缓存,用时再 up-project。
4 Q · 1 latent c latent c_t^KV cache 这个
cache/token(d_c+d_h^R)·l
≈ GQA2.25 组水平
质量> MHA
关键观察:MQA / GQA 仍然是「cache 里直接放 K 和 V」,只是数量从 H → G → 1 在缩小;MLA 是cache 里放一个低秩 latent c,K 和 V 由 c 经过权重矩阵 up-project 出来——这才让"质量超过 MHA"成为可能。
03 · MLA 怎么做

低秩联合压缩 + 矩阵吸收 · 让 cache 只存一个 latent

MLA 真正的秘诀有两部分。第一部分让 cache 变小:把 K 和 V 联合压成一个低秩 latent;第二部分让计算也不变贵:利用矩阵乘法的结合律,把"还原 K/V 的 up-projection"提前吸收进 W^Q 和 W^O,推理时根本不需要解压回 K/V

第一招:低秩联合压缩。给定输入 ht d,MLA 不再像 MHA 那样直接算出全维度的 K 和 V,而是先经过一个 down-projection 矩阵 WDKV 把它压成一个小很多的 latent 向量 ctKV dc,其中 dc nh·dh。需要 K 时再用 WUK up-project 回去,需要 V 时再用 WUV up-project 回去。

注意"联合"两个字:K 和 V 共用同一个 latent,意味着 cache 里只存这一个 latent 就行,cache 量直接从 2·nh·dh 砍到 dc——而且这是每个 token 都拥有自己的 latent,不是 MQA/GQA 那种"多个 head 共用一组 K/V"的有损共享。

第二招:矩阵吸收(associativity trick)。看起来 cache 是省了,但推理时每来一个新 query,还要把 WUKWUV 作用在缓存的 latent 上去把 K/V "解压"出来——这步算量加回来怎么办?利用矩阵乘法结合律,WUK 可以提前合并进 query 投影矩阵 WQWUV 可以合并进输出投影矩阵 WO。这样 attention 算分数时直接用 query 和 latent 算内积,解压 K/V 这一步在数学上根本不存在(具体推导见下文 trick 块)。

结果是:cache 里只放一个 dc 维向量,attention 计算也不需要把它展回 K/V 全维度。这一步就是 MLA 区别于"普通低秩 KV"的关键——不只是省内存,连计算也是直接在 latent 空间里做的

▸ 把"压缩"和"吸收"拆成两段直观看
MLA 的两步戏法 · 一步省 cache,一步省计算
阶段 1 怎么"压",阶段 2 怎么"不必再展开"
下面用方块宽度直观表达维度大小,再用矩阵卡片演示"吸收"到底是把哪两个矩阵合并成了什么。两步合起来才是 MLA 真正的优势——光压不吸收,每步都要还原 K/V,省 cache 但加算量;光吸收不压,cache 一点没小。
阶段 1
压缩 · 把全维度 hidden 砸成一个小 latent
ht
d 维(例如 5120)
输入 hidden state · 全维度
× WDKV · down-project(实际发生)
ctKV
dc 维 (≈ 512) ★ CACHE 只存它
维度从 d 缩到 dc,约 10× 落差
× WUK / WUV · up-project(朴素实现要算 → 阶段 2 把它抹掉
K
nh·dh
V
nh·dh
看点:方块宽度反映了真实维度。ht 是宽方块,c 是窄方块——"压"就是这一步。下方灰色 K / V 是 MLA 数学公式里仍然定义的中间量,但它们到底要不要被实际算出来,取决于阶段 2。
阶段 2
吸收 · 用结合律让"还原 K"那一步从代数上消失
朴素写法
3 个矩阵相乘
每一步都要先算 K
query 头
qt
1 × dh
×
MLA 上投影
WUK
dh × dc
×
从 cache 读
cjKV
dc × 1
=
输出
attn
score
用矩阵乘法结合律,把跟 cache 无关的两块提前合并
q · WUK · c   =   (WUK⊤ · q) · c   =   · c
WUK(来自 MLA 的上投影)和 q(由 WQ 算出)都跟 cache 里的 c 无关——它们能被提到一起合并成一个新矩阵 ,这个合并离线一次性算好,推理时不再发生。
吸收后
只剩 2 个矩阵相乘
K 永远不算
latent 空间的 query
t
1 × dc
合并自: WUK⊤ × WQ · ht
×
从 cache 读
cjKV
dc × 1
=
输出
attn
score
输出侧同样地:把 WUV 合并进输出投影矩阵
WO · WUV · (∑ softmax · c)   =   O · (∑ softmax · c)
attention 加权后的"值"也直接用 c 来算,再乘合并矩阵 O = WO · WUV 投回 d 维。V 同样从不被还原
融合矩阵的来源一目了然:q̃ 卡片下方明确写了它由 WUK⊤(MLA 阶段 1 的上投影)和 WQ(MHA 时代就有的 query 投影)相乘而来。颜色对应上面卡片的边框色——读者可以一一对照。
▸ 训练时的全流程 vs 推理时的 latent-only 路径
两种视角看同一个机制
训练时算的样子 vs 推理时实际跑的样子

切换两个 tab 看清楚:训练时为了得到正确的 attention 输出,K 和 V 是真的被算出来过的;推理时由于矩阵吸收,那一步根本不发生,cache 里只有一个 latent。

输入token t 的 hidden
h_t ∈ R^d
压缩down-projection
W^DKV
c_t^KV ∈ R^{d_c}
d_c 远小于 n_h·d_h(DeepSeek-V2 取 d_c = 4·d_h)
解压up-projection
c_t^KV
W^UK
k_t^C(n_h 个 head 的 key)
c_t^KV
W^UV
v_t^C(n_h 个 head 的 value)
Query同样压缩
h_t
W^DQ
c_t^Q
W^UQ
q_t^C
Query 的低秩压缩主要为训练时省 activation 显存,不影响 KV cache
Attention常规计算
softmax(q · kᵀ / √d) · v
W^O
u_t(输出)
压缩后的 latent(待 cache) 原始输入 / 中间张量 实算的 K / V(训练时存在)
缓存每 token 只存这个
c_t^KV ∈ R^{d_c}
所有历史 token 的 latent 拼成 cache(外加后面 RoPE 部分的 k_t^R,下一节讲)
Query 侧把 W^UK 吸收
h_t
(W^UK)^T · W^UQ · W^DQ
q̃_t(latent 空间的 query)
三个矩阵在离线时合并成一个。query 直接落在 latent 空间
Attention在 latent 空间
attn_score = q̃_t · c_jᵀ
直接 query · latent 求内积,从来不还原 K
Output 侧把 W^UV 吸收
∑ softmax · c_j(值是 latent 的加权和)
W^O · W^UV
u_t
同样 offline 合并;从来不还原 V
已消失这些步骤不再发生
c_t^KV → W^UK → k_t^C ✗
c_t^KV → W^UV → v_t^C ✗
cache 实际存放的内容 合并后的等价 query 被矩阵吸收,不再算
⚡ 矩阵吸收 trick
为什么"压成 latent 再展回去"不会让 attention 变贵

关键是注意力分数 q · k 的形式。把 key 的定义代入并利用矩阵乘法结合律重组:

qt · kj   =   qt · ( WUK · cjKV )   =   (WUK⊤ · qt) · cjKV
同理:WO · WUV · softmax · cjKV (把 WUV 也提到外面合并进 WO

也就是说,WUK 可以被吸收进 WQ(实质是 WUQWUV 可以被吸收进 WO。这两步合并是离线的、一次性的;推理时 attention 直接在 latent 空间做,K 和 V 永远不需要被还原成全维度。这是 MLA 区别于普通 "low-rank KV" 想法的关键工程点。

04 · RoPE 兼容

遇上 RoPE 就破功 · 怎么再补回来

矩阵吸收依赖于矩阵乘法的结合律和可交换的因子顺序。但 RoPE 是位置敏感的旋转矩阵,它会卡在 W^Q 和 W^UK 中间,不能交换位置——吸收技巧立刻失效。MLA 的解法是「解耦 RoPE」:让一小部分 head 维度专门承担位置编码,其余维度仍然走 latent 通路。

RoPE 的工作方式是给 query 和 key 各自乘一个由位置 t 决定的旋转矩阵 Rt。在 MHA 里这无伤大雅,因为 q 和 k 都是显式存在的。但在 MLA 里,如果想给 key 加 RoPE,就变成 kt = Rt · WUK · ct。这时 WQWUK 之间多了一个跟当前 query 位置有关的 Rt,矩阵乘法不再可交换——WUK 没法再被合并进 WQ每生成一步都要重新 up-project 所有历史 token 的 K,吞吐立刻崩盘。

DeepSeek 的解法很巧:与其让 RoPE 和 latent 通路硬碰硬,不如把它们分开两条路。给每个 head 额外开一段维度 dhR(论文里 dhR = dh/2)专门用来承载 RoPE 信息,这部分的 query 仍然按 head 分开算(保留位置敏感性),但 key 是所有 head 共享一个(这部分回到 MQA 的做法,反正只是辅助位置信号)。

原本走 latent 通路的部分(content)继续保持矩阵吸收的红利;带 RoPE 的小尾巴单独缓存一份。最终 attention 时把 content 和 rope 两段拼接起来一起做内积。cache 多出来一份 dhR · l,但量级很小:DeepSeek-V2 总 cache = (dc + dhR) · l 4.5·dh·l相当于 GQA 只有 2.25 组的水平,但效果比 MHA 还强。

RoPE 为什么破坏吸收
问题与解法 · 一图看穿
✗ 直接给 latent K 上 RoPE
乘法链中冒出"位置矩阵 R_t"

attention 分数变成 qt · Rt · WUK · cjKVRt 与位置有关,没法跟 WUKWQ 互换位置。"把 WUK 吸收进 WQ"的等价变形不再成立。

后果:每个 step 都要重新算所有历史 token 的真实 key,cache 缩了但计算量爆炸,工程上得不偿失。

✓ Decoupled RoPE
把 head 维度切成「content + rope」两段

Content 段(d_h 维):走 latent 通路,按原方案吸收 W^UK / W^UV,不带 RoPE。
RoPE 段(d_h^R 维):专门承载位置信息。query 按 head 分开算,key 是所有 head 共享一个 k_t^R。

attention 时拼接两段一起算内积。Content 享受吸收红利,RoPE 段单独 cache 一份。

解耦后两条路并行

最终的 query / key 结构

每个 head 的 query 和 key 都是「Content 部分 ⨁ RoPE 部分」拼接而成。Content 部分走 MLA 的 latent 通路,RoPE 部分走类 MQA 的共享通路。

Content 通路 d_h 维 · 每 head 独立

q^C ← W^UQ · c_t^Q(query 由低秩 latent up-project)
k^C ← W^UK · c_t^KV(key 由 KV latent up-project)
推理时整条路被矩阵吸收,cache 只放 c_t^KV。

RoPE 通路 d_h^R 维 · key 全 head 共享

q^R ← RoPE(W^QR · c_t^Q)(每 head 一份)
k^R ← RoPE(W^KR · h_t)(全 head 共享一份
承担位置编码,cache 单独存 k_t^R。

q_{t,i} = [ q_{t,i}^C ⨁ q_{t,i}^R ] · k_{j,i} = [ k_{j,i}^C ⨁ k_j^R ] → attention 一次算完
cache 总量:(d_c + d_h^R) · l 个元素 / token。论文中 d_c = 4·d_h、d_h^R = d_h/2,合起来 = 9/2 · d_h · l,相当于 GQA 仅 2.25 组的水平。
05 · 数字说话

cache 缩、质量升、吞吐翻 · 每个数字都来自论文

论文里给了两组对照实验:4 种注意力机制的每 token 缓存大小公式,以及在小 / 大 MoE 上 MLA 与 MHA 的实测对比。MLA 在两种规模下都既省 cache 又涨点。

每 token 的 KV cache 大小(公式)

机制cache (元素数)能力
MHA2·n_h·d_h·lStrong
GQA2·n_g·d_h·lModerate
MQA2·d_h·lWeak
MLA(d_c+d_h^R)·l ≈ 9/2·d_h·lStronger

DeepSeek-V2 取 d_c = 4·d_h,d_h^R = d_h/2。cache 量级介于 GQA-2 与 GQA-3 之间,但效果反而比 MHA 强。

MLA vs MHA · Large MoE 模型实测

指标MHAMLA
KV cache / token860.2K34.6K
BBH (3-shot)46.650.7
MMLU (5-shot)57.559.0
C-Eval (5-shot)57.959.2
CMMLU (5-shot)60.762.5

两个 ~250B 总参数的 MoE 模型,唯一差别是注意力机制。MLA 用 MHA 4% 的 cache,质量四项全胜。

vs DeepSeek 67B 老模型(MHA → MLA)

KV cache
100%
↳ MLA 后
6.7%
训练成本
100%
↳ V2 全栈优化后
57.5%
生成吞吐
↳ V2 max throughput
5.76×

消融:单纯换 attention 的代价(7B Dense)

机制MMLUC-EvalCMMLU
MQA37.930.034.6
GQA-841.237.738.4
MHA45.242.943.5

单看 dense 模型,"共享 KV 越狠 → 质量掉得越多"是非常清楚的趋势。GQA 比 MHA 也明显落后近 4 个点。

06 · 还有别的解法吗

压 KV cache 的几种思路 · 各自适合什么场景

MLA 是「改 attention 数学」这条路的代表。但围绕 KV cache 的优化是个生态:还有量化它的、扔掉一部分的、跨层共享的、彻底换掉 attention 的、以及在系统层做内存管理的。下面是六个值得知道的方向。

G
GQA / MQA · 共享 K/V head
Shazeer 2019 · Ainslie 2023
让多个 query head 共享同一组 K 和 V。实现简单、和现有 kernel 兼容,LLaMA-2/3、Mistral、Qwen 主流都在用。代价是质量随共享度线性下降,cache 下界是 GQA-1(即 MQA)。MLA 出现后定位变成「ROI 高的折中方案」。
易实现 生态好 有损共享
Q
KV 量化 · INT8 / INT4 / KIVI
KIVI · ZipCache · KVQuant
cache 还是按 MHA / GQA 存,但精度从 fp16 降到 INT8 甚至 INT4,体积直接对半砍或更狠。和 MLA / GQA 正交,可以叠加使用。挑战是 K 和 V 分布差异大,需要不同的量化策略,长上下文时误差会累积。
和 MLA 正交 易部署 误差累积
W
Sliding Window Attention · 只看最近 N 个
Mistral 7B · Longformer
放弃"看全部历史 token",每个 token 只跟最近 W 个做 attention。cache 体积 cap 在 W,跟序列长度无关。代价是模型只能看到一段近距离上下文,远距离信息要靠层堆叠或外部检索补回来。Mistral 7B 是最知名实践。
cache 有上界 远距离衰减
S
StreamingLLM · 滑窗 + 起始 sink
Xiao 2023 · Attention Sink
发现纯滑窗会让模型崩溃,因为开头几个 token 是天然的 attention sink。解决:始终保留前 4 个 token + 一个滑动窗口,cache 维持常数。适合长流式对话场景,但仍是"丢历史"的策略,不适合需要精确召回的任务。
流式部署 常数 cache 丢历史
P
PagedAttention · 系统层管 cache
vLLM · Kwon 2023
不缩小 cache 本身,而是像操作系统管虚拟内存一样管 cache 块。把 KV cache 切成固定大小的 page,按需分配,消除碎片,让多个请求共享 prefix。和模型架构无关,能跟 MLA / GQA 自由组合。是 vLLM 高吞吐的关键。
架构无关 多请求共享 和 MLA 叠加
M
Mamba / RWKV / Linear Attention
SSM · 替换 Attention
根本性方案:把 attention 换成 RNN-like 结构,状态是常数大小的 hidden,没有 KV cache 的概念。Mamba、RWKV、Linear Transformer、RetNet 都属于这条路。优势是 O(1) 推理内存,劣势是召回精度仍在追赶 Transformer,目前更多以 hybrid 形式出现(如 Jamba)。
无 KV cache 需重训 精度待追平
速查:什么场景用什么
这些方案不互斥,关键看你能改到哪一层
从零训练新模型 · 想吃深度红利
MLA:架构层最大化的 cache 压缩,质量还涨。但需要从训练阶段就用,且 RoPE 解耦增加实现复杂度。
已有 MHA 模型 · 想低成本提速
GQA + KV 量化:GQA 可以从 MHA checkpoint 用少量 uptraining 转换;KV 量化是部署期改动。
长流式对话 · 上下文不重要
StreamingLLM 或 sliding window:cache 有常数上界,吞吐稳定,丢失早期内容可接受。
高并发服务 · 多请求共享 prefix
PagedAttention(vLLM):和 attention 形式无关,纯系统层优化,可以叠在 MLA / GQA 之上。
边端 / 超长文档 · 极致内存约束
Mamba 系 或 hybrid:彻底跳出 KV cache 的范式,但要重新训练且要接受精度差异。