多个请求如何复用相同 Prompt 前缀?
简化版
多个请求共享完全相同的 Token 前缀时,可以只对该前缀做一次 Prefill,把生成的 KV Cache 块缓存起来;后续请求引用这些只读块,只计算各自不同的后缀。这通常称为 Prefix Caching 或 KV Cache Sharing。
关键不是“文本看起来一样”,而是 Token 序列及影响 KV 的执行条件一致。缓存键要绑定模型权重、Tokenizer、聊天模板、位置编码和适配器版本,并做租户权限隔离。它主要降低重复 Prefill 和 TTFT,不会减少每个请求后续 Decode 的成本。
详细版
典型场景包括统一系统提示、公共 Few-shot、相同长文档上的多次问答和 Beam Search 分支。实现上通常将前缀切成固定 Token 块,对块内容做哈希,命中后通过逻辑到物理块表共享;分叉位置之后使用 Copy-on-Write,避免一个请求修改另一个请求的缓存。
shared: [system][policy][document]
/--> [question A][decode A]
physical KV blocks -----+
\--> [question B][decode B]
收益取决于公共前缀长度、复用次数和命中率,同时受缓存占用与哈希查找成本影响。动态时间戳、请求 ID 或不同模板会使前缀过早分叉,应将动态字段后移。上线至少监控命中 Token 比例、节省 Prefill Token、TTFT、占用显存、淘汰率和跨租户访问审计。
完整版教学
1. 为什么前缀可以复用
因果注意力保证某个位置的隐藏状态只依赖它之前的 Token。若两个请求的前 k 个 Token 完全相同,且模型执行条件一致,那么这 k 个位置在每层产生的 K/V 也相同。
因此可以缓存一次计算结果:
KV(prefix + suffix) = KV(prefix) + compute_KV(suffix | prefix)
这里的加号表示逻辑拼接,不是数值相加。后缀仍需基于共享前缀继续计算。
2. 哪些场景收益最大
系统提示只有几十 Token、每分钟请求很少时,缓存管理可能得不偿失。长且稳定、重复频繁的前缀更有价值,例如数千 Token 的政策文档、多轮会话历史或固定 Few-shot 示例。
收益粗略与 公共前缀 Token × 重用次数 成正比。还应看流量是否集中在缓存存活期内,否则条目刚写入就过期,只有占用没有复用。
3. “相同”为什么必须到 Token 级
空格、换行、Unicode 规范化和聊天模板差异都可能改变 Token 序列。两个肉眼相似的 Prompt 若 Token ID 不同,不能直接共享对应 KV。
缓存键应基于模板展开和 Tokenize 之后的序列,而不是原始字符串。预处理版本也要纳入键,避免模板升级后命中旧缓存。
4. 哪些执行条件必须一致
同一 Token 前缀在不同模型权重下会生成不同 KV。LoRA Adapter、RoPE 配置、位置偏移和部分模型并行实现也可能改变结果。
| 缓存键维度 | 不一致的风险 |
|---|---|
| 模型权重版本 | KV 数值完全不同 |
| Tokenizer/模板 | Token 序列不同 |
| Adapter 版本 | 层输出不同 |
| 位置编码配置 | 相同 Token 在不同位置含义不同 |
| KV 数据类型 | 布局或精度不兼容 |
采样温度不影响已给定前缀的 KV,通常无需进入键;但实现相关配置必须根据引擎验证。
5. 块级哈希如何工作
将 Token 前缀切成固定大小块,每个块的哈希包含父块哈希和当前 Token。只有从根开始连续命中的块才可复用,不能跳过中间差异。
h0 = hash(model_version)
h1 = hash(h0, tokens[0:16])
h2 = hash(h1, tokens[16:32])
h3 = hash(h2, tokens[32:48])
父哈希使相同局部 Token 出现在不同前缀路径时不会错误共享。哈希碰撞仍需用足够强的摘要或二次校验控制。
6. Copy-on-Write 为什么必要
共享前缀块应视为只读。两个请求在前缀末尾分叉后,各自分配新块;若最后一个共享块尚未填满,需要复制后再追加,不能原地写入。
引用计数记录物理块被多少序列使用。请求结束时只减少引用,计数归零才可回收到空闲池,从而避免提前释放其他请求仍使用的缓存。
7. Radix Tree 与块哈希的区别
Radix Tree 按 Token 前缀组织请求,天然支持最长前缀匹配;块哈希更容易与分页 KV 结合并做分布式索引。二者都需要处理插入、引用和淘汰。
选择取决于引擎数据结构与流量模式,不应只背名称。关键能力是找到最长安全公共前缀,并让物理 KV 块只存一份。
8. 如何提高实际命中率
把稳定内容放在 Prompt 前部,把用户 ID、时间戳、随机数等动态字段后移;固定 System Prompt 和工具 Schema 的序列化顺序;避免每次请求插入无意义的空白变化。
但不能为了命中率把权限上下文删除。若租户或用户权限会改变模型可见信息,应保留在缓存边界或使用独立命名空间,安全优先于复用率。
9. 会话缓存与公共前缀缓存
同一会话多轮请求可复用上一轮历史 KV,命中高但生命周期与会话绑定。公共模板或文档缓存跨请求复用范围更大,同时需要更严格的权限、版本和淘汰策略。
| 类型 | 复用范围 | 主要风险 |
|---|---|---|
| 会话 KV | 单会话后续轮次 | 会话过期、状态一致性 |
| 公共模板 | 多用户 | 模板版本失效 |
| 私有文档 | 同权限主体 | 跨租户数据泄漏 |
不同类型最好使用独立命名空间和容量配额。
10. 多租户隔离如何保证
即使共享内容相同,缓存元数据、命中时序或错误配置也可能形成侧信道。对私有前缀应按租户、权限域或加密上下文隔离,不能仅凭内容哈希跨租户复用。
权限撤销、文档删除或租户下线时要主动失效相应条目,并保留审计记录。敏感场景可只共享公开系统模板,不共享用户数据。
11. 缓存淘汰怎样设计
缓存空间有限,可综合最近访问、访问频率、占用块数和节省的 Prefill 时间。大而极少复用的前缀可能不如多个短而高频的条目有价值。
benefit_per_byte = reuse_probability × saved_prefill_ms / kv_bytes
模型或模板发布新版本时应批量失效旧命名空间,而不是等待 LRU 自然淘汰,以避免错误命中并及时回收容量。
12. 如何估算收益
设公共前缀为 P Token,每秒命中 R 次,完整 Prefill 吞吐为 T Token/s,理想节省计算时间约为 P × R / T 个 GPU 秒每秒。实际还要扣除查找、块映射和缓存写入成本。
更直接的线上指标是“避免计算的 Prefill Token 数”和命中请求的 TTFT 降幅。请求命中率可能很高,但若每次只命中很短前缀,价值仍有限。
13. 如何测试与上线
测试应覆盖完全命中、部分块命中、首 Token 即不同、模型升级、Adapter 切换、权限变化、并发分叉和哈希碰撞模拟。命中与未命中的输出在确定性配置下应一致。
灰度记录缓存命中 Token 比例、TTFT P95/P99、额外显存、淘汰率、错误命中和租户审计。发现版本或隔离问题时必须能一键关闭缓存并清空对应命名空间。
前缀复用的红线不是命中率,而是只有在计算语义和权限域都一致时才允许共享。
14. 常见误区与追问
- 误区:原始 Prompt 字符串相同就能复用。 应比较模板展开后的 Token 和执行配置。
- 误区:前缀缓存能降低 Decode 成本。 它主要跳过重复 Prefill,后续生成仍需 Decode。
- 误区:哈希相同即可跨租户共享。 权限命名空间必须先满足隔离要求。
- 误区:动态字段放哪里都一样。 放在前部会让后续所有块无法命中。
- 误区:缓存越大越好。 它会挤占活跃请求 KV,需要按收益淘汰。
- 追问:为什么只能复用连续前缀? 后续位置的 K/V 已依赖之前全部 Token,中间差异会传播。
- 追问:如何保证分叉后互不影响? 共享块只读,尾块使用 Copy-on-Write,并维护引用计数。
15. 加强记忆
- 先记前提:相同 Token 前缀产生相同 KV。
- 再记键:模型、Tokenizer、模板、Adapter、位置配置。
- 再记结构:块哈希或前缀树寻找最长连续命中。
- 再记写入:共享块只读,分叉使用 Copy-on-Write。
- 再记布局:稳定内容前置,动态字段后移。
- 再记红线:租户与权限域隔离,版本变化主动失效。
- 最后记指标:命中 Token、节省 Prefill、TTFT、显存和淘汰率。