监控 GPU 常用命令:nvidia-smi,nvidia-smi -l可以每五秒输出一次GPU信息 ;
watch --color -n1 gpustat -cpu # 动态事实监控 GPU,linux会一直处于监测状态,ctrl+z中止;
GPU的内存占用率主要由两部分组成:
一是优化器参数,模型自身的参数,模型中间每一层的缓存,都会在内存中开辟空间来进行保存,所以模型本身会占用很大一部分内存。模型自身的参数指的就是各个网络层的 Weight 和Bias,显存在模型加载完成之后就会被占用,有些层是有参数的,如CNN, RNN,有些层是无参数的, 如激活层, 池化层等。Pytorch 执行 model.to(device) , 模型就加载完毕了。
二是batch size的大小,模型结构固定时,将batch size设置大,会充分利用GPU内存。
计算模型参数量
import torch as t
from torchsummary import summary
rgb = t.randn(1,3,352,480).cuda()
net = FCN(12).cuda()
out = net(rgb)
summary(net,input_size=(3,352,480),batch_size=1)
GPU使用率低
训练模型时GPU的利用率有时会很低,而CPU的利用率却非常高。通常 CPU 进行数据读取和预处理,GPU 做模型的正向传播和反向传播。CPU数据读取跟不上(读到内存+多线程+二进制文件),而 GPU 处理速度太快,导致GPU的利用率不高。
模型训练慢并不是因为显卡不行或者模型太大,而是在跑模型过程中有一些其他的操作导致速度很慢,尤其是文件的IO操作,这会导致GPU得不到连续性使用,整体速度特别慢。
此时解决方法:
· 关闭一些日志记录,减少日志IO操作频率。
· NVIDA提供了DALI库,可将数据处理转移到GPU上。
GPU内存占用通常是由模型的大小以及batch size的大小决定。如果GPU占用率很低,通常只需要改变batch size的大小。合理的batch size可以提高显存的效率和数据的处理速度,跑完一次epoch需要迭代的次数也会随之减少,并且训练过程中的loss震荡也会减小。但batch size太小会导致模型震荡且不收敛,batch size太大则会导致训练速度慢且模型不易收敛。
不是IO操作导致的GPU使用率低,可考虑如下办法:
1、提高batch_size;
2、提高模型输入尺寸;
3、增加模型深度;
4、使用DataLoader处理数据是可将 pin_memory设为 True,其会直接映射到GPU上,减少数据传输时间,也可调整 num_workers数量,通常为 2,4,8,16,过大反而可能会降低效率;
优先提高batch_size, 其他方法会对模型结构产生影响。
显存不足问题解决
降低batch size
适当降低batch size,则模型每层的输入输出会线性减少,batch_size 是训练神经网络中的重要超参数,该值决定了一次将多少数据送入神经网络训练。现存允许时,batch_size 越大越好,使显存尽可能占满, batch_size尽可能大。
选择更小的数据类型
通常网络采用 32 位浮点数,如使用16 位的浮点数,显存占用量将接近呈倍数递减。
精简模型
设计模型时适当精简模型,如原来两层的LSTM转为一层;原来使用LSTM, 现在使用GRU;减少卷积核数量;尽量少的使用 Linear ,因为全连接层参数较多,较少参数或则不用全连接层,使用全局平均池化替代等。
数据处理
文本数据中,长序列会导致参数呈线性增加, 适当缩小序列长度可极大降低参数量。
total_loss
loss 是包含梯度信息的tensor,正确求损失和的方式为:total_loss += loss.item()
释放不需要的张量和变量
用 del 删除你不需要的张量和变量。
Relu 的 inplace 参数
激活函数 Relu() 默认参数 inplace = Flase, 为True 时在通过relu() 计算得到的新值不会占用新的空间而是直接覆盖原来的值,可以节省一部分显存。
梯度累积
在 Pytorch 中,执行 loss.backward() 时, 会为每个参数计算梯度,并将其存储在 paramter.grad 中,paramter.grad 是一个张量, 会累加每次计算得到的梯度,调用 optimizer.step() 进行梯度下降更新网络参数。而 batch size 与占用显存相关,有时候 batch size 不能设置太小,就可以使用梯度累加。
通常每个batch_size使用 optimizer.zero_grad() 清空梯度,梯度累加本质上就是累加 若干个 batch_size 的梯度,即不是每个batch都清空梯度,而是根据累加的梯度更新网络参数,达到调整batch_size 的效果。在使用时,需要注意适当的扩大学习率。
假设 batch size = 4 , accumulation steps = 8 , 梯度积累首先在前向传播的时候以 batch_size = 4 计算梯度,但不更新参数,将梯度积累下来,达到 accumulation steps 个 batch 再更新参数,等价于:真正的 batch_size = batch_size * accumulation_steps,梯度积累能很大程度上缓解GPU显存不足的问题,推荐使用。
梯度检查点
梯度检查点是以时间换空间的方法,通过减少保存的激活值压缩模型占用空间,在计算梯度时必须重新计算没有存储的激活值。参考:陈天奇 Training Deep Nets with Sublinear Memory Cost
混合精度训练
混合精度训练在单卡和多卡情况下都可以使用,通过cuda计算中的half2类型提升运算效率。一个half2类型中会存储两个FP16的浮点数,进行基本运算时可同时进行,因此 FP16 的期望速度是 FP32 的两倍。
分布式训练
数据并行 Data Parallelism
模型并行 Model Parallelism
多GPU并行训练
设置使用GPU:device = 'cuda' if torch.cuda.is_avaiable() else 'cpu'
DataParallel 和 DistributedDataParallel 两个类可用于GPU并行;
以 DataParallel 为例:model = nn.DataParallel(model)
在单卡上写好的 model 直接调用,别的都跟单卡形式一样,程序会自动把数据拆分放到所有已知的GPU上来运行,数据是直接从第一维拆开平均放到各个GPU上,相当于每个GPU放 batch_size / gpu_num 个样本。设置已知的GPU,可以在运行代码的 python 加上 CUDA_VISIBLE_DEVICES 参数,CUDA_VISIBLE_DEVICES=0,1,2,3 python example.py,如果要使用nohup的话,参数要加在nohup的前面,CUDA_VISIBLE_DEVICES=0,1,2,3 nohup python -u example.py >> nohup_output.log 2;如果不设置则默认为所有GPU,对GPU数量计数:torch.cuda.device_count() 代码。直接用 DataParallel 可能导致各卡空间不均衡的问题,建议使用 DistributedDataParallel。
参考:
模型训练时GPU利用率太低的原因及解决_python_脚本之家 (jb51.net)
优化GPU显存不足,提高GPU利用率_显存不足怎么解决方案_L_bloomer的博客-CSDN博客
深度学习PyTorch中GPU利用率较低,且模型训练速度很慢的问题总结与分析 - 哔哩哔哩 (bilibili.com)