在不修改网络结构的情况下, 有如下操作:
def inplace_relu(m): classname = m.__class__.__name__ if classname.find('ReLU') != -1: m.inplace=True model.apply(inplace_relu)
2.进一步,比如ResNet 和 DenseNet 可以将 batchnorm 和relu打包成inplace,在bp时再重新计算。使用到了pytorch新的checkpoint特性,有以下两个代码。由于需要重新计算bn后的结果,所以会慢一些。
3. 每次循环结束时 删除 loss,可以节约很少显存,但聊胜于无。可见如下issue
Tensor to Variable and memory freeing best practices
4. 使用float16精度混合计算。我用过 @NVIDIA英伟达 apex,很好用,可以节约将近50%的显存,但是要小心一些不安全的操作如 mean和sum,溢出fp16。
补充:最近我也尝试在我CVPR19的GAN模型中加入fp16的训练,可以从15G的显存需求降到约10G,这样大多数1080Ti等较为常见的显卡就可以训练了。欢迎大家star一波 https://github.com/NVlabs/DG-Net
5. 对于不需要bp的forward,如validation 请使用 torch.no_grad , 注意model.eval() 不等于 torch.no_grad() 请看如下讨论。
'model.eval()' vs 'with torch.no_grad()'
6. torch.cuda.empty_cache() 这是del的进阶版,使用nvidia-smi 会发现显存有明显的变化。但是训练时最大的显存占用似乎没变。大家可以试试。
How can we release GPU memory cache?
另外,会影响精度的骚操作还有:
把一个batchsize=64分为两个32的batch,两次forward以后,backward一次。但会影响 batchnorm等和batchsize相关的层。
相关链接:老外写的提高pytorch效率的方法,包含data prefetch等
Optimizing PyTorch training code
最后感谢大家看完~欢迎关注分享点赞~也可以check我的一些其他文章
郑哲东:【新无人机数据集】从 行人重识别 到 无人机目标定位
郑哲东:利用Uncertainty修正Domain Adaptation中的伪标签
郑哲东:NVIDIA/悉尼科技大学/澳洲国立大学新作解读:用GAN生成高质量行人图像,辅助行人重识别
来说说实际点的吧。
首先你要放弃Python的思维
用C语言的思路去写pytorch代码。
思路一:显存释放
制作一个显存管理类,可以根据需要自行定制。
我贴一份我的解决思路,想要的可以直接抄。
class MemCache: @staticmethod def byte2MB(bt): return round(bt / (1024 ** 2), 3) def __init__(self): self.dctn = {} self.max_reserved = 0 self.max_allocate = 0 def mclean(self): r0 = torch.cuda.memory_reserved(0) a0 = torch.cuda.memory_allocated(0) f0 = r0 - a0 for key in list(self.dctn.keys()): del self.dctn[key] gc.collect() torch.cuda.empty_cache() r1 = torch.cuda.memory_reserved(0) a1 = torch.cuda.memory_allocated(0) f1 = r1 - a1 print('Mem Free') print(f'Reserved {MemCache.byte2MB(r1 - r0)}MB') print(f'Allocated {MemCache.byte2MB(a1 - a0)}MB') print(f'Free {MemCache.byte2MB(f1 - f0)}MB') def __setitem__(self, key, value): self.dctn[key] = value self.max_reserved = max(self.max_reserved, torch.cuda.memory_reserved(0)) self.max_allocate = max(self.max_allocate, torch.cuda.memory_allocated(0)) def __getitem__(self, item): return self.dctn[item] def __delitem__(self, *keys): r0 = torch.cuda.memory_reserved(0) a0 = torch.cuda.memory_allocated(0) f0 = r0 - a0 for key in keys: del self.dctn[key] r1 = torch.cuda.memory_reserved(0) a1 = torch.cuda.memory_allocated(0) f1 = r1 - a1 print('Cuda Free') print(f'Reserved {MemCache.byte2MB(r1 - r0)}MB') print(f'Allocated {MemCache.byte2MB(a1 - a0)}MB') print(f'Free {MemCache.byte2MB(f1 - f0)}MB') def show_cuda_info(self): t = torch.cuda.get_device_properties(0).total_memory r = torch.cuda.memory_reserved(0) a = torch.cuda.memory_allocated(0) f = r - a print('Cuda Info') print(f'Total {MemCache.byte2MB(t)} MB') print(f'Reserved {MemCache.byte2MB(r)} [{MemCache.byte2MB(self.max_reserved)}] MB') print(f'Allocated {MemCache.byte2MB(a)} [{MemCache.byte2MB(self.max_allocate)}] MB') print(f'Free {MemCache.byte2MB(f)} MB')
然后这么使用
mc = MemCache() mc.show_cuda_info() mc['X_train'], mc['Y_pred'] = NNMaker.load_nnBuffer_mono(Fpath, mode='train') mc['X_test'], mc['Y_pred'] = NNMaker.load_nnBuffer_mono(Fpath, mode='test') mc['X_pred'], mc['Y_pred]'] = NNMaker.load_nnBuffer_mono(Fpath, mode='pred') mc['X_train_tensor'] = mm.predict(ModelManager.aray2torch(mc['X_train'])) mc['X_test_tensor'] = mm.predict(ModelManager.aray2torch(mc['X_test'])) mc['X_pred_tensor'] = mm.predict(ModelManager.aray2torch(mc['X_pred'])) mc['pred_Y_train'] = mc['X_train_tensor'][:, 0].tolist() mc['pred_Y_test'] = torch.cat([mc['X_train_tensor'][-1:, 0], mc['X_test_tensor'][:, 0]], dim=0).tolist() mc['pred_Y_pred'] = torch.cat([mc['X_test_tensor'][-1:, 0], mc['X_pred_tensor'][:, 0]], dim=0).tolist() # 训练 mc.mclean() mc.show_cuda_info()
记得每一个epoch,在结束的时候都清空内存。
mclean是清空所有,如果不想全部清空,你还可以用del方法手动清空。
总结一句话,用完的东西立刻free memory。
所有的torch.tensor 都不要直接裸在外面,放到MemoryCache的字典里,用完之后,立刻清除。
PS:本来显存爆炸,做不起来的,这么一通操作之后完美运行,萌新大佬均可使用,没有太多技巧。
思路二:古典方法
一整块tensor如果放不进GPU,就切小块分批训练。当然这会造成运行IO时间稍长的问题。
能用卷积的一定要用卷积,卷积可以快速缩小input tensor的大小。避免在大tensor之间使用fully connected。
总之就是input拿到,尽量快速的把tensor大小收敛,然后输出的时候尽量快速放大。
很多图片缩小个1-2倍没啥区别的,直接缩小切块。时间序列使用slide window,moving average(本质还是缩小切块)
激活函数relu解释了,永远滴神。除非你能证明这个东西一定要用其他激活函数,否则就用relu。
思路三:神经网络分析师
仔细查看,神经网络试在哪个步骤的时候显存激增,到底有没有必要使用这么多显存。没必要的就砍掉。查看有没有leek memory 有的话就清空。把每个在一些关键的步骤打印log,然后查看通过分析log的异常数字,来反推哪个结构出了问题
每个层的参数和数据大小全部列出来,然后对着每层的tensor大小思考,哪个可以小一点。
思路四:硬盘换显存
基本上用不到,但如果你实在穷得买不起GPU,又想训练大模型,这是最后的方法。
也就是你训练一小块,存进硬盘,然后再读另一小块训练,结果存硬盘。然后再把之前存进硬盘的结果读起来训练下一层,然后存硬盘。最终这个方法能解决一切不能训练的问题。
PS:这是玩黄油的时候给我的启发,当时玩CM3D2,电脑跑不起来,于是使用了virtual Memory的思路。
只要到这一步,你就可以暴力破解一切显存问题。硬盘和显存可以互换。
你还可以在硬盘和GPU之间建立RAM cache。
当然缺点也是有的,IO会变得特别长,甚至远超模型真正训练的时间。
节省显存方面,欢迎关注我们团队最近开源的工作:
个人认为这个工作把单卡训练,或者是数据并行下的显存节省做到极致了~
这里主要介绍一下单机训练上的思路。
随着模型越来越大,GPU 逐渐从一个计算单元变成一个存储单元了,显存的大小限制了能够训练的模型大小。微软的 DeepSpeed 团队提出我们其实可以把优化器状态(Adam 的 momentum 和 variance)放在 CPU 上,用一个实现的比较快的 CPU Adam 来做更新,这样既不会变慢很多,也可以明显省出来很多空间。我们把这个思想再往前推一步,我们是不是可以只把需要计算的模型参数放在 GPU 上,其余的模型参数,优化器状态都放在 CPU 上,这样就可以尽最大能力降低对显存的需求,让 GPU 回归它计算单元的本色。
为了达成这样的效果,我们就需要一个动态的显存调度——相对于 DeepSpeed 在训练前就规定好哪些放在 CPU 上,哪些放在 GPU 上,我们需要在训练过程中实时把下一步需要的模型参数拿到 GPU 上来。利用 pytorch 的 module hook 可以让我们在每个 nn.Module
前后调用回调函数,从而动态把参数从 CPU 拿到 GPU,或者放回去。
但是,相信大家能够想象到,如果每次都运行到一个 submodule 前,再现把参数传上来,肯定就很慢,因为计算得等着 CPU-GPU 的传输。为了解决计算效率的问题,我们提出了 chunk-based management。这是什么意思呢?就是我们把参数按照调用的顺序存储在了固定大小的 chunk 里(一般是 64M 左右),让内存/显存的调度以 chunk 为单位,第一次想把某个 chunk 中的参数放到 GPU 来的时候,就会直接把整个 chunk 搬到 GPU,这意味着虽然这一次的传输可能需要等待,但是在计算下一个 submodule 的时候,因为连着的 module 的参数都是存在一个 chunk 里的,这些参数已经被传到 GPU 上来了,从而实现了 prefetch,明显提升了计算效率。同时,因为 torch 的 allocator 会缓存之前分配的显存,固定大小的 chunk 可以更高效利用这一机制,提升显存利用效率。
在 chunk 的帮助下,我们的模型可以做到,CPU 内存加 GPU 显存有多大,模型就能训多大。和 DeepSpeed 相比,在同等环境下模型规模可以提升 50%,计算效率(Tflops)也更高。
对于多卡的数据并行场景,我们扩展了上述方法,可以做到多卡中只有 1 整份模型,1 整份优化器状态,同时具备了数据并行的易用性和模型并行的显存使用效率。如果想了解多卡训练的方案,以及更详细的一些优化,也欢迎来看看我们的论文:
以上。
女王:求求题主放过我,我可不敢有什么政绩。。。
这个问题问得很好啊,我的建议是看今年年会的摘要集:
中国化学会第32届学术年会 - 论文检索系统 - 中国化学会
可以看到有很多分会,不过计算化学分布得比较散,夹杂在各个分会中。各分会的主题可以从这里找到,可能相关的包括:
有一些主题是理论计算夹杂着实验的,还需要仔细辨别。回到摘要集,以第一分会为例:
中国化学会第32届学术年会摘要集-第一分会:物理化学前沿 - 论文检索系统 - 中国化学会
可以看到题目和单位全都标出来了,而且还可以下载。
显然,能找到相关方向的摘要的单位,就是开设了相关方向的院校,甚至还能精确到具体的某个课题组。