TL;DR
- 场景:高并发、长上下文、RAG 与 Agent 场景下的 LLM Serving 显存压力排查与容量规划
- 结论:模型权重只是入场券,KV Cache 才是动态显存账本;并发、上下文、输出三项 workload 共同决定系统能不能稳定跑
- 产出:KV Cache 大小估算公式 + 8 类常见故障速查卡 + 7 条工程优化方向 + vLLM / SGLang / TensorRT-LLM 版本矩阵
版本矩阵
| 功能 | 状态 | 说明 |
|---|---|---|
| PagedAttention(KV 分页管理) | ✅ 已验证 | vLLM 提出,论文 arXiv:2309.06180(2023-09),借鉴 OS 虚拟内存分页;vLLM docs 明文支持 block 组织 KV |
| RadixAttention(radix tree 前缀复用) | ✅ 已验证 | SGLang,LMSYS Blog 2024-01-17 发布;用 radix tree 跨请求复用前缀 KV |
| TensorRT-LLM paged KV cache | ✅ 已验证 | NVIDIA TensorRT-LLM 官方支持,思路与 PagedAttention 一致;与 in-flight batching 协同 |
| Continuous / In-flight Batching | ✅ 已验证 | vLLM/HF 文档明文支持;TensorRT-LLM 称为 in-flight batching,核心思想一致 |
| Prefix Caching(Automatic Prefix Cache) | ✅ 已验证 | vLLM 0.4+ 支持,使用 hash-based radix tree;SGLang RadixAttention 同源 |
| KV Cache FP8 量化 | ✅ 已验证 | vLLM --kv-cache-dtype fp8(fp8_e4m3 / fp8_e5m2);实测可省约 50% 显存,但需硬件支持 |
| Disaggregated Prefilling | ✅ 已验证 | vLLM 官方 docs 列入生产能力;prefill / decode 分到不同资源池,需解决 KV 跨节点传输 |
| KV Cache offload(CPU/NVMe/远端) | ⚠️ 待验证 | 多框架支持雏形;传输延迟与带宽成本需结合 workload 实测 |
| PyTorch 原生连续分配 KV Cache | ❌ 反模式 | LLaMA-13B 单序列 KV 占约 1.7GB,连续分配极易碎片化,已被 PagedAttention 取代 |
| 只看显存总占用,不监控 KV 指标 | ❌ 反模式 | 显存总量足够不代表 KV 形状够;必须监控 used/free blocks、prefix hit rate、preemption |

LLM Serving 的核心瓶颈,很多时候不是模型权重,而是 KV Cache。
模型权重是固定的。一个 14B 模型用 BF16 大约 28GB 权重,部署前就知道要占多少显存。但 KV Cache 是动态增长的:请求越多、上下文越长、输出越长,它越大。在线高并发时,系统真正容易失控的就是这部分。
理解 KV Cache,是理解 vLLM、PagedAttention、prefix caching、RadixAttention、disaggregated serving 的前提。
如果把 GPU Serving 当成"模型权重放进显存,然后请求来了就 generate",很快会遇到一个现实问题:权重没变,显存却越来越紧;并发一上来,长上下文一增多,系统开始排队、抢占、拒绝请求,或者流式输出变慢。很多时候,那个正在吞掉系统稳定性的东西,就是 KV Cache。
一、为什么需要 KV Cache

Transformer 自回归生成时,每生成一个 token,都需要基于前面所有 token 的上下文做 attention。
如果没有 KV Cache,生成第 1000 个 token 时,模型需要重新计算前面 999 个 token 的 Key 和 Value。生成第 1001 个 token 时,又要重新计算前面 1000 个 token。这样会产生大量重复计算。
KV Cache 的作用就是:前面 token 的 Key 和 Value 已经算过了,就存下来。后续生成新 token 时,只需要计算新 token 的 Q/K/V,然后用新 token 的 Query 去和历史 KV 做 attention。
这会极大降低 decode 阶段的重复计算,让自回归生成变得可用。
所以 KV Cache 本质是用显存换时间。它让模型不用反复计算历史 token,但代价是每个请求都要占用一份不断增长的缓存。
可以把它理解成一份"历史上下文账本"。每个请求一开始进入 prefill,系统会为输入 prompt 建立账本;进入 decode 后,每生成一个 token,账本就追加一行;直到请求结束、取消、到达最大输出长度,或者被系统淘汰,账本才有机会释放。
二、KV Cache 到底有多大

一个简化估算公式:
text
KV Cache 显存 ≈ 层数 × token 数 × batch size × KV 维度 × 2 × 单元素字节数
这里的 2 代表 Key 和 Value 两份缓存。单元素字节数取决于精度,比如 FP16/BF16 是 2 字节,FP8 是 1 字节。
注意几个变量。
- 层数越多,KV Cache 越大。
- 上下文 token 越长,KV Cache 越大。
- 并发请求越多,KV Cache 越大。
- 输出越长,KV Cache 也越大,因为生成 token 也会追加进入缓存。
这就是长上下文和高并发难以共存的原因。一个 32K 上下文请求可能消耗大量 KV Cache;多个 32K 请求同时在线,会迅速吃掉显存。
更麻烦的是,KV Cache 的占用不是静态配置项,而是 workload 的结果。用户今天问短问题,显存看起来很宽松;明天 RAG 开始塞长文档,Agent 开始多轮调用,P95 上下文长度上来,显存压力就会突然变成服务问题。
因此,做容量规划时不能只问"模型权重占多少"。还要问:
| 问题 | 为什么重要 |
|---|---|
| 平均上下文长度是多少 | 影响常态 KV Cache 占用 |
| P95 / P99 上下文长度是多少 | 决定尾部请求是否挤爆显存 |
| 最大输出长度是多少 | decode 越久,缓存越大 |
| 同时 running sequence 有多少 | 并发直接放大 KV Cache |
| 是否有共享前缀 | 决定 prefix cache 是否有价值 |
三、为什么 Decode 经常是 memory-bound

Prefill 阶段通常是大块矩阵计算。prompt 很长时,模型可以并行处理多个输入 token,因此更容易形成大规模计算。
Decode 阶段每轮只生成一个 token。每个请求每 step 的计算规模很小,但需要访问模型权重和历史 KV Cache。随着上下文变长,读取 KV Cache 的成本越来越高。
这导致 decode 阶段经常不是算力不够,而是显存带宽和内存访问效率不够。
表现出来就是:
- GPU Util 看起来可能不低,但 tokens/s 上不去;
- batch size 增大后,TPOT 迅速变差;
- 长上下文请求进入系统后,所有人的流式输出变慢;
- 显存占用没有马上 OOM,但 KV block 已经接近上限;
- queue time 和 preemption 开始上升。
这类问题不能只靠加线程解决,也不能只靠扩大 batch 解决。必须管理 KV Cache 的布局、复用、淘汰和调度。
很多人第一次做 LLM Serving,会把注意力放在算力上:GPU 型号、TFLOPS、quantization、batch size。但在线生成的体验常常卡在另一条线上:每个 decode step 都在读历史 KV。上下文越长,这份历史越重;并发越高,这份历史账本越多。
四、KV Cache 的生命周期很复杂

一个请求的 KV Cache 不是一次性申请、一次性释放那么简单。
请求进入 prefill 后,需要为输入 prompt 建立 KV Cache。进入 decode 后,每生成一个 token,KV Cache 增加一点。如果请求流式输出持续很久,它的 KV Cache 会一直占用显存。
如果用户取消请求,系统要尽快释放对应缓存。如果请求生成到 max_tokens,系统要释放缓存。如果多轮对话使用相同前缀,系统可能希望复用前面的 KV Cache。如果 beam search 或并行采样,多个分支可能共享一部分前缀 KV Cache,然后在分叉后各自增长。
所以 KV Cache 是一个动态对象。它有创建、增长、共享、分裂、释放、淘汰等状态。粗糙管理会造成大量浪费。
显存碎片化也来自这里。传统思路里,可以为每个请求分配一段连续显存来存 KV Cache。但 LLM 请求长度不确定,这种方案很容易碎片化。
假设系统中有三个请求:
| 请求 | 最终缓存长度 | 风险 |
|---|---|---|
| A | 1000 token | 中等缓存,生命周期可能很短 |
| B | 200 token | 如果预留太大,会浪费 |
| C | 4000 token | 如果预留太小,需要扩容 |
如果一开始不知道它们最终长度,就很难一次性分配刚刚好的连续显存。分少了要扩容,分多了浪费。请求结束时间也不一致,释放后会留下大小不一的空洞。
碎片化的结果是:显存总量看起来还够,但找不到足够合适的连续区域;或者为了避免扩容,系统过度预留,导致实际可并发请求数下降。
PagedAttention 正是为了解决这个问题:不要求 KV Cache 连续存储,而是像操作系统分页一样,把缓存拆成 block 来管理。vLLM 文档中也明确把 key/value cache 拆成固定 token 数的 block 来组织,每个请求通过 block 映射来访问自己的上下文。
五、KV Cache 复用:从省计算到省延迟

很多 LLM 场景有共享前缀。
- 聊天机器人有固定 system prompt。
- Agent 有固定工具描述。
- RAG 可能多次使用同一份文档前缀。
- 代码助手有重复仓库上下文。
- 多轮对话中,前几轮历史经常保持不变。
如果每次请求都重新 prefill 这些相同 token,就会浪费计算,也会拉高 TTFT。Prefix caching 的价值就在这里:相同前缀的 KV Cache 可以复用,后续请求从已有缓存继续计算。
SGLang 的 RadixAttention 更进一步,把共享前缀组织成 radix tree,让不同请求可以复用公共前缀的 KV Cache。对于系统 prompt 重复、few-shot 示例重复、Agent 模板重复的 workload,这类缓存复用很有价值。
缓存复用的收益不只是省算力,也是在省首 token 延迟。对于在线交互来说,TTFT 直接决定用户感知;对于 Agent 链路来说,多次 LLM 调用会把 TTFT 放大成整体等待。
但要注意,prefix caching 不是万能的。如果每个请求 prompt 都完全不同,缓存命中率就低。缓存系统本身也有元数据管理、淘汰策略和显存占用成本。
因此,做 prefix caching 前要先看 workload:
| workload 特征 | 缓存价值 |
|---|---|
| 固定 system prompt 多 | 高 |
| 工具 schema 重复 | 高 |
| 同一批文档被反复问 | 中到高 |
| 用户 prompt 完全随机 | 低 |
| prompt 模板不稳定 | 命中率容易下降 |
六、KV Cache 和 RAG / Agent 的关系

RAG 系统经常把检索到的文档塞进 prompt。这样会显著增加 prefill token 数,也会扩大 KV Cache。
如果每次检索内容都不同,prefix cache 命中率可能不高。如果业务中有大量用户围绕同一批文档提问,比如企业知识库、固定产品手册、代码仓库文档,那么文档前缀可能有复用价值。
RAG 系统优化不能只看召回质量,还要看 token 成本。文档越长,prefill 越慢,KV Cache 越大,TTFT 越差。很多时候,压缩上下文、重排文档、减少冗余段落,比换更强 GPU 更有效。
Agent 场景比普通聊天更依赖 KV Cache 管理。
一个 Agent 请求可能包含系统提示词、工具列表、工具 schema、历史状态、用户问题、规划过程、工具返回结果。工具调用几轮后,prompt 迅速膨胀。
如果每一步都重新 prefill 完整上下文,延迟会被放大。一次用户请求可能调用模型 5 次,每次 TTFT 增加 500ms,整体体验就会多等几秒。
Agent 还经常有共享模板。比如相同系统 prompt、相同工具描述、相同输出格式约束。这些都是 prefix caching 的潜在命中点。
所以面向 Agent 的 Serving 不能只关注单请求吞吐,还要关注多轮调用中的 KV 复用、上下文裁剪和 prompt 结构稳定性。
七、KV Cache 的常见优化方向

KV Cache 的优化方向可以分成两类:减少占用,提升复用。
第一,分页管理。把 KV Cache 拆成固定大小 block,避免连续显存分配和碎片浪费。这是 PagedAttention 的核心思路之一。
第二,prefix caching。复用相同前缀,降低 prefill 成本,改善 TTFT。
第三,KV Cache quantization。用更低精度存 KV,降低显存占用,但可能影响质量或速度,需要实测。
第四,cache eviction。显存不足时淘汰低价值缓存,比如很久不用的 prefix。
第五,offload。把部分 KV Cache 放到 CPU 内存、NVMe 或远端 KV 存储,但会带来传输延迟。
第六,context compression。减少进入模型的 token,本质上是从源头减少 KV Cache。
第七,prefill/decode 分离。不同 GPU 池处理不同阶段,但需要解决 KV Cache 跨节点传输问题。
生产系统里必须直接监控 KV Cache,而不是只看显存总占用。
建议关注:
- KV cache used blocks;
- free blocks;
- block allocation failure;
- prefix cache hit rate;
- eviction count;
- preemption count;
- 平均上下文长度;
- P95 上下文长度;
- running sequences;
- waiting sequences。
如果 KV Cache 使用率长期接近上限,系统会频繁拒绝请求、抢占请求或者排队变长。如果 prefix cache hit rate 很低,说明缓存策略没有命中 workload,或者 prompt 不稳定。如果 P99 TTFT 很高,同时长 prompt 比例上升,说明 prefill 和 KV 分配可能是瓶颈。如果 TPOT 变差,同时上下文长度变长,说明 decode 阶段 KV 读取压力上升。
八、核心结论
KV Cache 是 LLM Serving 的核心资源。模型权重是固定成本,KV Cache 是动态成本。
高并发、长上下文、长输出、多轮 Agent、RAG 文档都会放大 KV Cache 压力。系统是否能稳定服务大量请求,很大程度取决于它是否能高效管理 KV Cache。
vLLM 的 PagedAttention、SGLang 的 RadixAttention、TensorRT-LLM 的 paged KV cache,本质上都在围绕这个问题做工程优化。
如果要自研 GPU Serving,KV Cache Manager 不是附属模块,而是核心模块。它决定了系统能同时容纳多少请求,也决定了 TTFT、TPOT 和尾延迟能否稳定。
可以把这篇文章压缩成一句话:
模型权重决定你能不能把模型放进显存,KV Cache 决定你能不能把服务稳定跑起来。
参考资料
- vLLM Documentation: docs.vllm.ai/
- vLLM Paged Attention: docs.vllm.ai/en/latest/d...
- vLLM Blog, Easy, Fast, and Cheap LLM Serving with PagedAttention: vllm.ai/blog/2023-0...
- Paper, Efficient Memory Management for LLM Serving with PagedAttention: arxiv.org/abs/2309.06...
- NVIDIA TensorRT-LLM Documentation: nvidia.github.io/TensorRT-LL...
- NVIDIA TensorRT-LLM Memory Usage: nvidia.github.io/TensorRT-LL...
- NVIDIA Developer Blog, KV Cache Reuse Optimizations in TensorRT-LLM: developer.nvidia.com/blog/introd...
- LMSYS Blog, Fast and Expressive LLM Inference with RadixAttention and SGLang: www.lmsys.org/blog/2024-0...
- SGLang Documentation: sgl-project.github.io/
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 长 prompt 进入后排变长 / TTFT 飙升 | KV block 接近上限,新请求分配失败 | 监控 KV used/free blocks、block allocation failure | 启用 PagedAttention、扩大 KV 预算、上 prefill/decode 分离 |
| 流式输出节奏变慢 / TPOT 变差 | 长上下文让 decode 阶段显存带宽成为瓶颈 | 监控 P95 上下文长度、KV read 占比 | context compression、chunked prefill、KV FP8 量化 |
| GPU Util 不低但 tokens/s 上不去 | Decode 是 memory-bound 不是 compute-bound | 对比 GPU Util 与 tokens/s,看 KV 读取时延 | 收窄 max-num-seqs、prefix cache 复用、控制并发度 |
| prefix cache hit rate 持续偏低 | prompt 模板不稳定 / 每请求 prompt 都不同 / 没上 RadixAttention | 看 prompt 哈希分布、workload 特征 | 固定 system prompt 顺序、用 RadixAttention、整理模板 |
| 突发 OOM / 显存看似没用满但崩溃 | KV Cache 碎片化,连续分配失败 | 看显存总量 vs 最大连续块 | 上 PagedAttention、用 block 管理、限制单序列预留 |
| preemption 频繁上升 / waiting 排长队 | running 序列过多 + 长输出占用 KV 不释放 | 监控 preemption count、waiting sequences | 限制 max-num-seqs、调小 max_tokens、加 cache eviction |
| Agent 链路上整体等待秒级 | 多次 LLM 调用 TTFT 累积放大 | 端到端打点每跳 TTFT | prefix cache 共享模板、context compression、压缩工具描述 |
| 高并发长上下文上线后显存被吃光 | KV 是 workload 乘法结果,不是配置项 | 看 P99 上下文长度 × running seqs | 容量规划按 P99 算、上 disaggregated serving、KV 量化 |
作者:武子康的个人博客