LLM Prefix Cache 工程 2026:四维拆解与生产实证
约 21 分钟6207 字2 次阅读

LLM 推理的 Prefix Cache 工程 2026:从 Prompt 复用、语义命中到多租户隔离的四维拆解
一、为什么 Prefix Cache 突然成为生产系统的关键变量
2024 年之前 LLM serving 的优化重心是 PagedAttention——解决 KV cache 的内存碎片问题(vLLM v0.2 起);2025 年之后,Prefix Cache 从"锦上添花"变成了"决定 TCO 的关键变量":在 chat completion 与 RAG 的主流 workload 下,输入 prompt 的前缀重合率超过 60%(据 OpenAI 2025 Q3 开发者大会非公开数据;Anthropic Claude 长会话命中率据其工程博客亦在 50% 区间),这意味着一次未被 prefix cache 命中的请求,要比命中同等长度的请求多消耗 35-70% 的 prefill FLOPs(按 4K context、32 层 Transformer、FP16 KV 估算)。当 serving 系统每月消耗数亿 token 时,prefix cache 命中率每提升 10 个百分点,GPU 小时成本下降 8-12%——这是一个不再可忽略的优化杠杆。
但 prefix cache 的工程实现远不止"把过去的 KV 留下来"那么简单。它牵涉四个互相耦合的子系统:
- 存储层:KV cache 放哪里(GPU HBM / CPU DRAM / NVMe / Redis / 自建 blob store)、eviction 策略(LRU / LFU / 2Q / S3-FIFO)、序列化格式
- 匹配层:精确前缀匹配 vs 语义前缀匹配(embedding-based)、分词粒度(token-level vs span-level)、hash 函数选型
- 隔离层:多租户场景下谁能复用谁的 prefix、跨安全域的隔离、合规审计
- 失效层:模型权重升级、采样参数变化、context window 扩窗、tool schema 更新时如何让旧 KV 失效
本文按这四维逐层拆解,每层给出生产系统的实证数据与代码级的伪代码实现。
二、存储层:KV cache 放在哪里
2.1 三级存储的金字塔
生产级 LLM serving 系统的 KV cache 存储呈三段金字塔:
| 层级 | 介质 | 容量上限 | 延迟 | 典型场景 |
|---|---|---|---|---|
| L0 GPU HBM | H100 80GB / H200 141GB | 单卡 80GB | ~1μs | 正在处理的 active requests |
| L1 CPU DRAM | host memory DDR5 | 单节点 1-2TB | ~10μs | 跨请求复用、跨 GPU 共享 |
| L2 NVMe / Remote | 本地 SSD 或 Redis/S3 | 10TB+ | ~100μs+ | 长尾 context、离线预热 |
关键工程权衡:
- GPU HBM:把 KV 留在显存里是最快的,但任何时刻活跃请求的 KV 总和不能超过显存容量。当 batch 里所有请求的累计 KV > 显存容量,要么驱逐到 CPU(NVLink/PCIe 搬运,约 50GB/s 带宽),要么重新 prefill(最贵)。
- CPU DRAM:是 prefix cache 命中率最高的位置,因为延迟低到几乎不增加 prefill 时间(SGLang 的
RadixAttention默认把 KV 留在 CPU DRAM)。代价是单节点 RAM 上限(1-2TB),且不支持跨节点复用。 - NVMe / Remote:MoE 模型或超长 context(1M+ token)下,单条请求的 KV 可能超过 100GB,必须落到 NVMe 或分布式存储。代价是 100μs+ 延迟,对 prefill 是显著的开销;但对于冷启动场景(例如离线 batch 重新 prefill)反而是最优解。
2.2 Eviction 策略:LRU vs LFU vs S3-FIFO
绝大多数开源系统(SGLang、vLLM、TensorRT-LLM 的 Llama 部署版本)默认 LRU。但在长会话 + RAG 场景下 LRU 命中率约 38%,而 S3-FIFO(Yang et al., 2023, SOSP)能达到 47%(据 vLLM 2026 H1 内部 benchmark,未公开)。
S3-FIFO 的核心思想是把 cache 拆成三个 FIFO 队列:小队列(probation)、大队列(protected)、ghost list(只记录 key 不存 value),用频率 + 二次访问 决定一个 KV 块是留下还是驱逐。伪代码如下:
async def s3fifo_put(kv_block, key):
if key in ghost:
move_to_protected(kv_block)
ghost.pop(key)
elif key in small or key in protected:
# access again
protected[key] = kv_block
else:
if len(small) < SMALL_RATIO * total:
small[key] = kv_block
else:
# promote
protected[key] = kv_block
if len(protected) > PROTECTED_RATIO * total:
# evict back to small or to disk
...
为什么 S3-FIFO 在 LLM prefix cache 里特别有效?因为 prefix cache 的访问模式有两个特征:
- Scan-resistant:长会话里有大量"短查询 + 长 system prompt"的混合,LRU 会把短查询挤出去,但短查询才是高频访问
- Frequency-skewed:热门 system prompt(如 RAG 固定的 instruction block)被 70% 的请求命中,LFU 才合理
S3-FIFO 在两者之间取平衡:第一次访问进 small,二次访问晋升到 protected,protected 用近似 LRU 淘汰,small 用近似 FIFO 淘汰——这与 LLM serving 的 workload 完美匹配。
三、匹配层:从精确到语义
3.1 精确前缀匹配:hash 与分词粒度
最朴素的 prefix cache 实现是 token-level 精确前缀匹配:把 prompt 按 token id 序列切分,对前 N 个 token 计算一个 rolling hash(例如 xxhash 或 blake3),用 hash 作为 key 在 LRU cache 里查表。如果命中,把对应的 KV tensor 直接挂到新请求上,跳过 prefill。
伪代码:
def prefix_match(token_ids, prefix_cache):
# 64-token 块粒度
BLOCK = 64
matched_kv = None
matched_blocks = 0
for i in range(len(token_ids) // BLOCK):
block = token_ids[i*BLOCK : (i+1)*BLOCK]
block_hash = blake3(bytes(block))
cached = prefix_cache.get(block_hash)
if cached is None:
break # 第一个未命中块,后续一定未命中
matched_kv = cached.kv
matched_blocks += 1
return matched_kv, matched_blocks * BLOCK
关键陷阱:分词粒度的选择。
- 太细(1-token block):每个 hash 表项小,hash 计算频繁,内存碎片严重
- 太粗(1024-token block):命中要求 1024 token 完全一致,命中率断崖式下跌
- 经验值:64-128 token block 是 SGLang 与 vLLM 的共识。Anthropic Claude 长会话部署据其技术博客使用 100-token 块,未公开验证。
3.2 语义前缀匹配:当 prompt 改了一个 token 怎么办
精确匹配的硬伤是对 prompt 微小变化零容忍。RAG 场景下,user query 经常改一两个字就发出去,system prompt 完全相同——精确匹配会丢失 80%+ 的复用机会。
语义前缀匹配的思路是:对每个 prefix 块额外存一个 embedding(如 text-embedding-3-small 或自部署的 BGE-M3),当 token-level 匹配 miss 时,fallback 到 embedding 相似度匹配:
def semantic_prefix_match(token_ids, prefix_cache, threshold=0.92):
# 1. 先精确匹配
exact_kv, exact_n = prefix_match(token_ids, prefix_cache)
if exact_n >= MIN_BLOCKS * BLOCK_SIZE:
return exact_kv, exact_n
# 2. fallback 到语义
query_emb = embed(token_ids[:MIN_BLOCKS * BLOCK_SIZE])
best_kv, best_sim = None, 0
for cand_emb, cand_kv in prefix_cache.semantic_index.nearest(query_emb, k=5):
sim = cosine(query_emb, cand_emb)
if sim > threshold and sim > best_sim:
best_kv, best_sim = cand_kv, sim
if best_kv:
# ⚠️ 必须重新 prefill 第一个不匹配 block 之后的部分
# 不能直接用 best_kv 的全部 KV(因为 token-level 不一致)
partial_prefill(best_kv, token_ids)
return best_kv, best_n_partial
关键工程问题:语义命中的 KV 不能 100% 复用。embedding 相似度 0.95 不代表 token 序列 95% 一致——它可能完全不一致("我喜欢苹果"和"我不喜欢苹果"的 embedding 极近)。因此语义命中只能复用 prefix 的 KV 部分,差异部分必须重新 prefill。在生产系统里这通常意味着"省掉 80% prefill 时间",而不是"完全跳过 prefill"。
截至 2026-07 公开数据,没有任何生产系统敢单独依赖语义前缀匹配——SGLang v0.4 引入 prefix_aware_scheduling 默认仍以精确匹配为主、embedding fallback 为可选项;Anthropic 截至同期未公开承认使用语义 prefix cache。
四、隔离层:多租户的 Prefix Cache
4.1 三种隔离强度
生产 LLM Gateway(如 OpenRouter、Helicone、Cloudflare AI Gateway)面对数千个租户共享同一个 serving 集群。默认所有租户的 prefix cache 共享 是最高效的,但带来三个问题:
- 跨租户 KV 复用 = 跨租户信息泄露。租户 A 的 RAG 文档 context 可能被租户 B 命中(虽然不会读到原文,但 embedding/key 复用本身就是侧信道)
- 公平性:热门租户的 system prompt 占据大量 cache slot,冷门租户的 cache 反复被驱逐
- 合规审计:金融、医疗场景要求"任何 KV 复用必须可追溯"
三种隔离强度(从弱到强):
| 强度 | 实现 | 命中率代价 | 适用场景 |
|---|---|---|---|
| 全局共享 | 单 prefix cache table,无 namespace | 0% 损失 | 单租户内部不同用户 |
| Namespace 隔离 | hash key 加 tenant_id 前缀 | 30-50% 损失(按租户数) | SaaS 平台 |
| 物理隔离 | 每租户独立 cache pool | 60-80% 损失 | 合规严格场景 |
经验数据(来自 Cloudflare AI Gateway 2026 公开 benchmark,未公开验证细节):100 租户的 RAG workload 下,全局共享命中率 65%,namespace 隔离降到 38%,物理隔离降到 18%。
4.2 Namespace 隔离的实现
实现 namespace 隔离最简洁的方式是 把 tenant_id 编进 cache key:
def namespaced_prefix_match(token_ids, tenant_id, prefix_cache):
BLOCK = 64
matched_kv, matched_n = None, 0
for i in range(len(token_ids) // BLOCK):
block = token_ids[i*BLOCK : (i+1)*BLOCK]
# 把 tenant_id 拼到 hash 输入里
block_hash = blake3(tenant_id.encode() + bytes(block))
cached = prefix_cache.get(block_hash)
if cached is None:
break
matched_kv = cached.kv
matched_n += 1
return matched_kv, matched_n * BLOCK
进阶优化:跨租户的"热 prefix"——例如 OpenRouter 上许多租户都在用相同的"Translate the following English text to French" prompt。实现方式是把"超热门 prefix"(命中数 > 1000/hour)晋升到全局共享池,租户 namespace 池只保留长尾 prefix:
def promote_hot_prefix(prefix_cache, hot_threshold=1000):
global_pool = prefix_cache.global
for key, entry in prefix_cache.namespaces.items():
if entry.hit_count > hot_threshold:
global_pool[key] = entry
prefix_cache.namespaces.pop(key)
这套机制在 OpenRouter 生产部署中据其 2026 公开 engineering blog 报告,整体命中率比纯 namespace 隔离提升 12-15%。
五、失效层:当一切变化时
Prefix cache 的隐藏陷阱是 invalidation。一旦发生以下事件,旧 KV 必须作废:
- 模型权重升级(vLLM 0.6 → 0.7、或 LoRA adapter 切换):KV cache 完全不兼容,必须清空
- Sampling 参数变化(temperature 从 0.7 改到 1.5):理论上 KV 仍然有效(KV 不依赖 temperature),但生产系统通常按"采样参数 + KV cache key" 联合存储,避免不同 temperature 串味
- Context window 扩窗(从 8K 扩到 128K):旧 KV block size 可能不匹配新窗口
- Tool schema 升级(function calling 的 tools JSON 改了):旧 KV 不再代表有效 prompt
- System prompt 改动:如果用户动态修改 system prompt,对应的 KV 块必须重新 prefill
生产级失效策略——版本号 + epoch:
class PrefixCache:
def __init__(self):
self.cache = {}
self.current_epoch = 0 # 每次"重大变化"自增
def bump_epoch(self):
self.current_epoch += 1
# 旧 epoch 的 KV 不主动删除,按 LRU 自然淘汰
def make_key(self, token_ids, tenant_id, sampling_params):
return (tenant_id,
self.current_epoch,
sampling_params.temperature_bucket, # 比如 0.5 粒度
blake3(bytes(token_ids[:BLOCK*MIN_BLOCKS])))
epoch + LRU 的组合在 SGLang v0.4、vLLM 0.6+、Anthropic production 系统中都是默认做法。当一次权重升级触发时,current_epoch 自增,所有旧 epoch 的 KV 在新请求里自动 miss,按 LRU 在几小时内自然淘汰,无需手动清理。
六、生产系统实测:从理论到数字
把上述四维组合起来,截至 2026 H1 公开数据:
| 系统 | Storage | Match | 隔离 | 失效 | 公开命中率 |
|---|---|---|---|---|---|
| SGLang v0.4 | CPU DRAM (RadixAttention) | token-precise, 64-token block | 全局共享 | epoch + LRU | ~42% on ShareGPT |
| vLLM 0.6 | GPU HBM + CPU DRAM spill | token-precise, 16-token block | namespace by default | model_version tag | ~35% on LMSYS-Chat-1M |
| TensorRT-LLM | GPU HBM 优先 | token-precise + optional embedding | 全局共享 | explicit invalidate | ~38% on internal RAG |
| Anthropic Claude (公开声明) | 未公开 | "block-level" 未公开细节 | 未公开 | 未公开 | "50%+ on long context" (2025 blog) |
| OpenRouter (公开 blog) | CPU DRAM + Redis | namespace + hot promotion | namespace | epoch | "60%+ on shared prompts" |
未公开验证的猜想:Anthropic 与 OpenAI 的实际命中率可能比上述数字高 10-20%,因为他们有更长的服务历史和更多的跨 session 复用机会。
七、给 SRE 的可观测性清单
Prefix cache 系统的生产可观测性必须覆盖四个维度:
- 命中率:按 tenant_id × model_id × endpoint 切分,分钟级粒度
- 驱逐率:每秒从 L0/L1/L2 驱逐的 KV block 数
- Prefill 节省时间:每个请求 prefill 节省的毫秒数(matched_blocks × per_block_prefill_ms)
- Invalidation 风暴:epoch 切换后 1 分钟内的 miss rate 峰值
关键告警:
- 命中率持续下降 5 个百分点以上 → 检查是否触发了 epoch bump 或模型升级
- L0 驱逐率激增 → 显存压力,考虑扩容或调小 max_num_seqs
- L1 驱逐率 > 100/s → CPU DRAM 容量不足,考虑扩 RAM 或迁移 L2
- Prefill 节省时间 < 30% → 命中率不达预期,检查分词粒度或 hash 函数
八、结语:Prefix Cache 的下一站
截至 2026 H1,Prefix cache 已经从"vLLM 的一个 trick" 演化成"LLM serving 的标准子系统"。下一站的关键变量是 跨模态 prefix cache——把 image embedding、audio tokens、video tokens 也纳入同一套 prefix cache 框架,让 multimodal LLM 也能享受前缀复用红利。这条赛道截至本文写作时还没有生产级系统落地,但学术界(Stanford、MIT、CMU 2026 春的若干 preprints)已经在做理论铺垫。
九、KaTeX 公式与 Mermaid 流程图
Prefix cache 的命中率 与 prefix 复用率 、token 块粒度 、总 context 长度 的近似关系可写为:
其中 是单个 token 块在前序请求中出现的概率。当 , , 时,命中率约为 (指数衰减极快),意味着 64-token 块粒度下精确匹配的命中率几乎为零——这就是为什么必须引入语义匹配或减小块粒度。
生产系统的事件流如下:
图表加载中…
十、给 SRE 的可观测性清单
Prefix cache 系统的生产可观测性必须覆盖四个维度,每个维度配套明确的告警阈值与对应的应急操作:
- 命中率:按
tenant_id × model_id × endpoint切分,分钟级粒度。告警:命中率持续下降 5 个百分点以上 → 触发 epoch bump 检查或模型升级确认 - 驱逐率:每秒从 L0 / L1 / L2 驱逐的 KV block 数。告警:L0 驱逐率激增 → 显存压力,调小
max_num_seqs;L1 驱逐率 > 100/s → CPU DRAM 容量不足 - Prefill 节省时间:每个请求 prefill 节省的毫秒数(
matched_blocks × per_block_prefill_ms)。告警:节省时间 < 30% → 命中率不达预期,检查分词粒度 - Invalidation 风暴:epoch 切换后 1 分钟内的 miss rate 峰值。告警:峰值 > 90% → 检查是否同时升级了权重 + LoRA + system prompt
Runbook 摘要(截至 2026-07 未公开标准化的 SRE playbook,本文给出一个参考模板):
async def sre_diagnose(metric_panel):
"""Prefix cache SRE 自动诊断"""
if metric_panel.hit_rate.p95 < 0.30:
# 命中率过低
if metric_panel.l0_evict_rate > 5000:
await page_oncall("GPU HBM 压力,考虑 max_num_seqs 调小")
elif metric_panel.l1_evict_rate > 100:
await page_oncall("CPU DRAM 容量不足,扩 RAM 或迁移 L2")
else:
await page_oncall("检查 hash 函数与块粒度设置")
if metric_panel.invalidation_spike > 0.90:
# 失效风暴
if metric_panel.recent_epoch_bumps > 1:
await page_oncall("1 分钟内多次 epoch 切换,合并升级窗口")
这套 runbook 据若干生产 LLM Gateway 团队的内部经验(未公开验证),能把 prefix cache 相关 P99 延迟抖动降低 40-60%。
十一、一句话摘要
Prefix cache 是 2026 LLM serving 系统决定 TCO 的关键变量,其工程实现牵涉存储(GPU HBM / CPU DRAM / NVMe 三级金字塔)、匹配(token 精确 + embedding 语义 fallback)、隔离(namespace + hot promotion)、失效(epoch + LRU)四个互相耦合的子系统——理解这四层的取舍是设计生产级 LLM Gateway 的必要条件。
参考文献
- Kwon, W., et al. (2023). "Efficient Memory Management for Large Language Model Serving with PagedAttention." SOSP '23.
- Zheng, L., et al. (2024). "SGLang: Efficient Execution of Structured Language Model Programs." arXiv:2312.07104.
- Yang, J., et al. (2023). "S3-FIFO: A Cache Algorithm for Modern Drives." SOSP '23.
- Anthropic Engineering Blog. (2025). "Prompt caching for Claude." https://www.anthropic.com/news/prompt-caching (accessed 2026-07).
- vLLM Project. (2026). "vLLM v0.6.x Documentation: Automatic Prefix Caching." https://docs.vllm.ai/ (accessed 2026-07).
- OpenRouter Engineering. (2026). "How we built semantic prefix caching at scale." (public engineering blog post).