【Pytorch-7】-Pytorch的初级GPU训练

现如今模型都比较大,光用CPU是训练模型基本不可能,不过做模型推理还是可以的。本小节主要回顾如何使用GPU训练模型,当然环境配置就不啰嗦了,这里直接进入正题:

  1. 将相关数据放到GPU上;
  2. 如何将模型放到GPU上;
  3. 最为粗暴的多GPU训练方法;

首先,我们可以用如下方法判断我们是否可以用GPU训练模型:

torch.cuda.is_available():

还有相关的API:
https://pytorch.org/docs/stable/cuda.html?highlight=cuda#module

关于环境配置的一点疑惑

看到很多博客说,需要根据自己的显卡driver版本,先下载合适版本的cuda和cudnn;

但实际上我发现似乎并不需要,我们只需要下载合适的GPU版本的pytorch就行,其内置有对应的cudatoolkit工具。这里需要注意一下,pytorch官方版本首页的下载,或者我们使用国内的源下载Pytorch时,虽然我们希望下载GPU版本的包,但是很可能还是下载的是CPU版本。

因为我的新主机上似乎只安装了driver,并没有安装cuda,而且nvcc等等的管理工具并不存在,也并不是没有加入环境变量之类的原因,确实没有工具包的文件夹存在。在相关设备管理的部分也看不到cuda的版本信息,但是确实可以使用GPU加速。例如在Minist数据集上训练VAE,用CPU可能需要半个小时,而用GPU加速后不到1分钟就训练完毕了。

1.数据放置

我们可以用粗暴的方法放置数据,即,我们直接通过tensor.cuda()方法就可以达到目标,如下所示:

 for i, (images, questions, answers,categories, qindices) in enumerate(data_loader):
      n_steps += 1
      # Set mini-batch dataset.
      if torch.cuda.is_available():
          images = images.cuda()
          questions = questions.cuda()
          answers = answers.cuda()
          categories = categories.cuda()
          qindices = qindices.cuda()

注意,一般服务器都是多GPU的,因此我们可以指定放在服务器上的哪个GPU上,而不是默认的把模型放到GPU上。如果我们习惯用上面的方法放置到默认GPU上,那么很可能导致自己的模型训练和别的模型撞车,因此我们应该学会放到指定的GPU上。
实际上,似乎这样就可以把数据放在指定GPU上了:

 if torch.cuda.is_available():
          images = images.cuda()
          questions = questions.cuda()
          answers = answers.cuda()
          categories = categories.cuda()
          qindices = qindices.cuda()

假如服务器上的GPU情况如下:
在这里插入图片描述
这里箭头所指的就是GPU的编号。我们可以通过如下方法指定放置的1号GPU:

GPUidx=1
device = torch.device('cuda:{}'.format(GPUidx) if torch.cuda.is_available() else 'cpu')

我们可以通过如下方法,将数据放置到GPU上:

self.scale = torch.sqrt(torch.FloatTensor([self.head_dim])).to(device)

我们可以用如下方法,得到张量的设备位置:

CurDevice=tensor.get_device()

一旦张量被分配,您可以直接对其进行操作,而不考虑所选择的设备,结果将始终放在与张量相同的设备上。
默认情况下,不支持跨GPU操作,唯一的例外是copy_()。 除非启用对等存储器访问,否则对分布不同设备上的张量任何启动操作的尝试都将会引发错误。

2.模型放置

实际上,我们只需要如下就可以粗暴的将模型直接放在GPU上了:

    # Loss criterion.
    pad = vocab(vocab.SYM_PAD)  # Set loss weight for 'pad' symbol to 0
    criterion = nn.CrossEntropyLoss(ignore_index=pad)
    l2_criterion = nn.MSELoss()

    # Setup GPUs.
    if torch.cuda.is_available():
        logging.info("Using available GPU...")
        vqg.cuda()
        criterion.cuda()
        l2_criterion.cuda()

这里把评价的函数和模型都通过.cuda()方法转换到GPU上面去了。但是这也和数据一样,相当于是直接把模型放到默认的GPU上了,也会出现和别人模型训练打架的情况。
因此我们如下指定数据存放的GPU。

GPUidx=1
device = torch.device('cuda:{}'.format(GPUidx) if torch.cuda.is_available() else 'cpu')

类似的,我们通过如下方法转移模型:

model = model.to(device)
  • 提个问题,到底哪些内容是可以通过上面的一句话,放在GPU上的?

如下,我们手动复现了Transformer的模型架构,其中的多头注意力如下所示:

class MultiHeadAttentionLayer(nn.Module):
    def __init__(self, hid_dim, n_heads, dropout, device):
        super().__init__()
        
        assert hid_dim % n_heads == 0
        
        self.hid_dim = hid_dim
        self.n_heads = n_heads
        self.head_dim = hid_dim // n_heads
        
        self.fc_q = nn.Linear(hid_dim, hid_dim)
        self.fc_k = nn.Linear(hid_dim, hid_dim)
        self.fc_v = nn.Linear(hid_dim, hid_dim)
        
        self.fc_o = nn.Linear(hid_dim, hid_dim)
        
        self.dropout = nn.Dropout(dropout)
        
        self.scale = torch.sqrt(torch.FloatTensor([self.head_dim])).to(device)

其中的QKV以及O乘法矩阵都是通过nn.Linear初始化的,而唯有一个归一化系数self.scale不是,是通过torch张量初始化的。这里需要特别的将他放到相应的位置,它不像pytorch的模块一样,可以通过model = model.to(device)一句话将模型转移到设备上。
如下展示这样的问题,我们不将这个张量,特别的放到设备上,而是直接在主函数使用:model = model.to(device)
在这里插入图片描述
在训练时,就会报错:
在这里插入图片描述
因此我们必须显示的将模型中额外定义的torch张量,特别设置到相应的位置上。

但是注意,我们在模型初始化,已经把相应的数据、模型都放到合适的位置后,相应前馈过程中,产生的数据都会自然的在相应的设备上,不需要我们进行特别的设置。如下所示,是上面的多头注意力的前馈部分:

	def forward(self, query, key, value, mask = None):
		batch_size = query.shape[0]      
        Q = self.fc_q(query)
        K = self.fc_k(key)
        V = self.fc_v(value)
      
        Q = Q.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
        K = K.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
        V = V.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
                
        energy = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale

        if mask is not None:
            energy = energy.masked_fill(mask == 0, -1e10)
            
        attention = torch.softmax(energy, dim = -1)             
        x = torch.matmul(self.dropout(attention), V)
        #注意一下,这里这个dropout似乎有点奇怪了,意味着我们并没有使用每一个分数!
        x = x.permute(0, 2, 1, 3).contiguous()
        x = x.view(batch_size, -1, self.hid_dim)    
        x = self.fc_o(x)

        return x, attention

其中输入的"query"、“key”、"value"和模型都放置在GPU0上时,所通过运算得到的QKV,以及后面的energy,attention等等张量,全部都是在GPU0上的,不需要额外把他们放在设备上。当然,如果此时我们用torch.FloatTensor这种方法定义一个新张量,那还是必须要显式地把它放在GPU上,否则默认是在CPU上的。

我们可以通过如下方法获得模型的位置:(可能得再好好思考思考)

CurDevice=next(model.parameters()).device() #因为model.parameters()获得的是一个迭代器

3.粗暴的多GPU训练:

首先,我们可以使用:

torch.cuda.device_count()

来查看服务器上的可用GPU的数目,返回值就是可用GPU的数目。

3.1 模型放置

在这里多GPU训练我们采用了Pytorch内置的最简单的方法DataParallel对模型进行包装,进行尝试。这种方法是效率最低的,更高级、高效的方法包括DistributedDataParallel方法。但是即便是这样暴力的方法,实际上坑点很多。

使用DataParallel方法对模型进行包装,表面上是只需要简单的一句话就可以实现,如下所示:
在这里插入图片描述
其中,model.to(device)是设置模型的主卡,这里的cudaidx实际对应的就是服务器上的GPU编号。相关的参数如下:
在这里插入图片描述
DataParallel方法的多GPU实现方法,其实是将主卡上的模型原样的复制到其它辅助显卡上,(当然这是在每一步梯度下降后都会进行的),将一个Batch的样本平均分到包括主卡在内的显卡上进行训练,当各显卡执行完后,将梯度转移到主卡上进行训练。因此,无论是模型的数据交换,还是梯度计算的效率上,都并不是很高,因为主卡的负载其实很重,如下图所示,其中,主卡的显存占用很大,而且计算负载非常不均衡:
在这里插入图片描述
不过计算负载也是会不断变化的,现在就比较均衡了。相比较之下,单个模型的显存占用和运算资源占用如下,介于主卡和辅助卡之间。
在这里插入图片描述

本小节,基于一个自己复现的Transformer架构,进行中-英机器翻译任务,来说明这一过程中一些微妙的地方。

首先,我们验证一下batch数据被平均分配的事实,我们设置的batch大小是128,将模型分在两个GPU上。我们在S2S包装的模型处,输出其中的相关信息。
在这里插入图片描述
在这里插入图片描述
我们可以看到输入的Batch已经被分为了两份,模型本身位置也被放到了不同的GPU上。

回到模型,由于我们需要在计算注意力时,计算一个mask,以防止将pad部分的注意力给计算进去,因此在模型接受到数据时有这一步。其中,由于需要用到当前数据存在于哪个设备中。在实际训练的时候,这一步报错了很多次。
在这里插入图片描述
报错信息如下:
在这里插入图片描述
这一点,我们从上面已经知道了,如果我们在推理过程中需要生成新的张量,那么必须要将它手工放到对应的设备上。那么问题来了,如果是多GPU的话,我们能否在模型初始化的时候,记录设备device信息,在训练的时候利用self.device将它放到GPU上呢?这里给出的答案是否定的!

虽然我们知道模型被分配到了不同的GPU上,但是实际上我们人为获得模型的时候,对模型放在主卡上初始化,因此实际上我们只记录了主卡的信息。而在模型被分配到不同GPU上时,这一步是pytorch帮我们分配的,如果仅仅通过一句model=torch.nn.DataParallel(),我们并不知道哪个模型的副本被分配到哪个设备上了。因此,我们所作的显式的初始化只做了一次。如果我们在模型的构造函数里输出信息,在推理时也输出信息,我们可以看到结果如下:
在这里插入图片描述
也就是说,模型通过构造函数显式的初始化只有一次,其它模型的副本没有通过构造函数产生。因此,我们想要知道模型的副本究竟在哪里,不能在构造函数中获得,只能通过训练的推理时,模型或者数据的位置得到结果,进而进行操作。不过这种副本的复制,似乎会把构造函数中的torch.tensor也转移过去(这里有点忘了。。不过好像其实是不会转移的,也得重新转移),只不过推理时用torch.tensor之类方法获得的张量,很麻烦,需要再重新放到新的位置。

相应的,我们可以通过如下方法,获得模型、数据的当前位置:

CurDevice=tensor.get_device()
CurDevice=next(model.parameters()).device() #因为model.parameters()获得的是一个迭代器

总结:

  1. 模型的初始化只有1次,所有的模型、数据的流动都是隐式的!
  2. 数据tensor初始化如果不指定生成位置,那就默认是CPU;运算而到的张量是在当前设备(或许是相关运算的张量所在的设备?)上。因此通过torch的tensor相关方法初始化的张量,必须要指定设备。我们需要通过获取当前设备来解决这个问题。
  3. torch.tensor和torch.nn.module的模块在被隐式转移的时候,处理不同,前者保持设备位置不变,而后者被转移到相应的GPU上。

3.2 损失处理

此外,即便是被分到了多个GPU上进行训练,我们收到的各个loss也并没有成为列表,依然是标量。我们在推理时,输出loss的形状,如下所示:
在这里插入图片描述
换句话说,多卡情况的loss,各个卡得到了自己的loss,然后将梯度传播到主卡上进行操作(不知道是累加,还是平均)。我们需要仔细想想,是否需要对loss进行操作,以确保效率问题。

  • 似乎我们在多卡的时候,不需要对loss进行缩放,但是会影响效率(如下)。。这里后期再研究了,先记录在这里。。

3.3 效率讨论

此外,如果在多卡训练时,不相应地增大Batchsize,那么训练速度其实比单卡更加慢。如下是使用了多卡,但没有增加BatchSize导致的结果,原本单卡训练时一个Epoch只需要4min,现在却要4min30s以上、
在这里插入图片描述

而采用多卡训练,单个EPOCH的训练速度没有如想象中的按比例减少,即用四张卡,一个epoch理想是一张卡一个epoch的1/4。而且明显的是loss下降速度不如单卡训练的快。总体来说效率降低,如下是采取4张GPU训练的结果:
在这里插入图片描述
如下是单卡训练的结果:

在这里插入图片描述

总体而言,由于经验不足,这里多卡训练的效果不佳,当BatchSize增大后,没有调整对学习率。

  1. 因此要加速训练的话,就应该把Batch变大!否则速度并没有提升,还会由于单GPU性能没有充分发挥却使用多GPU导致性能下降。
  2. 如果采用了较大的BatchSize,一般的也需要对应修改学习率,比如增大学习率。

3.4 模型保存

注意,由于此时

model=torch.nn.DataParallel(model,cudaidx)

未进行多GPU处理的模型,其实是现在的model.module,因此原本的操作例如:

torch.save(model.state_dict())

应该改为:

torch.save(model.module.state_dict())

但加载模型,一般我们放在DataParallel之前,因此不需要进行特别操作。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值