← 返回资讯

KV Cache 与显存占用:推理加速背后的显存杀手

2026-07-06

KV Cache 是 Transformer 推理能做到逐 token 生成而不重复计算的核心机制,但它也是高并发场景下最凶猛的显存消耗者。理解它,才能在显存有限的情况下把并发撑到最高。

什么是 KV Cache?

Transformer 的注意力机制每一步都要用到所有历史 token 的 Key 和 Value 向量。如果每生成一个新 token 都重新计算历史 token 的 KV,时间复杂度是 O(n²)。

KV Cache 的做法:把已经计算过的 Key/Value 缓存在显存里,生成下一个 token 时直接复用,只计算新 token 的 KV。时间复杂度降到 O(n),代价是显存线性增长。

打个比方:你在写一篇长文章,每写一句话都要把前面所有句子重新读一遍找灵感,这就是没有 KV Cache 时的做法,越写越慢。而 KV Cache 相当于你在旁边放了一份”已读要点摘要”,每写一句新的只需要看摘要加上刚写的这句,不用从头读起。摘要会越攒越厚,这就是显存持续增长的原因——它不是内存泄漏,是设计上必须留着的”记忆”。

如果你自己部署过 Llama 或 Qwen 系列模型,大概率见过服务跑着跑着并发一高就报 CUDA out of memory,但看权重文件明明没那么大——十有八九就是 KV Cache 在偷偷吃显存,而不是权重本身出了问题。这也是为什么调显存参数时,光看权重大小去估算是不够的,必须把 KV Cache 单独算一遍。

KV Cache 显存占用计算

单 token 的 KV Cache 占用(FP16 精度):

每 token KV 显存(Bytes) = 2 × 层数 × KV 头数 × 头维度 × 2字节

Llama-3-8B 为例(32层,KV头数8,头维度128):

每 token = 2 × 32 × 8 × 128 × 2 = 131,072 字节 ≈ 0.125 MB
并发数上下文长度KV Cache 占用(Llama-3-8B)
14K token~0.5 GB
84K token~4 GB
324K token~16 GB
816K token~16 GB
3216K token~64 GB

对比:Llama-3-8B 权重在 FP16 下约 16 GB。32 路并发 + 16K 上下文时,KV Cache 是权重的 4 倍

公式里每个数字是什么意思,为什么 KV 头数不等于总头数

公式最容易让人卡住的是”KV 头数”这个词,很多人第一次算的时候会拿模型的注意力头总数(Attention Heads)去代入,结果算出来的数字比实际大好几倍,对不上。

原因是现在主流模型基本都用了 GQA(Grouped Query Attention,分组查询注意力),而不是最早的 MHA(Multi-Head Attention)。以 Llama-3-8B 为准:它一共有 32 个 Query 头,但这 32 个头被分成 8 组,每组共用同一份 Key/Value,所以 KV 头数只有 8,不是 32。这就是为什么公式里的”KV 头数”要单独确认,不能想当然套用总头数——套错了,你的显存预算会直接偏差 4 倍。

这也解释了公式里那个容易被忽略的”2”:它不是随便加的系数,而是因为每个 token 要同时存一份 Key 和一份 Value,两者显存占用相同,所以是 2 ×。如果你看到别的资料写的公式里没有这个 2,大概率是把 K 和 V 分开算的,本质一样,别被不同写法搞晕。

顺带说一句:GQA 本身就是显存优化手段之一,它是模型架构设计阶段就定下来的,你部署时改不了。但理解这一点很有用——挑选开源模型时,如果两个模型参数量相近,KV 头数少(GQA 分组比例高)的那个,在同样并发下会更省显存,这是选型时容易被忽略的一条硬指标,最好去模型的 config.json 里核对 num_key_value_heads 这个字段,而不是只看参数量对比。

KV Cache 与权重的显存博弈

推理服务的显存分配逻辑:

总显存 = 权重占用 + KV Cache 预留 + 框架开销(1–3 GB)

权重是固定的,KV Cache 是可变的。vLLM 通过 --gpu-memory-utilization 参数控制为 KV Cache 预留多少显存(默认 90%,即 总显存 - 权重 - 框架开销 的 90% 分给 KV Cache Pool)。

量化权重的意义正在于此:把权重从 16 GB 压到 8 GB,节省的 8 GB 全部进入 KV Cache Pool,并发能力翻倍。

传统 KV Cache 的问题

传统实现为每个请求预分配连续显存块(按最大上下文长度),带来两个问题:

  1. 内部碎片:请求实际用了 500 token,但预留了 4096 token 的空间,剩余显存浪费
  2. 外部碎片:不同请求结束后,释放的显存块大小不一,难以拼合给新请求

结果是显存利用率只有 20–40%,并发数远低于理论上限。

这不是纸上谈兵,是你会真实碰到的坑。如果你用比较老的推理框架(或者自己拿 HuggingFace transformers 直接写 batch 推理服务),压测到某个并发数会突然报出类似这样的错误:

torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.34 GiB
(GPU 0; 23.65 GiB total capacity; 20.12 GiB already allocated;
1.03 GiB free; 21.88 GiB reserved in total by PyTorch)

看到”already allocated”和”reserved”这两个数字差得不多,但”free”却很小,这就是典型的显存碎片化信号——不是真的没显存了,是显存被切得七零八落,凑不出一块连续的空间。排查思路:先看当前并发请求的最大上下文长度设置是不是过度保守(比如统一按 8K 预留,实际大部分请求只有 1K),把预分配长度调小或者换成动态分配的框架(vLLM、TGI 都支持),基本能立刻缓解。

PagedAttention:像操作系统管理内存一样管理 KV Cache

vLLM 引入的 PagedAttention 参考了操作系统虚拟内存的分页思路:

  • 将 KV Cache 切成固定大小的物理 Page(默认 16 token 一块)
  • 为每个请求维护一个逻辑→物理的地址映射表
  • 按需分配 Page,请求结束后立即回收
  • 不同请求可以共享相同前缀的 Page(Prefix Caching)

实测效果:相同显存下,PagedAttention 相比传统方法可支撑 2–4 倍 的并发请求数。

Page 大小怎么选:不是越小越好

vLLM 的 Page 大小由 block_size 参数控制,默认 16 token。这个数字不是拍脑袋定的,是一个实打实的取舍:

  • Page 设得小(比如 8 token):内部碎片更少,显存利用率更高,但每个请求要维护的地址映射条目更多,元数据(Metadata)开销上升,调度器管理成本变高,极端情况下反而拖慢吞吐。
  • Page 设得大(比如 32 token):元数据开销小,管理简单,但如果一个请求实际只用了 5 个 token 却占了一整块 32 token 的 Page,浪费的比例又回去了,等于走回传统方案的老路。

16 是 vLLM 团队跑了大量真实工作负载后选出来的折中值,对大多数场景是合适的默认值。如果你的业务场景比较特殊(比如输出普遍很短,几十 token 就结束),可以试着调小 block_size 观察显存利用率是否提升;如果调度延迟变得明显,再调回去。这个参数改起来成本很低,值得在压测阶段实测对比,不要直接抄别人的配置。

Prefix Caching:前缀共享进一步省显存

当多个请求共享相同的系统提示(System Prompt)或文档前缀时,PagedAttention 可以让它们共享同一份 KV Cache Page,而不是每个请求各存一份。

典型场景:

场景前缀共享比例节省效果
统一系统提示(2K token)显存节省可达 30–50%
RAG 文档检索(同文档多问)中高减少重复计算和显存
多轮对话(历史相同)每轮节省历史部分计算

vLLM 从 0.4.0 起支持 Automatic Prefix Caching(APC),默认开启。

怎么估算自己能撑多少并发

光看理论公式没用,你需要一个能落地的估算步骤。思路很简单:先算出显存里能分给 KV Cache 的总量,再除以单个请求平均要占用多少,就是理论并发上限。

可用 KV 显存 = 总显存 - 权重占用 - 框架开销
理论最大并发数 = 可用 KV 显存 ÷ (单 token KV 显存 × 平均上下文长度)

拿文章开头 Llama-3-8B 的例子接着往下算:假设你用的是一张 24 GB 显卡,权重占 16 GB,框架开销按 2 GB 算,可用 KV 显存就是 24 - 16 - 2 = 6 GB。如果业务场景平均上下文在 4K token 左右(按前面表格,1 路并发约占 0.5 GB),理论并发数大约是 6 ÷ 0.5 ≈ 12 路。

注意这是”理论值”,不是”实测值”。实际部署时,因为请求长度参差不齐、Prefix Caching 能省下一部分、调度器还要留缓冲防止突发请求打满,实测能跑到的并发数通常会比理论值低一些(如果你用的是 vLLM 这类支持 PagedAttention 的框架,实际值会比传统框架更接近理论值,这也是它的价值所在)。所以这个公式的用法是拿来做”数量级判断”和”选卡预算”,不是拿来做精确的容量保证——真正上线前,还是要用你自己的真实请求分布跑一轮压测。

怎么监控自己服务的 KV Cache 水位

算是一回事,服务跑起来之后水位有没有到红线是另一回事,得实时盯着,不能只在部署前算一次就完事。

如果你用的是 vLLM,它自带 Prometheus 格式的监控端点,直接访问服务的 /metrics 路径就能拿到一堆指标,其中和 KV Cache 直接相关的是 vllm:gpu_cache_usage_perc(GPU 上 KV Cache Pool 的使用百分比)。用命令行简单看一眼:

curl -s http://localhost:8000/metrics | grep gpu_cache_usage

这个值长期贴着 90% 以上,说明你已经在显存红线边缘运行,稍微来一波突发请求就可能触发前面说的驱逐(Eviction)或者请求排队,延迟会明显抬头;如果这个值长期低于 30%,说明你的 --gpu-memory-utilization 设得偏保守,或者并发量还没跑起来,显存有富余可以进一步压榨,比如适当调高预留比例,或者干脆换更小的量化权重腾出更多 KV 空间。把这个指标接到你现有的 Grafana 或者随便什么监控面板里,比出问题后翻日志排查省心得多。

常见问题

KV Cache 溢出会发生什么?
vLLM 会触发 KV Cache 驱逐(Eviction),将部分请求的 KV Cache 换出(Swap)到 CPU 内存或直接终止该请求并重计算。换出会大幅增加延迟,生产环境应监控 cache_miss_rate 并提前扩容。

长上下文模型(128K token)的 KV Cache 能放得下吗?
通常放不下全量。128K token 的 KV Cache 对 Llama-3-8B 来说约 16 GB,超过很多卡的剩余显存预算。实践中可通过滑动窗口注意力或稀疏注意力机制截断,或直接分配专用大显存实例。

INT4 量化会影响 KV Cache 精度吗?
权重量化不影响 KV Cache 精度(KV Cache 默认仍以 FP16 存储)。部分框架支持 KV Cache 量化(KVQuant),可进一步压缩,但会有轻微精度损失,需实测评估。较新版本的 vLLM 已支持将 KV Cache 量化到 FP8 存储,理论上能再省下接近一半的 KV 显存,但精度损失和加速收益因模型而异,上生产前务必拿自己的评测集实测对比,具体支持范围和参数以官方文档为准。

为什么调高 --gpu-memory-utilization 之后服务反而更容易报错退出?
这个参数是”总显存的多少比例可以被 vLLM 使用”,调得越高,留给操作系统、显卡驱动、其他进程的余量就越少。如果你的机器上还跑着别的显存占用(比如监控 agent、别的模型实例、甚至同卡上跑着的 Jupyter 内核),调到 0.95 以上很容易在服务启动或者显存碎片整理时直接被系统杀掉,报错通常是启动阶段的 CUDA out of memory 或者进程被 OOM Killer 直接终止、日志戛然而止没有堆栈。稳妥的做法是先用默认的 0.9 跑通,确认显存水位(前面提到的 gpu_cache_usage_perc)长期有余量了,再一点点往上调,每次调完都跑一轮压测再上线,不要一步到位。


延伸阅读: