All writing Notes & observations

KV Cache 深入理解:为什么只存 K/V,不存 Q

从 Transformer 自注意力、Prefill/Decode、缓存张量形状、显存公式到工程优化,系统讲清楚 KV Cache 的工作方式,以及为什么旧 Query 没有缓存价值。

KV Cache 深入理解:为什么只存 K/V,不存 Q

KV Cache 是大模型推理里最重要的加速手段之一。它的名字里只有 K 和 V,没有 Q,不是命名偷懒,而是由自回归解码的计算结构决定的:未来 token 会反复读取过去 token 的 Key 和 Value,却不会再读取过去 token 的 Query。

KV Cache 封面

先把结论放在前面:

对象 在注意力里的角色 未来 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 实际内容 “如果你匹配到了我,真正拿走的是这些信息。”

把 Attention 看成查表

在普通的全序列计算中,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 与 Decode

在 Prefill 阶段,模型要把 prompt 里的所有 token 都跑一遍。比如 prompt 长度是 n,每一层都会产生:

1
2
K_cache[layer] = [k1, k2, ..., kn]
V_cache[layer] = [v1, v2, ..., vn]

然后进入 Decode 阶段。每生成一个新 token,模型并不会把从 x1 到 xt 的整段上下文重新计算一遍,而是只处理最新 token:

1
2
3
4
new token -> 当前层 hidden state
          -> q_new, k_new, v_new
          -> q_new 读取已有 K_cache / V_cache
          -> k_new, v_new 追加进缓存

用伪代码看会更清楚:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# prefill
for layer in layers:
    q, k, v = project(prompt_hidden_states)
    K_cache[layer] = k
    V_cache[layer] = v
    hidden_states = attention(q, K_cache[layer], V_cache[layer])

# decode: 每次只处理一个新 token
for step in generation:
    x = embedding(last_token)

    for layer in layers:
        q_new, k_new, v_new = project(x)

        K_cache[layer].append(k_new)
        V_cache[layer].append(v_new)

        x = attention(q_new, K_cache[layer], V_cache[layer])

    last_token = sample(lm_head(x))

这里有一个容易忽略的细节:KV Cache 是每一层都有一份,不是整个模型共享一份。 因为每一层的 hidden state 不同,投影出来的 K/V 也不同。

常见张量形状可以写成:

1
2
K_cache[layer]: [batch, kv_heads, seq_len, head_dim]
V_cache[layer]: [batch, kv_heads, seq_len, head_dim]

如果是普通多头注意力(MHA),kv_heads 通常等于 query heads;如果是 MQA/GQA,kv_heads 会少很多,这是现代大模型降低 KV Cache 显存压力的重要技巧。

三、为什么只存 K/V,不存 Q

这个问题是理解 KV Cache 的核心。直觉答案是:旧 Q 用完就没了;旧 K/V 会被未来反复访问。

为什么不缓存 Q

更严谨一点,可以从四个角度看。

角度 为什么旧 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 步真正需要的是新的一行分数:

1
score_t = q_t · [k1, k2, ..., kt]^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 的本质是用显存换时间。它显著减少重复计算,但显存会随着上下文长度线性增长。

KV Cache 显存成本

常见估算公式是:

1
2
KV cache bytes
≈ 2 × layers × batch × seq_len × kv_heads × head_dim × bytes_per_elem

其中前面的 2 表示 K 和 V 两份缓存。

举个近似例子,假设模型有:

参数 数值
层数 layers 32
query heads 32
head_dim 128
数据类型 FP16 / BF16,2 bytes

如果是普通 MHA,kv_heads = 32:

1
2
3
4
每 token KV cache
≈ 2 × 32 × 32 × 128 × 2 bytes
= 524288 bytes
≈ 512 KiB

如果是 GQA,假设 kv_heads = 8:

1
2
3
4
每 token KV cache
≈ 2 × 32 × 8 × 128 × 2 bytes
= 131072 bytes
≈ 128 KiB

所以同样 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 临时发出的读取动作。动作结束就结束了,记忆才需要留下来。