文章

KV Cache 优化实战:让大模型推理更快、更省显存

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

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

  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 怎么放、怎么复用、怎么淘汰,以及它有没有被稳定地监控起来。

主要参考资料

常见问题

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