背景:长上下文服务为什么会卡在 KV Cache
大模型推理通常分为两个阶段:prefill 和 decode。
- prefill:模型一次性处理用户输入、系统提示词、工具说明、RAG 文档、历史对话等上下文,并为每一层 Attention 生成 Key / Value 状态。
- decode:模型逐 token 生成输出,每生成一个 token 都会读取之前保存的 Key / Value 状态。
这批 Key / Value 状态就是 KV Cache。它的价值很直接:已经算过的历史 token 不必在每个生成步重复计算。但它也带来一个工程问题——上下文越长、并发越高、模型层数越多,KV Cache 占用的显存就越大。
在 RAG、Agent、代码助手、长文档问答、多轮客服等场景里,请求经常包含大量重复内容。例如同一份技术文档被多人反复提问,同一个系统提示词被每次请求重复携带,同一个 Agent 工具说明被放进每次上下文。此时,如果每个请求都从头 prefill,同样的 prefix 会被重复计算,GPU 时间和显存带宽都会被浪费。
因此,生产环境的大模型服务不能只关注模型权重量化和 batch size,还要把 KV Cache 复用 当成一等优化对象。
核心问题:不是”缓存越多越好”,而是”缓存是否值得恢复”
KV Cache 复用听起来像普通缓存,但它比 Web 缓存复杂得多。普通缓存通常只需要判断 key 是否命中;KV Cache 还要考虑:
- 前缀是否完全一致:token 序列、tokenizer、模板、系统提示词、工具说明都可能影响缓存 key。
- 恢复成本是否低于重算成本:从 GPU、CPU、远端内存或 SSD 恢复 KV Cache 都有数据搬运成本。
- 缓存是否挤占 GPU 显存:保留太多缓存可能降低当前可用 batch size。
- 命中是否发生在 prefill 瓶颈上:如果请求主要慢在 decode,prefix 命中并不一定改善整体延迟。
- 是否影响正确性:跨模型、跨 LoRA、跨 tokenizer、跨位置编码复用 KV Cache 都可能出错。
一句话:KV Cache 复用不是”把中间结果存起来”这么简单,而是一个横跨 调度、显存管理、I/O、路由、模板规范和线上指标 的系统问题。
核心原理:从 Prefix Caching 到多级 KV Offloading
1. Prefix Caching:复用共享前缀,跳过重复 prefill
Prefix Caching 的基本思路是:如果新请求和历史请求拥有相同前缀,就直接复用已经计算过的 KV Cache,让新请求跳过共享部分的 prefill。
典型适用场景:
- 固定系统提示词 + 不同用户问题
- 同一份长文档 + 多个问题
- 多轮对话中反复携带历史上下文
- RAG 中多个请求命中相同文档片段
- Agent 框架中稳定的工具说明、角色设定和约束说明
vLLM 的 Automatic Prefix Caching 就是这类能力的代表。它会缓存已有请求的 KV Cache,当新请求共享相同 prefix 时,直接复用缓存,跳过共享部分的计算。
但 Prefix Caching 的收益边界也很明确:它主要减少 prefill 时间,不减少生成新 token 的 decode 时间。如果用户请求没有共享前缀,或输出非常长、decode 才是主瓶颈,它的加速效果会被稀释。
2. RadixAttention:用前缀树管理复杂复用模式
真实业务里的共享前缀不总是”完整 system prompt 一样”这么简单。Agent、树搜索、多轮对话、few-shot 示例、自一致性采样会产生更复杂的复用结构。
SGLang 提出的 RadixAttention 选择用 radix tree 管理 token 前缀和对应 KV Cache。这样系统不仅能识别完整前缀命中,还能在复杂调用链中自动做 prefix matching、cache insertion 和 eviction。
它的工程意义在于:前端不需要手写大量缓存逻辑,运行时可以根据 token 前缀自动维护可复用 KV Cache。对于 LLM 程序化调用,这比”每个接口自己拼缓存 key”更可靠。
3. PagedAttention:让 KV Cache 以块为单位管理
KV Cache 的另一个问题是显存碎片。不同请求的上下文长度不同,生成长度也不同,如果提前为每个请求保留一大块连续显存,会浪费大量空间。
PagedAttention 借鉴操作系统分页思想,把 KV Cache 拆成固定大小的块,并用逻辑块到物理块的映射管理缓存。这样请求不需要占用连续物理显存,系统可以按需分配、复用和释放 KV block。
这解决的是 显存布局和碎片管理 问题,不等同于 Prefix Caching,但两者经常配合使用:前者让 KV Cache 更容易管理,后者让共享前缀能被复用。
4. KV Offloading:把缓存放到更大的层级里
当上下文很长、并发很多时,只靠 GPU 显存保存 KV Cache 不够。此时可以把完成的 KV block offload 到 CPU 内存、文件系统、远端内存或 SSD,再在命中时恢复到 GPU。
这类方案的核心取舍如下:
| 存储层级 | 优势 | 劣势 |
|---|---|---|
| GPU 显存 | 延迟最低,恢复最快 | 容量最小、成本最高 |
| CPU 内存 | 容量更大,延迟可控 | 恢复需 GPU-CPU 传输 |
| SSD/文件系统 | 容量极大,成本低 | I/O 延迟高,随机访问影响 TTFT |
| 远端缓存 | 可跨实例复用 | 网络延迟、路由与一致性问题 |
vLLM 的 KV Offloading Usage Guide 已经把 OffloadingConnector 描述为对 prefix cache 的扩展:已完成 KV blocks 可被 offload 到 CPU host memory 和可选二级存储,命中时再提升回 GPU。LMCache 进一步把 KV Cache 作为跨引擎、跨请求共享的中间层,服务于 prefix reuse 和 prefill-decode disaggregation。
工程落地:一套可执行的 KV Cache 复用方案
第一步:先规范 prompt 结构
KV Cache 复用的第一前提是 prefix 稳定。如果上游每次请求都动态拼接时间戳、随机 ID、无序 JSON、临时工具说明,缓存命中率会很差。
建议把 prompt 分成四段:
- 稳定系统段:角色、规范、输出格式、工具说明。
- 稳定知识段:长文档、产品手册、代码库摘要、FAQ。
- 会话历史段:多轮对话中可复用的历史部分。
- 当前请求段:用户本轮问题和临时变量。
只有前三段适合被重点复用,第四段通常不应纳入长期缓存策略。
示例结构:
[stable_system_prompt] [stable_tool_schema] [stable_retrieved_document]
[conversation_history] [current_user_question]
第二步:先开 Prefix Caching,再看命中率
以 vLLM 为例,可以先在实验环境开启 Automatic Prefix Caching:
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
enable_prefix_caching=True,
)
params = SamplingParams(temperature=0.2, max_tokens=512)
prompts = [
shared_prefix + "\n问题:请总结第一章。",
shared_prefix + "\n问题:请列出关键术语。",
]
outputs = llm.generate(prompts, params)
上线前不要只看单次响应时间。更重要的是关注以下指标:
- prefix cache hit rate
- 平均 TTFT
- P95 / P99 TTFT
- prefill tokens per second
- GPU KV cache 使用率
- eviction 次数
- cache hit 后是否真的减少 prefill 时间
第三步:把可复用内容路由到同一组实例
如果请求被负载均衡随机分发,即使命中前缀,也可能落到没有对应缓存的实例上。生产环境需要 cache-aware routing。
简单策略:
- 按租户 ID、文档 ID、知识库 ID、会话 ID 做一致性哈希。
- 同一份长文档的问题尽量路由到同一组推理实例。
- 热点文档可以预热到多个实例。
- 对高复用业务和低复用业务分池,避免互相污染缓存。
这一层通常比模型参数调优更容易被忽略,但对命中率影响很大。
第四步:再评估 Offloading,而不是一开始就上 SSD
Offloading 不是默认收益项。它适合 GPU 显存留不住足够 KV Cache,但重复上下文又足够多的场景。
推荐上线顺序:
- 只开 GPU 内 Prefix Caching,测命中率和 TTFT。
- 加 CPU tier,观察恢复延迟是否低于重算成本。
- 再评估 FS / SSD / 远端 KV Store。
- 对不同业务设置不同
max_offload_tokens,只 offload 高复用前缀。
vLLM 的 offloading 配置示例:
vllm serve <model> \
--kv-transfer-config '{
"kv_connector": "OffloadingConnector",
"kv_role": "kv_both",
"kv_connector_extra_config": {
"block_size": 64,
"cpu_bytes_to_use": 1000000000
}
}'
这里最容易犯的错误是把所有 token 都 offload。更好的做法是只缓存稳定 prefix,例如系统提示词、工具 schema、长文档正文,而不是把当前用户问题和一次性中间推理都放进长期缓存。
适用场景
| 场景 | 复用模式 | 预期收益 |
|---|---|---|
| 长文档问答 | 同一文档 + 不同问题 | prefill 时间显著下降 |
| 多轮对话 | 历史会话复用 | TTFT 降低明显 |
| Agent 与工具调用 | 稳定工具说明与策略 | 减少重复 prefill |
| RAG 热点知识库 | 高频文档片段 | 按文档 ID 路由可获高命中率 |
| Few-shot 与评测 | 固定示例 + 变化问题 | 批处理收益突出 |
长文档问答
同一份 PDF、手册、合同、代码说明文档会被反复提问。此时文档内容是稳定 prefix,用户问题只是尾部变化。Prefix Caching 收益通常明显。
多轮对话
每轮请求都携带完整历史会话时,历史部分会不断增长。如果服务端能识别并复用前几轮历史的 KV Cache,TTFT 可以显著降低。
Agent 与工具调用
Agent 框架经常在每个请求中带上长工具说明、JSON schema、行为约束和系统策略。这些内容稳定且重复,适合缓存。
RAG 热点知识库
如果大量用户查询命中同一批知识片段,按文档 ID 做路由和缓存预热,能减少重复 prefill。
Few-shot 与评测任务
固定示例 + 不同问题的 few-shot 任务天然适合 prefix reuse。评测平台、批处理任务、分类任务都可以受益。
常见误区
| 误区 | 真相 |
|---|---|
| Prefix Caching 能加速所有阶段 | 只节省共享 prefix 的 prefill,长输出 decode 效果有限 |
| 缓存命中率高就一定省钱 | 命中后还要恢复 KV Cache,慢 I/O 可能超过重算成本 |
| 文本一样就能复用 KV Cache | 还需 tokenizer、模板、模型版本、LoRA、dtype、block size 一致 |
| Offloading 越多越好 | 占用 CPU 内存和 I/O,低复用内容反而增加延迟 |
| 只看平均延迟 | 容易改善平均值却恶化 P95/P99,须跟踪尾延迟 |
误区一:Prefix Caching 能加速所有阶段
不能。它主要节省共享 prefix 的 prefill 计算。如果瓶颈在长输出 decode,效果有限。
误区二:缓存命中率高就一定省钱
不一定。命中后还要恢复 KV Cache。如果恢复路径经过远端存储、慢 SSD 或网络,成本可能超过重算。
误区三:只要文本一样就能复用 KV Cache
不够。还要保证 tokenizer、chat template、模型版本、位置编码策略、LoRA adapter、并行配置、dtype 和 block size 等一致。
误区四:Offloading 越多越好
Offloading 会占用 CPU 内存、I/O 带宽和恢复调度资源。对一次性请求、低复用内容、短 prompt,offload 反而可能增加延迟。
误区五:只看平均延迟
KV Cache 相关优化很容易改善平均值,却恶化尾延迟。必须看 P95 / P99 TTFT、I/O queue、eviction storm、GPU stall。
上线检查清单
Prompt 与缓存 key
- 固定系统提示词是否完全稳定
- 工具 schema 是否排序稳定
- RAG 文档是否有稳定 document ID 和 chunk ID
- chat template 是否版本化
- tokenizer、模型版本、LoRA adapter 是否纳入缓存 key
路由与实例池
- 是否按会话、文档或租户做 cache-aware routing
- 热点知识是否有预热策略
- 高复用业务和低复用业务是否分池
- 实例重启后是否接受缓存冷启动
显存与 Offloading
- GPU KV cache watermark 是否可观测
- CPU tier 容量是否大于实际可复用 prefix 规模
- FS / SSD 线程数是否压测过
- offload block size 是否和 GPU block size 兼容
- 是否限制只 offload 前 N 个稳定 token
指标与告警
至少应有这些指标:
prefix_cache_hit_rate
prefix_cache_hit_tokens
prefix_cache_miss_tokens
ttft_avg / ttft_p95 / ttft_p99
kv_cache_gpu_usage_bytes
kv_cache_cpu_usage_bytes
kv_cache_offload_read_latency
kv_cache_offload_write_latency
kv_cache_eviction_count
gpu_stall_due_to_kv_restore
告警重点不是”缓存满了”,而是:缓存满以后是否频繁 eviction,eviction 是否导致命中率下降,命中率下降是否推高 TTFT。
一个实用判断公式
是否值得复用 KV Cache,可以用一个简化判断:
reuse_gain = recompute_prefill_cost - restore_kv_cost - cache_management_cost
当 reuse_gain > 0 且命中频率足够高时,缓存才值得保留。
更工程化一点,可以按业务维度统计:
worth_cache = shared_prefix_tokens × reuse_count × prefill_cost_per_token
> restore_bytes / effective_bandwidth + eviction_penalty
这不是精确模型,但能帮助团队避免”只要能缓存就缓存”的误区。
FAQ
Prefix Caching 和 PagedAttention 是一回事吗?
不是。Prefix Caching 解决重复 prefix 的计算复用问题;PagedAttention 解决 KV Cache 的块级显存管理和碎片问题。实际服务框架可能同时使用两者。
RAG 场景应该缓存用户问题吗?
通常不应该。用户问题变化大、复用率低,更适合缓存稳定文档片段、系统提示词、工具说明和固定 few-shot 示例。
KV Offloading 适合小模型吗?
不一定。小模型、短上下文、低复用请求中,重算成本可能低于恢复成本。Offloading 更适合长上下文、高复用、GPU 显存不足的场景。
如何判断缓存策略是否生效?
看四个指标:命中 token 数、TTFT 是否下降、P99 是否稳定、GPU 显存是否支持更高 batch。只看 hit rate 不够。
总结
KV Cache 复用的关键不是”多存一点缓存”,而是把 稳定 prefix、实例路由、显存管理、I/O 恢复和业务命中率 放在一起设计。
一套稳妥的落地顺序:
- 先规范 prompt,减少无意义的 prefix 波动。
- 开启 Prefix Caching,验证长文档、多轮对话、Agent 工具说明等高复用场景。
- 加 cache-aware routing,让相同文档或会话尽量落到同一组实例。
- 在 GPU cache 不够时再引入 CPU / FS / SSD Offloading。
- 用 TTFT、P99、命中 token 数、I/O 恢复延迟和 eviction 行为判断是否真的省钱。
对于长上下文大模型服务,KV Cache 已经从”推理框架内部细节”变成了”系统架构层面的成本控制点”。谁能把可复用上下文稳定地识别、路由、保存和恢复,谁就能在同样 GPU 预算下承载更多请求。
主要参考资料
- vLLM Automatic Prefix Caching: https://docs.vllm.ai/en/latest/features/automatic_prefix_caching/
- vLLM KV Offloading Usage Guide: https://docs.vllm.ai/en/latest/features/kv_offloading_usage/
- LMSYS: Fast and Expressive LLM Inference with RadixAttention and SGLang: https://www.lmsys.org/blog/2024-01-17-sglang/
- PagedAttention paper: https://arxiv.org/abs/2309.06180
- LMCache paper: https://arxiv.org/abs/2510.09665
- KV Cache Offloading for Context-Intensive Tasks: https://arxiv.org/abs/2604.08426
- Tutti: Making SSD-Backed KV Cache Practical for Long-Context LLM Serving: https://arxiv.org/abs/2605.03375
- Comparative Characterization of KV Cache Management Strategies for LLM Inference: https://arxiv.org/abs/2604.05012