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,所有大模型推理服务的第一块基石。
亲眼看看省了多少
点「▶ 自动生成」跑完整句,然后盯住两组数字:
- 累计格数:左边「❌ 无缓存」的橙色三角形每一步都整个重画,累计格数疯涨;右边「✅ 有缓存」只有最后一行是新算的橙色,其余全是安静的蓝色缓存。
- 计算量之比:这句才 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 起逐字相同,一处不同、后面全废。
- 由此得到一条硬规则:稳定的放前面、变化的放后面(系统提示 → 文档 → 示例 → 用户问题),别在提示词里塞时间戳,也别让重排改变文档的拼接顺序。
- 收益是首字延迟大降 + 输入成本常降到两三成;但它有有效期、可能要显式声明、且和上下文压缩天生打架。
延伸阅读
- GQA 论文(2023)——「多个 Q 头共享 KV 头」如何把缓存砍下一个量级。
- 下一节 6.3 量化:搬运太贵?那就把要搬的东西变小。