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 的全部价值。

相关资源