转载自(侵删):
https://www.cnblogs.com/chentiao/p/17901639.html
一般在训练神经网络时,显存主要被网络模型和中间变量占用。
就地操作 (inplace) 字面理解就是在原地对变量进行操作,对应到 pytorch 中就是在原内存上对变量进行操作而不申请新的内存空间,从而减少对内存的使用。具体来说就地操作包括三个方面的实现途径:
nn.ReLU(inplace=True)
tensor.add_()
,tensor.scatter_()
,F.relu_()
y += x
,y *= x
在自定义网络结构的成员方法 forward 函数里,避免使用不必要的中间变量,尽量在之前已申请的内存里进行操作,比如下面的代码就使用太多中间变量,占用大量不必要的显存:
def forward(self, x):
x0 = self.conv0(x) # 输入层
x1 = F.relu_(self.conv1(x0) + x0)
x2 = F.relu_(self.conv2(x1) + x1)
x3 = F.relu_(self.conv3(x2) + x2)
x4 = F.relu_(self.conv4(x3) + x3)
x5 = F.relu_(self.conv5(x4) + x4)
x6 = self.conv(x5) # 输出层
return x6
为了减少显存占用,可以将上述 forward 函数修改如下:
def forward(self, x):
x = self.conv0(x) # 输入层
x = F.relu_(self.conv1(x) + x)
x = F.relu_(self.conv2(x) + x)
x = F.relu_(self.conv3(x) + x)
x = F.relu_(self.conv4(x) + x)
x = F.relu_(self.conv5(x) + x)
x = self.conv(x) # 输出层
return x
上述两段代码实现的功能是一样的,但对显存的占用却相去甚远,后者能节省前者占用显存的接近 90% 之多。
网络模型对显存的占用主要指的就是卷积层,全连接层和标准化层等的参数,具体优化途径包括但不限于:
nn.AdaptiveAvgPool2d()
代替全连接层 nn.Linear()
拆分 batch 跟技巧 4 中减小 batch_size 本质是不一样的, 这种拆分 batch 的操作可以理解为将两次训练的损失相加再反向传播,但减小 batch_size 的操作是训练一次反向传播一次。拆分 batch 操作可以理解为三个步骤,假设原来 batch 的大小 batch_size=64
:
batch_size=32
的小 batch一个 batch 训练结束会得到相应的一个损失值,如果要计算一个 epoch 的损失就需要累加之前产生的所有 batch 损失,但之前的 batch 损失在 GPU 中占用显存,直接累加得到的 epoch 损失也会在 GPU 中占用显存,可以通过如下方法进行优化:
epoch_loss += batch_loss.detach().item() # epoch 损失
上边代码的效果就是首先解除 batch_loss 张量的 GPU 占用,将张量中的数据取出再进行累加。
model.cuda().half() # 网络模型设置半精度
# 网络输入和目标设置半精度
x, y = Variable(x).cuda().half(), Variable(y).cuda().half()
import torch
from torch.nn.functional import mse_loss
from torch.cuda.amp import autocast, GradScaler
EPOCH = 10 # 训练次数
LEARNING_RATE = 1e-3 # 学习率
x, y = torch.randn(3, 100).cuda(), torch.randn(3, 5).cuda() # 定义网络输入输出
myNet = torch.nn.Linear(100, 5).cuda() # 实例化网络,一个全连接层
optimizer = torch.optim.SGD(myNet.parameters(), lr=LEARNING_RATE) # 定义优化器
scaler = GradScaler() # 梯度缩放
for i in range(EPOCH): # 训练
with autocast(): # 设置混合精度运行
y_pred = myNet(x)
loss = mse_loss(y_pred, y)
scaler.scale(loss).backward() # 将张量乘以比例因子,反向传播
scaler.step(optimizer) # 将优化器的梯度张量除以比例因子。
scaler.update() # 更新比例因子
import torch
import torch.nn as nn
from torch.utils.checkpoint import checkpoint
# 自定义函数
def conv(inplanes, outplanes, kernel_size, stride, padding):
return nn.Sequential(nn.Conv2d(inplanes, outplanes, kernel_size, stride, padding),
nn.BatchNorm2d(outplanes),
nn.ReLU()
)
class Net(nn.Module): # 自定义网络结构,分为三个子网络
def __init__(self):
super().__init__()
self.conv0 = conv(3, 32, 3, 1, 1)
self.conv1 = conv(32, 32, 3, 1, 1)
self.conv2 = conv(32, 64, 3, 1, 1)
self.conv3 = conv(64, 64, 3, 1, 1)
self.conv4 = nn.Linear(64, 10) # 全连接层
def segment0(self, x): # 子网络1
x = self.conv0(x)
return x
def segment1(self, x): # 子网络2
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
return x
def segment2(self, x): # 子网络3
x = self.conv4(x)
return x
def forward(self, x):
x = checkpoint(self.segment0, x) # 使用 checkpoint
x = checkpoint(self.segment1, x)
x = checkpoint(self.segment2, x)
return x
requires_grad=True
,在给出的代码中将一个网络结构拆分为 3 个子网络进行训练,对于没有 nn.Sequential()
构建神经网络的情况无非就是自定义的子网络里多几项,或者像例子中一样单独构建网络块。nn.Sequential()
包含的大网络块 (小网络块时没必要),可以使用 checkpoint_sequential
包来简化实现,具体实现过程如下:import torch
import torch.nn as nn
from torch.utils.checkpoint import checkpoint_sequential
class Net(nn.Module): # 自定义网络结构,分为三个子网络
def __init__(self):
super().__init__()
linear = [nn.Linear(10, 10) for _ in range(100)]
self.conv = nn.Sequential(*linear) # 网络主体,100 个全连接层
def forward(self, x):
num_segments = 2 # 拆分为两段
x = checkpoint_sequential(self.conv, num_segments, x)
return x
import gc
gc.collect() # 清理内存
output = myNet(input_) # 输入送入网络
loss = mse_loss(target, output) # 计算损失
loss = loss / 4 # 累积 4 次梯度
loss.backward() # 反向传播
if step % 4 == 0: # 如果执行了 4 步
optimizer.step() # 更新网络参数
optimizer.zero_grad() # 优化器梯度清零
在运行测试程序时不涉及到与梯度有关的操作,因此可以清楚不必要的梯度以节约显存,具体包括但不限于如下操作:
model.eval()
将模型置于测试状态,不启用标准化和随机舍弃神经元等操作。with torch.no_grad():
中,不进行图构建等操作。myNet.zero_grad() # 模型参数梯度清零
optimizer.zero_grad() # 优化器参数梯度清零
torch.cuda.empty_cache() # 释放显存
执行这条语句释放的显存资源在用 Nvidia-smi 命令查看时体现不出,但确实是已经释放。其实 pytorch 原则上是如果变量不再被引用会自动释放,所以这条语句可能没啥用,但个人觉得多少有点用。
下采样从实现上来看类似池化,但不限于池化,其实也可以用步长大于 1 来代替池化等操作来进行下采样。从结果上来看就是通过下采样得到的特征图会缩小,特征图缩小自然参数量减少,进而节约显存,可以用如下两种方式实现:
nn.Conv2d(32, 32, 3, 2, 1) # 步长大于 1 下采样
nn.Conv2d(32, 32, 3, 1, 1) # 卷积核接池化下采样
nn.MaxPool2d(2, 2)
del 功能是彻底删除一个变量,要再使用必须重新创建,注意 del 删除的是一个变量而不是从内存中删除一个数据,这个数据有可能也被别的变量在引用,实现方法很简单,比如:
def forward(self, x):
input_ = x
x = F.relu_(self.conv1(x) + input_)
x = F.relu_(self.conv2(x) + input_)
x = F.relu_(self.conv3(x) + input_)
del input_ # 删除变量 input_
x = self.conv4(x) # 输出层
return x
进行网络训练时比较常用的优化器是 SGD 和 Adam,抛开训练最后的效果来谈,SGD 对于显存的占用相比 Adam 而言是比较小的,实在没有办法时可以尝试改变参数优化算法,两种优化算法的调用是相似的:
import torch.optim as optim
from torchvision.models import resnet18
LEARNING_RATE = 1e-3 # 学习率
myNet = resnet18().cuda() # 实例化网络
optimizer_adam = optim.Adam(myNet.parameters(), lr=LEAENING_RATE) # adam 网络参数优化算法
optimizer_sgd = optim.SGD(myNet.parameters(), lr=LEAENING_RATE) # sgd 网络参数优化算法