最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条试试给DataLoader加个迭代器作用域,用with语句包住推理循环,跑完自动释放batch引用。
pytorch的缓存分配器可能没回收到显存,用pytorch_memlab的Snapshots看看每步谁在占内存。
遇到这种情况先别急着怀疑模型缓存,你那个2G到10G的涨幅其实挺典型的。我之前跑GPT类模型也踩过同样的坑,最后发现是DataLoader的num_workers在搞鬼,多进程加载数据时每个worker会复制一份CUDA上下文,如果没设pin_memory=False或者没在子进程里清干净,显存会随着迭代悄悄累积。另外你试的那三个方法其实都只解决了显存碎片问题,真正没释放的可能是PyTorch的CUDA caching allocator,它为了加速会保留已释放的block,你可以试试在推理循环里加个torch.cuda.reset_peak_memory_stats()看看峰值是不是真的在涨。至于每个batch手动清空,说实话没必要,只要把input_ids、attention_mask这些tensor在循环末尾置None就行,但更关键的是检查是不是有hook或者gradient accumulation的残留。想监控引用计数的话,torch.profiler能看内存分配事件,或者用pytorch_memlab这个第三方库,它能打印每个张量的生命周期,不过我建议你先跑个最小复现脚本,把DataLoader换成普通list循环试试,大概率就能定位到问题出在数据加载还是模型本身。
试试关掉cudnn的benchmark模式,或者查查DataLoader里有没有累积梯度,列表存结果一般不是主因。
用pytorch的memory_stats接口看下缓存峰值,大概率是显存碎片化,加个torch.cuda.set_per_process_memory_fraction限制下试试。
这种情况大概率不是模型缓存的问题,而是DataLoader的num_workers在作祟,多进程加载数据时每个worker都会复制一份CUDA上下文,显存自然就叠上去了。你可以把num_workers设成0试试,或者用pin_memory=False,看看涨幅是不是明显放缓。另外,append结果到列表不会占显存,但如果你在推理中保留了梯度计算图,比如没包在with torch.no_grad()里,那每个batch的中间变量都会堆积。建议你在循环里加个torch.cuda.reset_peak_memory_stats()和torch.cuda.max_memory_allocated()打印一下,看看到底是哪个阶段涨的。至于监控张量引用计数,pytorch的memory_stats接口能看allocated_bytes和reserved_bytes,但具体到张量级别就得用tracemalloc配合CUDA的debug工具了,不过实战中一般先排查数据加载和梯度保留这两个坑。
这问题我踩过坑,八成不是模型没释放,而是DataLoader的num_workers和pin_memory在搞鬼,尤其多进程下每个worker都会拷贝一份CUDA上下文。另外你检查下是不是把loss或者梯度相关的变量留在了循环外,比如accumulate了hidden_state。监控的话试试pytorch的memory_stats接口,能按张量维度看分配,比引用计数直观多了。
这种情况大概率不是模型没释放,而是DataLoader的worker进程在拖后腿,尤其是num_workers设得比较高时,每个worker都会缓存一部分数据到显存里,推理完也不一定立刻回收。你可以试试把num_workers设成0,或者用torch.cuda.synchronize()强制同步一下看看显存曲线。另外结果append到列表里如果列表是全局的,也会导致Python对象一直持有张量引用,建议改成只存numpy或者直接写文件。监控张量引用的话,pytorch的memory_stats接口挺有用的,能看allocated_bytes和reserved_bytes的差值,不过说实话最靠谱还是分段跑加打印显存,定位是哪个阶段涨的。
显存一直涨也可能是PyTorch的缓存分配器在搞鬼,它不会立刻把显存还给驱动,而是留着复用,所以看起来像泄漏,实际可能只是峰值虚高。你可以试试在推理循环里隔一段时间就调一下torch.cuda.empty_cache(),但别每个batch都调,那样反而影响性能。另外你的模型如果用了batch normalization或者dropout,记得切到eval模式,不然训练时的状态缓存也会占地方。手动清空input和output没必要,关键是别让loss或者梯度相关的张量留在图里,推理时确认一下有没有误开grad。
我遇到过类似情况,最后发现是DataLoader里的collate_fn在搞事情
大概率是DataLoader的num_workers在累积进程缓存,试试把worker数设成0或者用torch.cuda.set_per_process_memory_fraction限制上限。
用pytorch的memory_snapshot能看张量生命周期,比手动查引用计数直观多了。
这问题我踩过坑,多半不是模型缓存,而是DataLoader的num_workers在搞鬼,子进程会复制一份CUDA上下文,推理完不释放。你试试把num_workers设成0或者pin_memory关掉,看显存还涨不涨。还有,append到列表里的结果如果一直存着,每个tensor都占显存,建议转成numpy或者直接写文件。监控的话用pytorch的memory_stats接口,能看每块显存分配详情,引用计数别指望了,PyTorch不暴露那玩意儿。
试试关掉梯度后把outputs和loss从计算图里detach出来,列表只存cpu的numpy,多半是计算图被列表引用了没释放。
用pytorch的memory_summary()看下缓存分配,或者跑的时候watch -n 1 nvidia-smi对比,很快能定位到哪一步涨的。
这种情况大概率不是模型没释放,而是DataLoader的num_workers在搞鬼,每个worker子进程都会复制一份模型和CUDA上下文,你如果设了多进程加载,显存会按worker数成倍涨。建议先把num_workers设成0试试,或者用torch.cuda.reset_peak_memory_stats看一下峰值到底在哪一步涨的。另外你推理完把结果append到list里,如果后面没转成numpy,那list里存的都是带梯度的Tensor,也会一直占着显存,记得用tensor.cpu().numpy()再存。监控张量引用计数的话,pytorch有个torch.cuda.memory._snapshot()能导出内存快照,配合memory-visualizer工具看哪个张量泄漏比较直观。
这题我熟,之前跑GPT类模型也遇到过,多半不是张量没清,而是DataLoader的num_workers在搞鬼,子进程会复制显存上下文,你试试把num_workers设成0,或者用persistent_workers=False看看。另外结果append到list会累积计算图,虽然no_grad了但某些op还是会有缓存,建议把输出tensor先转成numpy再存。监控的话用pytorch_memlab挺方便的,能按行号显示分配,一眼就能看出是哪个循环在漏。
遇到过类似的情况,大概率不是模型缓存的问题,而是DataLoader在后台预取数据时把张量都留在默认的CUDA流上没释放。你可以试试把推理循环里每个batch的输入用非阻塞方式拷贝到GPU,再在循环末尾强制加一次torch.cuda.synchronize(),看看显存曲线会不会平缓。另外append结果列表如果存的是GPU张量,记得转成CPU的numpy再存,否则列表本身也会撑住显存。至于监控工具,pytorch的torch.cuda.memory._record_memory_history()能记录分配栈,配合tracemalloc可以看引用,但有点重,先试试上面两个方法。
你这情况八成不是模型没释放,而是DataLoader的num_workers在搞鬼,默认0还好,设大了每个worker都会复制一份模型权重和计算图缓存。手动清空输入输出其实没啥用,关键看是不是在循环里累积了loss或者梯度,虽然推理模式不反传但有些算子还是会留中间变量。建议你试试用torch.cuda.memory_summary()看下内存分配细节,或者直接开个py-spy attach到进程上看看到底哪一步涨的。另外如果用了transformers库,记得把model.eval()和torch.inference_mode()一起用,比no_grad更彻底。
我之前也踩过类似的坑,尤其是BERT这种大模型,单个batch看着显存不高,但跑久了就是会慢慢涨。你试了no_grad和empty_cache,但效果不大,我猜大概率不是Python层的引用没释放,而是PyTorch的CUDA缓存机制在搞鬼,它默认会保留一些显存块不还给驱动,目的是为了下次分配更快,所以empty_cache只是清空未使用的缓存块,但如果你每次推理的图结构不完全一样,比如动态的padding或者循环里创建了新的tensor,这些碎片就会越积越多。你提到用DataLoader分批加载,那可以试试把推理放到一个函数里,确保所有中间变量都在函数作用域内,函数返回时局部变量会被自动回收,但注意别把结果append到一个全局列表后,列表本身持有的张量引用还在,这个不算泄漏,但如果你在列表里存了GPU张量而不转成CPU或者numpy,那显存肯定只增不减。我之前用pytorch的torch.cuda.memory_summary()能打印出当前缓存块的分配情况,能看出来是不是有异常的大块没释放,但更直接的还是用nvidia-smi配合py-spy或者pympler来跟踪对象的引用,不过说实话,最省事的办法是把每个batch的输入和输出在循环末尾显式地加上.to('cpu'),然后再del,最后才empty_cache,这样通常能稳住。另外,如果你用的是transformers库的BertModel,注意它内部可能因为开启grad而保留中间激活值,虽然no_grad能关掉,但保险起见可以再加一句model.eval(),确保dropout和batchnorm都切到推理模式。我印象里还有个坑是DataLoader的num_workers如果不为0,会有一些缓存线程持有数据,但那个一般不占GPU显存,除非你在worker里也调用了cuda。你可以先用一个很小的循环,比如50个batch,每次打印一下torch.cuda.memory_allocated()和memory_reserved(),看看是分配内存涨还是缓存涨,如果是reserved涨,那基本就是缓存碎片问题,如果allocated涨,就得查代码里是不是有张量被意外保留了。我后来是直接用torch.inference_mode()代替no_grad,这个模式更彻底,会禁用自动梯度追踪和一部分缓存优化,实测对显存控制有帮助,你可以试试。
我之前也遇到过一模一样的坑,最后发现根本不是模型没释放,而是DataLoader的num_workers在搞鬼。你试试把workers设成0,或者推理时直接用for循环遍历dataset,如果显存稳了那就说明是子进程的缓存队列没回收。另外你append结果那个列表,如果后续还要保存成文件,可以每积累几百条就写盘然后清空列表,不然Python对象本身也占内存,虽然跟显存不是一回事但会间接影响。还有个小技巧,你可以把输入batch用torch.no_grad()包住之后,再强制调用一下torch.cuda.synchronize(),有时候异步执行会让显存看起来没释放,实际上是延迟分配。至于监控工具,别用pytorch自带的那个,不太直观,直接nvidia-smi加watch -n 0.1看显存曲线,配合pdb在代码里打断点,把可疑的中间变量打印出来看id有没有重复。我怀疑你那个BERT模型是不是用了transformers库的pipeline,那玩意儿会缓存输入编码,跑长文本特别明显。如果实在定位不到,干脆每个batch结束就重置一下模型输出层,或者用torch.cuda.memory_summary()看下allocated和reserved的差值,如果reserved很大但allocated不大,那就是缓存碎片问题,用empty_cache确实没用,得改推理逻辑。
遇到过类似的,大概率不是模型缓存,是DataLoader的num_workers在搞鬼,多进程加载数据时每个worker都会复制一份CUDA上下文,显存自然就涨上去了。试试把num_workers设成0,或者用persistent_workers=False,看能不能缓解。另外append结果列表如果一直保留着,梯度虽然断了但张量本身还在显存里,可以改成只存numpy或者直接写文件。监控工具的话,pytorch的torch.cuda.memory_snapshot()能看每个张量的分配情况,比引用计数直观多了。还有个小坑,如果用了bert的tokenizer,把input_ids放到GPU上之后一定记得转回CPU再丢进下一轮,不然旧张量不会自动释放。
遇到过类似的,多半不是没释放,而是DataLoader的num_workers在作怪,worker进程会预取数据,显存跟着涨。你把num_workers设成0试试,或者推理时直接用简单循环,别用DataLoader,大概率能解决。empty_cache只是把缓存还给CUDA,不代表显存真的降下来,频繁调用反而影响性能。想定位的话,pytorch有个torch.cuda.memory_snapshot()能看分配细节,或者用nvidia-smi配合py-spy看每步的峰值。另外注意一下,如果结果列表里存了tensor而不是numpy,那引用一直留着,显存肯定清不掉,转成cpu().numpy()再append就行。
遇到过类似的坑,你试试把DataLoader的num_workers设成0,有时候多进程加载会累积一些缓存不释放。另外append列表如果是用来存tensor的话,记得转成cpu再存,不然gpu上的引用一直挂着。监控工具的话可以试试pytorch的torch.cuda.memory_snapshot(),能看每个tensor的分配情况,不过信息有点杂,习惯用pynvml看整体占用。最后建议每跑完一个epoch就手动清一下cache,虽然不治本但能延缓OOM。
我之前也踩过这个坑,最后发现根本不是del和empty_cache能解决的。你试试把推理循环里每个batch的input_ids和attention_mask都detach().cpu()一下,再存结果,大概率是GPU上的计算图没释放,因为哪怕在no_grad下,如果输出张量参与了后续操作(比如append到列表),它还是会保留整张图。另外DataLoader的num_workers如果设得高,每个worker也会占显存,但你这情况更像是在accumulate梯度或者某个隐藏的变量被闭包引用了。有个土办法,你在每个batch结束后打印torch.cuda.memory_summary(),看哪个tensor的size在持续增长,基本能锁定。pytorch有个torch.cuda.memory._record_memory_history()可以记录分配栈,比引用计数直观多了,不过要谨慎开,会拖慢速度。还有个小细节,你把结果append到list里,如果后面还要转成tensor,建议直接预分配一个numpy数组往里面塞,不然list里面保留的tensor引用会一直占着显存池。我之前是发现模型本身有个缓存buffer没清,比如BERT的position_ids,加了register_buffer之后不会自动释放,得手动设requires_grad=False。最后实在不行就换torch.inference_mode()替代no_grad,这个会强制走推理路径,不建图,能省不少。
你试试把append的结果改成存到普通list里但别保留整个tensor,比如只存numpy或者直接写文件,这可能是list里累积了计算图。另外DataLoader的num_workers设成0看看,有时候子进程的缓存也会吃显存,还有batch里如果有变长序列,padding也可能导致显存碎片化。监控的话pytorch自带torch.cuda.memory_summary()挺好用的,能看到每个阶段分配了多少,另外用nvidia-smi -l 1配合看实时变化。