KV Cache#
本页内容5 个部分
1. 问题的来源#
Decoder-only 模型(如 GPT)的生成过程是自回归的——每次只生成一个 token,然后把它加入上下文,再生成下一个。
生成第 个 token 时,需要对前面所有 个 token 做注意力。**如果不做优化:**每生成一个新 token,都要对整个序列重新计算所有 token 的 Q、K、V。前面的 token 的 K 和 V 实际上已经算过了,但又重新算了一遍——纯粹浪费。
生成长度为 的序列,总的 K/V 计算量是 ,其中大量是重复计算。
2. KV Cache 的做法#
核心思想:把每一层已经算过的 K 和 V 缓存起来,后续 step 直接复用。
- 生成第 1 个 token:计算 ,缓存
- 生成第 2 个 token:只计算新 token 的 ,把 拼接到缓存中,用 和 做注意力
- 生成第 个 token:只计算 ,缓存中已经有
每步只需计算 1 个新 token 的 Q/K/V,K/V 从缓存读取
K/V 的计算量从 降到 ——每步只算 1 个 token 的 K/V。
3. 代价:显存#
KV Cache 是经典的空间换时间。每一层、每个头都要缓存 K 和 V 矩阵,而且随着生成长度增长,缓存越来越大。
以 LLaMA-7B 为例(32 层、32 头、),生成 2048 个 token 时:
2 = K和V两个矩阵,32 = 层数,32 = 头数,128 = ,2048 = 序列长度,2 = FP16 每个数 2 字节
对于更大的模型(如 70B)或更长的序列(如 128K),KV Cache 的显存开销会达到几十 GB,成为推理的主要瓶颈。
4. 解决方案的演进#
为了减小 KV Cache,社区发展出了一系列方法:
4.1 MQA → GQA → MLA#
| 方法 | 年份 | Q 头数 | KV 头数 | KV Cache | 性能 | 代表模型 |
|---|---|---|---|---|---|---|
| MHA | 2017 | 32 | 32 | 1× | 基线 | 原始 Transformer |
| MQA | 2019 | 32 | 1 | 0.03× | 略有下降 | PaLM |
| GQA | 2023 | 32 | 8 | 0.25× | 接近 MHA | LLaMA 2/3 |
| MLA | 2024 | 128 | 低秩压缩 | ~0.02× | 超越 MHA | DeepSeek-V2/V3 |
MQA(Multi-Query Attention)
**做法:**所有 Q 头共享 1 组 KV。
**效果:**KV Cache 缩小到 ,但太极端,性能下降明显。
**名字含义:**Q 还是多头(Multi-Query),KV 缩成了唯一一组——从 Q 的视角命名。
GQA(Grouped-Query Attention)
**做法:**32 个 Q 头分成 8 组,每组共享 1 组 KV。
**效果:**KV Cache 缩小到 ,性能损失很小。
**定位:**MHA 和 MQA 的折中,目前主流方案。
MLA(Multi-head Latent Attention)
**做法:**不减头数,而是把 KV 低秩压缩到极低维潜在空间。
**效果:**压缩比 ~50×,性能反而超过 MHA。
**思路转变:**从"减少 KV 头数"转向"压缩 KV 维度"。
4.2 MLA 详解(以 DeepSeek-V2 为例)#
第零步:回顾标准 MHA 的计算与缓存
先区分 整个多头投影的宽度 与 单个 head 的点积维度。MHA 为每个 head 独立计算 ,不能把所有 head 拼成一个向量再做一次点积,否则会把各头混为一个注意力分布。
MLA 用共享低维 latent 表示内容 KV。以行向量约定,单个 head 有
这是一种从头训练的低秩架构约束,不是把任意已有 MHA 权重无损压缩。解耦的 RoPE key 另行缓存;因此每 token、每层的缓存维度为 。例如 DeepSeek-V2 的 ;相比 的示例 MHA 配置约小 56.9 倍,实际字节数还取决于 dtype 等因素。
Key 吸收:
把 query 映射到 latent 维度后,可直接对缓存的 打分。该等式按 head 成立。若 query 路径含低秩投影和归一化,不能把整个路径武断合并成一个从 出发的线性矩阵。
**Value 吸收:**先在 latent 空间加权,再恢复输出:
所以无需为每个历史 token 恢复完整 V; 还可与该 head 对应的输出投影组合。Value 路径里没有 这一操作。
以下是单个 head 的 NumPy 形状示意,省略 batch、mask、query 的具体构造和多头求和。c、k_rope 已包含当前 token 的缓存:
import numpy as np
# q_content: [d_content]; W_UK: [d_c, d_content]
# c: [L, d_c]; q_rope: [d_r]; k_rope: [L, d_r]
q_absorbed = q_content @ W_UK.T
scores = (c @ q_absorbed + k_rope @ q_rope) / np.sqrt(d_content + d_r)
weights = np.exp(scores - scores.max())
weights /= weights.sum()
context = weights @ c
# W_UV: [d_c, d_v]; W_O_head: [d_v, d_model]
output_head = context @ W_UV @ W_O_head
MLA 的低缓存收益和算子性能还取决于 prefill/decode 阶段及具体实现。进一步推导见 MLA 专题。
💡 MLA vs GQA 的本质区别
GQA 是"共享"——多个 Q 头用同一组 KV,减少 KV 的"组数",但共享同一组的 Q 头看到的 KV 完全相同,表达能力受限。
MLA 是"压缩"——不减少头数,而是把所有头的 KV 信息联合压缩到一个低维空间。恢复时 为每个头生成不同的 K,保留了多头的表达能力。再加上吸收技巧消除了恢复开销。这解释了 MLA 的缓存效率;精度是否优于 GQA/MHA 取决于具体模型和训练实验,矩阵吸收本身不提供精度保证。
4.3 第二个方向:减少缓存的序列长度#
不是所有历史 token 的 KV 都同样重要。有些方法会选择性地丢弃不重要的 KV:
- 滑动窗口注意力(Mistral 用的):只缓存最近 个 token 的 KV,更早的直接丢掉。假设 ,不管序列多长,KV Cache 的大小永远是 4096。代价是模型看不到很远的历史信息。
- Token 淘汰策略:根据注意力权重判断哪些历史 token "不重要"(权重一直很低),把它们的 KV 从缓存中踢掉,只保留重要的。
4.4 第三个方向:压缩每个 KV 的精度#
- KV 量化:不减少数量,而是把每个 KV 向量从 FP16(2 字节)压缩成 INT8(1 字节)甚至 INT4(0.5 字节)。精度略有损失,但缓存大小减半甚至更多。
**优化 KV Cache 总结起来就三条路:**头数上减少(GQA/MQA)、长度上减少(滑窗/淘汰)、精度上压缩(量化)。目标都是一个:用更少的显存存储历史信息,同时尽量不影响模型效果。
🔥 面试标准回答
KV Cache 的作用是避免自回归生成时重复计算已有 token 的 K/V,用空间换时间。代价是显存开销随序列长度线性增长。GQA 通过让多个 Q 头共享 KV 来减小缓存大小,是现在的主流方案(LLaMA 2/3、Mistral 都在用)。
延伸阅读#
- 推理优化与部署速查:部署时怎样区分延迟、吞吐和显存问题?