把Agent服务部署上线,前几天一切正常。运行一段时间后,监控面板上的显存曲线开始一路向上:新请求排队变长,首Token延迟从几百毫秒涨到数秒,最后进程被OOM Killer终止,或者干脆返回显存不足。重启之后恢复正常,几个小时后又重复同样的过程。
这类问题十有八九不是模型本身泄漏,而是KV Cache的增长速度超过了显存预算。Agent的多轮、长上下文、工具调用特性,把这个矛盾放大得比普通问答明显得多。
一、先看现象:显存为什么会越跑越满
推理服务的显存大致由三部分构成:模型权重、KV Cache、框架与临时激活。模型权重在启动后就固定了,真正会随运行时间变化的只有KV Cache。它的容量由“序列长度×并发数×每Token缓存大小”共同决定,任何一项上涨,显存都会跟着上涨。
典型的表现是:GPU计算利用率并不高,甚至有空闲,但显存已经接近上限,调度器无法接纳新请求。这说明瓶颈在容量而不是算力,此时盲目加卡,只会把成本线性放大,问题仍然存在。
二、Agent场景为什么比普通问答更严重
普通问答是一问一答,请求结束即释放。Agent是一个持续的任务循环,以下几个因素会同时推高KV Cache占用。
上下文单调累积。每一轮都要把历史对话重新送入模型,序列长度只增不减,KV Cache随轮次线性增长。
工具结果回填。检索到的网页、查到的表格、代码执行日志,动辄数千Token,而且会在后续多轮中被反复携带。
系统提示与工具定义重复。长系统提示加上工具Schema,几乎每次请求都要重新prefill一遍,不做缓存就是纯浪费。
会话没有生命周期。客户端断开、用户放弃、任务超时后,请求没有被及时abort,KV仍被占着,形成“僵尸会话”。
长序列与高并发叠加。并发数乘以单会话缓存,是乘积关系而不是相加关系,这是最容易失控的一项。
三、先算清楚:KV Cache到底占多少显存
估算公式并不复杂,按每个Token计算:
每Token缓存字节 = 2 × 层数 × KV头数 × head_dim × 每元素字节数
前面的系数2,分别对应Key和Value两份缓存。以一个80层、采用GQA(8个KV头)、head_dim为128、FP16精度(每元素2字节)的70B级模型为例:
每Token ≈ 2 × 80 × 8 × 128 × 2 = 327,680字节,约320KB。
单会话32K上下文:约10.7GB;
并发32个长会话:约343GB;
作为对照,该模型FP16权重本身约140GB。
结论很直接:在长上下文、高并发场景下,KV Cache会超过模型权重,成为显存的第一消耗项。这也解释了为什么“模型明明能加载起来,一上量就崩”。规划显存时如果只按权重估算,几乎必然会踩坑。
四、排查清单:五个高频根因
遇到显存持续上涨,建议按下面顺序逐项核对,多数问题在前三条就能定位。
max_model_len按模型上限设置,没有按业务收敛。模型支持128K不代表业务需要128K,把上限直接配到最大值,等于给每个会话预留了最大缓存空间。
max_num_seqs过大。调度器同时接纳过多长请求,显存需求瞬间超过预算,随后进入排队与重试的恶性循环。
未开启prefix caching。系统提示和工具定义在每次请求中重复prefill,既浪费算力,也重复占用缓存。
会话结束后未释放。检查是否存在请求已超时但未abort、流式连接断开后生成仍在继续的情况。
KV Cache仍使用FP16,且未做张量并行规划。长上下文直接把显存吃满;或者TP切分不合理,导致部分卡先满、部分卡空闲。
五、分层优化:从请求层到架构层
请求层:控制进入模型的上下文长度。采用“最近N轮原文+滚动摘要”的方式压缩历史;工具返回结果只回填必要字段,长文档先在外部摘要再入上下文;为会话设置TTL,空闲超时自动回收;客户端断连时立即abort请求。
调度层:让长请求不再阻塞短请求。合理设置max_num_seqs与max_num_batched_tokens;开启chunked prefill,把长prompt切块与decode请求交错调度;开启prefix caching,复用系统提示与工具Schema对应的KV。
显存层:直接降低单位缓存成本。启用FP8 KV Cache量化,缓存占用约为FP16的一半,代价是需要验证任务精度;优先选择GQA或MQA结构的模型,从模型层面减少KV头数;必要时通过张量并行或流水线并行分摊单卡压力。
架构层:把prefill和decode拆开。prefill是计算密集型,decode是显存带宽密集型,两者的资源配比不同。PD分离部署后可以分别按各自特点配置机器,避免用一种配置迁就两种负载;同时在网关层做会话亲和路由,让同一会话尽量命中已有缓存。
六、vLLM关键参数参考
vllm serve /models/Qwen2.5-72B-Instruct --tensor-parallel-size 8 --max-model-len 32768 --max-num-seqs 64 --max-num-batched-tokens 8192 --gpu-memory-utilization 0.90 --kv-cache-dtype fp8 --enable-chunked-prefill --enable-prefix-caching
几点使用说明:max-model-len应按业务真实上下文分位数设定,而不是取模型上限;gpu-memory-utilization不建议长期贴着1.0运行,过高的预占会压缩KV空间并增加碎片风险;FP8 KV Cache上线前必须用业务样本验证精度,对数值敏感的任务(如结构化抽取、金额计算)尤其要谨慎;prefix caching在新版vLLM的V1引擎中默认开启,老版本需要显式加上参数。
七、什么时候该加卡,什么时候该换算力模式
先区分问题性质:如果GPU计算利用率长期偏低、显存却先打满,说明是容量与调度问题,优化上下文和缓存策略的收益远大于加卡;如果是计算利用率与显存同时接近上限,才是真的算力不足,此时扩容才合理。
扩容也有顺序:先压缩上下文与调度参数,再做KV量化,然后考虑TP/PD分离,最后才是增加机器。跳过前三步直接买卡,通常只是把同样的浪费放大一倍。
另一个常被忽略的因素是运行环境。共享资源池里,邻居任务的显存与带宽抢占会让长会话业务的延迟抖动明显,且参数调整受限于平台;对长期稳定运行的Agent推理,独占物理机的包月整租环境能把模型版本、推理参数、监控口径固定下来,性能表现可复现,排查问题也更容易定位。
八、总结
Agent服务显存越跑越满,多数时候不是模型泄漏,而是KV Cache的增长超出了显存预算。多轮对话让上下文单调累积,工具结果与长系统提示进一步推高占用,再叠加高并发后呈乘积放大。工程上应先算清容量:每Token缓存等于2×层数×KV头数×head_dim×元素字节,70B级模型在32K上下文下单会话约占10GB,并发32个即超过300GB。优化按四层推进:请求层压缩上下文与会话回收,调度层限制并发并开启chunked prefill与prefix caching,显存层启用FP8 KV量化与并行切分,架构层做PD分离与会话亲和路由。若计算利用率低而显存先满,应优先优化而非加卡。