最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条这种情况大概率不是模型没释放,而是DataLoader的num_workers在作怪,每个worker都会持有自己的显存上下文,批量推理时叠加起来就很恐怖。你可以试试把num_workers设为0,或者用torch.inference_mode()替代no_grad,前者会禁用更多梯度跟踪机制。另外,检查下是不是把整个batch的output都append到列表了,那个list会一直持有张量引用,显存自然只增不减。想监控的话,pytorch的torch.cuda.memory_snapshot()能看每个张量的分配情况,或者用nvidia-smi的python绑定pynvml,比逐个查引用计数直观很多。我之前遇到过类似问题,最后是发现模型里有个隐藏的dropout缓存没关,换成eval模式就好。
看到这个显存曲线我还以为是我自己发的帖子,之前跑GPT2也踩过一模一样的坑。你试的这几个方法其实都没打在点上,torch.no_grad()只是不存梯度,但不代表中间激活值不占缓存,而且DataLoader的num_workers>0时子进程会持有显存副本,这锅经常被忽略。建议先看看是不是dataloader的prefetch_factor设太大了,另外把推理循环包在with torch.inference_mode():里比no_grad更彻底。排查工具的话,pytorch的torch.cuda.memory_snapshot()可以看每个张量的内存段,或者直接上nvidia-smi的python绑定pynvml,但说实话最直接的办法还是把batch_size降到1跑一次看曲线,排除是不是批次累积导致的碎片化。
遇到过类似的坑,不过我之前是GPT类模型,最后定位到问题不在显存没释放,而是DataLoader的num_workers开太多,每个worker都会保留一部分缓存,加上推理时pytorch的caching allocator本来就不会立刻把显存还给驱动,你调empty_cache只是把空闲块归拢,但已经分配的块还是占着。你试试把batch size调成1跑100个样本看显存曲线,如果还是涨,那基本就是模型内部有变量在累积,比如某些层用了cache或者你代码里把中间结果append到了某个list没清理。
关于手动清空输入输出,其实没必要,只要确保每个batch的tensor没有循环引用,pytorch在下一轮迭代时会复用显存块,真正该查的是有没有在循环外保留了对某个tensor的引用,比如你append到列表里的结果,如果列表一直不释放,那这些张量就永远活着。你可以把结果改成只存numpy数组或者直接写文件,别在内存里攒着。
工具方面,nvidia-smi的显存占用只能看全局,想看每个tensor的引用计数可以试试pytorch的torch.cuda.memory_snapshot(),能dump出所有分配块的信息,配合脚本分析哪一步在涨。另外也可能跟你的模型用了transformers库有关,它的BertModel在forward时会把attention的hidden state存下来用于梯度,但你推理时没开no_grad的话,这些中间变量不会自动清,你虽然用了no_grad,但检查下是不是在model.eval()之外又调了某个需要梯度的函数。
我之前也踩过这个坑,最后发现根本不是模型缓存的问题,而是DataLoader的num_workers开太多,子进程里隐式引用了CUDA上下文,显存被多个worker占着不还。你试试把num_workers设成0,或者用persistent_workers=False,大概率能解决。另外torch.no_grad()只关梯度,但如果你推理时还开着model.eval(),某些层比如BatchNorm还是会缓存统计量,不过你的BERT应该没这个问题。手动del加empty_cache确实没啥用,因为PyTorch的缓存分配器不会立刻把显存还给驱动,你看到涨到10G可能只是它自己留着复用,不代表真泄漏,但OOM说明确实有东西没释放。建议你用pytorch的profiler或者torch.cuda.memory_snapshot()看下是哪个操作在分配,我之前发现是loss计算里的某个中间变量没被清,但你说推理阶段应该没loss。还有就是结果append到列表里,如果列表最后要保留所有预测值,那每个tensor都得detach().cpu()再存,不然GPU上的引用一直挂着,这个最容易忽略。你可以先跑一个batch然后看显存,再跑十个看增量,如果每个batch涨得差不多,那就是batch内部没释放;如果是跳着涨,去查DataLoader和缓存池。我最后是靠tracemalloc加torch.cuda.memory_summary()组合定位到是一个外部库在偷偷创建tensor,所以建议你查下是不是有别的包引入了隐式调用。
这问题我太熟了,之前跑GPT类模型也遇到过一模一样的坑。你试的那几个方法其实都没治到根上,因为PyTorch推理时显存上涨很多时候不是Python层的张量没释放,而是CUDA缓存和cuDNN的自动调优在搞鬼。cuDNN会为不同shape的输入缓存多种算法,你的DataLoader如果每个batch的padding长度不一样,它就会不断开辟新显存块,旧的不回收,越积越多。另一个常见元凶是loss或者梯度相关的中间变量,即使你用了no_grad,某些操作比如softmax或者layer norm的临时buffer也可能被保留。建议你先把batch里的输入序列pad到固定长度试试,同时关掉cudnn.benchmark,看看涨幅是不是明显下降。至于监控工具,别费劲去数引用计数了,直接上pytorch_memlab或者用torch.cuda.memory_summary()看每个阶段的显存分配,一眼就能看出是哪个操作在狂吃显存。还有个野路子,把推理逻辑包在一个函数里,每次调用完强制清一次cache,配合gc.collect(),虽然治标不治本,但能撑住长跑。
试试关掉cudnn的benchmark,还有检查下DataLoader的num_workers是不是在累积占显存。
遇到这种显存只涨不降的情况,我第一反应不是模型没释放,而是DataLoader那边在搞鬼。你试试把batch_size改成1跑一下,如果显存涨得慢了,基本就坐实了是累积的中间变量问题——尤其是BERT这种带attention的,每个token的梯度虽然关了,但某些缓存张量(比如kv cache或者dropout mask)在长序列下会悄悄留在计算图里。
我上次做类似任务时发现,torch.no_grad()只能防止梯度计算,但如果你在循环里把每个batch的loss或者logits存进list,那个list会一直持有张量的引用,哪怕你del了输入输出,只要list里有东西,显存就不会还给CUDA。建议你改成只存detach后转成numpy或者python float的标量,这样张量本体能被及时回收。
另外,empty_cache()不是用来救命的,它只是把缓存池里的碎片整理一下,不是真的释放显存给系统,你真正要盯住的是有没有张量被全局变量或者容器引用着。想定位的话,不用自己写引用计数器,直接装个pytorch_memlab,它能打印出每个张量的内存占用和所属的代码行,跑一次循环就能看到是哪个变量在累积。
我也遇到过类似情况,后来发现是torch.utils.data.DataLoader的num_workers>0时,子进程会复制GPU上下文,虽然每个worker只处理一个batch,但它们的显存缓存不会自动清,得在worker_init_fn里手动empty_cache。你可以先试试把所有推理逻辑写进一个函数,函数返回时局部变量全销毁,再把结果列表用numpy存,基本能解决。
大概率是DataLoader的num_workers在作怪,子进程不释放缓存,试试把worker数调成0或2。
用pytorch的memory_profiler或者torch.cuda.memory_summary()看下峰值在哪,比手动清空靠谱。
大概率是DataLoader的num_workers没设0,子进程的缓存一直没回收,试试设成0看还涨不涨。
pytorch的显存碎片化也会这样,可以先试试torch.cuda.set_per_process_memory_fraction限制到2.5G,看是不是真泄漏。
这问题我之前也踩过坑,大概率不是模型缓存,而是DataLoader的num_workers在搞鬼,每个worker都会复制一份显存上下文,样本多了自然爆。你可以把num_workers设成0试试,或者用torch.utils.data的BatchSampler手动控制流。另外append列表本身不占显存,但如果你把logits也存进去了,梯度图可能还挂着,推理时记得加torch.inference_mode()代替no_grad。监控引用计数的话,pytorch有个torch.Tensor的grad_fn可以看,但更实用的是用nvidia-smi盯显存曲线,配合gc.collect()看有没有周期性回落。
这问题我之前也踩过坑,多半不是模型缓存,而是DataLoader的num_workers在搞鬼,每个worker会复制一份模型和CUDA上下文,显存自然就叠上去了。你可以先把num_workers设成0试试,还有推理时别用batch_size=32,改成1或者梯度关闭后逐条过,内存碎片会少很多。监控张量引用的话,pytorch有个torch.cuda.memory_summary()能看个大概,但真要追引用计数得用gc.get_objects()配合tracemalloc,写个小脚本定期打印一下就能发现是不是有某个list在偷偷累积历史结果。另外你append的结果列表如果后续还要用,建议转成numpy存CPU,别留着GPU上的tensor,这玩意儿最吃显存。
这种情况大概率不是模型缓存的问题,而是DataLoader的num_workers在作怪,你试试把worker数设成0或者1,很多时候多进程加载数据会导致显存被复制几份。另外append列表本身也会占内存,建议改成先预分配列表或者直接用numpy数组存结果,省得Python对象堆积。torch.cuda.empty_cache只是释放缓存块,不会管实际还在引用的张量,所以排查的话可以用pytorch的memory_summary()看下内存分配热点,比手动数引用计数直观多了。我之前遇到过类似问题,最后发现是输入文本长度不固定导致每次动态padding,累积了一些中间变量没被回收,建议把batch里的序列统一截断或填充到固定长度试试。
我之前也踩过这个坑,问题大概率不在输入输出上,而是DataLoader的num_workers在每次迭代时都会重新创建进程,每个worker都会拷贝一份模型和CUDA上下文,几百个batch下来显存自然就炸了。建议把num_workers设成0试试,或者用torch.utils.data.dataloader的persistent_workers参数。另外,append到列表的Tensor会一直占着显存,如果只是要保存结果,直接转成numpy再存,别让Tensor留在GPU上。监控引用计数的话,pytorch的torch.cuda.memory._snapshot()能看分配历史,但更直接的办法是每跑几个batch就打印一下torch.cuda.memory_allocated(),看增长趋势就知道是哪个环节在漏了。
大概率是DataLoader的num_workers在累积显存,试试把worker数设成0或者关掉pin_memory。
用pytorch的torch.cuda.memory_summary()看下是缓存还是泄漏,顺便查查是不是有梯度被意外保留了。
这种情况大概率不是模型缓存没释放,而是DataLoader的num_workers在搞鬼,多进程预取数据时每个worker都会拷贝一份CUDA上下文,跑几百个batch后显存就被吃满了。你可以试试把num_workers设为0,或者在推理循环里显式把batch的input_ids和attention_mask移回CPU再del,这样能切断计算图的引用。监控张量引用的话,pytorch的torch.cuda.memory._snapshot()挺好用的,能dump出每个分配点的调用栈,比手动猜快多了。另外如果用了梯度累积或者模型里有dropout,记得确认下eval模式有没有真正关掉autograd,有时候no_grad包裹的不够彻底也会累积中间变量。
这种问题八成不是模型缓存,而是DataLoader的worker进程在偷偷累积显存,尤其是num_workers>0的时候,每个子进程会复制一部分CUDA上下文,跑几百个batch后就开始爆。你可以试试把num_workers设成0,或者用torch.utils.data.DataLoader的persistent_workers=False,看显存曲线是否平稳。另外,你append的列表如果一直持有tensor,哪怕只是用来存结果,也会让显存只增不减,改成先转成numpy或者Python标量再存。监控张量引用的话,pytorch的torch.cuda.memory_snapshot()能看每个分配块的栈信息,配合memory-profiler这个库挺好用的,我之前定位过类似问题,最后发现是某个中间变量被闭包引用住了,手动清空根本没用。
这问题我踩过类似的坑,多半不是模型缓存,而是DataLoader的num_workers在搞鬼,子进程会复制一份CUDA上下文,显存就只增不减。你试试把num_workers设成0,或者用torch.utils.data.dataloader的persistent_workers=False看看。另外如果你在循环里把每批结果append到列表,那个列表本身也会占显存,建议改成直接存到CPU内存或文件里。监控引用计数的话,pytorch的memory_stats接口能看allocated_bytes和reserved_bytes,比手动查张量快多了。
我之前也踩过类似的坑,特别是跑长文本分类的时候。你试的那几个方法其实都治标不治本,torch.no_grad()只能关梯度计算,但如果你在推理时还保留了模型输出的梯度或者中间变量,显存照样涨。最关键的问题可能出在DataLoader的num_workers上,如果开了多进程加载数据,每个worker都会拷贝一份CUDA上下文,显存占用会叠加,而且释放不及时。另外你每批结果append到列表里,如果这个列表一直保留着,那所有batch的输出张量都活着,相当于把整个测试集的特征都攒在显存里了。建议你把结果先转成numpy或者直接写进磁盘,别让PyTorch张量留在内存里。还有个容易被忽略的点,BERT模型本身有positional encoding和attention mask,如果每次前向都重新创建这些常量,它们也会累积在计算图里,最好把input_ids和attention_mask都提前放到cuda上,然后用with torch.inference_mode()替代no_grad,这个模式会禁用梯度跟踪和版本计数,省不少显存。至于监控工具,你可以用pytorch_memlab的MemReporter,能打印每个张量的内存占用和引用关系,比手动敲代码查方便多了。我上次就是靠它发现有个中间变量被循环外部的引用给hold住了,清掉之后显存直接稳如老狗。
这种问题我碰到过好几次,多半不是模型没清干净,而是DataLoader的num_workers在搞鬼,每个worker都会保留一份缓存上下文,加上梯度默认还在计算图里,建议推理时把model.eval()和with torch.no_grad()包住,同时把batch的input_ids和attention_mask用完立刻置None。另外你可以试试torch.cuda.memory_summary(),能看每块显存分配的细节,比empty_cache直观多了。如果还涨,检查下是不是有list在累积结果导致Python对象没释放,直接改成预分配numpy数组试试。
这问题我之前也踩过坑,多半不是模型缓存,而是DataLoader的num_workers在背后搞鬼,每个worker都会持有自己的显存副本。你可以试试把num_workers设成0,或者用torch.cuda.synchronize()卡一下看显存曲线是否还涨。另外append结果到list这个操作本身不占显存,但如果你把GPU tensor直接存了,梯度虽然关了,变量还是会挂在计算图里。想监控引用的话,pytorch的memory_stats能看当前分配,但真定位还得靠pdb打断点或者用tracemalloc,不过最粗暴的办法是每个batch后打印一下torch.cuda.memory_reserved(),看是不是有峰值没回落。