最近在做一个基于Qwen2.5的简单Agent,就是在工具调用循环里反复让模型推理、解析动作、执行工具再拼回对话历史。用的vLLM离线推理(OfflineBatch),但跑十几个回合之后显存就涨到爆,只能重启进程。我已经把history列表做了截断,只保留最近几轮,token长度也控制了。怀疑是vLLM的cache机制在长循环里没释放旧序列,或者是我对OfflineBatch的调用方式不对?有没有大佬遇到过类似情况,是应该换用HF原生pipeline还是给vLLM加什么参数?救救孩子,真不想每个Agent任务都手动清显存。
PyTorch写Agent循环时显存越用越多,是推理框架的坑还是我代码的问题?
全部回复
共 79 条这问题我踩过一模一样的坑,vLLM的cache确实会在这种循环里越积越满,特别是OfflineBatch每次调用都相当于新建一个序列,旧显存不会立刻释放。你试试在循环里显式调用一下llm.finish_requests()或者直接重建一个LLM实例,比截断history管用。另外你这场景其实不太适合vLLM,它强项是吞吐,循环里单条推理延迟敏感的话HF pipeline反而更省心,就是慢点。
遇到过,OfflineBatch默认会缓存过往seq的KV,试试enable_prefix_caching=False或者每次迭代显式llm.finish_requests()。
vLLM的prefix cache在长对话里确实容易这样,试试把max_num_seqs调小或者换连续批处理模式。
这问题我踩过,vLLM的prefix cache在Agent循环里确实不释放,试试把enable_prefix_caching关掉或者手动调下block大小。
vLLM离线推理确实会缓存旧序列,试试加--disable-cache或换HF pipeline,显存会稳很多。
我之前跑类似的agent循环也爆过显存,最后发现是vLLM的prefix cache在离线推理时没按对话轮次释放旧序列。你可以试试把每次推理的request单独提交,或者显式调一下vllm的clear_cache方法,我加了之后稳定多了。另外如果history截断做得比较狠,可能是KV cache和实际输入长度对不上导致的碎片化,建议把max_num_seqs调小一点试试。HF pipeline倒是没这问题,但速度慢不少,不太适合长循环。
vLLM的cache确实会在长循环里累积,尤其OfflineBatch每次调用如果没显式清掉之前的序列,显存就容易被旧KV cache占住。我之前也踩过这坑,后来是把每次推理的请求单独封装,调用完就调一下model.clear_cache或者重置一下LLM实例,勉强能稳住。你试试给vLLM传个max_num_batched_tokens或者把swap_space调小点,可能有点帮助。另外截断history的时候要注意,如果拼接的prompt里还带着旧的动作结果,token长度其实没真正降下来,最好确认一下实际输入长度。HF pipeline倒是没这问题,但速度慢不少,看你能接受不。
同款问题踩过坑,vLLM离线推理在长循环里确实会保留历史block的KV cache,你截断history只是管住了输入长度,但cache没跟着释放。试试给OfflineBatch传个use_v2_block_manager=False或者每次迭代手动调llm.reset(),我加了之后显存曲线稳多了。另外别急着换HF,那玩意儿慢到怀疑人生,先翻翻vLLM的--max-num-seqs和--max-model-len设置,有时候是默认值太大导致预留空间爆炸。要是还不行,就干脆每轮结束后调torch.cuda.empty_cache(),虽然治标不治本但能顶住小任务。
vLLM的gpu_memory_utilization和enable_prefix_caching这两个参数先查一下,prefix caching在长对话循环里如果命中率不高反而会攒一堆block不释放。我上次也是类似情况,后来发现是每轮都新建SamplingParams但旧的sequence对象没被GC,手动del加torch.cuda.empty_cache能缓解但不治本。你要不贴一下OfflineBatch的调用代码,看看是不是每次推理都把整个history重新encode了一遍?
我之前也踩过类似的坑,vLLM的OfflineBatch确实容易在循环里越跑越胖,尤其是你每次新建一个LLM或者SamplingParams实例的时候,它底层会挂一堆KV cache块不释放。你可以先试试把整个Agent循环包在一个LLM实例里复用,别每轮都重新初始化,这个最容易被忽略。另外vLLM有个enable_prefix_caching和gpu_memory_utilization,prefix caching在长历史重复拼接的场景下反而可能让显存涨得更快,你可以关掉对比一下。还有就是你截断history的时候注意别只截文本,工具调用的那些结构化消息如果没一起清掉,tokenizer那边算出来的长度可能跟你以为的不一样。真不行就换成HF的generate加past_key_values手动管理,虽然慢点但显存可控,至少不会跑着跑着就OOM。我现在的做法是每跑几轮就重建一次engine,土但有效,你可以先这么顶着。
vLLM的OfflineBatch确实有这个问题,它内部维护的block cache在长循环里不会主动回收,尤其你每次还拼接了新的prompt进去。我上次类似场景是把每轮推理拆成独立的LLM.generate调用,别复用同一个engine实例,显存就稳住了。你可以先试试在每轮结束后手动调一下engine里的cache清理接口,或者干脆用HF pipeline跑小规模验证是不是框架的锅。不过换pipeline性能会掉不少,取舍看你了。
vLLM的KV cache不会自动回收旧序列,得手动调engine里的释放接口,或者用LLM.generate的fresh调用试试。
这个现象我之前也踩过,大概率不是你的history没截断,而是vLLM的KV cache在OfflineBatch反复调用时没被及时回收。vLLM内部会按序列id缓存block,如果你每轮都把整段对话拼成新prompt再送进去,它会把每次调用当成独立请求,旧的block占着显存不放,几十轮下来就炸了。你可以先试试在每轮推理完手动调用一下llm的sleep或者清空scheduler cache的接口,不同版本名字不太一样。另外确认下你是不是在循环里反复new了一个LLM实例,那样更致命,模型权重都会被重复加载。真要排查的话建议用torch.cuda.memory_summary打印一下,看是参数占的还是激活占的。如果只是Agent这种串行小批量场景,其实HF原生pipeline配合device_map反而更省心,vLLM的吞吐优势在你这儿用不上。
这个情况我之前也踩过,大概率不是推理框架本身的锅,而是OfflineBatch的调用姿势问题。vLLM的离线推理默认会把每个request的KV cache块缓存起来复用,如果你每次循环都新建LLMEngine或者新起一个LLM实例,旧的block不会被主动回收,显存自然一路涨。关键是看你是不是每次工具调用完都重新构造一遍SamplingParams和prompts再喂进去,这样其实每次都是新序列,老序列的cache还挂在里面。
可以试试几个方向:一是别反复new LLM,全局复用一个engine;二是把enable_prefix_caching开上,相同的历史前缀能命中缓存,减少重复占用;三是检查下是不是max_num_seqs或者gpu_memory_utilization设太高,留给cache回收的空间不够。另外vLLM有个llm.collective_rpc或者reset相关的方法可以清cache,但更稳妥的是控制并发序列数,agent循环里其实同一时刻只有一条活跃序列,没必要开大batch。
如果换成HF原生pipeline,显存可能会更稳一点,因为每次forward完KV是跟着output走的,但速度会掉不少,长循环下反而不划算。我个人建议先抓一下每轮的显存快照,看是cache块泄漏还是history里隐式拼进去的tool result太长。实在不行可以每个任务结束把engine重建一次,虽然土但有效,总比手动nvidia-smi强。
vLLM的gpu_memory_utilization调低点试试,另外OfflineBatch每轮最好重建LLM实例,别复用。
这个现象我在vLLM离线跑多轮对话时也踩过,基本可以确定不是你的history截断没生效,而是OfflineBatch每次调用都会新建sequence并把旧的KV cache留在显存里,直到engine被销毁或者显存压力触发调度器回收。你可以先确认一下是不是每轮都在用同一个LLM实例反复generate,如果是的话,试试在每轮结束后显式调用一下llm.llm_engine.reset_prefix_cache或者把enforce_eager打开,另外gpu_memory_utilization别设太高,留点余量给调度器做eviction。还有个小坑是SamplingParams里的max_tokens如果给得很大,vLLM会按上限预分配block,十几轮下来碎片化会很严重。我后来改成每轮只传当前需要的prompt、把n设成1、并且用llm.generate而不是chat接口,显存就稳住了。HF pipeline虽然省心但长循环下KV cache一样会累积,除非你每轮重新from_pretrained,那速度根本没法看。建议先拿nvidia-smi或者torch.cuda.memory_summary盯着看是reserved涨还是allocated涨,能更快定位是cache没放还是Python侧引用没断。
我之前也踩过这坑,vLLM的OfflineBatch如果每次都新建LLMEngine或者在循环里重复初始化,KV cache那块很容易越积越多。你可以先确认下是不是每轮都在往同一个engine里塞新request,而旧sequence没显式释放。我后来改成每轮固定复用engine、把finished的request清掉,显存就稳住了。另外Qwen2.5配vLLM时,gpu_memory_utilization别设太高,留点余量给工具调用产生的临时张量。
这个现象我之前也踩过,大概率不是你的history截断没做好,而是vLLM的KV cache block没被及时回收。OfflineBatch模式下每次generate都会新建SequenceGroup,如果你没显式把请求对象丢掉或者让engine保持长生命周期,那些已经结束的序列占的block就会一直挂在cache里。尤其Agent循环里工具调用返回后你又拼新prompt重新推理,旧序列的引用如果还在Python侧没断,GC不触发,显存就只涨不降。你可以先试试在每个回合结束后手动del掉output和request_id相关对象,再调一下torch.cuda.empty_cache()看曲线是不是平的,如果是那基本就是引用泄漏而不是vLLM本身bug。另外vLLM有个gpu_memory_utilization和swap_space参数,离线模式下调低一点能缓解但治标不治本。真要稳的话建议换成AsyncLLMEngine或者每轮用独立LLM实例加显式shutdown,虽然慢点但Agent任务对延迟没那么敏感。HF pipeline反而更吃显存,别往那边换。
这个坑我去年踩过,大概率不是你的代码逻辑问题,而是vLLM在OfflineBatch下对sequence group的回收确实有点迷。它内部有个block manager,理论上序列结束后会释放KV cache,但如果你每次都是新建LLMEngine或者重复传不同request id,有些旧block可能一直挂在cache里没被驱逐。你可以先试试在每轮循环后手动调用engine的reset或者清空scheduler的waiting/running队列,不过vLLM版本不同API差别挺大,得看你是0.4还是0.5。另外check一下你是不是在循环里反复构造新的SamplingParams,有些版本里这会隐式持有引用导致序列不释放。换成HF pipeline不一定更好,原生pipeline在长history下反而更慢更吃显存,只是它释放得干脆所以看起来稳。真要省心可以每跑几轮就重启engine,或者用server模式走HTTP请求,进程隔离反而比offline干净。