大语言模型的缓存技术
LLM(大语言模型)推理中的缓存主要解决两个核心问题:成本(token 收费)和延迟(重复计算)。本文按缓存层级、核心概念到主流平台的策略逐层拆解。
LLM 缓存技术原理
1. KV Cache(KV 缓存)——最核心
Transformer 自回归生成时,每生成一个 token 都要重新计算之前所有 token 的注意力分数。KV Cache 把已生成 token 的 Key 和 Value 矩阵存下来,下次只需计算新 token 的 Query 与缓存 KV 的注意力,避免重复做前向传播。
- 显存占用大:
2 × 层数 × 隐藏维度 × 序列长度 × 精度 × 批次,是长上下文推理的主要瓶颈。 - 相关优化:
- PagedAttention(vLLM 核心):把 KV 按固定块分页管理,类似操作系统的虚拟内存,减少碎片、提高批处理吞吐。
- GQA/MQA(分组查询注意):多个 Query 头共享少数 Key/Value 头,大幅压缩 KV 缓存。
- KV 量化:把缓存的 KV 量化为 INT8/INT4。
- KV 驱逐/压缩(如 H2O、StreamingLLM):只保留重要 token 的 KV。
2. 前缀缓存 / Prompt Caching(提示词缓存)
系统提示词、few-shot 示例等前缀固定不变。推理服务把此前缀的 KV Cache 按内容哈希缓存,同一前缀的请求直接复用,跳过重复计算。
- 语义缓存(如 GPTCache):对查询做语义相似度匹配,完全相同或高度相似的请求直接返回缓存好的完整回答,完全跳过模型推理。
- OpenAI / Anthropic / Gemini 等 API 都对稳定前缀提供 prompt caching 折扣价(写入按全价、读取打折扣)。
3. 模型结构级缓存
- MoE(混合专家):缓存 token 被路由激活的专家路径,减少重复的路由计算。
- Speculative Decoding(投机解码):与小模型推断候选、大模型验证,其中”验证”部分依赖已缓存信息加速。
4. 硬件 / 系统级缓存
- GPU 厂商的 token 缓存 与 KV cache 专用加速(如 NVIDIA、AMD 的 KV 缓存优化,降低显存带宽瓶颈)。
- 把 KV 缓存放进主机内存 / 远程存储,做跨进程、跨节点共享(如 Mooncake 这类 KVCache 卸载系统)。
5. 缓存一致性 / 失效难点
- Prompt 哪怕多一个空格或换行,哈希就变,缓存即失效 → 需要规范化(去除多余空白、排序等)。
- 服务端按 token 级别(而非字符)计算,长度对齐、是否带 continue 标记都会影响命中率。
一句话总结:LLM 缓存的核心思路是——KV Cache 省掉重复计算(推理层),前缀/语义缓存省掉重复生成(服务层),配合显存管理与量化来对抗缓存的存储开销。
KV Cache 和 Prompt Cache 有本质区别吗?
Prompt caching 的底层就是 KV cache,但两者严格来说是不同抽象层级的两个概念,差异主要在”是谁在缓存、缓存给谁用、怎么判定命中”。
本质关系
KV cache: 推理引擎内部的"机制"(单请求内的缓存) ↓ 跨请求持久化 + 前缀对齐Prompt cache:服务层的"产品特性"(跨请求的缓存)可以这样类比:KV cache 是内存,prompt cache 是磁盘上的持久化缓存。内存本来就是干这个的,但”持久化 + 按 key 检索 + 淘汰策略”是另一层工程。
关键区别
| 维度 | KV Cache | Prompt Cache |
|---|---|---|
| 抽象层级 | 推理引擎内部机制,每个请求必然产生 | 服务层产品特性,需要显式设计才有 |
| 生命周期 | 通常随请求结束就释放(除非引擎特意保留) | 跨请求、跨会话持久,可被多个用户/请求共享 |
| 缓存范围 | 整个已生成序列(前缀 + 新生成的部分都缓存) | 只缓存稳定前缀(系统提示词、few-shot 等),新生成部分不进 prompt cache |
| 命中判定 | 无判定——同一请求内连续计算,天然”命中” | 需要按 token 级前缀对齐 + 哈希/规范化来判定是否复用 |
| 成本形态 | 纯工程成本(显存占用) | 产品化计费(如 OpenAI 写入全价、读取 4 折) |
| 管理复杂度 | 显存管理(PagedAttention、KV 量化) | 缓存容量、LRU 淘汰、跨节点一致性、前缀规范化 |
例证:为什么说 prompt cache “就是” KV cache
vLLM 的 prefix caching 就是直接把之前请求的 KV cache 块按哈希存下来(block hash 匹配前缀块),下一个请求如果前缀相同,直接从缓存块开始计算,跳过前缀的 prefill。这里 prompt cache 的存储单元就是 KV cache 块,没有第二种缓存介质。
但也有”不是 KV cache”的 prompt 缓存
- 语义缓存(GPTCache 等第三方):对问题做 embedding 相似度匹配,命中后直接返回完整回答,连模型都不调用——这本质是”应用层结果缓存”,和 KV cache 无关。
- 中间状态缓存:有些系统会缓存 prompt 的 prefill 输出(如 embedding、logits 中间态),不限于 KV 对。
一句话总结:Prompt cache ≈ KV cache 的跨请求持久化 + 前缀复用特化。它没有发明新的缓存数据,而是在 KV cache 之上加了生命周期管理、命中判定、计费策略这三层东西。KV cache 是”机制”,prompt cache 是”建立在机制之上的产品策略”。
交替前缀(A, B, A, B)会命中 Prompt Cache 吗?
如果多次请求的 prompt 前缀是间隔相同的——第一和第三次前缀相同(A),第二和第四次前缀相同(B)——会命中缓存吗?结论:会命中,但”间隔相同”本身不是决定因素。
决定命中与否的是两个东西:保留时长(TTL)和缓存容量(淘汰策略),而不是请求的交替模式。只要这两个条件满足,第 1、3 次用前缀 A,第 2、4 次用前缀 B,都能各自命中。
为什么”交替模式”不影响命中
你担心的场景是:请求 3(前缀 A)进来时,缓存里可能还留着请求 2(前缀 B)的东西,A 会不会被挤掉?这取决于:
1. 保留时长 / TTL(Anthropic 这类 API)
Anthropic 的 prompt cache 有 5 分钟默认 TTL(可配置到最多 1 小时)。只要第 3 次请求离第 1 次请求在 TTL 窗口内,前缀 A 的缓存就还”活着”,直接命中,跟中间隔了几个请求无关。TTL 是”从缓存被写入/读到的那一刻起倒计时”,不是”按请求次数”。
2. 容量 / 淘汰策略(vLLM 这类自托管)
vLLM 的 prefix caching 用 LRU(最近最少使用)淘汰,缓存块按 block hash 管理。关键问题:前缀 A 的块会不会在前缀 B 被加载时被踢出?
- 如果缓存容量足够大:A 和 B 的块都能并存,请求 3 命中 A,请求 4 命中 B,互不干扰。
- 如果容量吃紧:LRU 会优先淘汰最久没用的块。请求 2 用了 B,A 就成了”较久没用”的,可能被淘汰 → 请求 3 就 miss,需要重新 prefill A。
真正的判定条件
举个具体例子——假设缓存容量能同时装下 A 和 B:
| 请求 | 前缀 | 命中? | 原因 |
|---|---|---|---|
| 1 | A | miss | 首次,还没缓存 |
| 2 | B | miss | 首次 |
| 3 | A | hit | A 未被淘汰,且在 TTL 内 |
| 4 | B | hit | B 未被淘汰,且在 TTL 内 |
而如果容量只够装一个前缀,LRU 会让 A、B 互相驱逐,结果变成全部 miss(每次都重新 prefill),因为每次换前缀都会把上一个踢掉。
一句话总结:交替请求(A, B, A, B)能命中,前提是:① 时间上落在 TTL 窗口内;② 缓存容量足够容纳多个前缀,不会被 LRU 互相挤掉。真正决定命中率的是 TTL 和容量/淘汰策略,不是请求的先后交替模式。
百炼 / Kimi / 智谱 / DeepSeek 的 Prompt Cache 策略对比
以下策略信息来自各平台官方文档的一手核对。
1. 百炼(通义千问 / DashScope)—— 显式 + 隐式双模式
百炼是这四家里**唯一支持显式缓存(用户手动控制)**的平台,也提供自动隐式缓存。
| 维度 | 显式缓存 | 隐式缓存 |
|---|---|---|
| 开启方式 | 需在 content 里加 cache_control:{"type":"ephemeral"} | 全自动,无法关闭 |
| 命中规则 | 确定性命中(服务端从标记位置向前回溯) | 自动识别公共前缀,命中率不确定 |
| 最少缓存 Token | 1024 | 256 |
| 有效期 | 5 分钟(命中后重置) | 不确定,系统定期清理 |
| 创建缓存的 Token 计费 | 标准输入价 125% | 标准输入价 100% |
| 命中缓存的 Token 计费 | 标准输入价 10% | 标准输入价 20% |
关键细节:
- 单次请求最多 4 个缓存标记,回溯最近 20 个 content 块。
- 工具定义(function calling)会作为系统消息参与缓存计算,要求每次完全一致(顺序、字段都一致)否则失效。
- 支持模型:qwen3.8-max/plus/flash、glm-5.1、kimi-k2.6/k2.7-code、deepseek-v3.2 等。
2. Kimi(Moonshot)—— 全自动,无需配置
- 对所有模型自动启用,无需手动创建、无需缓存 ID、无需管理 TTL。
- 命中条件:前一个请求的 prompt tokens > 256 才缓存(小于 256 的请求会被丢弃)。
- 建议把固定大段上下文(system prompt、知识文档)放在
messages数组最前面以提高命中率。 - 定价(K3 为例):
| 项 | 价格(/1M tokens) |
|---|---|
| 输入(缓存命中) | ¥2.00 |
| 输入(缓存未命中) | ¥20.00 |
| 输出 | ¥100.00 |
即命中价 = 未命中价的 10%。
3. 智谱(GLM)—— 隐式自动 + 内容相似度
- 自动缓存:基于内容相似度识别重复上下文(系统提示词、历史对话),无需任何配置。
- 命中情况通过
usage.prompt_tokens_details.cached_tokens透明返回。 - 计费:缓存命中的 Token 按优惠价(文档示例为标准价的 50%),新内容按标准价,输出按标准价。
- 支持所有主流模型(GLM-5.2 / 5.1 / 5 系列等)。
- 注意:轻微格式差异可能影响命中;缓存有”合理时效”,过期会重算。
4. DeepSeek —— 硬盘缓存,折扣最狠
- 采用上下文硬盘缓存(存分布式硬盘阵列),全自动,无需改代码换接口。
- 命中规则最严格:要求从第 0 个 token 开始前缀完全相同,中间开始的重复不能命中。
- 以 64 tokens 为存储单元(不足 64 的不缓存);“尽力而为”,不保证 100% 命中。
- 长期不用的缓存自动清空(几小时到几天)。
- 命中统计:
prompt_cache_hit_tokens/prompt_cache_miss_tokens。 - 定价(V4 为例):
| 模型 | 缓存命中输入 | 缓存未命中输入 | 输出 |
|---|---|---|---|
| deepseek-v4-flash | 0.02元/1M | 1元/1M | 2元/1M |
| deepseek-v4-pro | 0.025元/1M | 3元/1M | 6元/1M |
命中价 ≈ 未命中价的 2%,折扣是四家里最大的。得益于 MLA 结构把 KV Cache 压缩到可落硬盘。
交替前缀(A,B,A,B)在各平台的表现
结合各平台策略,结论依然是 “能命中,取决于 TTL 和容量”,但各平台权重不同:
- 百炼显式:TTL 明确 5 分钟,只要 A、B 各在 5 分钟内被再次使用就能各自命中(命中会重置 TTL)。容量/淘汰由服务端管,通常两种前缀可并存。
- Kimi / 智谱:TTL 由系统自动管理、不透明,命中率不做保证,交替大概率能命中但不保证。
- DeepSeek:TTL 是”几小时到几天”的清空机制,A、B 交替基本都能命中;但记住必须从第 0 token 前缀完全一致,只要 A、B 各自的 system prompt 保持稳定即可。
一个共同陷阱:所有平台都是前缀匹配。交替请求时,[A, B, A, B] 中第 3 次请求(前缀 A)能命中,前提是 A 的稳定前缀部分(如系统提示词)在两次请求间完全一致——如果 A 前缀里混入了上轮对话历史等动态内容,命中率会大幅下降。
什么是前缀匹配(Prefix Matching)
在 prompt cache 语境里,“前缀”指的是 token 序列从最开头(第 0 个 token)开始的一整段连续内容。“前缀匹配”就是:新请求的前面这一段内容和缓存里存过的内容完全对齐,我们就认为它可以复用这段时间的缓存结果。
它只认”从头部开始、连续、完全一致”
缓存里存过的前缀: [系统提示词] [few-shot示例] [用户问题1]新请求的完整内容: [系统提示词] [few-shot示例] [用户问题1] [用户问题2] └────── 这部分和缓存一致 ──────┘ ↑ 命中点判定规则两条:
- 必须从第 0 个 token 开始——不能从中间某处开始匹配。
- 必须连续且完全一致(token 级,一个多余的空白/换行/顺序变化都会让后续全部失效)。
满足这两条,新请求就能跳过前缀这段的 prefill 计算,直接从命中点之后开始算。
为什么”前缀”而不是”任意位置”
因为 Transformer 的注意力是因果的(causal):每个位置的输出只依赖它之前的所有 token。所以一段内容只要在开头、且和之前完全一样,它的 KV cache 就完全可复用——前面没变,后面算出来必然一样。
反过来,如果重复内容出现在中间或后面,它前面的 token 变了(比如混入了上一轮对话历史),那它的 KV 就算重算也不对,所以中间/后段的重复无法命中。
具体例子
请求1:你是助手。 用户说:天气如何? → 首次,miss请求2:你是助手。 用户说:天气如何? → 前缀命中!(你是助手。)请求3:你好。 用户说:天气如何? → miss,因为开头变成了"你好。"请求4:你是助手。 用户说:股票如何? → 仍命中!(你是助手。 部分复用)注意请求 4:虽然用户问题变了,但开头”你是助手。“没变,所以这部分照样命中,只重算后半段。
各平台对”前缀”要求的严格度不同
| 平台 | 前缀规则 |
|---|---|
| DeepSeek | 最严格:必须第 0 个 token 开始完全一致,中间开始的重复一律不命中 |
| 百炼显式缓存 | 从 cache_control 标记位置向前回溯固定数量内容块,要求这些块完全一致(偏前缀性质) |
| Kimi / 智谱 | 自动识别公共前缀,命中率不保证,格式差异可能失效 |
一个常见误区
“前缀”不是指用户最近说的话,而是指一整个消息序列最开头的那部分——通常是系统提示词、固定知识库、或历史对话的早期部分。所以实践中把固定的大段内容放在 messages 最前面,才能最大化命中率。