最近在做一个基于Qwen2.5的简单Agent,就是在工具调用循环里反复让模型推理、解析动作、执行工具再拼回对话历史。用的vLLM离线推理(OfflineBatch),但跑十几个回合之后显存就涨到爆,只能重启进程。我已经把history列表做了截断,只保留最近几轮,token长度也控制了。怀疑是vLLM的cache机制在长循环里没释放旧序列,或者是我对OfflineBatch的调用方式不对?有没有大佬遇到过类似情况,是应该换用HF原生pipeline还是给vLLM加什么参数?救救孩子,真不想每个Agent任务都手动清显存。
PyTorch写Agent循环时显存越用越多,是推理框架的坑还是我代码的问题?
全部回复
共 56 条大概率不是vLLM的锅,你试试把每次推理的prompt单独构造,别复用同一个sequence对象,旧序列的显存不会自动释放。
遇到过同样问题,换个做法:每轮用OfflineBatch前手动清一下cache,或者干脆用单次推理接口,省心很多。
我之前也踩过这个坑,vLLM的prefix cache在长循环里确实容易把旧序列的KV block留着,哪怕你截断了history,只要prompt前缀有变化它不一定复用。建议试试把OfflineBatch的use_v2_block_manager关掉,或者每次循环后手动调一下llm.cache_config清理,不过更省事的做法是直接换HF的pipeline,虽然慢点但显存可控。另外你确认一下是不是工具结果拼进对话时没做tokenizer的截断,有时候隐藏的特殊token会悄悄涨长度。
我之前也踩过类似的坑,vLLM的cache在OfflineBatch里确实不会自动清,尤其是长循环里每个序列的KV cache都堆着。你可以试试给LLM传use_cache=False,或者在每轮循环后手动调llm.destroy再重新初始化,虽然慢点但能稳住显存。另外,如果工具调用不需要很长的上下文,HF原生pipeline反而更省心,就是速度慢半拍。你截断history的时候记得把系统提示和最近几轮完整保留,不然模型容易迷失。
之前跑过类似循环,vLLM开prefix caching配合手动清一下kv cache能缓解不少。
vLLM的OfflineBatch确实有这个问题,它不是按请求释放KV cache的,而是等整个batch结束才统一清。你这种循环里反复调用的场景,每个回合都相当于新请求,但旧序列的cache可能还在池子里占着,直到触发eviction策略。我之前跑多轮工具调用也踩过这个坑,后来发现是vLLM默认的max_num_batched_tokens或者gpu_memory_utilization设置得太保守,导致它宁可留着旧cache也不肯腾空间给新请求。
你可以试试在初始化LLM时把enable_prefix_caching打开,然后显式调用llm.cache_config.evict_after_generation=True,这个参数能强制在每次生成结束后清掉对应序列的cache。另外OfflineBatch的输入最好用同一个prompt对象反复update,而不是每次新建新的sequences列表,不然vLLM会认为全是新请求。我这边改成这样之后,显存曲线基本平稳了。
还有个偏方是每跑完一个回合手动调一下llm.cache_config.clear_kv_cache(),虽然有点暴力但确实管用。HF原生pipeline倒是没这问题,但速度慢太多,Agent场景下十几个回合的延迟你可能受不了。建议先排查是不是你每次拼接历史时把旧的tensor都保留在内存里了,python的list引用有时候比你想的顽固。
八成是vLLM的prefix cache在长轮次里把旧序列都攒着没释放,试试加个--disable-prefix-cache或者手动调下max_num_seqs。
我之前跑类似的多轮工具调用也踩过这个坑,vLLM的OfflineBatch确实会在内部维护KV cache,长循环里旧序列的显存不一定会立刻释放,尤其你每次拼history再传的话,等于不断在累积新的prefix。建议试试给vLLM传enforce_eager=True,或者用LLM.generate时显式调用clear_cache,再不行就分批次处理,把历史记录按轮次拆成独立请求,别一把梭全塞进去。HF原生pipeline反而没这个问题,但慢不少,看你能不能接受速度换稳定。
vLLM的OfflineBatch确实有这个问题,它内部的prefix cache和显存池不会主动释放旧序列,尤其在多轮循环里,每次推理都算新序列,缓存会越积越多。我建议你查一下vLLM的enable_prefix_caching参数,或者干脆在每轮结束后调用一下llm.reset(),能手动清掉缓存。另外,你截断history的时候要确认真的把旧的tensor从GPU上移走了,有时候列表切片只是引用的浅拷贝,旧数据还在显存里。我之前也踩过这个坑,后来直接换回HF的generate,虽然慢点但至少内存可控,你可以试试对比一下。
我之前跑类似的tool-calling循环也炸过,大概率不是vLLM批量推理的锅,而是每次循环你都在往同一个序列里追加新token,vLLM的KV cache是按请求粒度管理的,OfflineBatch不做显式释放就会一直占着。你可以试试每个回合都新建一个LLM实例,或者把history截断后重新构造一个全新请求,别复用同一个序列对象。另外vLLM有参数可以控制max_num_seqs和gpu_memory_utilization,调低一点能留出缓冲,但根治还是得手动清一下cache。我之前换过HF pipeline,倒是不会涨,但速度慢太多,Agent循环里体验很差,所以最后还是回到了vLLM加定期重启的老路。
这问题我之前也被折磨过,vLLM的显存增长大概率不是OfflineBatch的锅,而是它的KV cache管理在长序列复用场景下不会主动释放旧block,你截断history只是减少了输入token,但cache里那些历史计算的中间态还在占着。可以试试给vLLM加--max-num-seqs或者调整gpu_memory_utilization,再不行就手动调一下enable_prefix_caching参数,但说实话换成HF pipeline虽然慢点,循环里每次调用完把cache清掉反而稳。另外你如果用的是同一个LLM实例连续推理,建议每轮之间调一下vllm的reset状态,或者干脆每几轮重新初始化一次推理引擎。
vLLM的prefix cache在动态拼接历史时确实容易累积,试试加--enable-prefix-caching和--max-num-batched-tokens限制一下。
我遇到过类似问题,换HF原生pipeline反而稳定,就是慢点,但至少不用总重启。
说实话我第一反应不是vLLM的锅,你想想Qwen2.5的attention计算本身就有KV cache,而且你是在循环里反复调用同一个模型实例,即使你截断了history,vLLM的显存池是跟着整个序列的block table走的,它不会因为你删了旧token就立刻释放物理块,除非你显式调用reset或者重新创建LLM对象。我之前用OfflineBatch做多轮工具调用也踩过这个坑,后来发现每次循环里最好把prompt重新拼成一个新的sequence ID,而不是复用旧的,因为vLLM内部对prefix caching的哈希可能把你的旧序列和新序列混在一起,导致旧block一直占着。另外你确认一下是不是把整个对话历史都塞进了同一个prompt,如果工具返回内容特别长,哪怕只留最近几轮,token数也可能远超你预期,建议在每次模型调用前打印一下实际输入的token数。还有一个很隐蔽的点,vLLM的OfflineBatch默认会把所有请求都放进同一个队列,如果你在循环里没等上一个请求完全结束就发下一个,它会预分配显存给下一个batch,这个预分配是累积的。你可以试试把max_num_batched_tokens和max_num_seqs调小,或者干脆每次循环后调用torch.cuda.empty_cache看看有没有变化,不过说实话这个函数对vLLM的buffer池作用不大。要是实在不行,我建议你换成HF原生pipeline,虽然慢一点,但至少显存管理是动态的,循环里可以手动del再gc,问题会直观很多。最后想问你一句,你vLLM版本是多少,0.6.x和0.7.x的显存回收逻辑差挺多的,我之前升级后问题自动消失了。
我之前也踩过类似的坑,vLLM的cache在离线批量推理里确实不会自动清,尤其是长循环里旧序列的KV block会一直占着。你可以试试在每次迭代后手动调一下llm.cache_config或者直接重建一个LLM实例,代价是慢一点但能确认是不是缓存问题。另外检查下是不是history截断后没把旧的tensor从图上剥离,有时候detach或者复制一下能救回来。我后来干脆切回HF的pipeline加torch.cuda.empty_cache()配合梯度关闭,虽然慢但至少稳定,你可以先做个最小复现对比下。
八成是vLLM的prefix cache把旧轮次序列都缓存了,试试加--enable-prefix-cache=false或者手动调block大小。
之前调Agent也遇到过,把OfflineBatch的sampling_params里加个skip_special_tokens和use_cache=False能缓解不少。
我之前也踩过类似的坑,vLLM的OfflineBatch在循环里确实会累积一些隐式状态,尤其是max_num_seqs和gpu_memory_utilization的默认配置在长对话场景下特别容易把显存池占满。你试过每次迭代后手动调用llm.destroy_logits_processor或者直接重初始化LLM实例吗?虽然笨但有时候是最稳的。
另外,vLLM的cache是跟request_id绑定的,如果Agent循环里每次都新拼对话历史,旧序列的KV cache可能不会立刻释放,得显式设置use_v2_block_manager=False或者调低block_size试试。我之前在工具调用循环里加了torch.cuda.empty_cache()配合gc.collect(),虽然治标不治本但能多撑几轮。
HF原生pipeline倒没这问题,但速度慢一个量级,Agent交互频繁的话体验很撕裂。你不如先检查下是不是OfflineBatch的sampling_params里不小心把max_tokens设得过大,导致每轮都预分配了巨量显存。还有,别用OfflineBatch跑这种流式循环,它本来就是为一次性批量设计的,改成AsyncLLMEngine的add_request+await模式能更好控制释放时机。
要是实在排查不出来,可以试试给vLLM加--enable-prefix-caching,至少能缓解重复前缀的缓存压力。我自己的经验是,这类问题八成不是框架的锅,而是我们没按它的预期用法来写循环,但vLLM的文档确实写得像天书,遇到只能靠试。
我之前跑Qwen2.5的tool-calling loop也遇到过一模一样的情况,vLLM的OfflineBatch在连续多次调用同一个LLM实例时,显存增长特别明显。后来查了下,vLLM的prefix cache(自动前缀缓存)在长对话里其实会不断累积KV block,即便你截断了history,但之前那些被截掉的prompt片段可能还留在cache里没被清掉,等下一轮新请求进来,它会尝试匹配前缀,匹配不上的旧block又不会立刻释放,就慢慢涨上去了。我当时试过给LLM实例加--max-num-batched-tokens限制,还有--kv-cache-dtype改成fp8,能缓解一点但没根治。真正管用的做法是把每次对话独立成一个新的LLM实例(或者用llm.reset()之类的接口),但那样又损失了前缀复用带来的速度提升。另外你检查一下是不是每次循环里都把整个history拼成一条新消息传给OfflineBatch,而不是用ChatCompletion的message结构?vLLM对多轮对话的cache管理其实比单轮文本拼接要稳,因为它是按序列id追踪的。如果还是不行,建议直接看下vLLM的--disable-prefix-caching选项,对长Agent循环来说,关掉之后显存波动反而更可预测。HF pipeline说实话更省心,就是慢不少,但如果你的工具调用不是特别频繁,换过去能省很多调试时间。