KV Cache 深入理解:为什么只存 K/V,不存 Q
KV Cache 是大模型推理里最重要的加速手段之一。它的名字里只有 K 和 V,没有 Q,不是命名偷懒,而是由自回归解码的计算结构决定的:未来 token 会反复读取过去 token 的 Key 和 Value,却不会再读取过去 token 的 Query。
先把结论放在前面:
| 对象 | 在注意力里的角色 | 未来 token 还会用吗 | 是否适合长期缓存 |
|---|---|---|---|
Query Q |
当前 token 发出的“读请求” | 否 | 否 |
Key K |
历史 token 暴露出的“地址/索引” | 是 | 是 |
Value V |
历史 token 暴露出的“内容/信息” | 是 | 是 |
| Attention score | 当前 Query 与历史 Key 的匹配结果 | 通常否 | 否 |
如果只记一句话:Q 是读者,K/V 是被读的记忆。读者每一步都会换,记忆会被后面的读者反复读取。
一、把 Attention 看成一次“查表”
Transformer 里的自注意力通常写成:
$$ Q = XW_Q,\quad K = XW_K,\quad V = XW_V $$
$$ \text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$
这个公式看起来像矩阵乘法,但从直觉上可以拆成三件事:
| 名字 | 更像什么 | 含义 |
|---|---|---|
Q Query |
查询语句 | “我现在想找什么信息?” |
K Key |
索引地址 | “我这里有什么特征,可以被谁匹配到?” |
V Value |
实际内容 | “如果你匹配到了我,真正拿走的是这些信息。” |
在普通的全序列计算中,Q/K/V 都来自同一段输入序列 X。但在自回归生成里,时间方向非常关键:模型已经生成了前面的 token,现在只需要生成下一个 token。
假设当前正在生成第 t 个 token,在某一层 decoder self-attention 中真正需要的是:
$$ \text{Attention}(q_t,K_{\le t},V_{\le t}) =\text{softmax}\left(\frac{q_tK_{\le t}^T}{\sqrt{d_k}}\right)V_{\le t} $$
注意这个公式里只有 当前的 q_t 和 历史到当前的 K/V。它并没有用到 q_1, q_2, ..., q_{t-1}。
这就是 KV Cache 的根。
二、Prefill 与 Decode:缓存到底发生在哪
大模型生成通常可以分成两个阶段:
| 阶段 | 输入规模 | 主要工作 | KV Cache 的状态 |
|---|---|---|---|
| Prefill | 整个 prompt | 一次性处理用户输入、系统提示词、历史上下文 | 建立初始 K/V |
| Decode | 每次 1 个新 token | 用新 token 继续生成下一个 token | 每步追加新的 K/V |
在 Prefill 阶段,模型要把 prompt 里的所有 token 都跑一遍。比如 prompt 长度是 n,每一层都会产生:
|
|
然后进入 Decode 阶段。每生成一个新 token,模型并不会把从 x1 到 xt 的整段上下文重新计算一遍,而是只处理最新 token:
|
|
用伪代码看会更清楚:
|
|
这里有一个容易忽略的细节:KV Cache 是每一层都有一份,不是整个模型共享一份。 因为每一层的 hidden state 不同,投影出来的 K/V 也不同。
常见张量形状可以写成:
|
|
如果是普通多头注意力(MHA),kv_heads 通常等于 query heads;如果是 MQA/GQA,kv_heads 会少很多,这是现代大模型降低 KV Cache 显存压力的重要技巧。
三、为什么只存 K/V,不存 Q
这个问题是理解 KV Cache 的核心。直觉答案是:旧 Q 用完就没了;旧 K/V 会被未来反复访问。
更严谨一点,可以从四个角度看。
| 角度 | 为什么旧 Q 没必要存 |
|---|---|
| 公式 | 生成第 t 个 token 时用的是 q_t K_{\le t}^T,旧的 q_i 不在公式里 |
| 因果方向 | 未来 token 只能拿自己的 Query 去看过去的 Key,不能反过来让过去的 Query 查询未来 |
| 复用价值 | k_i/v_i 会被 q_{i+1}, q_{i+2}, ... 多次读取;q_i 只服务于第 i 行 attention |
| 显存收益 | 存 Q 会增加大量显存,却几乎不减少 decode 计算 |
可以把第 i 个 token 的三样东西拆开看:
token i 产生的东西 |
第 i 步怎么用 |
第 i+1 步以后怎么用 |
|---|---|---|
q_i |
用它查询 K_{\le i},生成当前 token 的注意力输出 |
不再需要 |
k_i |
被 q_i 匹配,也成为缓存的一部分 |
会被未来每个 q_j 匹配 |
v_i |
被当前注意力加权求和 | 会被未来每个注意力分布加权求和 |
再换一种说法:q_i 描述的是“第 i 个位置想问什么”,这个问题只对第 i 个位置有意义。到了第 j 个位置,模型关心的是“第 j 个位置想问什么”,也就是 q_j,而不是 q_i。
如果存 Q,能不能少算一点?
通常不能。
Decode 时新 token 的 q_new 必须根据当前层的 hidden state 现场算出来。这个 hidden state 又来自前面层的输出,而前面层的输出依赖当前 token、位置编码、残差、MLP、LayerNorm 以及上一层 attention 结果。换句话说,未来的 Query 在未来 token 出现之前根本不存在。
缓存旧 Query 最多能避免“重新计算旧位置的 Query”。但 KV Cache 的设计本来就已经不重新计算旧位置了:decode 阶段只跑新 token,旧位置不会再进前向计算图。于是旧 Query 没有可节省的计算。
旧 attention 分数要不要缓存?
也通常不缓存。
第 t 步真正需要的是新的一行分数:
|
|
旧分数 score_1, score_2, ..., score_{t-1} 描述的是旧 Query 如何看旧 Key。它们不能替代 q_t 对历史 Key 的新匹配。把所有 attention score 都存下来,显存会从线性增长变成更接近平方增长,收益却很小。
训练时为什么好像会保存 Q?
训练和推理要分开看。
训练阶段为了反向传播,框架可能会临时保留 Q/K/V、attention score、softmax 结果、MLP 激活等中间量,或者用 activation checkpointing 在反向时重算。那是为了求梯度,不是为了自回归生成加速。
推理阶段没有反向传播,目标是尽快生成下一个 token。因此长期留下来的只有未来还会被读到的东西:K/V。
四、KV Cache 换来的代价
KV Cache 的本质是用显存换时间。它显著减少重复计算,但显存会随着上下文长度线性增长。
常见估算公式是:
|
|
其中前面的 2 表示 K 和 V 两份缓存。
举个近似例子,假设模型有:
| 参数 | 数值 |
|---|---|
层数 layers |
32 |
| query heads | 32 |
head_dim |
128 |
| 数据类型 | FP16 / BF16,2 bytes |
如果是普通 MHA,kv_heads = 32:
|
|
如果是 GQA,假设 kv_heads = 8:
|
|
所以同样 8192 token 的上下文,单请求 KV Cache 可能就是:
| 注意力结构 | 每 token 约占 | 8192 token 约占 |
|---|---|---|
| MHA,32 个 KV heads | 512 KiB | 4 GiB |
| GQA,8 个 KV heads | 128 KiB | 1 GiB |
这还只是 batch size = 1 的估算。服务端如果要同时处理很多请求,KV Cache 很快就会成为吞吐瓶颈。
现在再看“不存 Q”的显存意义:
| 架构 | 如果额外缓存 Q,会发生什么 |
|---|---|
MHA:q_heads = kv_heads |
在原本 K+V 两份之外再加一份 Q,缓存大约增加 50% |
GQA/MQA:q_heads > kv_heads |
Q 的头数比 KV 多,额外缓存 Q 可能比现有 KV Cache 还大 |
也就是说,缓存 Q 不只是“多存一点”,在 GQA/MQA 模型里可能是非常昂贵的浪费。
五、工程实践里还要注意什么
KV Cache 听起来简单,真正服务化时会牵出不少工程问题:
| 问题 | 说明 |
|---|---|
| Prefix Cache | 如果很多请求共享同一段系统提示词,可以复用这段前缀的 K/V |
| PagedAttention | 把 KV Cache 切成页管理,减少长短请求混批时的显存碎片 |
| Sliding Window | 只保留最近窗口内的 K/V,适合局部注意力模型 |
| KV Quantization | 把 KV Cache 量化到更低 bit,降低显存与带宽压力 |
| Beam Search | 多个 beam 共享前缀时要正确复制、重排或引用缓存 |
| RoPE 位置 | 缓存的 K 通常已经带有位置信息,复用前缀时必须保证 position offset 一致 |
KV Cache 不会改变模型的理论输出,它只是避免重复计算。实际工程里如果使用低精度缓存、不同 attention kernel、不同 batch 合并策略,可能会有极小的数值差异,但原理上它是等价优化。
它也不是万能加速器:
| 场景 | KV Cache 的作用 |
|---|---|
| Prefill 很长 | 仍然要处理完整 prompt,KV Cache 不能消除 prefill 成本 |
| Decode 很长 | 很有用,因为每步都能复用历史 K/V |
| 上下文非常长 | 计算变少,但显存和显存带宽压力变大 |
| 请求很多且长度不一 | 需要分页缓存、连续批处理、调度器配合 |
最后用一张小表把核心逻辑收住:
| 问题 | 答案 |
|---|---|
| KV Cache 缓存的是什么 | 每一层历史 token 的 Key 和 Value |
| 为什么能加速 | Decode 阶段不再重复计算旧 token 的 K/V 和旧位置输出 |
| 为什么不存 Q | 旧 Query 只用于旧位置的 attention,不会被未来 token 读取 |
| 代价是什么 | 显存随层数、序列长度、batch、KV head 数线性增长 |
| 主要优化方向 | GQA/MQA、PagedAttention、Prefix Cache、KV 量化、窗口注意力 |
一句话总结:KV Cache 的名字不是偶然。K/V 是上下文留给未来的可读记忆;Q 是当前 token 临时发出的读取动作。动作结束就结束了,记忆才需要留下来。