解码 Prompt Caching:从 PagedAttention 原理到降本实践
本文基于 How Prompt Caching Works 整理,在原文基础上补充了底层机制的解释。
Prompt Caching 是 LLM 推理服务的一项优化技术:对于每次请求中重复出现的前缀内容(如系统提示词、工具定义),服务端直接复用之前计算好的中间结果,而不是重新计算。对于大量共享相同系统提示词的应用,这项技术可以显著降低延迟和成本。
为什么需要缓存?
LLM 的推理分为两个阶段:
- 预填充阶段(Prefill):对输入的所有 token(模型处理文本的最小单元,大致相当于一个词或几个字)并行做 Attention 矩阵运算,生成各层的 KV Cache(Key-Value Cache,Attention 计算产生的中间向量,后续生成每一步都需要读取)。由于要同时处理所有输入 token,这个阶段是计算密集型的。
- 解码阶段(Decode):每步只生成一个 token,但需要把整个 KV Cache 从显存中读出来做 Attention。计算量小但内存读取频繁,这个阶段是内存带宽密集型的。
如果没有缓存,每次请求进来,即便前面 90% 的 Prompt 都是一样的,模型都要重新算一遍 Prefill,延迟和成本都会显著增加。命中缓存后,可以跳过该前缀的 Prefill 计算,直接复用已有的 KV Cache。以 Anthropic Claude 为例,缓存命中的 token 价格约为原价的 10%,且首字生成速度显著提升。
技术揭秘:PagedAttention
要实现 Prompt Caching,首先要解决 KV Cache 的存储管理问题。传统方式必须为每个请求预先分配其可能用到的最大连续显存,大量显存因此被提前锁定,即使实际用不到也不能分给其他请求,碎片化浪费严重。
vLLM 引入了操作系统管理内存的思路 —— 分页(PagedAttention):不再分配连续大内存,而是将 KV Cache 切分成固定大小的“块”(每块通常包含 16 个 token 的 KV 向量),这些块在物理显存里可以是分散的、不连续的。这解决了显存管理效率问题,也为缓存复用打下了基础。
Prompt Caching(也叫 prefix caching)是在 PagedAttention 块管理之上叠加的一层机制:通过对块内容做哈希,识别出哪些块之前已经计算过,直接复用。
哈希链(Block Hashing)——缓存生效的关键
如何判断“这段话以前算过”?答案是块级哈希。
- 父块依赖:一个块的哈希值,不仅取决于它自身包含的 token ID 序列,还取决于前一个块的哈希值。
- 连锁效应:就像 Git 的 commit 链——每个 commit 的哈希都依赖父 commit,改了中间任何一个,后面的哈希全部失效。这里也一样:只有“从头开始的所有内容”都完全一致时,当前的哈希值才会匹配。所以无法只复用中间的一段,必须是前缀完全匹配。
新请求进来时,系统计算其提示词的块哈希,去全局哈希表中查找。如果命中,直接指向已有的显存块,跳过该前缀的 Prefill 计算;如果未命中,正常计算并将结果存入哈希表。
一个常见误区:缓存是全局的,不是私有的
理解了哈希链机制,一个常见误解就不攻自破了。
很多开发者以为 Prompt Caching 是基于“用户会话”的——只有同一个用户在同一个对话框里的后续发言,才能利用之前的缓存。但实际上,缓存是基于内容的,与用户无关。只要系统提示词或工具定义的文本完全一致,用户 A 产生的缓存完全可以被用户 B 复用。在高并发场景下,只要前缀一致,整个服务可以实现“全局复用”,极大降低重复计算。
不过,“全局复用”是服务端(如自部署 vLLM)的内部优化。如果使用云端 API,各厂商的缓存策略不尽相同——有的有 TTL 限制,有的对最小缓存 token 数有要求,有的需要显式标记缓存断点。开发者能控制的是前缀结构,而非缓存本身的行为。
给开发者的实操建议:如何最大化缓存命中率?
保持前缀稳定(Stable Prefix)
将所有静态内容(系统提示词、工具定义、示例文本)放在最前面。
反例:如果把“当前时间”或“用户名”放在提示词的开头,第一个块的哈希就已不同,后续所有块的缓存均无法复用。
确定性序列化(Deterministic Serialization)
在使用 JSON 格式传递数据时(例如工具调用),必须保证键的顺序固定。在 Python 中使用 json.dumps(..., sort_keys=True);其他语言同理,关键是保证序列化输出的字节级一致性。因为 { "a": 1, "b": 2 } 和 { "b": 2, "a": 1 } 虽然语义相同,但生成的字符串不同,会导致缓存未命中。
仅追加模式(Append-only)
在维护多轮对话历史时,只在末尾添加新内容。不要修改或截断中间的历史记录——一旦中间变了,后面的缓存链就全失效了。
警惕工具定义的变化
工具定义通常由推理框架拼接在系统提示词附近。如果动态地为不同用户开启/关闭不同的工具,前缀就会发生变化,从而导致缓存失效。应对策略:将工具定义排序后固定拼接,或为不同工具集维护独立的前缀段。
总结
Prompt Caching 的本质是“复用计算结果”。把所有不变的东西(系统指令、背景文档、工具列表)永远放在最前面,把变化的东西(用户提问、动态变量)放在最后面。