最近在做一个基于Qwen2.5的简单Agent,就是在工具调用循环里反复让模型推理、解析动作、执行工具再拼回对话历史。用的vLLM离线推理(OfflineBatch),但跑十几个回合之后显存就涨到爆,只能重启进程。我已经把history列表做了截断,只保留最近几轮,token长度也控制了。怀疑是vLLM的cache机制在长循环里没释放旧序列,或者是我对OfflineBatch的调用方式不对?有没有大佬遇到过类似情况,是应该换用HF原生pipeline还是给vLLM加什么参数?救救孩子,真不想每个Agent任务都手动清显存。
PyTorch写Agent循环时显存越用越多,是推理框架的坑还是我代码的问题?
全部回复
共 56 条我之前跑类似的多轮agent也踩过这个坑,vLLM的prefix cache在长对话里确实容易把显存吃满,尤其离线批处理时旧序列的KV cache不主动释放。你可以试试在每次循环后手动调一下llm.destroy或者重建一个OfflineBatch实例,别复用同一个。另外检查下是不是max_num_seqs或者gpu_memory_utilization设得太高,留点余量给工具返回结果。HF原生pipeline虽然慢但内存管理更直观,不过我觉得你这情况大概率不是代码逻辑问题,是vLLM的cache策略在长循环下的副作用。
vLLM的prefix cache在循环里确实容易把旧序列缓存堆着,试试加--disable-prefix-caching或者手动清一下cache。
我遇到过,offline batch的返回结果得显式del掉,不然显存不会自动释放,你试试gc.collect()。
vLLM的prefix cache在长轮次里确实容易不释放,试试加--swap-space或者--max-num-seqs限制一下。
我遇到过类似情况,把OfflineBatch的seqs_group改成逐个推理调用,显存稳多了,速度也没慢多少。
我之前跑类似的多轮Agent也碰到过一模一样的情况,vLLM的OfflineBatch在长循环里确实容易把显存吃满,不一定是你的代码问题。核心原因大概率是vLLM的prefix cache和显存管理是面向高并发服务设计的,它不会主动释放那些“陈旧”的KV cache块,除非你显式调用清理接口或者重建LLM实例,而离线批处理场景下这些缓存块会随着每轮新请求越积越多。你虽然截断了history,但vLLM内部可能还在为之前所有序列保留显存映射,尤其是当输入长度波动时,预分配的显存池不会自动收缩。我当时的做法是每轮推理后手动调用vllm的llm.cache_config相关方法清空cache,或者更干脆一点,每跑完一个Agent任务就重新初始化一个LLM对象(代价是加载模型权重多花几秒,但稳定得多)。另外你提到的HF原生pipeline,虽然显存回收更直接,但速度会掉一个档次,如果Agent对延迟不敏感倒可以试试。还有个取巧的办法是给vLLM设置--max-num-batched-tokens和--max-num-seqs更小的值,强制它更频繁地整理显存碎片,但治标不治本。建议你先在代码里打印一下每轮结束后torch.cuda.memory_summary(),看看是active内存还是缓存内存涨了,如果是缓存涨,那基本实锤是vLLM的问题了。
这问题我太熟了,之前搞多轮tool calling的时候也踩过一模一样的坑。先说结论:大概率不是vLLM离线推理的锅,而是你代码里对OfflineBatch的调用方式有隐藏问题,特别是序列长度和cache的交互。
我怀疑你虽然截断了history,但每次循环都往同一个llm.generate()里塞了新的完整对话列表,vLLM的prefix cache会试图缓存每个不同前缀的KV状态,而你的工具返回结果每次都不一样,导致前缀频繁失效,旧内存块没被及时释放。试着每次循环后调一下llm.offline_cache_clear()(如果有的话),或者干脆把整个vLLM引擎实例重建一次,虽然慢但能确认是不是cache的锅。
另外,一个很隐蔽的坑是:你用OfflineBatch时,如果传入的prompt列表里有重复的system prompt,vLLM会为每个序列单独分配KV空间,哪怕内容一样。建议把system prompt和工具定义合并成一条固定前缀,然后只把动态部分放在后面。如果这样还涨,就检查是不是你代码里保留了所有回合的完整生成结果(比如logprobs或token ids),那些对象本身占显存,不只是文本。
换HF原生pipeline不一定更好,它更吃显存而且慢,但好在内存管理更直观。你可以先试一个简单粗暴的方案:每5个回合手动把vLLM的cache清空一次(如果你用的接口支持),然后观察显存曲线是否变成阶梯状而不是直线上升。如果还是直线升,那就真是引擎内部泄漏,去GitHub提issue吧,别自己硬扛。
大概率不是vLLM的锅,OfflineBatch本身设计就是一次性处理完整个序列的,你这种循环调用的场景它内部的prefix cache会一直把旧序列的KV留着,截断history只是让输入变短但缓存没清。我之前也踩过这坑,后来改成每个回合单独建一个LLM实例或者显式调一下reset,虽然慢点但内存稳了。另外你试试把enable_prefix_caching设成False,代价是每个请求重新算一遍KV,但至少不会累积爆显存。HF pipeline的话更不推荐,长对话下它的内存管理比vLLM还糙,除非你愿意每轮手动清torch cache。
vLLM离线推理确实有显存不回收的老毛病,建议试下enable_prefix_caching参数或者直接换HF pipeline省心。
vLLM的OfflineBatch确实有这个问题,它内部的prefix cache是按序列ID管理的,你每次拼完history再传入时,如果序列ID没变,旧KV block不会自动释放,只会不断追加,所以显存只涨不跌。我之前也踩过这个坑,后来发现一个比较有效的做法是每个回合都重新创建一个新的LLM实例,或者手动调用一下llm.destroy()再重建,虽然慢一点但能彻底清掉缓存。另一个思路是直接给SamplingParams里加ignore_eos=False,然后强制在每次推理后调用llm.cache_events(如果你用的是异步版本)去清空事件队列,但离线模式不一定支持。其实更省心的方案是换HF原生pipeline,虽然慢但内存管理是确定性的,每个循环结束显存会自然回落。不过如果你不想牺牲速度,也可以试试vLLM的--disable-sliding-window参数,或者干脆把history截断逻辑改成固定token数而不是轮数,有时候是长文本导致KV cache膨胀。你现在的token长度控制和截断是基于什么标准?如果是按句话数截断,建议改成按字符或者模型最大长度比例来切,这样能减少很多无用缓存。
大概率不是vLLM的锅,OfflineBatch本来就不适合循环调用,每次推理新建序列会把历史KV缓存全留着,建议改用在线接口或手动清一下cache。
大概率不是vLLM的锅,OfflineBatch在循环里要手动清序列,试试加个prefix_caching=None或者每次重建LLM实例。
之前遇到过类似问题,把历史截断改成只传最近一轮加系统提示,显存就稳住了。
八成是vLLM的prefix cache在长会话里一直累积,试试加--disable-prefix-caching或者手动清一下KV cache看看。
这情况我也踩过,OfflineBatch默认复用序列空间,建议每次循环新建LLM实例或者显式释放旧请求,别全赖框架。
这大概率不是vLLM cache的锅,OfflineBatch本身不会自动管理跨请求的显存,你每次循环都重新初始化序列的话,旧prefix cache确实会堆着不释放。我之前也踩过这坑,后来改成用LLM.chat的流式接口,每个回合结束手动调一下LLM.reset_prefix_cache(),显存就稳住了。另外history截断后记得要重新encode整个对话,别复用之前的token id,不然vLLM会以为还是同一序列的延续。你可以先试试在循环末尾加个torch.cuda.empty_cache()看有没有改善,如果还涨就真得考虑换HF了,虽然慢点但至少不会爆。
大概率是vLLM的prefix cache在长对话里没清干净,试试加--swap-space或--max-num-seqs参数,实在不行就定期重建engine。
之前跑多轮工具调用也踩过这个坑,vLLM的显存增长多半是prefix cache在作祟,长对话里旧序列的KV block不会主动清,你试试把enable_prefix_caching关掉或者调低max_num_batched_tokens看看。另外OfflineBatch每次调用其实会重新申请显存池,可以在循环外面复用同一个LLM实例,别反复实例化。我之前是改成手动管理KV cache的release才稳住的,HF pipeline虽然省心但速度慢不少,不太建议换。
vLLM的prefix cache在长循环里确实容易出问题,尤其是OfflineBatch模式下每次调用都会重新分配KV cache,旧序列的显存不一定及时回收。你可以试试在每次推理前显式调用llm.destroy()或者换个思路,直接把vLLM的enable_prefix_caching关掉看看。另外检查下是不是工具返回的文本太长,history截断只做了token级但没控制消息条数,建议把系统提示和工具结果单独存,别全塞进对话列表。我之前用HF的pipeline反而没这毛病,就是慢点,但胜在稳定。
大概率是vLLM的prefix cache在作怪,试试加--disable-prefix-caching或者把enable_prefix_caching=False传进去。
我上次也这样,换成HF后慢是慢点但稳了,建议先排查下是不是长对话里旧seq没被释放。
我之前跑类似的多轮工具调用也踩过这坑,vLLM的cache确实默认不主动清,尤其OfflineBatch在循环里会攒着历史KV,你截断history只是管输入token,不解决cache占用。可以试试每次迭代显式调一下llm.cache_config或者干脆重建LLM实例,成本不高但能强制释放。还有个小技巧,把max_num_batched_tokens调小点,防止它一次缓存太多。不过说实话,agent这种动态长度场景,HF pipeline反而更直观,就是慢点,但至少显存可控。
vLLM的cache确实会一直占着显存不还,尤其是OfflineBatch在循环里反复调用时,旧序列的KV cache可能没被及时回收。你试试给LLM类加个swap_space或者gpu_memory_utilization参数,调低一点看有没有改善。另外,如果历史截断后还是涨,建议每次迭代手动del掉之前的输出对象再torch.cuda.empty_cache(),虽然治标不治本但能缓解。HF pipeline虽然慢,但显存管理确实省心,数据量不大的话换过去也行。
这问题我熟,之前用vLLM跑多轮tool calling也踩过一样的坑。OfflineBatch的显存增长多半不是推理框架的锅,而是你每次把新生成的token拼回对话历史后,整个序列的KV cache都要重新计算,旧cache又没被及时回收,所以越堆越多。试试在循环里显式调用llm.free_cache()或者干脆用LLM对象新建一个SamplingParams,把max_num_batched_tokens调小点,能缓解不少。另外建议把历史截断改成只保留系统提示和最近两轮工具结果,别留中间推理过程,亲测有效。真要省心的话,换HF pipeline加torch.cuda.empty_cache()也行,就是速度慢点。
vLLM对OfflineBatch的显存复用确实有问题,建议试试在循环里手动调llm.destroy()或切换成AsyncLLMEngine,我之前这么改完就稳了。