# LLM 为何缓存 K 和 V 却不缓存 Q

- 来源：Dongxi 东锡 NLP (@dongxi_nlp)
- 发布时间：2026-09-13 09:22
- AIHOT 分数：44
- AIHOT 链接：https://aihot.news/items/cmtz5c8td0ul0roupnvyedcoi
- 原文链接：https://x.com/dongxi_nlp/status/2098945504289669481

## AI 摘要

这篇长文解释 LLM 推理中 KV cache 的原理：每生成一个 token，历史位置的 K/V 会被保留供后续位置读取，而 Q 随新位置重新产生，用完即可丢弃。

## 正文

https://x.com/i/article/2098863329817051136

LLM 为什么缓存 K 和 V，却通常不缓存 Q？

你把一份很长的项目文档交给 AI，让它帮你总结。

回答开始出现，一个 token 接着一个 token 生成。生成下一段内容时，模型仍然需要利用前面的文档和已经生成的回答。

难道每多写一个 token，都要把越来越长的全文从头再算一遍？

KV cache 让模型保留了一部分已经完成的计算。 但它为什么保留 K 和 V？同样参与 attention 的 Q，为什么通常可以用完就放下？

这篇就来讨论这个问题，理解 KV cache。

每生成一个 token，都要从头再算吗？

把输入缩短为 “The sky is”，假设模型接着生成 “ blue”，再生成 “.”。

模型先处理整个 prompt，建立各层的 KV cache。这一步叫 prefill。最后一个 prompt 位置的输出经过 vocabulary head，得到用来选择第一个输出 token 的 logits。

选出“ blue”后，模型把它作为新输入，继续预测“.”。

看下面的动画：“blue”出现前后，哪些方块留在原位，哪些方块是新加入的？

已有 K/V 留在原位，新位置的 K/V 加在后面。新 token 仍然要经过模型计算，只是省去了对有效历史状态的重复计算。

留意顺序：选出“blue”时，它的 K/V 还没有生成；把它送回模型、预测后续 token 时，才会计算它的 K/V。

为什么留下 K/V，却通常不留下 Q？

在一个 attention 层里，每个位置都有一个输入向量，也叫 hidden state。三个可学习的投影把它变成 query（Q）、key（K）和 value（V）。

处理 blue 这个位置时，它的新 Q 会与 The sky is 以及 blue 自身的 K 计算匹配分数。

分数经过缩放和 softmax，变成 attention weights。再按这些权重对对应的 V 加权求和，得到这个 head 的输出。

下一位置会产生自己的 Q，但仍可能需要读取这些位置的 K/V。 历史 Q 已经完成了对应位置的计算，普通 decode 无需再次使用它。

把这个过程写成公式，就是下面这一行：

这解释了 KV cache 这个名字：K/V 继续供后续位置读取；Q 随新位置重新产生。

Context 变长了，旧计算为什么仍然有效？

回到项目文档。假设开头写着“预算为 100 万元”，后面是进度、人员和交付计划。

现在比较两个操作：

• 在文档末尾追加：“请总结风险。”

• 把开头的“100 万元”改成“50 万元”。

哪一种操作会让旧 cache 失效？先想一下，再看动画。 图中的字母代表 token 位置。

区别来自 causal attention 的信息方向：一个位置只能使用它自己和前面的输入，无法读取后来的内容。

所以，末尾追加问题，不会反过来改变文档位置已经完成的计算。修改开头的预算，则可能影响后续位置的表示，不能直接沿用原来的整份 cache。

也就是说，开头的 “100 万元”改成“50 万元”，影响了接下来每一个 token 生成时的 context，那么旧的 cache 就会失效。

新 token 到来时，究竟计算了什么？

现在放大“blue”被送回模型的那一步。只看一个 attention head：历史 K/V 不动，新位置产生 Q/K/V，新的 Q 读取可用的 K/V。

新 K/V 先加入 cache，再参与 attention，所以当前 Q 也能关注自身位置。这个 head 的输出被后续计算使用后，K/V 仍然保留，供下一位置读取。

图中的 Head output 还需要经过层内后续计算和模型其他部分，才能得到 next-token logits。在输入和执行条件兼容时，cached 与 uncached 结果应在适当的数值误差范围内一致。

省下重复计算，为什么又增加了显存开销？

项目文档越长，需要保留的位置就可能越多。使用随序列增长的完整 KV cache 时，新加入的位置会继续占用存储空间。

可以按维度数一遍：多少条序列、多少层、每层保留多少位置、多少个 KV heads、每个 head 多宽、每个元素占多少 bytes。

假设 K/V 分开存储、宽度相同、各层结构一致，乘起来就是：

给这份文档安排一个假设配置：24 层、1 条序列、8 个 KV heads、head width 为 64，每个元素占 2 bytes。

• 保留 4,096 个位置：KV tensor 共 192 MiB。

• 保留 8,192 个位置：增加到 384 MiB。

看动画里的两次变化：先增加位置，再回到原来的长度，比较 KV heads 更少的另一种配置。

同一份文档，换个问题，还能接着用吗？

你刚让 AI 总结完风险，又想知道截止日期。假设两条请求这样组织：

请求 A： [同一份项目文档] + “总结风险。”

请求 B： [同一份项目文档] + “列出截止日期。”

如果文档部分的实际 token prefix 完全一致，模型与执行设置兼容，而且系统仍保留这部分 cache，就可以复用它，再分别计算两个问题及其续写。这叫 prefix caching。

prefill，KV cache，prefix caching 三个词可以这样区分：prefill 是处理 prompt 的计算阶段；KV cache 是保存下来的 K/V 状态；prefix caching 是复用已有前缀状态的策略。 即使没有启用 prefix caching，普通生成也可以使用 KV cache。

vLLM 官方的典型例子是：针对同一份年报或软件手册反复提问。上面的项目文档就是同一种用法：第一次通过 prefill 建立缓存，后续命中共同前缀时，直接复用相应 K/V，省去这部分重复计算。

新的问题仍需继续做 prefill，并读取文档的 K/V；新的回答仍需逐 token decode。Prefix caching 主要节省重复的 prefill 工作，不会省掉新回答的生成计算。

如果请求 B 在文档前多放了一段不同的说明，共同 prefix 就可能缩短。即使后面的文档文字相同，也不能直接认定它的 causal states 相同。

如果重新设计 KV 的来源和共享方式呢？

前面从“每层保存自己的 K/V”出发。理解这些计算依赖，就能看懂 DeepSeek-V4.1-Flash 的几个变化。

先看来源：Causal Encoder–Decoder。 Decoder 的 global KV 来自 causal encoder 的最终输出。各 decoder 层仍然生成自己的 Q，以及 local sliding-window KV。

再看跨层：Cross-layer sharing。 在所讨论的配置中，第 21 层利用 encoder output 生成一份 decoder global KV bank，供第 21–40 层使用。

可以盯住第 21、22 层看：它们读取共享的 global entries，但 Q 和 local states 各自保留。共享条目不会强制产生相同的 attention weights；sparse selection 决定读取哪些 global entries。

最后看 K/V 的表示：Shared KV representation。 在 reference core-attention kernel 中，同一个被选中的向量既参与 Q 的匹配评分，也参与输出的加权求和。

看动画：一份 global KV 如何连接多个层？放大一个向量后，它又参与了哪两项计算？

模型仍保留 local caches、indexer state 等数据。Shared KV representation 与把两份独立 K/V 数组相邻存放，有明确区别。

回到项目文档：假设这次输入有 10 万个 token。报告中的 serving 方案可以这样处理：

1. Encoder 处理完整 prompt，提供构建 decoder global KV 的表示。

1. Decoder 随后处理最后 128 个 prompt tokens，重建 local state。更早的历史仍可通过 global KV 访问。

这说明 global KV 的来源会影响 prefill 中哪些计算可以调整。生成后续 token 时，新输入仍然经过 causal encoder 和 decoder。

再回到开头的那份文档

现在可以沿着同一场景，把 KV cache 连起来：

• 继续写回答： 新位置产生自己的 Q，读取仍然有效的历史 K/V。

• 修改文档开头： 从实际 token prefix 的变化处重新检查复用范围。

• 换一个问题： 检查共同 prefix、执行条件，以及 cache 是否仍被保留。

• 读一份更长的文档： 关注保留位置带来的存储增长，以及架构如何组织这些状态。
