为什么 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 |
这样做能减少重复计算,但代价也很明确:缓存会随着上下文长度线性增长。上下文越长、并发越高,显存压力越大。
真正的瓶颈经常不是算力,而是缓存显存
很多线上推理服务不是”GPU 算不过来”,而是”显存放不下更多请求”。这个区别很关键。
模型权重是固定的,加载后基本常驻显存;激活是临时的;但 KV Cache 是跟请求数量、上下文长度、输出长度一起动态增长的。请求来了要分配,生成结束要释放,中间还会因为不同请求长度不一致出现碎片。
这也是为什么 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,
)
不过它不是所有场景都有收益。如果请求之间没有共享前缀,或者响应很长、主要时间花在 decode 阶段,Prefix Caching 的收益就会变小。换句话说,它主要优化 prefill,不是把每个 token 的 decode 都变快。
优化方向三:KV Cache 量化,不只是权重量化
很多人熟悉 W8A8、INT4、GPTQ、AWQ 这些权重量化方案,但 KV Cache 也可以量化。
KV Cache 量化的目标是减少缓存占用,让同样的显存放下更多 token 或更多并发请求。比如 vLLM 支持 FP8 KV Cache,并提供不同的 scale 计算方式:默认 scale、随机 token warmup 估计,以及使用数据集做校准。
示例配置大概像这样:
from vllm import LLM, SamplingParams
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.8,
)
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
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:Cache strategies
- vLLM:Automatic Prefix Caching
- vLLM:Quantized KV Cache
- Kwon et al.:Efficient Memory Management for Large Language Model Serving with PagedAttention
- Prabhu et al.:vAttention: Dynamic Memory Management for Serving LLMs without PagedAttention