你把一份很长的项目文档交给 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 的表示。
  2. Decoder 随后处理最后 128 个 prompt tokens,重建 local state。更早的历史仍可通过 global KV 访问。

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

再回到开头的那份文档

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

  • 继续写回答: 新位置产生自己的 Q,读取仍然有效的历史 K/V。
  • 修改文档开头: 从实际 token prefix 的变化处重新检查复用范围。
  • 换一个问题: 检查共同 prefix、执行条件,以及 cache 是否仍被保留。
  • 读一份更长的文档: 关注保留位置带来的存储增长,以及架构如何组织这些状态。