为什么 KV Cache 值得单独拿出来讲
做大模型推理优化时,很多团队第一反应是换更小的模型、开权重量化、上更好的 GPU。这个方向没错,但服务真正跑起来以后,经常会遇到另一个更扎实的问题:显存不是只被模型权重占着,KV Cache 也在持续吃显存。
尤其是长上下文、多轮对话、RAG、并发请求比较多的时候,KV Cache 很容易从一个“加速技巧”变成一个“资源瓶颈”。你会发现模型算力还没完全打满,batch 已经上不去了;请求还没明显变慢,TTFT 已经开始抖;上下文长度一拉长,显存余量马上变得很紧。
这篇文章不按论文摘要的方式写,而是站在工程落地的角度,把 KV Cache 拆成几个具体问题:它到底缓存了什么,为什么会卡显存,PagedAttention 解决了什么,Prefix Caching 什么时候有效,以及量化、淘汰和监控应该怎么做。
KV Cache 到底缓存什么
自回归大模型生成文本时,是一个 token 一个 token 往后生成的。每生成一个新 token,模型都要“看见”前面已经出现过的 token。Transformer 的注意力层会计算 Query / Key / Value,其中历史 token 的 Key / Value 在后续步骤里会被反复用到。
如果每一步都重新计算所有历史 token 的 Key / Value,推理会非常浪费。KV Cache 的做法很直接:过去算过的 Key / Value 存起来,下次生成新 token 时直接拿来用。
可以粗略理解为:
| 模式 | 行为 |
|---|---|
| 没有 KV Cache | 每一步都重新处理历史 token |
| 有 KV Cache | 历史 token 的 K/V 复用,只计算当前 token 的 K/V |
这样做能减少重复计算,但代价也很明确:缓存会随着上下文长度线性增长。上下文越长、并发越高,显存压力越大。
Hugging Face 的缓存文档也用类似方式解释了这个机制:KV Cache 会保存来自注意力层的 Key / Value 对,并在后续 token 生成时复用它们,从而避免重复计算历史上下文。
真正的瓶颈经常不是算力,而是缓存显存
很多线上推理服务不是“GPU 算不过来”,而是“显存放不下更多请求”。这个区别很关键。
模型权重是固定的,加载后基本常驻显存;激活是临时的;但 KV Cache 是跟请求数量、上下文长度、输出长度一起动态增长的。请求来了要分配,生成结束要释放,中间还会因为不同请求长度不一致出现碎片。
PagedAttention 论文把这个问题讲得很清楚:LLM Serving 要提高吞吐,通常需要把多个请求放进 batch;但每个请求的 KV Cache 都很大,而且会动态增长和收缩。管理不当时,碎片和重复复制会浪费显存,进而限制 batch size。
这也是为什么 vLLM / PagedAttention 这类系统会把 KV Cache 当作核心优化对象。问题不只是“有没有缓存”,而是:
- 缓存怎么分配;
- 缓存能不能按块管理;
- 不同请求能不能共享相同前缀;
- 旧缓存该怎么淘汰;
- 显存不够时能不能分层存储;
- 开启量化后准确率和吞吐是否划算。
优化方向一:把连续大块内存改成分页块
早期或简单实现里,KV Cache 可能会按请求预留一大段连续显存。这个方式实现简单,但线上很容易浪费。
原因也不复杂:你不知道用户到底会生成多长。有的人只生成几十个 token,有的人可能生成上千个 token。如果提前按最大长度预留,就会出现大量空洞;如果不提前预留,又会遇到动态扩容和碎片问题。
PagedAttention 的思路是借鉴操作系统里的分页机制:把 KV Cache 拆成固定大小的块。请求逻辑上看到的是连续上下文,物理显存里可以分散存放,再通过 block table 做映射。
这种设计带来几个好处:
- 按需分配,减少预留浪费;
- 固定块大小,降低外部碎片;
- 请求之间可以按块共享前缀;
- 更容易做调度、回收和淘汰。
简单说,它不是让注意力机制本身变聪明,而是让缓存管理更像一个成熟的内存系统。
优化方向二:让相同前缀不要反复算
在真实业务里,重复前缀非常常见。
比如:
- 每次请求都带一段固定 system prompt;
- RAG 问答里,多次查询同一份长文档;
- 多轮对话每次都会带上历史消息;
- 同一模板下做批量内容生成;
- 代码助手反复携带同一个仓库上下文。
这些场景里,前面一大段 token 其实是一样的。如果每次都重新 prefill,一方面 TTFT 会变高,另一方面 GPU 也在重复干活。
Prefix Caching 的价值就在这里:把已经计算过的前缀 KV Cache 留下来。新请求如果命中相同前缀,就可以跳过这部分 prefill,直接复用缓存。
from vllm import LLM
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
enable_prefix_caching=True,
)
不过它不是所有场景都有收益。vLLM 文档也明确说明,Automatic Prefix Caching 主要减少的是 prefill 阶段 的处理时间;如果大部分时间花在生成新 token 的 decode 阶段,或者请求之间没有共享前缀,收益就会变小。
优化方向三:KV Cache 量化,不只是权重量化
很多人熟悉 W8A8、INT4、GPTQ、AWQ 这些权重量化方案,但 KV Cache 也可以量化。
KV Cache 量化的目标是减少缓存占用,让同样的显存放下更多 token 或更多并发请求。vLLM 文档中提供了 FP8 KV Cache 能力,说明将 KV Cache 量化到 FP8 可以显著降低内存占用,从而支持更长上下文和更高吞吐。
示例配置大概像这样:
from vllm import LLM, SamplingParams
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.8,
)
llm = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
kv_cache_dtype="fp8",
calculate_kv_scales=True,
)
但这里要克制一点:KV Cache 量化不是一个只看显存下降的开关。你还要看模型质量、attention backend、硬件支持、校准方式和任务类型。对事实问答、长上下文检索、代码生成、金融医疗类严肃场景,最好单独跑评测,不要只看压测吞吐。
优化方向四:缓存淘汰和分层存储
如果服务一直运行,缓存不可能无限增长。问题就变成:哪些缓存值得保留,哪些应该淘汰?
一个比较实用的判断标准是:缓存是否可能再次命中。固定系统提示词、热门文档、热门会话上下文,保留价值高;一次性超长请求、低频文档、生成结束后不会复用的缓存,保留价值低。
常见策略包括:
| 策略 | 说明 |
|---|---|
| LRU | 最近使用过的缓存更晚淘汰 |
| TTL | 超过时间直接清理 |
| Prefix hit rate | 命中率低的前缀不长期保留 |
| 分层存储 | 高价值缓存留在 GPU,低价值缓存转 CPU / SSD |
| 按租户隔离 | 避免不同用户之间错误复用或产生侧信道风险 |
这里最容易踩的坑是“什么都想缓存”。缓存不是越多越好,缓存本身也要占资源。一个成熟的推理系统,应该把缓存当作受控资源,而不是免费加速器。
工程上怎么判断优化有没有用
KV Cache 优化不能只看一条平均延迟。至少要看下面这些指标:
metrics:
ttft_p50_ms: 420
ttft_p95_ms: 980
decode_tokens_per_second: 1450
prefix_cache_hit_rate: 0.62
kv_cache_gpu_usage_gb: 48
kv_cache_eviction_count: 320
gpu_memory_utilization: 0.88
request_queue_time_p95_ms: 210
oom_count: 0
重点看三组关系。
第一,TTFT 和 prefix hit rate。 如果前缀命中率上升,但 TTFT 没降,说明瓶颈可能不在 prefill,或者缓存复用链路有额外开销。
第二,显存占用和吞吐。 KV Cache 压缩后,如果吞吐没上去,要检查 batch 是否真的变大,调度器是否受其他限制,或者 decode 已经被内存带宽卡住。
第三,准确率和成本。 量化、裁剪、淘汰都可能影响质量。不要只用一组简单 prompt 测,至少要覆盖长文档问答、多轮对话、代码、摘要和边界长度样本。
一个落地建议:先从复用率最高的场景做
如果你现在维护的是一个 LLM 应用,不建议一上来就做复杂的多级缓存系统。更稳的顺序是:
- 先统计请求长度分布、输出长度分布、system prompt 占比;
- 找出是否存在大量共享前缀;
- 开启 Prefix Caching,观察 TTFT 和命中率;
- 如果显存仍然紧,再评估 PagedAttention、FP8 KV Cache 或更细的缓存淘汰策略;
- 对长上下文任务单独做质量回归;
- 最后再考虑 CPU / SSD offload、跨节点缓存共享等复杂方案。
这个顺序比较务实。因为缓存优化最怕做成“架构很复杂,命中率很一般”。
常见误区
| 误区 | 事实 |
|---|---|
| 只要有 KV Cache,推理就一定快 | 不一定。KV Cache 能减少重复计算,但会增加显存占用。上下文短、并发低、输出很短时,收益可能没那么明显。 |
| Prefix Caching 能优化所有阶段 | 它主要减少共享前缀的 prefill 成本。如果生成阶段很长,整体耗时仍可能主要花在 decode。 |
| KV Cache 量化没有质量风险 | 量化会改变数值表示。即使很多场景表现稳定,也应该用自己的数据集验证,尤其是长上下文检索和高准确率任务。 |
| 缓存越多越好 | 缓存会占显存,也会增加管理复杂度。低命中缓存长期保留,反而可能挤占高价值请求的空间。 |
结论
KV Cache 优化的本质,不是简单打开一个参数,而是把 LLM 推理当成一个内存密集型系统来治理。
一条比较可靠的工程路线是:
先理解请求分布 → 再提高前缀复用 → 然后优化缓存布局 → 必要时做 KV Cache 量化 → 最后用监控和回归测试兜底
别只盯模型参数量。在线上推理里,真正决定成本的,经常是 KV Cache 怎么放、怎么复用、怎么淘汰,以及它有没有被稳定地监控起来。
主要参考资料
- Hugging Face Transformers:Caching — https://huggingface.co/docs/transformers/main/en/cache_explanation
- Kwon et al.:Efficient Memory Management for Large Language Model Serving with PagedAttention — https://arxiv.org/abs/2309.06180
- vLLM:Automatic Prefix Caching — https://docs.vllm.ai/en/latest/features/automatic_prefix_caching/
- vLLM:Quantized KV Cache — https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/
- Prabhu et al.:vAttention: Dynamic Memory Management for Serving LLMs without PagedAttention — https://arxiv.org/abs/2405.04437