上周排查推理服务容量告警时,我在监控面板上看到KV cache 用量一路冲到 90%,按这个趋势不扩容就等着OOM。可我按权重、按请求数一算,显存明明还空着一大半,怎么KV cache就快满了?后来扒开指标逐个看,才发现这个“90%”里有一半是虚的——或者说,系统确实只塞满了一半的容量,统计口径却把另一半也记上了。这类现象在LLM推理部署里非常常见,尤其当你用vLLM这类带PagedAttention的框架,或者用自研框架做连续批处理时,很容易被一个孤立的百分比带偏。如果你也被“90%告警”吓到过,或者想搞清楚KV cache到底怎么算、怎么调,这篇记录应该能帮你省下不少弯路。
定位问题只是一半,另一半是理解容量模型。在展开排查过程之前,我得先把KV cache的“显存账本”讲清楚,因为很多误判,都是因为脑子里对KV cache的成本模型是空的。
1. KV cache的显存账本:先算清“大头”到底从哪里来
1.1 KV cache是什么,为什么自回归推理离不开它
KV cache,全称是 Key-Value Cache,也就是Transformer在自回归解码过程中缓存下来的Key矩阵和Value矩阵。理解它不需要背公式,你只需要记住一个事实:大模型生成token是一个字一个字往外蹦的,生成第N+1个token时,注意力机制要重新看一遍前面N个token的Key和Value。如果不做缓存,每生成一个token都得把前面所有token的Key和Value从头算一遍,计算量随序列长度平方级增长,没人受得了。
所以KV cache就是一张“草稿纸”:第一次看到前面的token时,顺手把算好的Key和Value抄在草稿纸上,后面每次生成新token,直接翻草稿纸查,不再重复计算。草稿纸上的内容越多,计算越省,但显存也占得越多。这里的矛盾就是整篇排查的核心——KV cache是用显存换算力的典型产物,它在工程上既是性能加速器,又是容量黑洞。
很多刚接触部署的朋友会本能的把KV cache当成一个“还挺重要的小组件”,觉得模型权重动辄几十GB,缓存能占多少?这个直觉恰恰是错的。权重确实大,但它是固定的;KV cache却是随着“并发数 × 序列长度”线性增长的,而且每条请求都有自己独立的一份。并发一多、上下文一长,KV cache的体量会迅速超过权重,成为真正的显存大头。
1.2 用公式估算:一个请求到底吃多少显存
KV cache的占用有一套非常机械的计算公式,我建议每个做推理部署的人都把它刻在脑子里:
KV cache 大小 = 2 × 层数 × KV头数 × 单头维度 × 序列长度 × 每元素字节数 × 批大小公式里第一个常量“2”对应Key和Value两份;层数就是Transformer层数;KV头数和单头维度看模型结构;每元素字节数由数据类型决定,FP16是2字节,FP8是1字节,INT8也是1字节。算下来,7B模型单条4096 token的请求,KV cache大概是2GB左右。我带大家手算一遍:
Llama-2-7B,32层、32个KV头、head_dim=128,FP16下每token每层要缓存:
- K cache:32 × 128 × 2字节 = 8KB
- V cache:同样8KB
- 合计16KB/token/层
32层全部算上,每个token要512KB。一个4096 token的请求,就是512KB × 4096 ≈ 2GB。也就是说,光一条长请求的KV cache,就已经吃掉了2GB显存。如果是32条请求并发,哪怕平均长度只有2048,KV cache总量也有32GB,比权重本身的14GB还高一截。这还没算GQA/MQA这类优化,如果模型用了分组查询注意力,KV头数变少,占用能再低好几倍。
所以你会看到很多80GB显卡部署7B模型看似“很有富余”,但并发稍微上去,KV cache立刻顶到天花板。那个90%告警,往往就是这种动态增长带来的。
1.3 权重只是“定值”,KV cache才是“活水”
我习惯把显存占用分为“定值”和“活水”。模型权重、CUDA context、框架内部buffer是定值,加载完就固定不变;KV cache是活水,池子有多大就能涨多高,而且它跟着流量波动走。运维LLM服务时,真正需要盯的不是权重大小,而是这池活水的进出速度。
这个认知会改变你排查问题的思路。看到“KV cache用量90%”,第一反应不该是“显存不够,要加卡”,而该是“池子到底多大?有效载荷到底有多少?这90%是按什么口径算出来的?”下面进入正题——90%但只装了一半,在真实环境里通常由三个原因造成。
2. 用量虚高的三个典型原因:为什么90%可能只装了一半
2.1 batch padding:载荷被“平均长度”稀释
最容易被忽略、也最贴合“只装了一半”字面意思的原因,是batch内padding。想象一个朴素的服务:一个batch同时处理16条请求,其中最长的序列是2048个token,其余15条平均只有256个token。很多简化实现为了保证batch维度对齐,会让每条请求都按2048预分配整条KV cache空间——哪怕实际只有256个token在跑。
来算这笔账:16条请求,每条都占2048 token的缓存容量,总分配就是32768 token的容量;但有效token只有2048 + 15×256 = 5888,有效载荷只有18%。就算KV cache用量显示100%,真正“干活”的缓存也只有不到两成。显示90%但实际只有一半,在这个场景下一点也不夸张,因为预分配机制本身就是按最长序列“虚”满的。
即便是vLLM这类按块分配缓存、不用整条预占的框架,也很难完全避免另一种形式的“一半”。PagedAttention把KV cache切成固定大小的块,比如每块16个token,一个只有1个token的请求也会占满一个16 token的块,剩下15个token的位置闲置。当请求长度参差不齐、块又偏大时,内部碎片照样会吞掉不少容量。看到90%后别急着加内存,先看看平均序列长度和最大序列长度的比例,往往真相就在这道除法里。
2.2 gpu_memory_utilization:池子本来就小,百分比自然容易飘
第二个常见原因藏在框架配置里,典型的就是vLLM的--gpu-memory-utilization参数。这个参数默认值是0.9,含义是“模型权重 + KV cache + 框架临时内存,最多占用总显存的90%”。听起来很合理,但你真去线上看,会发现情况没那么理想。
显卡上不是只有模型和KV cache,还有CUDA context、cudnn/cublas的工作区、分布式通信库的buffer、KV cache池申请时的对齐碎片。这些零零碎碎先吃掉一块显存后,gpu_memory_utilization=0.9留给KV cache池的空间会被压缩得很小。池子总容量小了,池内使用率自然容易飘。原本可能应该分到20GB的KV cache池,实际只圈出来12GB;池子里稍微跑几个长请求,百分比就站上90%,可整卡显存其实还有8GB、10GB空着,只是这些空余由于池子边界固定,根本拿不到KV cache头上。
所以看到“KV cache 90%”时,不要以为整卡快满了,先看一眼nvidia-smi的实际显存占用量,再对照池子的总容量。如果两个数字差距很大,说明问题不在物理显存不够,而在分配模型把池子圈小了,或者临时内存吃掉了太多份额。
2.3 prefill与decode的统计口径:峰值被当成常态
第三个原因和KV cache的生命周期有关。一条请求在prefill阶段会一次性把整个prompt的Key和Value算完并写进缓存,这个阶段KV cache会瞬间冲高;到了decode阶段,每次只新增一个token的缓存,增长很缓慢。如果监控面板长期显示高水位,很可能就是一批带有超长系统提示词的请求常驻在那儿,把水位顶住了。
拿我自己线上一个业务举例:系统提示固定有5000个token,每轮用户对话只有几百token。模型同时服务20个用户时,prefill阶段要为每个用户申请5000 token的KV cache块,这一下就把池子填掉大半。但系统实际每秒生成的token数很低,真正的计算压力远没有90%看起来那么可怕。gpu_cache_usage_perc这个指标衡量的只是“KV cache块池已经用掉几成”,并不等于“显存快满了”或者“服务马上不可用”。
这一点非常关键。如果你只盯着一个百分比做告警,很容易在流量正常时收到一堆假警报,然后花半天加卡、迁移,最后发现根因只是统计口径没对齐。看KV cache相关指标时,至少要同时看三个数:池子利用率、剩余空闲块数、当前运行中的请求数和平均序列长度。只有把这三个数放在一起,才能还原真实压力。
2.4 不是所有“一半”都是虚的:多卡不均衡
讲完三个主要原因,补一个容易被忽略的物理场景。标题里那句“可能只装了一半”,有时候是字面意思:多卡部署下,KV cache只装满了其中一张卡,另一张卡闲置着。
有次我在TP=2的双卡环境排查性能,nvidia-smi显示GPU0已经用了72GB,GPU1只用了41GB。模型权重按理说两份卡对称,KV cache也应该是两份对称分片,结果差距这么大,明显是KV cache的分配没有跟上张量并行切分,大量注意力计算和缓存堆到了GPU0身上。这种情况如果单看聚合监控里的“KV cache用量”,你会误判成整体快满;但实际是负载不均衡,单卡早满了,另一张卡还有一半闲在那边。排查方式很简单:打开每卡视角,看显存占用和利用率分布是否一致,不一致就优先怀疑框架的并行切分配置或attention后端实现。
3. 线上排查实录:从指标到配置的定位流程
3.1 第一步:用metrics确认“90%”是指哪一层
我自己习惯的排查路径,第一件事永远是确认告警里的90%到底取自哪个指标。拿vLLM举例,最常被接进监控的是vllm:gpu_cache_usage_perc,它的定义是:
gpu_cache_usage_perc = num_used_gpu_blocks / (num_used_gpu_blocks + num_free_gpu_blocks)分子是已分配的KV cache块数,分母是KV cache池总块数。这个指标描述的是池内使用率,不是整卡显存使用率。启动服务时,vLLM会先预估KV cache池能分配多少块,这个池是固定的。池子小、请求长,指标自然居高不下。
在线上可以直接抓指标验证:
curl -s localhost:8000/metrics | grep -E "vllm:(num_used_gpu_blocks|num_free_gpu_blocks|gpu_cache_usage_perc)"同时执行nvidia-smi看整卡显存剩余。两者一对比,立刻能区分“池子真的被填满了”和“池子太小导致百分比虚高”。
3.2 第二步:评估有效载荷,别只看百分比
确认口径后,要评估有效载荷。我会做三件事:看启动日志里的KV cache池总大小、看当前所有运行请求的平均序列长度、看batch内的长度分布。
vLLM启动日志里通常会打印类似“GPU KV cache size: 12.00 GB”或“# GPU blocks: 800”的信息。用这个数对比你根据模型权重和总显存推算的应有池大小,能直接发现池子是否被压缩。比如A100-80G上部署7B模型,权重约14GB,0.9占比下理论上KV cache池应该能分出50GB以上,但日志只给了12GB,说明池子被临时内存、context和通信buffer挤占得很严重,这就是“虚高”的温床。
再看请求长度分布。我习惯把Prometheus里的“平均生成token数与最大序列长度”做成时间序列,如果连续几小时平均长度只有最大长度的三分之一,那大概率存在padding浪费。在自研框架里,我会额外打个日志,统计每个batch内有效token和分配token的比例,低于50%就该改代码或改分桶策略了。
3.3 第三步:调整参数,把容量拉回正轨
排查完再动手,方向就清晰很多。不同根因对应不同动作,切忌一上来就疯狂调gpu-memory-utilization。
如果是池子被压缩,优先考虑把显存让回KV cache:关闭不必要的Profiler、精简通信buffer、评估关掉框架里用不到的额外组件,必要时再把--gpu-memory-utilization从0.9调到0.85或者0.88。很多人以为调高这个参数能扩大池子,但实际上调得太高反而会让KV cache池申请失败或因为没给context留空间而OOM,调低一点往往更稳。同时用--max-num-seqs限制最大并发请求数,让每个请求都能分到足够的池内块数,避免请求太多导致排队甚至拒绝。
如果是batch padding导致有效载荷过低,我会在自研框架里引入序列长度分桶(bucketizing):把请求按长度归入256、512、1024、2048、4096这样的桶,每个batch只装同一桶内的请求,并把batch内的KV cache预分配调整到桶上限而不是全局最大长度。这个方法简单有效,能把有效载荷从20%拉到70%以上。如果是vLLM,则确认启用了PagedAttention的连续批处理,不要再按整条序列预分配。
如果只是prefill峰值抬高水位,优化重点转向降低峰值:开启prefix caching,让相同前缀的系统提示词在不同请求间复用KV cache,而不是每个请求都重新写一遍5000 token的缓存。启用的方式很直接,启动参数加--enable-prefix-caching即可,收益在系统提示词很长的业务里非常可观。
这三个动作做完后,一定要重启服务再压一轮流量,看相同负载下KV cache利用率是否回落,而不是凭感觉收工。我见过太多人只改了参数不看效果,最后发现新配置把正确率或者吞吐拖垮了。
3.4 修改前后的效果观测
有一次典型的案例:服务跑在A100-80G上,gpu_cache_usage_perc常年在92%,告警频发。第一反应是加卡,但nvidia-smi显示显存还有22GB空闲,明显是分配模型的问题。我按上面三步排查后确认三件事叠加:池子被临时内存压缩到了18GB、请求长度分布悬殊导致padding浪费、长系统提示词反复prefill。随后把gpu-memory-utilization从0.9调到0.88,限制max-num-seqs=24,开启enable-prefix-caching,顺便把框架版本升级到修复KV cache分片问题的版本。
改动之后相同流量下指标稳定在60%上下,最高峰也没再超过75%,告警彻底消失。这个结果说明,多数“90%告警”根本不是容量不够,而是指标和分配模型错位。你看到的百分比,往往只是问题表面的一个切片。
4. 从“虚高”到“真满”:容量规划与进阶优化
4.1 长序列请求是真正的“显存刺客”
排掉虚高部分后,也要警惕真实容量风险。KV cache增长是动态的,一条长序列请求就可能吃掉GB级缓存。前面算过,70B模型在GQA结构下,8K token的单条请求KV cache也要2.5GB左右,100条并发请求同时跑到8K,就是250GB,得三四张80GB显卡才扛得住。长上下文业务的扩容压力,很多时候根本不是权重,而是KV cache。
所以容量规划要懂得“算峰值”而不是“算平均”。我会用一个简单的公式提前估算:
单请求峰值KV占用 = 2 × 层数 × KV头数 × head_dim × max_seq_len × 2字节把并发数乘上去,就得到KV cache的峰值需求。再用gpu_memory_utilization和权重推算出池上限。两者一对比,能提前发现“池上限远小于并发峰值”的隐患,而不是等到告警出现。
4.2 压KV cache占用的几个可靠手段
如果真实负载确实压得很满,降KV cache占用就是刚需。我按效果和性价比排序推荐几个方向:
- GQA/MQA:模型结构层面的优化,多个query头共享KV头,KV cache直接除以分组因子。选型时优先选GQA类模型,能从根上减少缓存。
- KV cache量化:在不明显掉点的情况下把缓存从FP16压到FP8或INT8,占用直接减半。推理框架如果支持
--kv-cache-dtype fp8_e4m3这类选项,优先用硬件支持的格式。 - Prefix caching:让相同前缀复用缓存,适合有公共system prompt的场景,实测能砍掉一大半重复prefill的高峰。
- Sliding Window Attention / StreamingLLM:限定注意力窗口,把窗口外的KV cache丢掉,适合只关心近期上下文的业务,能显著压制长序列增长。
- 序列分桶和长度限制:从业务侧限制单条请求的最大长度,把最坏情况钉死,避免被极端长尾请求打爆。
这些手段不冲突,可以组合用。我线上一般第一选择是GQA模型 + KV量化 + prefix caching三件套,基本能把KV cache压力降到原来的三分之一到四分之一。
4.3 多副本与多卡场景的负载均衡
最后回到多卡不均衡那个场景。多副本服务遇到“一个实例KV cache 90%,另一个只有50%”时,别只盯着显存,先查负载均衡策略。粘滞连接、会话保持、重试风暴都会导致流量集中在单个副本上,让KV cache分布像扁担一样一头沉。真正的修复是把负载均衡策略切成按请求数或按实时连接数分发,并关掉不必要的会话粘滞。这一点在自建网关和K8s Service负载均衡里都能配置,成本低、见效快。
5. 常见问题速查与实操心得
5.1 常见问题速查表
我把排查中积累的典型现象整理成一张表,方便大家直接对照排查:
| 异常现象 | 可能原因 | 快速定位方法 | 解决动作 |
|---|---|---|---|
| KV cache用量90%,但nvidia-smi还有大量空闲 | 池子被临时内存/context压缩 | 看启动日志中KV cache size,对比总显存 | 调低gpu-memory-utilization,精简临时buffer |
| 短请求多,但用量虚高 | batch padding或块内碎片 | 统计平均长度与最大长度比例 | 序列分桶、换用PagedAttention、缩小块大小 |
| 长系统提示词请求占满水位 | prefix频繁重复写入 | 看运行请求数是否远小于用量 | 开启prefix caching |
| 每卡显存占用差异极大 | TP分片/KV分配不均 | 逐卡看nvidia-smi和利用率 | 升级框架,检查并行配置与attention后端 |
| 并发一高就OOM或拒绝请求 | 池上限小于并发峰值 | 算单请求KV峰值×并发数 | 限制max-num-seqs,再加KV量化或GQA选型 |
5.2 几条值得记住的经验
我在这种排查上踩过不少坑,有几条经验特别想分享。第一条,监控指标永远不要孤立看,KV cache 90%和整卡显存90%是两码事,必须同时看池大小和剩余空闲块数。第二条,调整参数之前先想清楚“为什么调整”,很多人把gpu-memory-utilization从0.9改成0.95,以为能扩大KV cache池,结果服务OOM,问题更严重。第三条,修完一定要压测验证,线上流量环境复杂,改一个参数可能同时影响吞吐和延迟,别省这一步。
还有一条容易被忽略:block size不是越小越好。块越小碎片越少,但块索引和调度的开销越大,CPU占用会上去。线上我通常先试默认值,再根据平均序列长做一次敏感性测试,选一个吞吐和显存利用率的平衡点。
5.3 我的几个踩坑记录
讲一个真实教训。有一阵子我的服务稳定运行,KV cache利用率只有50%,我以为很安全,结果某天运营做活动,用户上传了一大段长文本,全部变成长上下文请求,池子瞬间从50%冲到95%,然后开始拒绝服务。那次的根因就是我在容量规划时只按平均长度算,没按最大长度算峰值,差点造成线上事故。后来我养成了习惯:每次上线前,把所有业务允许的极端输入长度全部枚举一遍,逐个估算KV cache峰值,写进容量清单。
另一次,我为了压缩显存,给KV cache开了量化,结果某天模型输出质量崩了,排查半天才发现是新版框架的量化kernel对某个特殊长度序列处理有问题,回退后恢复正常。从那之后,凡是涉及KV cache的优化,我都会先跑一轮标准评测集,确认效果没有崩坏再上线。优化不是越激进越好,先保住质量,再谈显存。
结尾:最后一次检查KV cache水位前,先问自己三个问题
我个人在实际操作里,最后给服务下结论之前都会做一次“三层对账”。第一层,看池子总量和整卡显存是否匹配,排除池子被压缩的虚高;第二层,看有效载荷和百分比是否匹配,排除padding和碎片浪费;第三层,看峰值流量下的长序列是否超出池上限,排除真实容量风险。这三层对完,基本就不会被一个孤立的90%吓到,也不会明明危险了还自我安慰“只是虚高”。
最后再分享一个小技巧:上线前故意用一个超长请求去压一次KV cache,比如直接跑一个接近max-model-len的长序列,记录KV cache用量和剩余块数。这一步能逼出很多隐藏问题。等你哪天在监控面板上再看到“用量90%”,你会很自然地想:这个90%的池子有多大?里面真正装着的有效token又是多少?想明白这两个问题,告警就不再是恐慌的来源,而是数据决策的起点。