AI 工程基础体系 · 第 84/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
Prompt Cache 工程:前缀复用、缓存键、隔离、失效和成本
Prompt Cache 通常指:当多个请求共享同一段 prompt 前缀时,系统保存这段前缀在 Transformer 中已经计算出的中间状态,后续请求直接复用,而不是从第一个 token 重新计算。
这里的“缓存”不是把某个问题的最终答案保存下来,也不是根据语义相似度寻找相近问题。它通常缓存的是模型执行 prefill 阶段后得到的 Key/Value 状态,因此更准确地说,是一种面向生成式模型推理的 前缀 KV cache。
1. 先区分四种容易混淆的缓存
1.1 HTTP 响应缓存
HTTP 响应缓存保存:
请求 URL、请求体、请求头 → 完整响应
如果请求完全相同,可以直接返回旧响应,不必调用模型。
它适合缓存稳定、可复用的最终结果,例如:
GET /embedding-models/latest
但它不能处理“前缀相同、后缀不同”的请求。下面两个请求通常没有相同的完整响应:
系统提示词 + 用户问题 A
系统提示词 + 用户问题 B
1.2 语义缓存
语义缓存先计算问题的 embedding,再根据相似度寻找历史问题:
“如何申请退款?”
≈
“退款流程是什么?”
它可能返回历史答案,因此必须解决相似度阈值、答案时效性、权限和事实变化等问题。它不是 Transformer 前缀复用。
1.3 Transformer 的 KV cache
自回归 Transformer 每生成一个 token,都要让这个 token 关注之前的 token。工程上通常保存已经计算过的 Key 和 Value,避免在每一步重新计算历史 token。
这类 KV cache 通常只在一次请求的生成过程中使用。例如,模型已经处理:
“请用中文回答:”
接着生成第一个答案 token;在生成第二个、第三个 token 时,前面的上下文 KV 可以复用。
1.4 Prompt Cache
Prompt Cache 把 KV cache 的生命周期从“一次生成请求”延长到“多个请求之间”。
例如多个请求共享:
你是公司内部知识库助手。
回答必须使用中文。
只能根据给定文档回答。
以下是公司规章制度全文:
...
只要后面的用户问题不同,公共前缀的 KV 仍然可以复用。
因此:
Prompt Cache ⊂ 跨请求复用 KV cache
它不是模型规范中的统一 API 名称。不同云模型服务、推理框架和自建系统可能对它使用不同名称、计费方式和可配置能力。必须以具体模型服务或框架版本的文档为准,不能假设一个服务支持另一个服务的参数。
2. 为什么前缀 KV 可以复用
2.1 自回归 Transformer 的计算结构
给定 token 序列:
因果自注意力要求位置 只能看到当前位置及之前的 token:
其中:
- :当前位置 的 Query;
- :位置 到 的 Key;
- :位置 到 的 Value;
- :Key 向量维度。
一个 Transformer 层会为每个输入位置计算 和 。对于已经处理过的前缀:
x1 x2 x3 x4
如果下一个请求仍然从完全相同的 token 序列开始,那么这些 token 在相同模型和相同执行配置下产生的 Key/Value 可以复用。
新请求:
x1 x2 x3 x4 y1 y2
只需:
- 读取
x1...x4的缓存 KV; - 对
y1、y2执行新增计算; - 生成后续输出。
2.2 前缀复用的形式化条件
设请求 的 token 序列为:
请求 的 token 序列为:
如果满足:
- 两个序列的前 个 token ID 完全相同;
- 使用同一个模型权重;
- 使用同一个 tokenizer 和 tokenization 配置;
- 位置编码规则相同;
- 影响前向计算的模型配置相同;
- KV 的 dtype、量化格式、张量布局和实现兼容;
则可以复用前 个 token 的 KV 状态。
这里最重要的是“token ID 完全相同”,而不是字符串看起来相同,更不是语义相同。
例如,以下字符串差异可能导致 token 序列不同:
"请回答问题"
"请回答问题\n"
"请回答问题 "
Unicode 规范化、换行符、特殊 token、模板格式和 tokenizer 版本都可能改变 token ID。
2.3 为什么前缀必须从序列开头连续匹配
因果注意力中的位置 会依赖它之前的上下文。假设两个请求只有中间部分相同:
请求 A: 系统提示词 + 用户身份 A + 文档
请求 B: 系统提示词 + 用户身份 B + 文档
“文档”虽然相同,但它在两个请求中的前置上下文不同,因此文档 token 对应的 KV 也可能不同,不能直接按文本片段独立复用。
一般缓存结构是前缀树或哈希链:
根
└── 系统提示词
└── 工具定义
└── 规章全文
├── 用户问题 A
└── 用户问题 B
只有从根开始连续命中的路径才能复用。中间出现一个不同 token 后,后续节点通常都不能继续命中。
3. Prefill、Decode 和 Prompt Cache 的数据流
一次生成请求通常可以分为两个阶段。
3.1 Prefill 阶段
Prefill 一次处理输入 prompt:
系统提示词 + 工具定义 + 用户问题
它的目标是建立输入序列对应的隐藏状态和 KV。输入越长,prefill 计算量和显存访问量通常越大。
3.2 Decode 阶段
Decode 阶段每次生成一个或少量 token,并使用已有 KV:
输入 prompt → 输出 token 1 → 输出 token 2 → 输出 token 3
Decode 受序列长度和内存带宽影响明显。Prompt Cache 主要节省的是重复请求中的 prefill 工作,并减少需要重新写入和加载的 KV。
典型流程如下:
sequenceDiagram
participant C as 客户端
participant G as 网关
participant K as Prompt Cache
participant M as 模型服务
C->>G: 请求:公共前缀 + 用户后缀
G->>G: tokenizer 与规范化
G->>K: 查找最长匹配前缀
alt 命中公共前缀
K-->>G: 返回前缀 KV 与已匹配 token 数
G->>M: 仅计算未命中的后缀并生成
else 未命中
K-->>G: miss
G->>M: 计算完整 prompt 并生成
M-->>G: 返回可缓存前缀 KV
G->>K: 写入缓存
end
G-->>C: 流式或完整响应
这张图中有一个重要细节:缓存命中后,模型服务仍然要处理未命中的后缀。Prompt Cache 不是“无需调用模型”,而是“减少模型调用中的一部分计算”。
4. 缓存粒度:缓存整个 prompt 还是缓存前缀节点
4.1 整个 prompt 缓存
最简单的实现把完整 token 序列作为键:
完整 token 序列 → KV
只有完全相同的 prompt 才能命中。这种实现容易管理,但无法利用“公共系统提示词 + 不同用户问题”的共享部分。
4.2 前缀树缓存
更高效的实现按 token 或 token 块建立前缀节点:
节点 N0: 空前缀
节点 N1: 前 128 个 token
节点 N2: 前 256 个 token
节点 N3: 前 384 个 token
请求到达时,系统寻找最长的已缓存前缀。例如:
缓存已有:前 256 个 token
新请求长度:410 个 token
命中长度:256 个 token
需要计算:第 257 到 410 个 token
为了降低哈希和元数据开销,工程实现常按固定大小的 token block 缓存,而不是每个 token 一个节点。
4.3 块边界的影响
如果缓存块大小为 128 token,则:
已有公共前缀:250 token
可复用块:128 token
剩余 122 token:可能不能作为完整缓存块复用
具体行为依赖实现。有些系统支持部分块,有些只缓存完整块。文档没有明确说明时,不能把“文本前缀长度”直接等同于“可复用 token 数”。
5. 缓存键:决定什么可以共享
缓存键不是简单地对 prompt 字符串做 hash。一个安全的逻辑键至少要包含影响 KV 正确性和访问权限的字段。
可以抽象为:
其中:
scope:租户、用户、会话或数据权限范围;model identity:模型名称、权重版本、量化版本、适配器版本;tokenizer identity:tokenizer 文件或版本;generation context:系统提示词、工具定义、模板和其他会进入上下文的内容;token IDs:经过最终模板渲染和 tokenization 后的序列;execution format:KV dtype、量化方式、张量布局和硬件后端等。
5.1 为什么只用字符串 hash 不够
以下请求文本可能完全相同:
“请总结这份文档。”
但它们使用的模型不同:
model-a-v1
model-a-v2
或者使用不同 LoRA:
base-model + legal-adapter
base-model + medical-adapter
相同 token 的 KV 也不能混用。
反过来,即使模型名称相同,服务端滚动更新权重后,旧 KV 也可能不再兼容。因此生产系统通常把不可变的模型版本或权重摘要纳入键,而不是只使用一个浮动的模型别名。
5.2 Tokenizer 版本必须进入兼容条件
缓存的 KV 对应的是 token 序列,而不是原始字符串。若 tokenizer 变化:
字符串 → token ID
这个映射可能改变。
即使模型权重名称不变,tokenizer 文件、special token 配置或 chat template 发生变化,也应视为缓存空间变化。
5.3 生成参数是否影响前缀 KV
需要区分两类参数。
通常不影响已输入前缀 KV 的参数:
- temperature;
- top-p;
- top-k;
- 最大新生成 token 数;
- 随机种子。
这些参数主要影响 decode 阶段的采样。
可能影响前缀状态或缓存可用性的参数:
- 模型权重或 LoRA;
- chat template;
- 视觉输入的预处理;
- position ID;
- RoPE scaling;
- attention 实现和 KV 格式;
- 影响输入序列的工具调用格式;
- 多模态输入中的图像 token 或音频特征。
因此,不能简单规定“所有请求参数都必须进键”,也不能简单规定“只有 prompt 字符串进键”。正确做法是建立一份明确的兼容性清单。
6. 一个完整的缓存键例子
假设请求经过模板渲染后变为:
<|system|>
你是内部财务助手,只能引用授权文档。
<|user|>
请计算报销金额。
<|assistant|>
可以构造如下逻辑键:
{
"tenant_id": "tenant-42",
"model_digest": "sha256:...",
"tokenizer_digest": "sha256:...",
"chat_template_digest": "sha256:...",
"adapter_digest": null,
"rope_config_digest": "sha256:...",
"kv_format": "bf16",
"auth_scope": "finance-read",
"token_prefix_hash": "sha256:..."
}
其中:
model_digest保证权重兼容;tokenizer_digest保证 token ID 解释一致;chat_template_digest保证角色和特殊 token 格式一致;auth_scope防止不同权限范围错误共享;token_prefix_hash标识具体 token 前缀。
实际系统可能将部分字段放到命名空间,部分字段放到哈希输入中。关键不是 JSON 的具体形式,而是:所有影响正确性或权限的因素必须参与隔离。
7. 前缀布局决定命中率
Prompt Cache 只能复用前缀。因此,同样的信息,如果排列顺序不同,缓存收益可能完全不同。
7.1 低命中布局
系统提示词
当前时间:2025-03-08
请求 ID:abc123
用户问题
长篇知识库内容
每次请求开头都包含变化字段:
当前时间不同
请求 ID 不同
如果变化字段位于公共前缀中间,后续长篇知识库内容也无法复用。
7.2 更适合缓存的布局
系统提示词
长篇稳定知识库内容
当前时间:2025-03-08
请求 ID:abc123
用户问题
这样可以复用:
系统提示词 + 长篇稳定知识库内容
动态字段位于后部,不会破坏前缀。
但这不是无条件的建议。动态字段如果会改变模型对前面内容的解释,或者权限过滤本身需要发生在文档之前,就不能为了命中率把它盲目移到后面。缓存优化不能违反上下文语义和访问控制。
7.3 系统提示词的版本化
将公共前缀拆成稳定段和动态段:
稳定段:
- 角色定义
- 输出格式
- 工具协议
- 文档规则
动态段:
- 当前日期
- 用户身份
- 会话状态
- 请求追踪 ID
稳定段可使用显式版本:
prompt-schema: finance-assistant-v7
policy-version: 2025-03-01
版本不仅方便缓存键设计,也方便在内容更新后主动失效。
8. 成本模型:节省的是重复前缀计算
8.1 Token 级成本模型
设:
- :prompt token 数;
- :输出 token 数;
- :命中的前缀 token 数;
- :未缓存 prompt token 的单价;
- :缓存命中 token 的单价;
- :输出 token 的单价;
- :请求数量。
未使用 Prompt Cache 时,近似成本为:
使用缓存后,若第一次请求完整计算,后续请求命中 个 token:
还应减去或加上缓存存储与管理成本:
实际云服务可能按“缓存写入 token”“缓存读取 token”“普通输入 token”“输出 token”分别计价,也可能对缓存命中没有独立价格。公式必须根据具体服务的计费规则调整。
8.2 数值算例
假设:
- 每个请求有 个输入 token;
- 其中 个 token 是稳定公共前缀;
- 每次输出 个 token;
- 共 个请求;
- 普通输入价格为每百万 token 1 美元;
- 缓存命中输入价格为每百万 token 0.1 美元;
- 输出价格为每百万 token 2 美元;
- 暂不计算存储成本。
无缓存时:
若第一次请求完整计算,后续 99 次请求的 8,000 token 命中:
结果约为:
这只是一个假设价格下的输入成本模型。真实收益还受以下因素影响:
- 缓存是否真的命中;
- 服务是否只在完整块命中时计入缓存价格;
- 首次请求是否收费为缓存写入价;
- 缓存 TTL;
- 多副本之间是否共享缓存;
- KV 在 GPU、CPU 或远程存储中的迁移成本;
- 命中后是否仍受上下文窗口限制;
- 缓存查询和序列拼接是否增加延迟。
8.3 延迟收益不等于成本收益
Prompt Cache 可能降低 prefill 延迟,但不一定显著降低端到端延迟。
如果输出很长,decode 阶段可能占主要时间;如果缓存 KV 需要从远程存储搬运到 GPU,读取开销可能抵消部分计算收益。
因此至少要分别观察:
cache lookup latency
prefill latency
KV restore latency
decode time
time to first token, TTFT
tokens per second
GPU memory pressure
不能只看平均总耗时判断缓存是否有效。
9. KV 的内存成本和容量估算
对于 decoder-only Transformer,KV cache 的大小可以近似写为:
其中:
- :batch 或序列数量;
- :Transformer 层数;
- :缓存 token 数;
- :分别保存 Key 和 Value;
- :KV head 数;
- :每个 head 的维度;
bytes:每个元素占用的字节数,例如 BF16 通常为 2。
例如一个模型使用:
- 32 层;
- 32 个 KV heads;
- 每个 head 维度 128;
- 10,000 个 token;
- BF16;
单条序列的 KV 大小约为:
约为 5.24 GB 的原始张量空间。
这说明一个重要事实:长公共前缀虽然可能节省大量 prefill 计算,但把它长期放在 GPU 上也可能迅速耗尽显存。实际模型可能使用 GQA 或 MQA,使 小于 Query head 数;也可能使用 KV 量化、分页内存或 CPU/远程分层存储,因此最终大小要以具体实现为准。
10. 缓存隔离:正确性之外的安全边界
Prompt Cache 中保存的 KV 不一定能够直接还原原始文本,但不能因此把它视为无敏感性数据。
它可能:
- 反映用户输入或内部文档;
- 通过模型输出影响后续请求;
- 占用共享 GPU 和缓存容量;
- 产生命中时间、容量和淘汰等侧信道;
- 在实现缺陷下导致跨租户状态复用。
10.1 三种隔离级别
公开共享
适合:
所有请求都使用相同的公开系统提示词
例如公开产品说明、固定格式协议。此时可以跨用户共享,但仍需隔离模型版本和执行格式。
租户级共享
适合:
同一企业租户内共享企业知识库前缀
缓存键中至少应包含:
tenant_id + authorization_scope + document_version
不能仅因为两个用户属于同一组织,就假设他们拥有相同文档权限。
用户或会话级隔离
适合包含:
- 用户隐私;
- 个人聊天历史;
- 未脱敏客户数据;
- 权限高度动态的检索内容。
此时缓存应绑定用户、会话或更细粒度的授权上下文,牺牲共享率换取更清晰的安全边界。
10.2 权限不能只在缓存写入时检查
一种错误设计是:
- 用户 A 有权访问文档;
- 系统将文档前缀写入共享缓存;
- 用户 B 请求同一前缀;
- 系统只根据 token hash 命中,不重新检查权限。
缓存命中是一个数据访问动作,权限检查必须在命中路径上成立。可以采用:
缓存键绑定授权范围
或:
缓存内容只包含公开数据,权限数据不进入共享前缀
如果权限集合是动态的,必须将权限版本、策略版本或文档可见性版本纳入键,并在权限变化时失效。
10.3 加密不能替代隔离
静态加密和传输加密可以保护缓存介质,但不能解决:
- 错误租户命中;
- 错误模型复用;
- 进程内对象引用泄漏;
- 共享 GPU 显存的生命周期问题;
- 通过输出或时延推断缓存存在。
加密保护的是存储路径,命名空间和授权检查保护的是访问语义,两者不能互相替代。
11. 失效:什么时候旧 KV 必须不能再用
缓存失效不是“过期时间到了就删掉”这么简单。至少应区分正确性失效、权限失效、隐私删除和容量淘汰。
11.1 正确性失效
以下变化通常要求新建缓存命名空间或主动删除旧条目:
- 模型权重变化;
- tokenizer 变化;
- chat template 变化;
- system prompt 变化;
- 工具 schema 变化;
- 文档内容变化;
- 文档排序、分块或拼接方式变化;
- RoPE 或 position ID 配置变化;
- KV dtype、量化格式或张量布局变化。
最稳妥的方法是使用内容寻址版本:
model_digest
tokenizer_digest
prompt_policy_digest
document_snapshot_digest
kv_format
任何一个摘要变化,都会产生不同的逻辑键。
11.2 TTL 与 LRU 解决不同问题
TTL 控制条目的最长存活时间,适合处理:
即使没有显式更新,也希望定期重新计算
LRU 按最近使用情况淘汰条目,适合控制容量。
它们不是替代关系:
- 长时间不使用的条目可能先被 LRU 删除;
- 高频使用但内容已经不符合策略的条目仍可能存活到 TTL;
- 发生权限撤销或隐私删除时,不能等待 TTL。
11.3 主动失效的事件链
文档更新时,推荐发出带版本的事件:
document_id = handbook-2025
old_version = v12
new_version = v13
缓存服务根据文档版本索引删除旧前缀,或者让新版本自然使用新的键。
如果只把文档 ID 放入键、不放文档版本,那么更新后旧内容仍可能命中;如果只使用 TTL,过期窗口内就可能回答旧事实。
11.4 隐私删除比逻辑失效更严格
“从索引中不可见”不等于“数据已删除”。如果法规或内部政策要求删除,可能需要处理:
- GPU 内存中的 KV;
- CPU 内存中的 KV;
- Redis、对象存储或本地磁盘;
- 副本和跨区域副本;
- checkpoint 或快照;
- 访问日志、debug dump 和 trace;
- 备份恢复系统。
一个逻辑 tombstone 可以阻止后续命中,但不能自动清除已经复制的字节。
11.5 淘汰时的并发问题
考虑下面的时序:
请求 A:命中旧条目,开始恢复 KV
管理线程:删除并释放旧条目
请求 A:继续访问已释放内存
实现必须使用引用计数、租约、读写锁或不可变对象,保证正在使用的 KV 不会被提前释放。
12. 并发、重复计算和故障路径
12.1 Cache stampede
当一个热门前缀刚过期时,100 个请求可能同时 miss:
请求 1:miss → 计算 10,000 token
请求 2:miss → 计算 10,000 token
...
请求 100:miss → 计算 10,000 token
这会造成 GPU 计算突增。
常见的处理方式是 per-key singleflight:
- 第一个请求成为 leader;
- 后续请求发现该键正在构建;
- 后续请求等待 leader;
- leader 成功后,所有请求复用新条目;
- leader 失败时,等待者走回退路径,并设置重试退避。
singleflight 只应针对有限时间等待。不能让所有请求无限等待一个损坏的构建任务。
12.2 写入必须是原子的
缓存条目至少有以下状态:
ABSENT → BUILDING → READY
└→ FAILED
READY → EVICTING → ABSENT
只有 READY 条目允许被读取。不能先写入部分 KV,再把状态标记为可读,否则请求可能拿到长度不完整或层数不完整的缓存。
12.3 GPU 资源不足
命中并不保证一定能恢复成功。可能发生:
- GPU 显存不足;
- KV block 碎片化;
- 当前 batch 无法拼接;
- cache 与当前 attention kernel 不兼容;
- 远程 KV 下载超时;
- 模型副本正在滚动升级。
正确的故障策略通常是:
缓存恢复失败
→ 释放本次临时资源
→ 将请求按完整 prefill 执行
→ 记录失败原因
→ 不要把损坏条目标记为 READY
不能因为缓存失败就返回错误,除非业务明确要求缓存可用;Prompt Cache 通常应是性能优化,而不是正确性依赖。
13. 一个可运行的前缀缓存模型
下面的 Python 示例不执行神经网络,只模拟核心逻辑:
- token ID 作为真实匹配单位;
- 按租户和模型版本隔离;
- 查找最长前缀;
- 只有完整构建后才进入可读状态。
from dataclasses import dataclass
from typing import Dict, Tuple, Optional
TokenSeq = Tuple[int, ...]
@dataclass
class CacheEntry:
prefix: TokenSeq
state: str # BUILDING or READY
fake_kv: str
class PrefixCache:
def __init__(self):
self.entries: Dict[Tuple[str, str, TokenSeq], CacheEntry] = {}
def _key(self, tenant: str, model_digest: str, prefix: TokenSeq):
return tenant, model_digest, prefix
def longest_ready_prefix(
self,
tenant: str,
model_digest: str,
tokens: TokenSeq,
) -> Optional[CacheEntry]:
for length in range(len(tokens), 0, -1):
prefix = tokens[:length]
entry = self.entries.get(self._key(tenant, model_digest, prefix))
if entry and entry.state == "READY":
return entry
return None
def begin_build(
self,
tenant: str,
model_digest: str,
prefix: TokenSeq,
) -> CacheEntry:
key = self._key(tenant, model_digest, prefix)
if key in self.entries:
raise RuntimeError("prefix is already cached or being built")
entry = CacheEntry(
prefix=prefix,
state="BUILDING",
fake_kv=f"kv-for-{prefix}",
)
self.entries[key] = entry
return entry
def commit(self, tenant: str, model_digest: str, prefix: TokenSeq):
key = self._key(tenant, model_digest, prefix)
entry = self.entries[key]
if entry.state != "BUILDING":
raise RuntimeError("only BUILDING entries can be committed")
entry.state = "READY"
def invalidate_model(self, model_digest: str):
keys = [
key for key in self.entries
if key[1] == model_digest
]
for key in keys:
del self.entries[key]
cache = PrefixCache()
tenant = "tenant-a"
model_v1 = "model-sha256-v1"
public_prefix = (101, 102, 103, 104)
request_a = public_prefix + (201, 202)
request_b = public_prefix + (301, 302)
# 第一次请求:没有命中,构建公共前缀
entry = cache.begin_build(tenant, model_v1, public_prefix)
cache.commit(tenant, model_v1, public_prefix)
# 第二次请求:找到最长 READY 前缀
hit = cache.longest_ready_prefix(tenant, model_v1, request_b)
print("命中 token 数:", len(hit.prefix) if hit else 0)
print("需要新计算的 token:", request_b[len(hit.prefix):] if hit else request_b)
# 使用新模型版本时,旧版本缓存不会被错误复用
model_v2 = "model-sha256-v2"
miss = cache.longest_ready_prefix(tenant, model_v2, request_b)
print("新模型版本命中:", miss is not None)
预期输出:
命中 token 数: 4
需要新计算的 token: (301, 302)
新模型版本命中: False
这个示例中的 fake_kv 只是占位字符串,不能用于真实推理。真实实现还必须处理:
- 多层、多头 KV 张量;
- GPU 内存分配;
- block table;
- batch 拼接;
- 引用计数;
- 并发构建;
- TTL 和 LRU;
- 模型副本间一致性;
- 序列位置和 attention mask。
它仍然有价值,因为它展示了一个容易被忽略的事实:缓存命中的是 token 前缀,而不是请求对象或字符串后缀。
14. Hugging Face Transformers 中的边界
Hugging Face Transformers 文档中的 use_cache 和 past_key_values 主要描述生成过程中的 KV 缓存接口。它们支持模型在逐 token 生成时复用历史 Key/Value,但这不自动等同于一个完整的跨请求 Prompt Cache 产品。
典型生成过程的概念代码如下:
from transformers import AutoTokenizer, AutoModelForCausalLM
model_name = "your-compatible-causal-lm"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto",
)
inputs = tokenizer("请用中文解释因果注意力。", return_tensors="pt")
inputs = {k: v.to(model.device) for k, v in inputs.items()}
outputs = model.generate(
**inputs,
max_new_tokens=64,
use_cache=True,
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
这里的 use_cache=True 表示生成期间使用 KV cache。它通常不会替工程师完成以下工作:
- 在两个独立请求间安全保存和查找公共前缀;
- 处理租户隔离;
- 识别模型权重变化;
- 管理跨进程或跨机器 KV;
- 执行 TTL、LRU 和主动删除;
- 防止 cache stampede;
- 把缓存条目与权限策略绑定。
不同 Transformers 版本、模型架构和生成接口对 past_key_values 的具体类型、形状和生命周期可能不同。直接持久化内部对象属于实现级集成,应锁定依赖版本并写兼容性测试,不能只依赖一个通用示例。
自建系统一般需要在模型服务内部实现:
tokenize
→ 找最长公共前缀
→ 将对应 KV block 映射到当前请求
→ 只对剩余 token prefill
→ 继续 decode
如果把 KV 从一个模型实例传到另一个实例,还必须保证模型权重、设备类型、dtype、attention kernel、量化格式和位置编码设置兼容。仅仅让 Python 对象可以序列化,并不代表它可以被另一个推理实例正确消费。
15. 失效和版本变更的状态设计
一个可操作的状态机可以如下:
stateDiagram-v2
[*] --> ABSENT
ABSENT --> BUILDING: miss / 获得构建锁
BUILDING --> READY: KV 完整且校验通过
BUILDING --> FAILED: 超时、OOM、模型错误
FAILED --> ABSENT: 清理并允许重试
READY --> READING: 请求引用
READING --> READY: 引用释放
READY --> EVICTING: TTL、LRU、版本失效
EVICTING --> ABSENT: 无活动引用后释放
关键规则是:
BUILDING不能被普通读请求当作可用缓存;READY必须通过长度、模型版本和张量元数据校验;EVICTING不能立即释放仍被请求引用的 GPU 内存;- 版本失效应阻止新请求继续命中旧条目;
- 失败条目不能无限阻塞后续请求。
若模型正在灰度发布,建议把缓存空间按模型摘要隔离:
model-v1 digest → cache namespace v1
model-v2 digest → cache namespace v2
切换流量后再逐步回收旧空间。这样可以避免滚动升级过程中不同副本对同一个键返回不同格式的 KV。
16. 监控指标必须能解释“为什么没有收益”
只记录一个 cache_hit=true/false 不够。至少需要:
16.1 命中相关
request_count
full_miss_count
prefix_hit_count
hit_tokens
requested_prompt_tokens
hit_ratio_by_tokens
hit_ratio_by_requests
请求命中率和 token 命中率可能差异很大:
100 个请求中 90 个命中 10 token
10 个请求中 10 个命中 10,000 token
按请求统计会显得命中率很高,但实际节省的计算可能很少。成本分析更应关注 token 加权命中率。
16.2 原因分类
miss 应区分:
NO_ENTRY
TOKENIZER_MISMATCH
MODEL_VERSION_MISMATCH
AUTH_SCOPE_MISMATCH
PREFIX_TOO_SHORT
ENTRY_EXPIRED
EVICTED
KV_RESTORE_FAILED
OOM
否则“命中率下降”无法判断是 prompt 布局变化,还是模型升级导致的预期失效。
16.3 资源相关
GPU KV bytes
CPU KV bytes
remote KV bytes
entry count
eviction count
build wait time
singleflight waiters
restore latency
prefill latency before/after cache
如果命中率很高,但 KV restore latency 和 GPU 内存回收时间上升,系统仍可能变慢。
17. 常见错误与反例
17.1 误以为语义相同即可命中
“请解释 Transformer”
“请介绍 Transformer”
即使语义近似,token 序列通常不同,前缀缓存不会因此命中。需要语义缓存的是另一套系统,不能混用正确性假设。
17.2 把动态请求 ID 放在公共前缀中
系统提示词 + request_id + 长文档 + 用户问题
request_id 每次变化会阻断长文档复用。应将其移到不会破坏稳定前缀的位置,或者不要把它放入模型上下文。
17.3 只按 prompt 文本做键
忽略模型版本、模板、适配器和 KV 格式,可能出现两类问题:
- 错误复用:模型读取不兼容的 KV;
- 隐蔽错误:没有崩溃,但输出质量下降。
后一类更危险,因为它很难通过基础可用性监控发现。
17.4 把检索结果直接放进共享缓存
RAG 系统中,检索结果可能带有用户权限。如果把“公共系统提示词 + 当前用户可见文档”作为跨租户公共前缀,后续用户可能错误复用到不应看到的上下文。
更安全的拆分是:
跨用户共享:公开、稳定的系统前缀
租户内共享:租户授权一致的文档前缀
用户级缓存:用户特有检索内容
17.5 把 Prompt Cache 当作答案缓存
Prompt Cache 命中后,模型仍然需要处理新的后缀并生成答案。因此它不能保证:
- 相同输出;
- 不再消耗输出 token;
- 不再受随机采样影响;
- 不再需要模型调用。
如果业务目标是“相同问题直接返回历史答案”,应设计响应缓存并单独处理答案新鲜度和权限。
17.6 只看输入 token 价格,忽略显存
长前缀重复率很高时,缓存可能在账单上节省输入费用,但大量 GPU KV 会降低并发能力,导致:
更多请求排队
decode 速度下降
OOM 增加
模型副本数量上升
成本优化必须同时计算 token 成本、GPU 成本和延迟成本。
18. 生产系统中的边界决策
是否值得建立 Prompt Cache,取决于以下条件是否同时成立:
- 请求之间有稳定且足够长的 token 前缀;
- 这些请求能安全共享相同的授权范围;
- KV 生命周期和模型版本可被可靠管理;
- KV 存储成本没有抵消 prefill 节省;
- 命中后的恢复路径不会成为新的瓶颈;
- miss 时可以透明回退到普通推理。
尤其要测量“前缀长度分布”,而不是只看平均 prompt 长度。一个系统的平均 prompt 可能有 20,000 token,但如果每次请求前 2,000 token 都不同,Prompt Cache 的收益仍然有限。
可以用下面的估算指标:
其中:
- :第 个请求的输入 token 数;
- :第 个请求实际命中的前缀 token 数。
再结合:
只有 EffectiveSaving 为正,并且没有引入权限或数据删除风险时,缓存才是生产优化,而不是把计算成本转移成内存和运维成本。
Prompt Cache 的核心不是“把 prompt 存起来”,而是维护一个满足以下不变量的中间状态系统:
相同且兼容的 token 前缀
→ 可复用的 KV 状态
→ 在正确权限范围内被安全读取
→ 在模型、策略、数据或隐私要求变化时及时失效
→ 在缓存失败时不影响普通推理
其中任何一个条件不成立,缓存都可能从性能优化变成错误来源、数据隔离漏洞或不可控成本。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:LLM 长上下文工程:位置外推、注意力成本、检索与有效上下文
- 下一篇:RAG 文档解析:PDF、Office、HTML、OCR、版面与结构恢复
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论