文章

KV Cache 优化实战:LLM 推理成本真正卡在显存里

系统拆解 LLM 推理中的 KV Cache 优化,从缓存原理、分页管理、前缀复用、量化缓存到监控指标,帮助工程团队降低延迟、节省显存并提升线上吞吐。

为什么 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 应用,不建议一上来就做复杂的多级缓存系统。更稳的顺序是:

  1. 先统计请求长度分布、输出长度分布、system prompt 占比
  2. 找出是否存在大量共享前缀
  3. 开启 Prefix Caching,观察 TTFT 和命中率
  4. 如果显存仍然紧,再评估 PagedAttention、FP8 KV Cache 或更细的缓存淘汰策略
  5. 对长上下文任务单独做质量回归
  6. 最后再考虑 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

常见问题

KV Cache 为什么会影响 LLM 推理成本?
KV Cache 会随着上下文长度、并发请求数和模型层数增长,占用大量显存。显存被缓存吃掉后,batch size 会下降,吞吐和单请求成本都会受到影响。
Prefix Caching 适合哪些业务场景?
它适合同一批请求共享长前缀的场景,例如固定 system prompt、多轮对话、同一长文档的多次问答和 RAG 文档复用。
KV Cache 量化可以直接打开吗?
不建议盲目打开。量化能节省显存,但要结合模型、硬件、注意力后端和业务评测集验证准确率、延迟和吞吐变化。