KV Cache 为什么叫 KV Cache?——Q 去哪了
每个刚接触 KV Cache 的人都会问这个问题:既然 K 和 V 可以缓存,那 Q 呢?Prefill 时明明也算出了所有 token 的 Q,为什么不一起存起来、Decode 时复用?
答案藏在一个简单的类比里——以及因果注意力的数学约束中。
一、检索类比:Q 是问题,K/V 是答案库
想象你在图书馆查资料。你走进来,翻了一遍目录卡片和书架上的摘要,问了一个问题,得到了答案。然后你接着问第二个问题。
第二个问题不需要回忆第一个问题是什么——那个问题已经完成了使命。但它需要同一套目录卡片和同一批书架摘要。越问越多,卡片越摞越厚,但每次只有最新的问题在查它们。
这就是 Q、K、V 在注意力中扮演的角色:
| 角色 | 含义 | 生命周期 |
|---|---|---|
| Q (Query) | 当前 token 提出的「问题」——”我该关注哪些之前的 token?” | 一次性:提出并得到答案后即废弃 |
| K (Key) | 每个 token 提供的「索引标签」——”我包含哪些信息?” | 持久:后续每个 token 都要检索 |
| V (Value) | 每个 token 提供的内容摘要——”关注我的话,我贡献什么?” | 持久:后续每个 token 都要读取 |
Prefill 阶段,prompt 的每个 token 都产生自己的 Q、K、V。Q 被用来查询当时存在的所有 token——然后它就完成了使命。进入 Decode 阶段后,每个新 token 的 Q 只查询更早的 token(因果掩码保证),而不会去查询过去的 Q。
Decode Step t:
Q_t 需要查询: K_1, K_2, ..., K_t ← 所有 Key(含自己)
Q_t 需要读取: V_1, V_2, ..., V_t ← 所有 Value(含自己)
Q_t 不需要: Q_1, Q_2, ..., Q_{t-1} ← 过去的 Query 对当前 token 无意义
二、因果掩码决定了 Q 的一次性
Decoder 的 causal mask 只允许每个 token 关注自己及之前的 token。这意味着:
- token t 的 Q_t 查询
[1, t]范围 - token t+1 的 Q_{t+1} 查询
[1, t+1]范围 - token t+1 永远不需要用 Q_t 去查任何东西
Q 是单次使用的:提出、得到答案、丢弃。K 和 V 是被反复使用的:每生成一个新 token,在标准 full causal attention 下所有历史的 K 和 V 都要被读一次。这就是 Q 不该被缓存的根本原因——缓存一种不会被再次使用的东西,纯粹浪费显存。
三、如果硬要缓存 Q 会怎样?
以 LLaMA-2 70B(GQA, 8 KV heads, 64 Q heads)在 seq_len=4096 时为例:
| 缓存项 | 计算 | 大小 |
|---|---|---|
| K | 80 层 × 8 heads × 128 dim × 4096 tokens × 2 bytes | ~0.63 GB |
| V | 同上 | ~0.63 GB |
| KV Cache 合计 | ~1.25 GB | |
| Q(如果缓存) | 80 层 × 64 heads × 128 dim × 4096 tokens × 2 bytes | ~5 GB |
缓存 Q 需要额外 5 GB——但这 5 GB 的数据永远不会被再次访问。在显存本就紧张的推理场景中,没有比这更纯粹的浪费了。
四、一句话总结
Q 是「问题」,每次都是新的;K/V 是「答案库」,越积越多。缓存前者无意义,缓存后者是 KV Cache 的全部价值。
相关资源
- KV Cache 原理简介 — KV Cache 的工作机制、显存公式与工程实践
- 为什么 GPU 生成每个 token 时利用率不到 5%?——Prefill 与 Decode 深度拆解 — §4.1-4.2 有因果掩码和不缓存 Q 的数学推导