最近在搭一个多步骤推理的Agent,每个step要调用好几次LLM和工具,中间状态都放GPU上。我发现只要跑超过30轮,显存就稳定上涨,最后直接OOM。我已经试过torch.cuda.empty_cache(),也确认过每个step的tensor都del了,梯度也关掉了。但显存曲线还是像楼梯一样一步一步往上走。有人说是PyTorch的缓存分配器不会主动释放,也有人说是CUDA graph或者autograd的hook没清干净。想问问大家:这种长循环场景下,有没有什么官方的best practice?还是说我应该干脆每轮把推理放到子进程里隔离?有点迷茫,求指条明路。
用PyTorch写Agent循环时显存越跑越高,是框架问题还是我代码问题?
全部回复
共 59 条看到这个标题我直接点进来了,因为我上周刚被同样的问题折磨了两天。先别急着怪PyTorch,你大概率是踩了缓存分配器和Python GC之间的那个经典坑——empty_cache只是把缓存块还给分配器,但分配器未必会立刻把显存还给驱动,尤其是你每个step里如果有不同shape的中间tensor,碎片化会让缓存越攒越多。我试过最有效的办法是每N轮强制跑一次torch.cuda.synchronize()配合empty_cache,但治标不治本。后来我把所有LLM调用都包进一个独立的函数,用torch.inference_mode()包住整个循环,并且每次step结束用gc.collect()手动触发Python垃圾回收,显存曲线才平缓了一些。另外你提到autograd的hook,这个我确实遇到过——哪怕你关梯度,但如果你用了任何带requires_grad的buffer或者模型有未注册的参数,hook还是可能被留存在全局列表里。建议你跑一轮前后打印一下torch.cuda.memory_summary(),看看是tensor占用的pool没释放,还是单纯缓存块堆积。子进程隔离是最暴力的解法,但如果你Agent要频繁交互状态,通信开销会很难受,我最后是改成把每个step的输入输出都移到CPU上,GPU只做纯推理,瞬间清爽了。你可以先试试把中间状态全部pin到CPU内存,再决定要不要上多进程。
大概率是你代码里有隐藏引用没释放,试试用gc.collect()配合empty_cache,或者查查dataloader的worker。
子进程隔离治标不治本,重点检查是不是每次step都新建了graph或保留中间结果,建议用torch.no_grad()包住整个循环。
我之前也踩过这个坑,最后定位到是PyTorch的缓存分配器在搞鬼,它确实不会主动把显存还给驱动,empty_cache只是清空未使用的缓存块,但分配器自己留着的池子还在涨。你可以试试在循环里固定用同一个显存形状,或者干脆每N步强制torch.cuda.synchronize()再加一次empty_cache,看曲线会不会平缓。另外检查下是不是有retain_graph=True或者自定义autograd.Function没写好,hook没释放也会这样。子进程隔离倒是个粗暴但有效的方案,就是进程间通信和模型加载开销有点大,如果轮次不是特别多,先试试把中间tensor统一挪到CPU上存,只留当前step的在GPU,可能就解决了。
我之前也踩过这个坑,最后定位到问题根本不在你的tensor上,而是PyTorch的缓存分配器把显存块越切越碎,empty_cache只是清空未使用的缓存,但分配器内部的碎片不会合并。你可以试试跟踪一下每个step里是否有新的autograd.Function被创建,或者用torch.profiler看看是哪一层的保留内存。子进程隔离确实是个笨但有效的办法,不过开销大,我最后是用torch.cuda.memory_stats来监控,发现是CUDAGraph的私有池没释放,手动清掉就好了。
这种长循环显存涨大概率是缓存碎片化,试试用torch.cuda.memory_stats看下allocated和reserved的差值就知道了。
我遇到过,八成是缓存分配器的问题,试试torch.cuda.set_per_process_memory_fraction限制一下。
建议用pytorch的profiler看下内存峰值在哪,我之前就是torch.no_grad()没包住某个中间变量。
这问题我也踩过坑,多半不是框架的锅,查查是不是有变量意外被graph捕获了,用torch.cuda.memory_summary()看下分配细节。
这问题我当初也踩过,大概率真不是你的锅,PyTorch的缓存分配器确实会一直占着显存不还,哪怕你del了tensor。你可以试试在循环里定期调一下torch.cuda.reset_peak_memory_stats()看看实际峰值,或者直接把每个step的推理包进with torch.no_grad(),顺便检查下模型输出有没有偷偷带着梯度图。要是还不行,子进程隔离确实是最粗暴但最省心的方案,就是通信开销你得权衡下。
大概率是你代码问题,检查下是不是有Variable没detach或者缓存了中间结果,子进程隔离有点重没必要。
这问题我也踩过,多半不是你的锅,是PyTorch缓存分配器在长循环里不主动归还显存,试试torch.cuda.set_per_process_memory_fraction或者每轮清一次cache。
这问题我踩过一模一样的坑,大概率不是你代码逻辑的问题,就是PyTorch缓存分配器在搞鬼。你试试在每次step结束加个torch.cuda.memory_reserved()看看,如果数值只增不减,那就是缓存碎片化没跑了。我之前是直接把LLM调用封装成独立进程,用IPC传结果回来,虽然慢点但显存曲线稳如老狗。另外也可以查一下是不是有隐性的CUDA context没释放,比如某个库在后台偷偷初始化了额外的东西。
八成是你代码里某处悄悄存了计算图或hook引用,试试每个step末尾显式把中间变量设None再gc.collect()。
这问题我熟,之前跑长序列推理也踩过。torch.cuda.empty_cache()其实只是把缓存块标记为空,并不会真还给驱动,所以显存曲线看着涨很正常。建议你先把每个step的中间变量用torch.no_grad()包严实,再看下是不是有通过closure传给工具函数的tensor被隐式引用了。子进程隔离是个思路,但pickle开销在每轮多次调用时会很肉疼。真要根治,试试把LLM推理和工具执行分开,工具结果直接转numpy放CPU,只留模型必要的激活在GPU上。
这问题我踩过坑,大概率不是你代码逻辑的锅,就是PyTorch缓存分配器在长循环里只扩不放。empty_cache只是清未使用的缓存块,但如果你每个step里有微小显存碎片,累积起来照样涨。建议先用nvidia-smi监控下是不是cuda context本身在涨,另外试试把每个step的输入输出都强制.cpu()再处理,别让中间结果留在GPU上。子进程隔离有点重,但确实是终极大法,我最后是改成每N轮手动重置一次cuda context才稳住的。
大概率不是框架的锅,是缓存分配器在搞鬼。PyTorch的缓存池确实不会主动把显存还给驱动,但你del了tensor之后,只要下次申请的大小能和池子里对上,就不会涨,所以建议先打印一下每步tensor的形状,看是不是有些动态shape导致缓存碎片化。
我之前跑长循环agent也遇到过类似的,最后是给每个子模块单独用torch.cuda.memory.set_per_process_memory_fraction限制上限,再配合每50轮手动重置一次缓存,曲线就平了。子进程隔离太夸张了,序列化来回传结果的开销反而更亏。
另外检查下是不是工具返回的numpy数组没转成CPU,或者某个库内部偷偷开了梯度。你关了autograd,但有些第三方库的hook还是会挂到tensor上,可以用torch.autograd.set_grad_enabled(False)全程包住试试。
大概率不是框架问题,你试试把每轮输出统一detach到cpu再清一下计算图,三十轮前卡爆八成是变量引用没断干净。
子进程隔离有点重,我跑长循环都是先torch.cuda.set_per_process_memory_fraction限个额,再配合定期reset峰值,稳得很。
我之前也踩过这个坑,大概率不是你代码逻辑的问题,就是PyTorch缓存分配器在搞鬼,它确实不会把显存立刻还给驱动。你试过empty_cache其实只是清空未使用的缓存块,但分配器本身持有的内存还是留着。可以试试在循环里固定用同一个推理函数,把输入输出都放到同一个device上,避免隐式创建新tensor,再看看显存曲线。子进程隔离是个笨办法但确实有效,不过进程间通信和模型加载开销会很大,如果轮次特别多建议先排查下有没有在循环里不小心累积了计算图,比如把loss或中间变量append到list里,即使del了但引用还在。
这问题我踩过一模一样的坑,大概率不是框架bug,就是缓存分配器在搞鬼。你每步生成的中间tensor只要没被完全覆盖,PyTorch就会留着那块显存不还给CUDA,empty_cache只是清空缓存池,并不会释放已经分配给torch的块。建议你试试给每个step的LLM调用单独套一个torch.inference_mode(),再配合禁用grad的上下文,能把autograd的临时节点彻底掐掉。另外子进程隔离确实能根治,但开销不小,先看看是不是某些工具返回的tensor偷偷持有了计算图引用,我上次就是被一个返回dict没深拷贝的bug坑了两天。
这情况太典型了,八成不是框架bug,就是PyTorch的缓存分配器在“囤内存”。它默认会保留已释放的block以便复用,长循环里碎片化加上每次LLM调用产生的临时tensor,显存自然只涨不跌。你可以试试在循环里定期调一下torch.cuda.reset_peak_memory_stats()看真实峰值,或者更狠一点,把每个step的推理包在with torch.no_grad()里并且把输入输出都显式移到CPU。子进程隔离是个笨但有效的办法,不过开销大,建议先查查是不是有哪个hook或者闭包在偷偷引用旧tensor。
这种楼梯式上涨我调过,大概率是你代码里某个list或者dict在累积历史输出,哪怕tensor删了,引用还在就释放不了。建议在每个step末尾强制跑一遍gc.collect()再加empty_cache,虽然治标不治本但能缓解。想彻底解决,最好把LLM调用封装成独立函数,确保所有中间变量都出作用域就销毁,别靠del手动删,容易漏。子进程方案真没必要,杀鸡用牛刀了,先检查是不是有缓存了每个step的logits或者attention map在某个变量里没清干净。
我遇到过一模一样的情况,最后发现是transformers库的tokenizer和model内部有缓存,跟你代码无关。PyTorch的缓存分配