最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条遇到这种情况先别急着怀疑是缓存没清,大概率是DataLoader的num_workers在搞鬼。你试试把num_workers设成0,如果显存不涨了那就是子进程的显存副本没被正确回收,这问题在PyTorch老版本里特别常见。另外你检查下是不是把整个batch的loss或者中间变量存进了列表,有时候append的是tensor而不是标量,引用链断不掉,empty_cache根本没用。我之前跑GPT模型也遇到过,最后发现是torch.no_grad()只包住了前向,但Embedding层在某些版本里会保留梯度图,建议把整个推理函数用更严格的上下文管理器包起来。至于监控工具,pytorch_memlab的MemReporter能打印每个模块的显存占用,或者用tracemalloc配合torch.Tensor记录id来手动查引用,但说实话自己写个循环打印每层的输出shape和显存增量更直观。还有个坑是BatchNorm在推理模式下的running_mean更新,如果你的模型没调eval(),它会在每个batch更新缓冲区,显存也会缓慢增长,虽然不常见但值得排查一下。最后实在不行就换用torch.inference_mode()替代no_grad(),这个是纯推理模式,连autograd的元数据都不生成,省掉一大块开销。
这种情况我之前也踩过坑,罪魁祸首往往不是模型本身,而是DataLoader的worker进程和Python的垃圾回收机制。你试试把dataloader的num_workers设为0,然后推理循环里加个torch.cuda.synchronize(),看显存是否还涨。另外,append结果到列表确实会累积,但2G到10G的涨幅不太像单纯列表的问题,更像是cuda caching allocator没有释放碎片。监控工具的话,pytorch自带torch.cuda.memory_summary()能看块分配,或者用nvidia-smi配合pynvml查进程级占用,引用计数用gc.get_objects()配合objgraph能画出来。还有个野路子,把推理函数包进子进程,每个batch结束后主动杀进程,彻底释放显存。
我之前也踩过类似的坑,最后发现根本不是pytorch没释放,而是DataLoader的num_workers坑人。你试试把num_workers设成0,或者把pin_memory关掉,有时候这几个进程会一直持有CUDA上下文,显存就被吃掉了。另外你那个append结果到列表的操作,如果列表里存的是tensor而不是numpy或者python对象,那整个计算图可能一直被引用着,虽然你调了no_grad,但tensor本身还是占显存的,建议每轮推理完直接转成float()或者cpu().numpy()再存。torch.cuda.empty_cache()其实只是把缓存还给pytorch的分配器,不是还给系统,所以你看进程占用可能没变化,得看nvidia-smi里的“程序占用”而不是“显存使用”。还有个比较隐蔽的问题,如果你用了transformers库,它有些版本会在forward时缓存hidden_state,哪怕你只取logits,那个变量也会被保存,建议用torch.no_grad()包裹整个推理循环,然后每次batch之后加一句torch.cuda.synchronize(),再配合gc.collect()试试。至于监控工具,pytorch有个torch.cuda.memory_snapshot(),可以dump出分配细节,但看引用计数比较麻烦,可以试试pympler或者tracemalloc,不过对GPU tensor可能不灵。实在不行,就把输入输出都显式del,然后每隔几十个batch重启一下子进程,粗暴但有效。
这问题我踩过类似的坑,感觉不是del没生效,而是DataLoader的num_workers在作祟,子进程会持有batch数据引用,加上推理结果append到列表里,列表本身也在占内存。你可以先用torch.cuda.reset_peak_memory_stats()配合nvidia-smi看下是不是pytorch缓存池没释放,而不是真在涨。另外试试把batch size降到1跑几百个样本,如果显存稳了,那就是batch内动态图累积问题,考虑用torch.inference_mode替代no_grad,会省不少开销。至于引用计数,pytorch没有现成工具,但我之前用gc.get_objects()配合torch.Tensor类型过滤,能粗略看到哪些张量还活着,你可以试试。
这问题我熟,之前调一个GPT2生成式模型的时候也遇到过一模一样的坑。你那个torch.no_grad()和empty_cache()其实都做了,但大概率问题出在DataLoader的num_workers上,如果开了多进程加载,每个worker会持有独立的CUDA context,而且默认的persistent_workers=False的话,每个epoch结束worker销毁时显存不会立刻归还,得等整个进程结束才释放,你试试把num_workers设成0或者1,看看显存曲线是不是瞬间就平了。另外你说推理完结果append到列表,这个列表如果一直累积,里面存的是Python对象还好,但如果某些中间tensor被无意间保留了引用(比如通过loss或者logits的grad_fn链),那del跟empty_cache根本救不回来,建议你在append之前先对结果做cpu().numpy()或者tolist(),彻底断开计算图。至于监控工具,别用pytorch的debug工具了,直接pip装个pympler,它能递归追踪对象引用,比torch的memory_snapshot直观得多。还有个偏方,你可以在每个batch结束后强制跑一次torch.cuda.synchronize()再empty_cache,有时候异步执行会让显存看起来一直涨,实际是积压的任务没清完。最后检查下模型里有没有dropout或者batch_norm在eval模式下还保留训练状态的缓存,某些自定义层会自己存东西。
这种情况大概率不是模型没释放,而是DataLoader的num_workers在后台预取数据时,张量会驻留在CPU侧,加上你每批的输入输出如果没及时从显存搬回内存,累积下来就会爆。建议先确认下是不是梯度缓存的问题,推理时model.eval()加上torch.inference_mode()比no_grad更彻底,能关掉自动求导的中间节点。另外你可以用nvidia-smi -l 1盯一下显存曲线,看是不是锯齿状增长,如果是那就是每批的tensor没被回收。至于引用计数,pytorch的memory_stats接口能看allocated_bytes和reserved_bytes的差值,大概能判断是不是碎片化问题。我上次遇到类似情况是DataLoader的pin_memory=True导致的,关掉就好了,你可以试下。
我之前跑GPT类模型也踩过这个坑,你试试把DataLoader的num_workers设成0,然后推理循环里显式把input_ids那些tensor移回cpu再del,光靠empty_cache其实不解决根本问题。另外检查下是不是有梯度被意外保留了,虽然no_grad了但某些op可能还是会在图上挂引用。监控工具的话用pytorch的torch.cuda.memory_snapshot()看分配段最直观,或者直接pip装个pympler查python对象引用。还有个野路子,把batch size降到1跑几百个样本对比下显存曲线,能快速排除是不是数据加载那边在累积。
八成是DataLoader的worker进程缓存了CUDA上下文,试试把num_workers设成0或者用persistent_workers=False。
pytorch的缓存分配器不会自动归还显存给驱动,建议用torch.cuda.memory_summary()看下峰值分配在哪。
我之前也遇到过类似情况,重点其实不在输入输出,而是你那个append的列表,如果一直存着所有结果,显存当然只增不减,试试把结果直接写文件或者用生成器。另外torch.no_grad()只管梯度,如果模型里有dropout或者BN的缓存,或者用了transformers的forward里带past_key_values,也会造成累积,检查下是不是有隐藏状态没关。至于监控工具,可以试试pytorch_memlab,能按行定位张量分配,比看引用计数直观多了。
这种情况大概率不是模型缓存的问题,而是DataLoader的num_workers在搞鬼,默认0的话每个batch的pin_memory和GPU拷贝可能没及时回收。你可以试试在推理循环里加torch.cuda.synchronize()强制同步,然后监控一下是不是列表在无限膨胀,因为append的只是tensor的引用,如果后续没做detach或者转成numpy,整个计算图都会留在显存里。工具方面pytorch自带torch.cuda.memory_snapshot()能看每个分配器的状态,或者用nvidia-smi配合py-spy看进程,不过最直接的办法还是把每个batch的output.cpu()再del,然后看一下显存曲线是否稳定。另外如果用了transformers库,记得关掉model.eval()里的return_dict,有时候hidden_state会被缓存。
之前跑检测模型也遇到过,后来发现是DataLoader的num_workers开太多,子进程缓存没回收,试试把worker数调成0或者用torch.cuda.set_per_process_memory_fraction限制一下。另外append列表如果后续不用,不如直接存到磁盘,列表本身也会占内存。监控引用的话,pytorch的memory_snapshot挺管用的,能看每个step谁在申请显存。
试试把累积的results列表清空或者改存到磁盘,大概率是这里占着显存不释放。
用pytorch的memory_summary()看看每步分配情况,比瞎猜快多了。
这问题我之前也踩过,大概率不是模型没释放,而是DataLoader的num_workers在搞鬼,子进程会复制一份CUDA上下文,跑完不自动回收。你试试把num_workers设成0,或者用torch.cuda.synchronize()强制同步一下再看显存。另外保存结果那个列表如果一直累积,最好改成batch处理完就写盘,别全放内存里。监控的话可以试试pytorch_memlab,能按行号定位到具体分配点,比裸看nvidia-smi好用多了。
我之前也踩过这个坑,多半不是模型没释放,而是DataLoader的num_workers在搞鬼,worker进程会缓存一部分数据,加上CUDA的缓存池本身就不怎么还显存给系统,empty_cache只是清空未用的块。你可以试试在推理循环里固定用同一个batch张量,别每次新建,或者把batch_size调小看看涨幅是不是线性变缓。另外torch.cuda.memory_summary()能看内存分配细节,比盯引用计数直观多了,我一般直接打印这个定位是哪个op在累积。
试试把结果列表改成先存盘再清空,另外检查下是不是DataLoader的num_workers没设对,容易积压显存。
pytorch的缓存分配器就这样,empty_cache治标不治本,关键看有没有tensor被意外引用。建议用nvidia-smi盯下每个batch的峰值,做个二分定位。
这个问题大概率不是没释放,而是DataLoader的num_workers在搞鬼,子进程会复制CUDA上下文,如果没设num_workers=0或没用if name==__main__保护,每个epoch都会累积显存碎片。另外append列表本身不占显存,但如果你在循环里保留了loss或者梯度相关的中间变量,试试在batch结束加个torch.cuda.synchronize()看下峰值。监控工具的话,pytorch的torch.cuda.memory_snapshot()能看分配器细节,或者用nvidia-smi配合py-spy看栈。我上次遇到类似情况是无意中把模型输出了embedding没detach,你查查是不是有张量被隐式保留在计算图里了。
你这情况大概率不是模型没释放,而是DataLoader的num_workers在偷偷累积显存,特别是每个worker都持有一份模型副本时。试试把num_workers设成0或者1,然后推理循环里用with torch.inference_mode()替代no_grad,顺便把append改成存到numpy再转list。监控的话可以试试pytorch_memlab,能打印每个张量的分配位置,比手动查引用计数直观多了。我之前跑GPT也遇到过,最后发现是loss清零的问题,你如果没算loss就忽略这条。
我之前跑生成式模型也踩过这个坑,表面看是内存涨,其实多半不是没del干净,而是DataLoader的num_workers在后台预取数据,加上CUDA缓存策略本身就比较激进,PyTorch不会立刻把显存还给系统,而是留着复用。你试的torch.cuda.empty_cache只是清空缓存池,如果张量还在计算图里或者被列表引用着,那缓存根本清不掉。重点检查你append结果的那个列表,如果存的是Tensor而不是转成numpy或Python标量,那整个计算图的引用就一直挂着,每个batch的中间激活都跟着活下来,显存不涨才怪。建议推理时把整个forward包在with torch.no_grad()里,然后每个batch结束把输入输出都转成CPU上的numpy再存,最后再调empty_cache。想监控张量引用的话,可以用gc模块配合torch.cuda.memory_snapshot()抓当前分配块,或者直接上nvidia-smi看进程行为,但最省事的是跑一小段代码,在循环里打印torch.cuda.memory_allocated()看是阶梯式增长还是瞬时飙升,前者就是引用泄漏,后者可能是动态图缓存。我怀疑你模型里可能用了dropout之外的随机层,比如Bert的attention mask在某些版本下有缓存问题,试试把model.eval()放在循环外,并且确保输入不需要梯度,再把batch里的padding长度统一,有时候显存涨跟序列长度抖动也有关系。
之前跑生成模型也遇到过类似问题,后来发现是DataLoader的num_workers开太多,子进程会复制一份CUDA上下文,虽然不占显存但会累积句柄。你可以试试把worker设为0,或者用torch.cuda.reset_peak_memory_stats()看下峰值是不是每步都在涨。另外检查下是不是把loss或者梯度累加到了某个全局变量里,BERT的中间变量一般不会泄漏。监控工具的话,pytorch的memory_profiler或者nvidia-smi配合pynvml轮询就够用,不过引用计数得自己写hook,比较麻烦。
如果你确定每个batch的输入输出都只活在循环内,那大概率是DataLoader的预取机制在搞鬼,把batch_size调小或者pin_memory关掉试试。实在不行就换个思路,用torch.jit.script或者onnx导出推理,显存占用会稳很多。
这问题我踩过类似的坑,八成不是模型没释放,而是DataLoader的num_workers在搞鬼,子进程会复制一份CUDA上下文,跑完又不回收。你可以试试把num_workers设成0,或者用torch.utils.data.dataloader.default_collate手动清理,看显存曲线稳不稳。至于监控张量,pytorch有个torch.cuda.memory_snapshot()能看分配器内部,但引用计数得靠gc模块配合,写个回调打日志比瞎猜快。另外你append结果那个列表如果后续没用,记得整个list也del掉,有时候是Python容器持有张量引用导致没法回收。