Pytorch使用GPU进行训练注意事项

王 茂南 2019年9月12日07:01:551 1 22089字阅读73分37秒
摘要这一篇文章会介绍关于Pytorch使用GPU训练的一些细节. 包括同时使用多个GPU来进行训练, 一些较大的网络如何训练(减少显存的使用量的方法), 以及使用过程中会遇到的一些问题.

简介

这一篇会介绍Pytorch使用GPU的一些内容. 包括使用多个GPU的例子, 使用多个GPU进行训练的原理, 以及一些措施, 来避免我们在train的时候遇到"CUDA error: out of memory"的报错. 我们来总结一下会出现的问题.

  • 模型过于复杂, batchsize只能设置得很小; 解决办法: 梯度累加; 多GPU训练
  • 模型过于复杂, 以至于一个GPU都无法进行训练; 解决办法: 使用checkpoint

参考资料(整个文章得结构是参考这一篇文章): Training Neural Nets on Larger Batches: Practical Tips for 1-GPU, Multi-GPU & Distributed setups

这里有一个部分我是没有进行讲(因为暂时自己还是没有用到), Distributed training: training on several machines, 这一份的内容可以参考上面的链接进行了解.

 

Pytorch大batch size训练指南

有的时候我们的模型过于复杂, 此时我们的batchsize就不能设置的太大. 我们可以使用accumulating gradients来解决这个问题.

所以, 总结一下我们要解决的问题是:

How can you train your model on large batches when your GPU can't hold more than a few samples?

我们知道Pytorch训练过程可以写成下面的5个步骤.

  1. predictions = model(inputs)               # Forward pass
  2. loss = loss_function(predictions, labels) # Compute loss function
  3. loss.backward()                           # Backward pass
  4. optimizer.step()                          # Optimizer step
  5. predictions = model(inputs)               # Forward pass with new parameters

loss.backward()过程中, 我们会对每一个参数来计算梯度, 然后会存储在parameter.grad中. 正常情况下, 我们在使用optimizer.step()进行参数优化之后, 会将梯度清零, 也就是model.zero_grad() or optimizer.zero_grad(). 所以accumulating gradients就是我们不会每次去对梯度清0, 而是会将梯度进行累计. 我们下面来看一个例子.

  1. model.zero_grad()                                   # Reset gradients tensors
  2. for i, (inputs, labels) in enumerate(training_set):
  3.     predictions = model(inputs)                     # Forward pass
  4.     loss = loss_function(predictions, labels)       # Compute loss function
  5.     loss = loss / accumulation_steps                # Normalize our loss (if averaged)
  6.     loss.backward()                                 # Backward pass
  7.     if (i+1) % accumulation_steps == 0:             # Wait for several backward steps
  8.         optimizer.step()                            # Now we can do an optimizer step
  9.         model.zero_grad()                           # Reset gradients tensors
  10.         if (i+1) % evaluation_steps == 0:           # Evaluate the model when we...
  11.             evaluate_model()                        # ...have no gradients accumulated

上面的例子中, 每evaluation_steps更新一次参数, 然后进行梯度清零. 同时我们需要注意的是, 我们要将loss去除evaluation_steps, 相当于计算一个平均, 来模拟batch的效果.

 

 

Pytorch同时使用多GPU

我们使用DataParallel来实现多GPU同时进行训练. 他会拆分你的batchsize, 分给不同的GPU进行训练. DataParallel使用起来非常简单, 我们只需要加一行就可以了.

  1. parallel_model = torch.nn.DataParallel(model) # Encapsulate the model
  2. predictions = parallel_model(inputs)          # Forward pass on multi-GPUs
  3. loss = loss_function(predictions, labels)     # Compute loss function
  4. loss.mean().backward()                        # Average GPU-losses + backward pass
  5. optimizer.step()                              # Optimizer step
  6. predictions = parallel_model(inputs)          # Forward pass with new parameters

上面的例子比较简单, 所以下面我们来看一个同时使用多个GPU进行训练的例子, 这个例子主要参考自下面的链接. 所有的注释都写在代码里了.

  1. import torch
  2. import torch.nn as nn
  3. from torch.utils.data import Dataset, DataLoader
  4. # --------------
  5. # 导入库和确定参数
  6. # --------------
  7. input_size = 5
  8. output_size = 2
  9. batch_size = 25
  10. data_size = 100
  11. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  12. # -----------
  13. # 使用的数据集
  14. # -----------
  15. # 虚拟数据集
  16. class RandomDataset(Dataset):
  17.     def __init__(self, size, length):
  18.         self.len = length
  19.         self.data = torch.randn(length, size)
  20.     def __getitem__(selfindex):
  21.         return self.data[index]
  22.     def __len__(self):
  23.         return self.len
  24. # 定义数据集: 这里一共有data_size组数据, 每组数据有input_size个特征, batch_size大小
  25. dataset = RandomDataset(size=input_size, length=data_size)
  26. rand_loader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True)
  27. # ------------------------------------------
  28. # 定义模型(我们使用DataParallel来保证使用多GPU)
  29. # ------------------------------------------
  30. class Model(nn.Module):
  31.     def __init__(self, input_size, output_size):
  32.         super(Model, self).__init__()
  33.         self.fc = nn.Linear(input_size, output_size)
  34.     def forward(selfinput):
  35.         output = self.fc(input)
  36.         print("\tIn Model: input size: {}, output size: {}".format(input.size(), output.size()))
  37.         return output
  38. # 创建模型
  39. model = Model(input_size=input_size, output_size=output_size)
  40. if torch.cuda.device_count() > 1:
  41.     print("Let's use {} GPUS".format(torch.cuda.device_count()))
  42.     model = nn.DataParallel(model) # 支持多GPU
  43.     print('-'*10)
  44. model.to(device)
  45. print(model)
  46. print('-'*10)
  47. # ---------
  48. # 运行模型
  49. # --------
  50. for data in rand_loader:
  51.     inputData = data.to(device)
  52.     print(inputData.size())
  53.     outputData = model(inputData)
  54.     print("Outside inputData size: {}, Outside outputData size: {}".format(inputData.size(), outputData.size()))
  55.     print('-'*10)

我们注意一下最终输出的结果.

Pytorch使用GPU进行训练注意事项

可以注意到, 使用DataParallel之后, 会自动一个batch中的数据分配到多个GPU上进行训练. 最后返回的时候会进行merge. 就像上图所展示的结果, 将两个GPU上的inputsize相加的结果是Outside Inputsize. 这样做就可以同时使用多个GPU来进行训练.

原理总结: DataParallel会自动将数据进行分割, 并放入不同的GPU上. 训练完毕之后, 他会收集和合并最后的结果, 用来更新模型. (DataParallel splits your data automatically and sends job orders to multiple models on several GPUs. After each model finishes their job, DataParallel collects and merges the results before returning it to you.)

 

多GPU训练的问题--unbalanced GPU usage

在使用多GPU进行训练的时候, 会出现GPU缓存不均匀的问题, 也就是第一个GPU的显存会高于其他的GPU, 我们通过下图来看一下多GPU训练的原理, 来看一下这个问题出现的原因(原图的链接: Training Neural Nets on Larger Batches: Practical Tips for 1-GPU, Multi-GPU & Distributed setups).

Pytorch使用GPU进行训练注意事项

从上图的forward部分的step4(最右侧), 可以看到所有模型的output需要gather到第一块GPU上. 这个时候就会出现第一块GPU的缓存使用比其他的GPU大的情况. 最后模型的更新是在一块GPU上进行更新, 这样到下一轮的时候, 就重新进行模型的复制和数据的分发.

我们有下面的两个方式来解决unbalanced GPU usage的情况(目的就是为了解决loss的计算, 不要在一个GPU上进行计算).

  • computing the loss in the forward pass of your model,
  • computing the loss in a parallel fashion.

第一种方法使用起来会有一些限制, 同时由于forward变复杂之后使得计算变慢, 所以我们主要来说明一下第二种方式.

在第二种方式中, 我们的解决办法就是每一个GPU分别进行计算loss, 而不是汇总到一起进行计算. (In that case, the solution is to keep each partial output on its GPU instead of gathering all of them to GPU-1. We well need to distribute our loss criterion computation as well to be able to compute and back propagate our loss.)

我们可以通过在下面的链接中下载parallel.py, 并包含在你的代码中, 他主要包含两个部分, 分别是DataParallelModel和DataParallelCriterion, 我们可以通过下面的方式来进行使用.

  1. from parallel import DataParallelModel, DataParallelCriterion
  2. parallel_model = DataParallelModel(model)             # Encapsulate the model
  3. parallel_loss  = DataParallelCriterion(loss_function) # Encapsulate the loss function
  4. predictions = parallel_model(inputs)      # Parallel forward pass
  5.                                           # "predictions" is a tuple of n_gpu tensors
  6. loss = parallel_loss(predictions, labels) # Compute loss function in parallel
  7. loss.backward()                           # Backward pass
  8. optimizer.step()                          # Optimizer step
  9. predictions = parallel_model(inputs)      # Parallel forward pass with new parameters

下面看一下这种方式的结构图, 可以看到他是将label也进行分发, 使得loss也可以在不同的GPU上来进行计算.

Pytorch使用GPU进行训练注意事项

下面有两个注意点, 在我们使用的时候需要进行注意:

  • 模型输出, Your model outputs several tensors: you likely want to disentangle them: output_1, output_2 = zip(*predictions)
  • 使用同一个GPU进行计算, Sometimes you don't want to use a parallel loss function: gather all the tensors on the cpu: gathered_predictions = parallel.gather(predictions)

 

参考资料

 

Pytorch显存使用查看

关于Pytroch显存的查看, 在这个链接: 再次浅谈Pytorch中的显存利用问题(附完善显存跟踪代码)中是提供了一个查看显存的工具. 工具的地址为: Pytorch-Memory-Utils.

其中可以查看总体的运行的显存和每一步的显存, modelsize->查看模型总的显存; gpu_mem_track分布查看每一步显存使用情况(但是我尝试了以下modelsize, 还是和实际的结果有点不同).

所以说, 我还是会使用下面的命令进行查看.

  1. nvidia-smi
Pytorch使用GPU进行训练注意事项

参考资料机器学习服务器使用一些命令记录

 

牺牲计算速度减少显存使用--CHECKPOINT

有的时候, 我们的模型特别深的时候, 会出现一个模型占用的显存就太大了. 这个时候我们可以使用pytorch提供的checkpoint, 可以帮助我们将一个模型分成几个部分来进行计算, 从而可以减少显存的使用, 但是同样代价就是会使得计算速度变慢(他计算完毕之后会进行释放, 之后再次需要的时候会再次进行计算, 所以计算速度会慢一些). 关于checkpoint, 会有两个函数, 一个是checkpoint_sequential, 另一个是checkpoint, 我们可以分别来看一下两者的使用和具体的效果.

checkpoint_sequential

这个函数的作用就是将sequential分成好几个部分来分别进行计算. 我们来看一个例子. 就是我们在定义好模型之后, 传入x的时候使用下面的方式进行传入.

  1. # ------------------------------
  2. # 定义模型(这里假设我们的网络很深)
  3. # ------------------------------
  4. fc = []
  5. fc = fc + [nn.Linear(input_size, input_size)]
  6. fc = fc + [nn.Linear(input_size, input_size) for _ in range(70)]
  7. fc = fc + [nn.Linear(input_size, output_size)]
  8. model = nn.Sequential(*fc)
  9. # 模型的运行
  10. num_segments = 10
  11. outputData = checkpoint_sequential(model, num_segments, inputData)

下面是一个完整的例子, 核心还是上面所讲的内容.

  1. import torch
  2. import torch.nn as nn
  3. from torch.utils.data import Dataset, DataLoader
  4. from torch.utils.checkpoint import checkpoint_sequential
  5. # --------------
  6. # 导入库和确定参数
  7. # --------------
  8. input_size = 1000
  9. output_size = 100
  10. batch_size = 16
  11. data_size = 30000
  12. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  13. # -----------
  14. # 使用的数据集
  15. # -----------
  16. # 虚拟数据集
  17. class RandomDataset(Dataset):
  18.     def __init__(self, size, length):
  19.         self.len = length
  20.         self.data = torch.randn(length, size)
  21.     def __getitem__(selfindex):
  22.         return self.data[index]
  23.     def __len__(self):
  24.         return self.len
  25. # 定义数据集: 这里一共有data_size组数据, 每组数据有input_size个特征, batch_size大小
  26. dataset = RandomDataset(size=input_size, length=data_size)
  27. rand_loader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True)
  28. # ------------------------------
  29. # 定义模型(这里假设我们的网络很深)
  30. # ------------------------------
  31. fc = []
  32. fc = fc + [nn.Linear(input_size, input_size)]
  33. fc = fc + [nn.Linear(input_size, input_size) for _ in range(70)]
  34. fc = fc + [nn.Linear(input_size, output_size)]
  35. model = nn.Sequential(*fc)
  36. model.to(device)
  37. print(model)
  38. print('-'*10)
  39. # ------
  40. # 优化器
  41. # ------
  42. optimizer = torch.optim.Adam(model.parameters(), lr=0.002, betas=(0.5, 0.999))
  43. # ---------
  44. # 运行模型
  45. # --------
  46. for data in rand_loader:
  47.     inputData = data.to(device)
  48.     print(inputData.size())
  49.     optimizer.zero_grad()
  50.     # 分成两个部分
  51.     num_segments = 10
  52.     outputData = checkpoint_sequential(model, num_segments, inputData)
  53.     # outputData = model(inputData)
  54.     outputData.sum().backward()
  55.     optimizer.step()
  56.     print("Outside inputData size: {}, Outside outputData size: {}".format(inputData.size(), outputData.size()))
  57.     print('-'*10)

我们可以比较一些分成不同部分所占用的显存的大小.

首先是不进行分割, 分割数为0的时候, 直接运行所占用的显存, 为1717MB.

Pytorch使用GPU进行训练注意事项

接着是分割为两个部分, 运行时所占用的显存大小为1283MB.

Pytorch使用GPU进行训练注意事项

接着是分割为5个部分, 占用显存大小为1043MB.

Pytorch使用GPU进行训练注意事项

最后是分割为10个部分, 占用显存大小为959MB.

Pytorch使用GPU进行训练注意事项

可以看到确实是分割的数量越多, 模型运行的时候所占用的显存大小也是越小的, 但是并不是呈现线性的关系, 后面减小的幅度会是越来越小的.

但是我们在使用checkpoint_sequential的时候限制会比较多, 只能是使用nn.Sequential来定义的模型, 同时也不能使用多GPU同时进行训练. 如果使用checkpoint, 那么限制就会少一些. 下面我们就来看一下checkpoint的用法.

 

torch.utils.checkpoint.checkpoint

这个checkpoint的用法, 真的是找了好久才找到一个使用的例子. 参考链接为: densenet. 他里面有这样的一段例子.

  1. import torch
  2. import torch.nn as nn
  3. import torch.nn.functional as F
  4. import torch.utils.checkpoint as cp
  5. class _DenseLayer(nn.Module):
  6.     def __init__(self, num_input_features, growth_rate, bn_size, drop_rate, efficient=False):
  7.         super(_DenseLayer, self).__init__()
  8.         self.add_module('norm1', nn.BatchNorm2d(num_input_features)),
  9.         self.add_module('relu1', nn.ReLU(inplace=True)),
  10.         self.add_module('conv1', nn.Conv2d(num_input_features, bn_size * growth_rate,
  11.                         kernel_size=1, stride=1, bias=False)),
  12.         self.add_module('norm2', nn.BatchNorm2d(bn_size * growth_rate)),
  13.         self.add_module('relu2', nn.ReLU(inplace=True)),
  14.         self.add_module('conv2', nn.Conv2d(bn_size * growth_rate, growth_rate,
  15.                         kernel_size=3, stride=1, padding=1, bias=False)),
  16.         self.drop_rate = drop_rate
  17.         self.efficient = efficient
  18.     def forward(self, *prev_features):
  19.         bn_function = _bn_function_factory(self.norm1, self.relu1, self.conv1)
  20.         if self.efficient and any(prev_feature.requires_grad for prev_feature in prev_features):
  21.             bottleneck_output = cp.checkpoint(bn_function, *prev_features)
  22.         else:
  23.             bottleneck_output = bn_function(*prev_features)
  24.         new_features = self.conv2(self.relu2(self.norm2(bottleneck_output)))
  25.         if self.drop_rate > 0:
  26.             new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
  27.         return new_features

也就是上面第23行的地方, 显示了checkpoint是如何使用的. 所以我就根据上面的例子, 自己做了一个小的例子, 来完成了相关的实验. 所以我们直接来看一个例子.

在说明checkpoint如何运作之前, 有一个地方需要注意的是, 使用checkpoint, 不能放在第一个位置, 或者说放在第一个就要传入一个有梯度的, 不然就无法进行反向传播.

以下的内容参考自, Checkpoint with no grad requiring inputs PROBLEM

他会出现以下的warning.

  1. UserWarning: None of the inputs have requires_grad=True. Gradients will be None warnings.

我们可以先看下面的一个例子, 看一下具体在说什么问题.

  1. m = torch.nn.Linear(4,3)
  2. x = torch.randn(10,4)
  3. z1 = checkpoint(lambda _:m(_), x)
  4. print(z1.requires_grad)
  5. z2 = m(x)
  6. print(z2.requires_grad)
Pytorch使用GPU进行训练注意事项

正常情况下, 网络输出的变量是有梯度的, 因为要进行反向传播, 所以会记住运输的过程. 但是可以看到上面, 如果使用了checkpoint之后, 输入结果是没有梯度的.

我们可以对输入变量直接设置requires_grad=True来保证最后的结果有梯度, 就像下面这样, 最后输出就是True

  1. x.requires_grad_(True)
  2. z1 = checkpoint(lambda _:m(_), x)
  3. print(z1.requires_grad)

但是这样就会存在问题. 首先, 这样会将input和模型参数同等对待, 通常情况下, 我们是要更新模型的参数, 而不是更新input. 这样会增加运算时间和gpu memory.

对于上面出现的问题, 有两种方式可以解决上面的问题, 其实两者的想法是一样的. 都是在最开始的时候, 输入一个有梯度的变量.

也就是下面的这段话.

In case of checkpointing, if all the inputs don't require grad but the outputs do, then if the inputs are passed as is, the output of Checkpoint will be variable which don't require grad and autograd tape will break there. To get around, you can pass a dummy input which requires grad but isn't necessarily used in computation.

上面这段话中的pass a dummy input很重要, 也是后面的方法的思路.

 

方法一

第一个方法就是在checkpoint之前加入一层网络, 这个网络加上一个数, 再减去一个数, 对最后的结果不影响. 但是此时因为checkpoint没有在前向传播的第一个位置, 所以之后的output就可以有梯度了.

  1. # ------------------------------
  2. # 定义模型(这里假设我们的网络很深)
  3. # ------------------------------
  4. class DummyLayer(nn.Module):
  5.     def __init__(self):
  6.         super().__init__()
  7.         self.dummy = nn.Parameter(torch.ones(1, dtype=torch.float32))
  8.     def forward(self,x):
  9.         return x + self.dummy - self.dummy #(also tried x+self.dummy)
  10. class Model(nn.Module):
  11.     def __init__(self, input_size, output_size):
  12.         super(Model, self).__init__()
  13.         fc = []
  14.         fc = fc + [nn.Linear(input_size, input_size)]
  15.         fc = fc + [nn.Linear(input_size, input_size) for _ in range(10)]
  16.         self.model1 = nn.Sequential(*fc)
  17.         fc = []
  18.         fc = fc + [nn.Linear(input_size, input_size) for _ in range(10)]
  19.         fc = fc + [nn.Linear(input_size, output_size)]
  20.         self.model2 = nn.Sequential(*fc)
  21.         self.dummy_layer = DummyLayer()
  22.     def forward(selfinput):
  23.         x = self.dummy_layer(input)
  24.         x = checkpoint(self.model1, x)
  25.         output = checkpoint(self.model2, x)
  26.         # x = self.model1(input)
  27.         # output = self.model2(x)
  28.         print("\tIn Model: input size: {}, output size: {}".format(input.size(), output.size()))
  29.         return output
  30. # 创建模型
  31. model = Model(input_size=input_size, output_size=output_size).to(device)
  32. model(dataset[0:1].to(device))

注意看上面的DummyLayer, 和在Model的前向传播中, 我们先进行了dummy_layer的运算. 虽说这个运算很少, 但是这个不是最好的方式, 我们可以通过下面的方式达到同样的目的.

 

方法二

第二种方法就是在传入的时候, 多传入一个变量, 这个变量是有梯度的, 但是没有参与运算. 可以看到下面的代码中, ModuleWrapperlgnores2ndArg中就会传入x和dummy_arg, 其中x是正常的input, dummy_arg的作用就是传入一个带有梯度的变量.

  1. class ModuleWrapperIgnores2ndArg(nn.Module):
  2.     def __init__(self, module):
  3.         super().__init__()
  4.         self.module = module
  5.     def forward(self, x, dummy_arg=None):
  6.         # 这里向前传播的时候, 不仅传入x, 还传入一个有梯度的变量, 但是没有参与计算
  7.         assert dummy_arg is not None
  8.         x = self.module(x)
  9.         return x
  10. class Model(nn.Module):
  11.     def __init__(self, input_size, output_size):
  12.         super(Model, self).__init__()
  13.         fc = []
  14.         fc = fc + [nn.Linear(input_size, input_size)]
  15.         fc = fc + [nn.Linear(input_size, input_size) for _ in range(10)]
  16.         self.model1 = nn.Sequential(*fc)
  17.         fc = []
  18.         fc = fc + [nn.Linear(input_size, input_size) for _ in range(10)]
  19.         fc = fc + [nn.Linear(input_size, output_size)]
  20.         self.model2 = nn.Sequential(*fc)
  21.         self.dummy_tensor = torch.ones(1, dtype=torch.float32, requires_grad=True)
  22.         self.module_wrapper = ModuleWrapperIgnores2ndArg(self.model1)
  23.     def forward(selfinput):
  24.         # 这里传入三个参数是因为, ModuleWrapperIgnores2ndArg前向传播的时候需要传入两个
  25.         x = checkpoint(self.module_wrapper, inputself.dummy_tensor)
  26.         output = checkpoint(self.model2, x)
  27.         print("\tIn Model: input size: {}, output size: {}".format(input.size(), output.size()))
  28.         return output
  29. model = Model(input_size=input_size, output_size=output_size).to(device)
  30. model(dataset[0:1].to(device))

这样上面的代码的输入结果就是有梯度的, 也就是可以进行反向传播了.

 

总体代码

上面是一些关键的代码(主要是看上面的样例代码中的forward部分). 下面是完整的代码, 包含数据集的定义等, 可以自己实践一下, 我还实践了一下多GPU同时运行的场景.

  1. import torch
  2. import torch.nn as nn
  3. from torch.utils.data import Dataset, DataLoader
  4. from torch.utils.checkpoint import checkpoint, checkpoint_sequential
  5. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  6. print(device)
  7. # --------------
  8. # 导入库和确定参数
  9. # --------------
  10. input_size = 1000
  11. output_size = 100
  12. batch_size = 64
  13. data_size = 30000
  14. # -----------
  15. # 使用的数据集
  16. # -----------
  17. # 虚拟数据集
  18. class RandomDataset(Dataset):
  19.     def __init__(self, size, length):
  20.         self.len = length
  21.         self.data = torch.randn(length, size)
  22.     def __getitem__(selfindex):
  23.         return self.data[index]
  24.     def __len__(self):
  25.         return self.len
  26. # 定义数据集: 这里一共有data_size组数据, 每组数据有input_size个特征, batch_size大小
  27. dataset = RandomDataset(size=input_size, length=data_size)
  28. rand_loader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True)
  29. class ModuleWrapperIgnores2ndArg(nn.Module):
  30.     def __init__(self, module):
  31.         super().__init__()
  32.         self.module = module
  33.     def forward(self, x, dummy_arg=None):
  34.         # 这里向前传播的时候, 不仅传入x, 还传入一个有梯度的变量, 但是没有参与计算
  35.         assert dummy_arg is not None
  36.         x = self.module(x)
  37.         return x
  38. class Model(nn.Module):
  39.     def __init__(self, input_size, output_size):
  40.         super(Model, self).__init__()
  41.         fc = []
  42.         fc = fc + [nn.Linear(input_size, input_size)]
  43.         fc = fc + [nn.Linear(input_size, input_size) for _ in range(10)]
  44.         self.model1 = nn.Sequential(*fc)
  45.         fc = []
  46.         fc = fc + [nn.Linear(input_size, input_size) for _ in range(10)]
  47.         fc = fc + [nn.Linear(input_size, output_size)]
  48.         self.model2 = nn.Sequential(*fc)
  49.         self.dummy_tensor = torch.ones(1, dtype=torch.float32, requires_grad=True)
  50.         self.module_wrapper = ModuleWrapperIgnores2ndArg(self.model1)
  51.     def forward(selfinput):
  52.         # 这里传入三个参数是因为, ModuleWrapperIgnores2ndArg前向传播的时候需要传入两个
  53.         x = checkpoint(self.module_wrapper, inputself.dummy_tensor)
  54.         output = checkpoint(self.model2, x)
  55.         print("\tIn Model: input size: {}, output size: {}".format(input.size(), output.size()))
  56.         return output
  57. model = Model(input_size=input_size, output_size=output_size).to(device)
  58. # model结果测试
  59. model(dataset[0:1].to(device))
  60. # ---------------------------------
  61. # 运行模型, 查看模型参数是否改变
  62. # ---------------------------------
  63. optimizer = torch.optim.Adam(model.parameters(), lr=2, betas=(0.5, 0.999))
  64. for data in rand_loader:
  65.     inputData = data.to(device)
  66.     optimizer.zero_grad()
  67.     outputData = model(inputData)
  68.     outputData.sum().backward()
  69.     optimizer.step()
  70.     print("Outside inputData size: {}, Outside outputData size: {}".format(inputData.size(), outputData.size()))
  71.     print('-'*10)

我们看一下各种的测试结果. 首先是不使用checkpoint的时候的显存占用大小, 为1717MB.

Pytorch使用GPU进行训练注意事项

接着是使用check_point之后的显存占用大小. 可以看到减小为1283MB.

Pytorch使用GPU进行训练注意事项

注意, 将preserve_rng_state设置为False的时候, 速度可以加快.

 

一个综合的例子

下面是一个同时使用check_pointcheckpoint_sequential的例子, 可以参考一下.

  1. class Generator(nn.Module):
  2.     # 输入一张图片, 输出为一张相同大小的图片
  3.     def __init__(self, residualNum=0):
  4.         super(Generator, self).__init__()
  5.         # Initial convolution block
  6.         InitialBlock = []
  7.         InitialBlock = InitialBlock + [nn.ReflectionPad2d(padding=3),
  8.                                         nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7),
  9.                                         nn.InstanceNorm2d(64),
  10.                                         nn.ReLU(),
  11.                                         # Downsampling
  12.                                         nn.Conv2d(in_channels=64, out_channels=64*2, kernel_size=3, stride=2, padding=1),
  13.                                         nn.InstanceNorm2d(64*2),
  14.                                         nn.ReLU(inplace=True),
  15.                                         nn.Conv2d(in_channels=64*2, out_channels=64*4, kernel_size=3, stride=2, padding=1),
  16.                                         nn.InstanceNorm2d(64*4),
  17.                                         nn.ReLU(inplace=True)]
  18.         self.InitialBlock = nn.Sequential(*InitialBlock)
  19.         # Residual blocks
  20.         residualBlock = []
  21.         for _ in range(residualNum):
  22.             residualBlock.append(ResidualBlock(64*4))
  23.         self.residualblock = nn.Sequential(*residualBlock)
  24.         # Upsampling
  25.         UpsamplingBlock = []
  26.         UpsamplingBlock = UpsamplingBlock + [nn.ConvTranspose2d(in_channels=64*4, out_channels=64*2, kernel_size=3, stride=2, padding=1, output_padding=1),
  27.                                             nn.InstanceNorm2d(64*2),
  28.                                             nn.ReLU(inplace=True),
  29.                                             nn.ConvTranspose2d(in_channels=64*2, out_channels=64*1, kernel_size=3, stride=2, padding=1, output_padding=1),
  30.                                             nn.InstanceNorm2d(64*1),
  31.                                             nn.ReLU(inplace=True),
  32.                                             # Output layer
  33.                                             nn.ReflectionPad2d(padding=3),
  34.                                             nn.Conv2d(in_channels=64, out_channels=3, kernel_size=7),
  35.                                             nn.Tanh()]
  36.         self.UpsamplingBlock = nn.Sequential(*UpsamplingBlock)
  37.     def forward(self, x):
  38.         x.requires_grad_()
  39.         x = checkpoint(self.InitialBlock, x)
  40.         x = checkpoint_sequential(self.residualblock, 2, x)
  41.         x = checkpoint(self.UpsamplingBlock, x)
  42.         return x

 

 

参考链接

 

一些存在的问题

with torch.no_grad()

我们在验证集的时候, 可以使用with torch.no_grad()来结语memory, 这样可以在计算的时候不需要计算梯度. 详细的说明参考: model.eval()与torch.no_grad()的区别

下面是一个用法的例子, 当我们在验证集的时候, 可以在前面加上with torch.no_grad().

  1. with torch.no_grad():
  2.     correct = 0
  3.     total = 0
  4.     for data, labels in test_dataset:
  5.         data = data.to(device)
  6.         labels = labels.to(device)
  7.         outputs = model(data)
  8.         _, predicted = torch.max(outputs.data, 1)
  9.         total += labels.size(0)
  10.         correct += (predicted == labels).sum().item()

 

'CUDA error: out of memory' after several epochs

有的时候我们会在训练了几个epoch之后, 出现"CUDA error: out of memory"的报错, 这可能是因为我们在计算loss的时候, 直接将loss加了上去. 如下所示:

  1. iter_loss += loss

但是这样进行相加的时候, 我们会每一个epoch都会存储计算图, 所以memory会一直进行增加. ( If that's the case, you are storing the computation graph in each epoch, which will grow your memory.)

我们需要将loss提取出来, 这样计算图会被自动释放. (You need to detach the loss from the computation, so that the graph can be cleared.)

所以我们应该使用下面的方式进行loss的相加.

  1. iter_loss += loss.item()
  2. # 或者下面这种方式
  3. iter_loss += loss.detach().item()

参考链接'CUDA error: out of memory' after several epochs

这个链接讲得比较详细CUDA error: out of memory

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南
  • 本文由 发表于 2019年9月12日07:01:55
  • 转载请务必保留本文链接:https://mathpretty.com/11156.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

评论:1   其中:访客  1   博主  0
    • 白小纯
      白小纯

      请问在使用checkpoint的方法一中,self.dummy可以是全零参数吗?让输入加上零?