0 前言
随着深度学习快速发展,同时伴随着模型参数的爆炸式增长,对显卡的显存容量提出了越来越高的要求,如何在单卡小容量显卡上面训练模型是一直以来大家关心的问题。本文结合 MMCV 开源库对一些常用的节省显存策略进行简要分析,并希望能够对如何即插即用的使用 MMCV 中相应节省显存策略的用户提供一个简单的指引。
本文涉及到的 PyTorch 节省显存的策略包括:
混合精度训练
大 batch 训练或者称为梯度累加
gradient checkpointing 梯度检查点
现在的新型号显卡肯定是不存在显存不够的情况的,如果出现显存不够其实就意味着你这个显卡性能不够才是真正的原因。这些年新出的游戏显卡,显存一般搭配的都非常足,越高端显卡的显存也就越高,显存在游戏里一般也就专门针对。
1 混合精度训练
混合精度训练全称为 Automatic Mixed Precision,简称为 AMP,也就是我们常说的 FP16。在前系列解读中已经详细分析了 AMP 原理、源码实现以及 MMCV 中如何一行代码使用 AMP,具体链接见:
OpenMMLab:PyTorch 源码解读之 torch.cuda.amp: 自动混合精度详解89 赞同 · 17 评论文章
OpenMMLab:OpenMMLab 中混合精度训练 AMP 的正确打开方式50 赞同 · 6 评论文章
由于前面两篇文章已经分析的非常详细了,本文只简要描述原理和具体说明用法。
考虑到训练过程中梯度幅值大部分是非常小的,故训练默认是 FP32 格式,如果能直接以 FP16 格式精度进行训练,理论上可以减少一半的内存,达到加速训练和采用更大 batch size 的目的,但是直接以 FP16 训练会出现溢出问题,导致 NAN 或者参数更新失败问题,而 AMP 的出现就是为了解决这个问题,其核心思想是 混合精度训练+动态损失放大:
维护一个 FP32 数值精度模型的副本
当然,我们也可以在屏幕上用鼠标右键点击属性,然后打开高级设置按钮,需进行页面的设置,将页面设置成为调整最佳性能,如此一来可以解决掉显存不足的问题。总而言之,电脑的显存和内存在使用对象、容量以及存储的速度上都有着。
在每个 iteration
拷贝并且转换成 FP16 模型
前向传播(FP16 的模型参数),此时 weights,activations 都是 FP16
loss 乘 scale factor s
反向传播(FP16 的模型参数和参数梯度), 此时 gradients 也是 FP16
参数梯度乘 1/s
利用 FP16 的梯度更新 FP32 的模型参数
在 MMCV 中使用 AMP 分成两种情况:
在 OpenMMLab 上游库例如 MMDetection 中使用 MMCV 的 AMP
用户只想简单调用 MMCV 中的 AMP,而不依赖上游库
(1) OpenMMLab 上游库如何使用 MMCV 的 AMP
以 MMDectection 为例,用法非常简单,只需要在配置中设置:
fp16 = dict(loss_scale=512.)表示动态 scale fp16 = dict(loss_scale=&39;)39;dynamic&39;dynamic'
(2) 调用 MMCV 中的 AMP
直接调用 MMCV 中的 AMP,这通常意味着用户可能在其他库或者自己写的代码库中支持 AMP 功能。需要特别强调的是 PyTorch 官方仅仅在 1.6 版本及其之后版本中开始支持 AMP,而 MMCV 中的 AMP 支持 1.3 及其之后版本。如果你想在 1.3 或者 1.5 中使用 AMP,那么使用 MMCV 是个非常不错的选择。
使用 MMCV 的 AMP 功能,只需要遵循以下几个步骤即可:
9、按下回车键,选择一个较大的显存,按下回车键保存,之后按下F10 选择 y 保存BIOS设置并并重启计算机即可解决问题!
将 auto_fp16 装饰器应用到 model 的 forward 函数上
设置模型的 fp16_enabled 为 True 表示开启 AMP 训练,否则不生效
如果开启了 AMP,需要同时配置对应的 FP16 优化器配置 Fp16OptimizerHook
在训练的不同时刻,调用 Fp16OptimizerHook,如果你同时使用了 MMCV 中的 Runner 模块,那么直接将第 3 步的参数输入到 Runner 中即可
显存不足的解决方法如下:1、当提示内存不足的时候,首先打开任务管理器,然后查看各个软件的内存占用情况。2、然后找到后台不用的但是占有一定量内存的程序,点击结束进程,释放了一些内存。3、如果还是不行的话,建议直接重新。
(可选) 如果对应某些 OP 希望强制运行在 FP32 上,则可以在对应位置引入 force_fp32 装饰器
2 如果开启 AMP,则需要加入开启标志model.fp16_enabled = True4 传递给 runnerrunner.register_training_hooks(cfg.lr_config,optimizer_config,cfg.checkpoint_config,cfg.log_config,cfg.get(&39;,None))39;features',))def _loss(features,labels) :pass
注意 force_fp32 要生效,依然需要 fp16_enabled 为 True 才生效。
2 大 Batch 训练(梯度累加)
大 Batch 训练通常也称为梯度累加策略,通常 PyTorch 一次迭代训练流程为:
解决方法:\x0d\x0a1、显存不足被其他的程序占用大量的显存,尝试关闭一些程序在来运行游戏。\x0d\x0a2、使用一些修复或者优化软件来对系统进入优化,在尝试登陆游戏。比如360安全卫士、优化大师等。\x0d\x0a3、查看是。
而梯度累加策略下常见的一次迭代训练流程为:
y_pred = model(xx)loss = loss_fn(y_pred,y)loss = loss / cumulative_itersloss.backward()if current_iter % cumulative_iters==0optimizer.step() optimizer.zero_grad()
在 MMCV 中已经实现了梯度累加功能,其核心代码位于 mmcv/runner/hooks/optimizer.py
GradientCumulativeOptimizerHook 中,和 AMP 实现一样是采用 Hook 实现的。使用方法和 AMP 类似,只需要将第一节中的 Fp16OptimizerHook 替换为 GradientCumulativeOptimizerHook 或者 GradientCumulativeFp16OptimizerHook 即可。其核心实现如下所示:
需要明白 divisible_iters 和 remainder_iters 的含义:
(1) 从头训练
此时在开始训练时 iter=0,一共迭代 max_iters=102 次,梯度累加次数是 4,由于 102 无法被 4 整除,也就是最后的 102-(102 // 4)*4=2 个迭代是额外需要考虑的,在最后 2 个训练迭代中 loss_factor 不能除以 4,而是 2,这样才是最合理的做法。其中 remainder_iters=2,divisible_iters=100,residual_iters=102。
(2) resume 训练
假设在梯度累加的中途退出,然后进行 resume 训练,此时 iter 不是 0,由于优化器对象需要重新初始化,为了保证剩余的不能被累加次数的训练迭代次数能够正常计算,需要重新计算 residual_iters。
3 梯度检查点
梯度检查点是一种用训练时间换取显存的办法,其核心原理是在反向传播时重新计算神经网络的中间激活值而不用在前向时存储,torch.utils.checkpoint 包中已经实现了对应功能。简要实现过程是:在前向阶段传递到 checkpoint 中的 forward 函数会以 torch.no_grad 模式运行,并且仅仅保存输入参数和 forward 函数,在反向阶段重新计算其 forward 输出值。
具体用法非常简单,以 ResNet 的 BasicBlock 为例:
def forward(self,x):def _inner_forward(x):identity = xout = self.conv1(x)out = self.norm1(out)out = self.relu(out)out = self.conv2(out)out = self.norm2(out)if self.downsample is not None:identity = self.downsample(x)out += identityreturn out# x.requires_grad 这个判断很有必要if self.with_cp and x.requires_grad:out = cp.checkpoint(_inner_forward,x)else:out = _inner_forward(x)out = self.relu(out)return out
self.with_cp 为 True,表示要开启梯度检查点功能。
checkpoint 在用法上面需要注意以下几点:
模型的第一层不能用 checkpoint 或者说 forward 输入中不能所有输入的 requires_grad 属性都是 False,因为其内部实现是依靠输入的 requires_grad 属性来判断输出返回是否需要梯度,而通常模型第一层输入是 image tensor,其 requires_grad 通常是 False。一旦你第一层用了 checkpoint,那么意味着这个 forward 函数不会有任何梯度,也就是说不会进行任何参数更新,没有任何使用的必要,具体见 。如果第一层用了 checkpoint, PyTorch 会打印 None of the inputs have requires_grad=True. Gradients will be Non 警告
其核心实现如下所示:
4 实验验证
为了验证上述策略是否真的能够省显存,采用 mmdetection 库进行验证,基本环境如下:
显卡: GeForce GTX 1660PyTorch: 1.7.1CUDA Runtime 10.1MMCV: 1.3.16MMDetection: 2.17.0
(1) base
数据集:pascal voc
算法是 retinanet,对应配置文件为 retinanet_r50_fpn_1x_voc0712.py
为了防止 lr 过大导致训练出现 nan,需要将 lr 设置为 0.01/8=0.00125
bs 设置为 2
(2) 混合精度 AMP
在 base 配置基础上新增如下配置即可:
fp16 = dict(loss_scale=512.)
(3) 梯度累加
在 base 配置基础上替换 optimizer_config 为如下:
39;GradientCumulativeOptimizerHook',cumulative_iters=2)
(4) 梯度检查点
在 base 配置基础上在 backbone 部分开启 with_cp 标志即可:
model = dict(backbone=dict(with_cp=True),bbox_head=dict(num_classes=20))
每个实验总共迭代 1300 次,统计占用显存、训练总时长。
配置
显存占用(MB)
训练时长
1、显卡内存不足一般是玩儿大型的游戏导致的,可以尝试把游戏的一些特效关闭,或者把电脑的性能调到最佳。2、调整电脑性能方法如下,第一步,右击“计算机”,然后选择“属性”选项。3、然后点击“高级系统设置”选项。
base
7 分 45 秒
混合精度 AMP
36 分
梯度累加
7 分 32 秒
梯度检查点
8 分 37 秒
对比 base 和 AMP 可以发现,由于实验显卡是不支持 AMP 的,故只能节省显存,速度会特别慢,如果本身显卡支持 AMP 则可以实现在节省显存的同时提升训练速度
对比 base 和梯度累加可以发现,在相同 bs 情况下,梯度累加 2 次相当于 bs 扩大一倍,但是显存增加不多。如果将 bs 缩小一倍,则可以实现在相同 bs 情况下节省大概一倍显存
对比 base 和梯度检查点可以发现,可以节省一定的显存,但是训练时长会增加一些
从上面简单实验可以发现,AMP、梯度累加和梯度检查点确实可以在不同程度减少显存,而且这三个策略是正交的,可以同时使用。
5 总结
本文简要描述了三个在 MMCV 中集成且可以通过配置一行开启的节省显存策略,这三个策略比较常用也比较成熟。随着模型规模的不断增长,也出现了很多新的策略,例如模型参数压缩、动态显存优化、使用 CPU 内存暂存策略以及分布式情况下 PyTorch 1.10 最新支持的 ZeroRedundancyOptimizer 等等。
快速链接直达 MMCV 算法库,欢迎大家 Star: