跳到主要内容

6.2 KV Cache:不重算昨天的作业

第 1000 个字的荒唐账​

想象模型正在生成一篇长回答,写到第 1000 个字。按 3.2 节的注意力机制,这个新字要和前面全部 999 个字打交道:拿自己的 Q,去匹配它们每一个的 K,再加权取用它们的 V。

问题来了:那 999 个字的 K 和 V 向量,跟生成第 999 个字时算的一模一样——token 没变、权重没变,算出来的 K、V 就不会变。如果每写一个新字都把全部历史的 K、V 重算一遍,就等于每天上学前把从小学一年级到昨天的作业全部重写一遍。

傻子才重算。把算过的 K、V 存进显存,新字来了直接取用——这就是 KV Cache,所有大模型推理服务的第一块基石。

亲眼看看省了多少​

🎮 动手实验
KV Cache:不重算昨天的作业
两边在生成同一句话。每个格子 = 一次注意力计算:🟧 本步新算,🟦 直接取缓存。点「生成下一个 token」,看两边的计算量差距怎么拉开。
天空是▌(提示词 3 个 token + 已生成 0 个)
❌ 无缓存:每步全部重算
累计 0 格
✅ 有缓存:只算最后一行
累计 0 格 · 缓存占用 3 行
当前序列长度
3 tokens
计算量之比
—
KV 缓存显存
随长度线性增长
💡 要点:注意力需要每个新 token 和全部历史 token 打交道,但历史 token 的 K、V 向量根本没变——存起来(KV Cache)就不用每步重算,计算量从「三角形」变成「一行」。代价是显存:缓存随上下文长度线性膨胀,长对话里 KV Cache 常比模型本身还占显存——这正是 6.4 节推理框架要优化的头号对象。

点「▶ 自动生成」跑完整句,然后盯住两组数字:

  1. 累计格数:左边「❌ 无缓存」的橙色三角形每一步都整个重画,累计格数疯涨;右边「✅ 有缓存」只有最后一行是新算的橙色,其余全是安静的蓝色缓存。
  2. 计算量之比:这句才 10 个 token,比值已经拉到 4 倍多。它随长度越拉越大——按结尾消息里的账,上下文到 1 万 token 时,无缓存方案每生成一个字就要重算约 5000 万格。没有 KV Cache,就没有可用的对话产品。

天下没有免费的显存​

省下的计算不是凭空来的——代价是显存。实验里右边面板的「缓存占用」一行行往上加:每多一个 token,就多存一行 K、V。

这笔账在真实规模下相当吓人:70B 级模型如果每个注意力头都存自己的 K、V(早期的设计),聊到 3 万 token 时,光缓存就要几十 GB——比半个模型还大。现代模型靠「多头共享 KV」(细节在深入层)把它砍到十分之一,但 KV Cache 依然是长对话场景的显存大头。

这也直接回答了一个日常疑问:为什么长上下文那么贵? 上下文越长,缓存越大、每步注意力要扫的历史越多——显存和算力双重涨价(8.2 节还会回到这个话题)。

这个演示在哪里简化了?

实验只画了一层、一个头的注意力,而且只有 10 个 token;真实模型每一层的每个 KV 头都有自己的缓存,总量是「层数 × 头数」倍。另外,「无缓存」的实现现实中没人真的用——它存在的意义是让你看清缓存到底省掉了什么。最后,格子数对应注意力计算次数的相对比较,不是精确的 FLOPs。

换一个人来问,还能接着用吗​

到这里,缓存都还只在一次生成内部起作用:这轮聊完,缓存丢掉,下一个请求从头再来。

但真实的线上流量有个很扎眼的特点——大家的开头是一样的。

  • 同一个产品的所有请求,都带着同一份几千字的系统提示词;
  • 同一份 few-shot 示例,每个请求都抄一遍;
  • RAG 里同一份长文档,可能被一百个用户问到;
  • 智能体的第 8 步,前 7 步的历史和第 7 步时一模一样,只是末尾多了一段。

这些前缀每次都要重新算一遍 K、V。而 6.1 节说过:读进提示词的这个阶段(预填充)是算力密集的,长提示词就是实打实的算力开销。

于是有了一个顺理成章的主意:把前缀的 K、V 存下来,下次遇到同样的开头直接拿来用。 这就是前缀缓存(prompt caching,也叫 prefix caching)。

它成立的条件只有一个,但很硬:

必须从第 1 个 token 开始逐字相同。 中间任何一处不一样,从那里往后的缓存全部作废。

原因回到 3.1 节:每个 token 的 K、V 是由它和它前面所有 token共同决定的。前面改一个字,后面所有位置的 K、V 都变了——缓存对不上,只能重算。

这条约束直接决定了你该怎么排提示词​

「前缀必须逐字相同」听着像个技术细节,实际上是一条很硬的设计规则:

把稳定的东西放前面,把变化的东西放后面。

对照着看就很清楚:

❌ 缓存打不中✅ 缓存能复用
系统提示词里写「当前时间:2026-08-08 14:32」时间放到最后,或者干脆不放
把用户问题拼在最前面系统提示 → 文档 → 示例 → 最后才是用户问题
每次把检索到的文档重新排序固定顺序(比如按文档 id),别让重排改变拼接顺序
每个请求随机挑几条 few-shot 示例固定同一批示例

第三条最容易踩:中篇 K4 讲的重排会改变文档顺序,如果你把重排结果直接按名次拼进提示词,每次的前缀都不一样,缓存永远打不中。解法是把「用哪几篇」和「按什么顺序拼」分开——前者由重排决定,后者固定下来。

收益有多大?两方面,而且都很实在:首字延迟(用户等多久看到第一个字)能降到零头,因为几千个 token 的预填充被跳过了;成本上,各家对命中缓存的输入 token 都有大幅折扣(2025-2026 年常见的量级是原价的一到两成)。一个带 4000 token 系统提示的应用,用户实际只问 20 个字——不做缓存,你每次都在为那 4000 token 全额付费。

前缀缓存不是「打开就有」

一、它有有效期。 缓存占显存,服务方不可能一直留着。常见的存活时间是几分钟量级,冷门前缀很快被挤掉。所以它对高频、集中的流量最有效,对长尾请求基本无感。

二、它可能要你显式声明。 有的服务自动做前缀匹配,有的要你在请求里标出「这一段是可缓存的前缀」,还有的按缓存写入另外计费——首次写入可能比普通输入更贵,要靠后续命中赚回来。上线前先算一笔:你的前缀复用率够不够高。

三、它和上下文压缩天生打架。 下篇 A3.1 讲的上下文压缩会改写前文——一改,缓存整体作废,下一步全量重算。所以压缩要做成低频的大动作,而不是每步都修修补补。

四、别把它当安全边界。 缓存是按前缀内容匹配的。多租户场景下要确认服务方的缓存是否按租户隔离——这是选型时该问清楚的合规问题,不该假设。

小结​

本节要点
  • 历史 token 的 K、V 永远不变——KV Cache 把它们存起来,每步只算新 token 的一行。
  • 计算量从每步「整个三角形」塌缩成「一行」,长文本下节省成百上千倍。
  • 代价是显存随上下文线性膨胀,长对话里缓存常比模型权重还占地方——GQA 等共享技术就是为砍它而生。
  • 「长上下文为什么贵」的直接原因就在这里。
  • 前缀缓存把同一套缓存跨请求复用:条件是从第 1 个 token 起逐字相同,一处不同、后面全废。
  • 由此得到一条硬规则:稳定的放前面、变化的放后面(系统提示 → 文档 → 示例 → 用户问题),别在提示词里塞时间戳,也别让重排改变文档的拼接顺序。
  • 收益是首字延迟大降 + 输入成本常降到两三成;但它有有效期、可能要显式声明、且和上下文压缩天生打架。
📝 动手自测0/5
1. KV Cache 之所以成立,根本原因是什么?
2. 实验里左边「无缓存」面板每步画出整个三角形,右边只画一行新格子。三角形对应的是什么?
3. 为什么说 KV Cache「天下没有免费的午餐」?
4. 你的应用有 4000 token 的固定系统提示词,想靠前缀缓存省钱。下面哪个做法会让缓存完全失效?
5. 为什么「前面改一个字,后面所有位置的缓存就都作废了」?

延伸阅读​

  • GQA 论文(2023)——「多个 Q 头共享 KV 头」如何把缓存砍下一个量级。
  • 下一节 6.3 量化:搬运太贵?那就把要搬的东西变小。