LLM 系统里至少有四种机制都被叫作“缓存”,但它们保存的对象、作用范围和风险完全不同。很多性能分析之所以混乱,正是因为把这四件事揉成了一个概念。
Avichawla 的区分很清楚:KV cache 服务于一次生成,prefix caching 把 KV 状态留给后续请求,prompt caching 是模型供应商对同类前缀复用提供的计费接口,而 semantic cache 保存的是已经生成完的答案。
- KV cache 保存当前请求的 Key / Value 张量,在同一序列内复用;命中后仍运行模型,主要减少逐 token 解码计算,代价是显存和带宽压力。
- Prefix caching 跨请求保留 KV block,要求 token 前缀精确匹配;命中后仍运行模型,主要跳过重复 prefill,受驱逐、隔离与命中率影响。
- Prompt caching 保存供应商侧的前缀 KV 状态,要求渲染后的上下文精确匹配;主要降低首 token 延迟和输入费用,需要处理隐性失效、TTL 与计费规则。
- Semantic caching 保存完整响应文本,按 embedding 相似度匹配;命中后不再运行模型,能同时省输入和输出 token,却可能直接复用错误答案。
前三种机制都不会改变模型原本应该给出的结果。miss 的代价只是更多计算、延迟和费用。语义缓存则完全不同:它可能在 HTTP 200 的正常响应里,安静地返回一条不适用于当前问题的旧答案。
#1. KV cache:一次请求里的注意力记忆
自回归模型先对 prompt 执行 prefill,为每一层、每个 token 计算 Key 和 Value;进入 decode 后,每生成一个新 token,只需要计算这个 token 的新状态,再与之前保存的 K/V 做注意力,而不必从头处理整个序列。
Query 不需要保存。一个 token 的 Query 只在它被处理的那一步使用一次,而它的 Key 和 Value 会被后面所有 token 反复读取。因此,KV cache 保存的是未来仍有复用价值的两类张量。
这把逐步解码从对完整历史反复计算,变成只处理一个新增 token。但计算量下降后,瓶颈通常会转向显存带宽:每一步仍要从 HBM 读取越来越大的 KV cache,GPU 很可能不是在等算子,而是在等数据搬运。
KV cache 会随序列长度线性增长,大小还取决于层数、KV head 数量、head dimension 和数据精度。原文估算,一些 70B 级模型在 BF16、128K 上下文下,单条序列的 cache 可能达到约 40GB,足以决定一张 GPU 能并发容纳多少请求。
因此模型和推理框架会从几个方向压缩它:
- Grouped-query attention 让一组 Query head 共享 K/V head;
- Multi-head latent attention 把 K/V 压缩到更小的潜在表示;
- KV quantization 用更低精度换取更大容量;
- offloading 把暂时不用的层缓存移动到 CPU。
这些方案不是免费收益。量化需要额外的量化与反量化,短上下文、显存充足时可能反而更慢;StaticCache 便于编译和固定图优化,但会为短请求预留并扫描并未使用的空间。应根据上下文长度分布和真实瓶颈选择,而不是看到“省显存”就默认开启。
#KV cache 通常随请求结束而释放
普通调用结束后,KV cache 就被释放。多轮对话的第 20 轮虽然包含前 19 轮历史,服务端仍可能重新 prefill 整段上下文。
如果应用自己保留 past_key_values,并确保下一轮 token 序列严格以前一轮为前缀,就能只计算新增后缀。这里的“严格”不是肉眼看起来相同,而是 token ID 必须逐位一致。只要改动较早位置的一处字符、模板或工具定义,后面的 cache 就不再有效。
单进程变量只能解决一个会话的问题。要让大量请求共享历史计算,就进入 prefix caching。
#2. Prefix caching:把 KV block 留给后续请求
Prefix caching 的核心变化只有一个:请求结束后不立即释放 KV block,而是把它们留在共享池中并建立索引。新请求到达时,如果开头的 token 与某个缓存前缀完全相同,就直接复用对应张量,从第一个 miss 开始重新 prefill。
vLLM 使用链式 block hash。每个 block 的键不仅包含本 block 的 token,还包含父 block 的 hash,因此第五块只有在前四块也完全匹配时才可能命中。调度器按顺序查找,并在第一个 miss 处停止;正在使用的 block 通过引用计数避免被驱逐。
这种设计解释了几个容易忽视的事实:
- 只有完整 block 才容易被索引,尾部不足一块的 token 可能每次重算;
- 大 block 查询次数少、局部性好,但尾部浪费更多;小 block 共享更细,却增加索引成本;
- cache 与运行中的 batch 争夺同一块 GPU 内存,保留更多历史可能减少并发容量;
- 内存紧张时,未被引用的 block 会按 LRU 等策略驱逐,长前缀一旦丢失,重算代价也最大。
Prefix caching 只节省 prefill,不会缩短生成答案的 decode。如果响应很长,或者请求之间没有共同前缀,整体收益就很有限;哈希与管理本身也有成本,几乎全是唯一 prompt 的流量甚至可能出现吞吐下降。
#多租户共享还带来隔离问题
两个请求发送相同 token 时,会指向相同的物理 KV block。这对同一应用很划算,但跨客户共享可能形成时序侧信道:攻击者通过首 token 延迟差异,猜测某段前缀是否已经被其他用户请求过。
vLLM 支持把 cache_salt 混入第一个 block 的 hash。只有使用相同 salt 的请求才能共享 cache。代价是同一文本在不同租户下要保存多份副本,命中率和显存效率下降;收益则是信任边界变得明确。
缓存不是纯性能组件,它也属于数据隔离设计。
#RAG 为什么很难吃到前缀缓存
典型 RAG prompt 由系统指令、检索片段和用户问题组成。即使两次请求找到了同一批文档,只要片段顺序不同,链式 hash 就会在第一个差异处中断,后面所有 block 都无法复用。
把每个文档片段单独 prefill,再简单拼接 cache 也不正确。每段张量携带的位置信息不对,片段之间没有互相做过注意力,每一段还会把自己当成序列起点。结果不是等价于完整 prefill 的状态。
LMCache 的 CacheBlend 给出了一种更复杂的方向:允许片段在不同位置复用,但挑选少量偏差最大的 token 重新计算,用局部重算修复跨片段注意力和位置编码。原文给出的结果是,相比全量重算,首 token 延迟大约可改善两到三倍。重点不在具体倍数,而在于 RAG 的缓存复用需要部分重算,不能把独立 KV 张量直接首尾相接。
#3. Prompt caching:供应商包装后的前缀复用
托管 API 不会把 block table、张量和驱逐策略暴露给用户。它提供的是一套缓存断点、TTL、usage counters 和单独计价规则。
底层保存的仍然是 KV 状态,不是 prompt 文本,也仍要求渲染后的完整 token 前缀精确一致。这里的“完整”还可能包含供应商自动加入、用户看不到的系统内容,所以最低可缓存长度或失效行为有时会显得难以解释。
在显式缓存接口里,cache_control 通常标记缓存区域的终点:从请求开头到该 block 都写入缓存,变化频繁的用户问题放在断点之后。第一次调用会出现 cache_creation_input_tokens,后续命中则体现在 cache_read_input_tokens。两个计数都为零,可能只是上下文没有达到最低可缓存长度,而不是 API 报错。
以 Claude 当前的 5 分钟缓存为例,写入按基础输入价格的 1.25 倍计费,读取为 0.1 倍;1 小时写入为 2 倍。只要 TTL 内有后续复用,首次写入溢价就可能被很快摊薄。
Prompt caching 的真正控制面不是“开或关”,而是稳定内容如何排序、断点放在哪里,以及使用指标是否证明它真的命中了。
#4. Semantic caching:不再运行模型,直接返回旧答案
语义缓存会先把新问题编码成 embedding,在历史问题中做最近邻搜索;相似度超过阈值时,直接返回已经保存的响应,不再调用主模型。因此它不仅省 prefill,也省 decode 和输出 token。
但所有请求都要承担 embedding 和检索成本,包括 miss。数据规模变大后,暴力搜索还要换成近似最近邻索引,于是系统同时存在 embedding 模型误差、ANN recall 和相似度阈值三层变量。
更严重的是,语义相似不等于答案可以复用。下面几类问题在向量空间里可能非常接近,业务含义却相反:
- 同一句话增加一个否定词;
- 同一操作只改变一个金额、日期或环境名;
- 相似问题分别针对不同用户、权限或最新状态。
提高阈值会让命中率快速下降,同时仍要为每次 embedding 付费;降低阈值则会增加“高置信度返回错误答案”的概率。行业里流传的默认阈值跨度很大,恰好说明它是流量和风险的属性,不是一个可以复制的常数。
更稳妥的起点往往是先测 byte-identical 的重复率。精确响应缓存同样可以跳过模型和输出 token,又没有模糊匹配的假阳性;只有确认大量请求确实是不同表达的同一问题,而且错误复用后果可控时,才值得引入语义缓存。
#五种最常见的缓存破坏方式
生产中很多 miss 并不是模型或框架问题,而是 prompt 组装顺序不稳定。
#把变量放在最前面
时间戳、request ID、用户名等字段一旦位于 system prompt 开头,会让其后的所有 block 一起失效。稳定内容应放前面,变量放最后,缓存断点放在两者边界。
#工具 schema 的顺序变化
工具定义通常出现在 system prompt 之前。即使工具集合相同,只要序列化顺序变化,完整前缀也会改变,后面的缓存全部作废。
#隐式配置改变渲染结果
web search、citations、thinking 配置或 tool_choice 等开关可能改变供应商实际渲染的上下文。对不同 reasoning effort 做 A/B 测试,也等于把原本共享的缓存拆成两组。
#重写历史而不是追加后缀
对话摘要会改写早期 token,下一次调用必须冷启动。相比之下,如果能在不改动前缀的条件下追加压缩结果,或在设计阶段就限制工具输出长度,缓存更容易保留。
#切换模型
缓存通常与模型绑定。把长对话路由到更便宜的模型,不代表它能读取原模型的 KV 状态;新模型仍要为完整历史执行 prefill。
#调试时不要比文本,要比 token ID
日志里看起来相同的两个 prompt,可能因为 BOS token、尾部换行、默认 system message 或重新序列化的工具 schema 而不同。只比较字符串,很容易在错误的位置排查。
更可靠的方法是拿到最终渲染上下文的 token ID,找到两个序列第一次不同的索引,再把附近几个 token 解码出来。这样可以直接定位复用从哪里停止,以及那个差异究竟来自模板、配置还是业务变量。
#用一个问题选择缓存层
选型时先问:你真正想避免的重复工作是什么?
- 单次生成中不想反复计算历史 token:使用 KV cache;
- 多个请求共享稳定的长前缀:使用 prefix caching;
- 调用托管模型,希望降低重复输入的费用和首 token 延迟:使用 prompt caching;
- 大量问题可以安全复用完全相同的答案:先测精确响应缓存,再谨慎评估 semantic caching。
不要用一个总 hit rate 混合衡量四层。前三层应该关注 prefill token、TTFT、显存、驱逐和输入费用;语义缓存则必须优先报告错误命中率、答案时效性与业务损失。对它来说,一个错误的 hit 往往比十个 miss 更贵。