最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条大概率是DataLoader的worker进程没关干净,试试把num_workers设成0跑一下看还涨不涨。
监控张量引用可以用pytorch的memory_snapshot,但你这情况八成是缓存碎片,关掉cudnn.benchmark试试。
这个问题我之前也踩过坑,最后发现根本不是缓存没清,而是DataLoader的num_workers在搞鬼。你试试把num_workers设成0,或者用torch.cuda.Stream隔离一下每个batch的推理,看看显存曲线是不是就平了。另外你提到append到列表里,如果那个列表后面没被消费,里面存的是tensor的话,即使del了输入输出,列表本身还持有引用,显存也不会还回去——最好转成numpy或者Python对象再存。至于监控工具,torch.cuda.memory_snapshot()能看每个张量的分配栈,但引用计数得靠tracemalloc配合guppy3才能查,调试成本有点高。我怀疑你那个2G是不是包含了模型参数和CUDA上下文,如果单样本推理时显存没降下来,可能是cuDNN的autotune在自适应调整算法,它会在推理时缓存多个benchmark结果,这种只能靠torch.backends.cudnn.benchmark=False来关掉。还有个冷门原因,如果你用了transformers库,某些版本的tokenizer会在推理时往模型里塞额外的buffer,你可以看看模型对象的_dict_里有没有非参数的持久化张量。总之先别手动清空每个batch,那样反而会打断显存复用,优先排查数据加载和列表引用这两块。
大概率是你把每批loss或输出都存进list了,梯度图没释放,试试推理时用torch.inference_mode()替代no_grad。
用pytorch的memory_profiler钩子能查张量分配,但你这情况更像DataLoader的worker缓存问题,调num_workers=0验证下。
你append的是啥?如果是把每批的输出张量直接存列表里,那显存肯定一直涨啊,把结果转成cpu的numpy或者python标量再存。另外DataLoader的num_workers多的话,worker里也可能留着cuda上下文,试试设成0排查一下。监控的话可以看看torch.cuda.memory_summary(),或者用py3的gc加weakref自己查引用,比啥工具都直接。
是不是结果列表里存了带梯度的张量,append时没detach?显存涨多半是计算图没断干净。
你这情况我遇到过,大概率不是模型本身泄漏,而是你把每批的输出append到列表里了。如果append的是tensor,哪怕在no_grad下,这些tensor仍然持有计算图之外的内存,而且一直挂在Python列表里,GC根本回收不了。几百个batch累计下来,显存不炸才怪。可以试试每批推理完直接取.cpu().numpy()或者.item()再存,把GPU上的引用断掉。另外del和empty_cache只是把空闲块还回去,如果还有活引用,empty_cache也没用。监控的话可以用torch.cuda.memory_summary()看allocated和reserved的差异,或者用tracemalloc配合gc.get_objects()查Python侧对象增长。还有个坑是DataLoader的pin_memory和num_workers,如果worker里持有tensor也会拖住显存。建议先确认列表里存的是什么,十有八九问题就在那儿。
你append结果到列表里,如果存的是带梯度的tensor或者挂在计算图上的输出,那显存肯定一直涨。光no_grad不够,得把结果detach().cpu()再存,不然整个图都被引用着。另外DataLoader的pin_memory和num_workers有时也会留缓存,可以试试关掉看看。监控的话torch.cuda.memory_summary()能看个大概,真要追引用计数得上objgraph或者tracemalloc这类工具了。