使用Pytorch进行多卡训练

2023-10-26

  当一块GPU不够用时,我们就需要使用多卡进行并行训练。其中多卡并行可分为数据并行和模型并行。具体区别如下图所示:

  由于模型并行比较少用,这里只对数据并行进行记录。对于pytorch,有两种方式可以进行数据并行:数据并行(DataParallel, DP)和分布式数据并行(DistributedDataParallel, DDP)。

  在多卡训练的实现上,DP与DDP的思路是相似的:

  1、每张卡都复制一个有相同参数的模型副本。

  2、每次迭代,每张卡分别输入不同批次数据,分别计算梯度。

  3、DP与DDP的主要不同在于接下来的多卡通信:

  DP的多卡交互实现在一个进程之中,它将一张卡视为主卡,维护单独模型优化器。所有卡计算完梯度后,主卡汇聚其它卡的梯度进行平均并用优化器更新模型参数,再将模型参数更新至其它卡上。

  DDP则分别为每张卡创建一个进程,每个进程相应的卡上都独立维护模型和优化器。在每次每张卡计算完梯度之后,进程之间以NCLL(NVIDIA GPU通信)为通信后端,使各卡获取其它卡的梯度。各卡对获取的梯度进行平均,然后执行后续的参数更新。由于每张卡上的模型与优化器参数在初始化时就保持一致,而每次迭代的平均梯度也保持一致,那么即使没有进行参数复制,所有卡的模型参数也是保持一致的。

  Pytorch官方推荐我们使用DDP。DP经过我的实验,两块GPU甚至比一块还慢。当然不同模型可能有不同的结果。下面分别对DP和DDP进行记录。

DP

   Pytorch的DP实现多GPU训练十分简单,只需在单GPU的基础上加一行代码即可。以下是一个DEMO的代码。

import torch
from torch import nn
from torch.optim import Adam
from torch.nn.parallel import DataParallel

class DEMO_model(nn.Module):
  def __init__(self, in_size, out_size):
    super().__init__()
    self.fc = nn.Linear(in_size, out_size)
  def forward(self, inp):
    outp = self.fc(inp)
    print(inp.shape, outp.device)
    return outp

model = DEMO_model(10, 5).to('cuda')
model = DataParallel(model, device_ids=[0, 1]) # 额外加这一行
adam = Adam(model.parameters())

# 进行训练
for i in range(1):
  x = torch.rand([128, 10]) # 获取训练数据,无需指定设备
  y = model(x) # 自动均匀划分数据批量并分配至各GPU,输出结果y会聚集到GPU0中
  loss = torch.norm(y)
  loss.backward()
  adam.step()

  其中model = DataParallel(model, device_ids=[0, 1])这行将模型复制到0,1号GPU上。输入数据x无需指定设备,它将会被均匀分配至各块GPU模型,进行前向传播。之后各块GPU的输出再合并到GPU0中,得到输出y。输出y在GPU0中计算损失,并进行反向传播计算梯度、优化器更新参数。

DDP

  为了对分布式编程有基本概念,首先使用pytorch内部的方法实现一个多进程程序,再使用DDP模块实现模型的分布式训练。

Pytorch分布式基础

  首先使用pytorch内部的方法编写一个多进程程序作为编写分布式训练的基础。

import os, torch
import torch.multiprocessing as mp
import torch.distributed as dist

def run(rank, size):
  tensor = torch.tensor([1,2,3,4], device='cuda:'+str(rank)) # ——1—— 
  group = dist.new_group(range(size)) # ——2——
  dist.all_reduce(tensor=tensor, group=group, op=dist.ReduceOp.SUM) # ——3——
  print(str(rank)+ ': ' + str(tensor) + '\n')

def ini_process(rank, size, fn, backend = 'nccl'):  
  os.environ['MASTER_ADDR'] = '127.0.0.1' # ——4——
  os.environ['MASTER_PORT'] = '1234'
  dist.init_process_group(backend, rank=rank, world_size=size) # ——5——
  fn(rank, size) # ——6——

if __name__ == '__main__': # ——7——
  mp.set_start_method('spawn') # ——8—— 
  size = 2 # ——9—— 
  ps = []  
  for rank in range(size):
    p = mp.Process(target=ini_process, args=(rank, size, run)) # ——10—— 
    p.start()  
    ps.append(p)

  for p in ps: # ——11—— 
    p.join()

  以上代码主进程创建了两个子进程,子进程之间使用NCCL后端进行通信。每个子进程各占用一个GPU资源,实现了所有GPU张量求和的功能。细节注释如下:

  1、为每个子进程定义相同名称的张量,并分别分配至不同的GPU,从而能进行后续的GPU间通信。

  2、定义一个通信组,用于后面的all_reduce通信操作。

  3、all_reduce操作以及其它通信方式请看下图:

  4、定义编号(rank)为0的ip和端口地址,让每个子进程都知道。ip和端口地址可以随意定义,不冲突即可。如果不设置,子进程在涉及进程通信时会出错。

  5、初始化子进程组,定义进程间的通信后端(还有GLOO、MPI,只有NCCL支持GPU间通信)、子进程rank、子进程数量。只有当该函数在size个进程中被调用时,各进程才会继续从这里执行下去。这个函数统一了各子进程后续代码的开始时间。

  6、执行子进程代码。

  7、由于创建子进程会执行本程序,因此主进程的执行需要放在__main__里,防止子进程执行。

  8、开始创建子进程的方式:spawn、fork。windows默认spawn,linux默认fork。具体区别请百度。

  9、由于是以NCCL为通信后端的分布式训练,如果不同进程中相同名称的张量在同一GPU上,当这个张量进行进程间通信时就会出错。为了防止出错,限制每张卡独占一个进程,每个进程独占一张卡。这里有两张卡,所以最多只能创建两个进程。

  10、创建子进程,传入子进程的初始化方法,及子进程调用该方法的参数。

  11、等待子进程全部运行完毕后再退出主进程。 

  输出结果如下:

  正是各进程保存在不同GPU上的张量的广播求和(all_reduce)的结果。

  参考: https://pytorch.org/tutorials/intermediate/dist_tuto.html

Pytorch分布式训练DEMO

  我们实际上可以根据上面的分布式基础写一个分布式训练,但由于不知道pytorch如何实现GPU间模型梯度的求和,即官方教程中所谓的ring_reduce(没找到相关API),时间原因,就不再去搜索相关方法了。这里仅记录pytorh内部的分布式模型训练,即利用DDP模块实现。Pytorch版本1.12.1。

import torch,os
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.optim as optim
from torch.nn.parallel import DistributedDataParallel as DDP
from torch import nn


def example(rank, world_size):
    dist.init_process_group("nccl", rank=rank, world_size=world_size)  # ——1——
    model = nn.Linear(2, 1, False).to(rank) 
    if rank == 0: # ——2——
        model.load_state_dict(torch.load('model_weight')) 
    # model_stat = torch.load('model_weight', {'cuda:0':'cuda:%d'%rank})  #这样读取保险一点
    # model.load_state_dict(model_stat) 
    opt = optim.Adam(model.parameters(), lr=0.0001) # ——3——
    opt_stat = torch.load('opt_weight', {'cuda:0':'cuda:%d'%rank}) # ——4——
    opt.load_state_dict(opt_stat) # ——5——
    ddp_model = DDP(model, device_ids=[rank])# ——6
    inp = torch.tensor([[1.,2]]).to(rank) # ——7——
    labels = torch.tensor([[5.]]).to(rank)
    outp = ddp_model(inp)
    loss = torch.mean((outp - labels)**2)
    opt.zero_grad()
    loss.backward() # ——8——

    opt.step() # ——9
    if rank == 0:# ——10——
        torch.save(model.state_dict(), 'model_weight')
        torch.save(opt.state_dict(), 'opt_weight')
    

if __name__=="__main__":
    os.environ["MASTER_ADDR"] = "localhost"# ——11——
    os.environ["MASTER_PORT"] = "29500"
    world_size = 2
    mp.spawn(example, args=(world_size,), nprocs=world_size, join=True) # ——12——

  以上代码包含模型在多GPU上读取权重、进行分布式训练、保存权重等过程。细节注释如下:

  1、初始化进程组,由于使用GPU通信,后端应该写为NCCL。不过经过实验,即使错写为gloo,DDP内部也会自动使用NCCL作为通信模块。

  2、由于后面使用DDP包裹模型进行训练,其内部会自动将所有rank的模型权重同步为rank 0的权重,因此我们只需在rank 0上读取模型权重即可。这是基于Pytorch版本1.12.1,低级版本似乎没有这个特性,需要在不同rank分别导入权重,则load需要传入map_location,如下面注释的两行代码所示。

  3、这里创建model的优化器,而不是创建用ddp包裹后的ddp_model的优化器,是为了兼容单GPU训练,读取优化器权重更方便。

  4、将优化器权重读取至该进程占用的GPU。如果没有map_location参数,load会将权重读取到原本保存它时的设备。

  5、优化器获取权重。经过实验,即使权重不在优化器所在的GPU,权重也会迁移过去而不会报错。当然load直接读取到相应GPU会减少数据传输。

  6、DDP包裹模型,为模型复制一个副本到相应GPU中。所有rank的模型副本会与rank 0保持一致。注意,DDP并不复制模型优化器的副本,因此各进程的优化器需要我们在初始化时保持一致。权重要么不读取,要么都读取。

  7、这里开始模型的训练。数据需转移到相应的GPU设备。

  8、在backward中,所有进程的模型计算梯度后,会进行平均(不是相加)。也就是说,DDP在backward函数添加了hook,所有进程的模型梯度的ring_reduce将在这里执行。这个可以通过给各进程模型分别输入不同的数据进行验证,backward后这些模型有相同的梯度,且验算的确是所有进程梯度的平均。此外,还可以验证backward函数会阻断(block)各进程使用梯度,只有当所有进程都完成backward之后,各进程才能读取和使用梯度。这保证了所有进程在梯度上的一致性。

  9、各进程优化器使用梯度更新其模型副本权重。由于初始化时各进程模型、优化器权重一致,每次反向传播梯度也保持一致,则所有进程的模型在整个训练过程中都能保持一致。

  10、由于所有进程权重保持一致,我们只需通过一个进程保存即可。

  11、定义rank 0的IP和端口,使用mp.spawn,只需在主进程中定义即可,无需分别在子进程中定义。

  12、创建子进程,传入:子进程调用的函数(该函数第一个参数必须是rank)、子进程函数的参数(除了rank参数外)、子进程数、是否等待所有子进程创建完毕再开始执行。

  参考: https://pytorch.org/tutorials/intermediate/ddp_tutorial.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用Pytorch进行多卡训练 的相关文章

随机推荐

  • python 中wheel 安装_python中wheel的用法整理

    Python的第一个主流打包格式是 egg文件 现在大家庭中又有了一个叫做Wheel whl 的新成员 wheel 被设计成包含PEP 376兼容安装 一种非常接近于磁盘上的格式 的所有文件 在本文中 我们将学习如何创建一个wheel以及如
  • linux 命令行报bash command not found的解决办法

    命令行报bash command not found的解决办法 几乎所有命令 命令行输入命令执行后报 bash command not found 这是由于系统PATH设置问题 PATH没有设置对 系统就无法找到精确命令了 1 在命令行中输
  • Nginx 返回自定义 text 或 json

    叙述 有些时候请求某些接口的时候需要返回指定的文本字符串或者json字符串 如果逻辑非常简单或者干脆是固定的字符串 那么可以使用nginx快速实现 这样就不用编写程序响应请求了 可以减少服务器资源占用并且响应性能非常快 解决方案 固定文本
  • 实时音频编解码之十五 Opus编码-CELT编码

    本文谢绝任何形式转载 谢谢 4 3 1 基频预滤波 对预加重之后信号预滤波 其和解码器的后滤波相反 基频周期搜索应根据以下标准优化 1 连续性 对于连续帧 基频周期通常不会突变 2 避免基频倍数 当使用的周期是实际周期的倍数时 后滤波器失去
  • 蓝桥杯真题:算式问题

    emmm其实是算全排列的问题 按照STL文档的描述 next permutation函数将按字母表顺序生成给定序列的下一个较大的排列 直到整个序列为降序为止 prev permutation函数与之相反 是生成给定序列的上一个较小的排列 具
  • 超详细!!!Linux:利用Shell脚本使用case分支语句

    case分支语句 case 语句 case语句主要适用情况 case分支语句语法结构 case值得注意的特点 case语句应用示例 1 检查用户输入字符类型 2 编写系统服务脚本 case 语句 case语句主要适用情况 某个变量存在多种取
  • Nginx+tomcat 实现前后端分离(解决跨域)

    工具 nginx 1 15 3 apache tomcat 9 0 11 代码准备 前端 新建前端文件 结构如下 index html代码如下
  • 腾讯股票数据接口 http/javascript

    From http blog csdn net ustbhacker article details 8365756 之前使用了新浪的股票数据 由于新浪http javascript缺少一些数据 用chrome自带的开发工具监视腾迅财经HT
  • 【tensorflow】张量Tensor的操作(创建,变换和分割)

    参考链接 https blog csdn net yeshang lady article details 124615743 ops request misc request id biz id 102 utm term tensorfl
  • http

    一 简单分析 简单的分析 从输入 URL到回车后发生的行为如下 URL解析 DNS 查询 TCP 连接 HTTP 请求 响应请求 页面渲染 二 详细分析 URL解析 首先判断你输入的是一个合法的URL 还是一个待搜索的关键词 并且根据你输入
  • Clumsy-Windows下网络环境模拟工具

    下载页 http jagt github io clumsy cn download 项目的代码可以在github上获取 在下载页面有编译好的版本 强烈建议在使用前花点时间阅读一下文档 来 了解 clumsy 的功能和限制 目前的实现中有一
  • 【概率论与数理统计】猴博士 笔记 p41-44 统计量相关小题、三大分布的判定、性质、总体服从正态分布的统计量小题

    文章目录 统计量相关小题 三大分布的判定 三大分布的性质 总体服从正态分布的统计量小题 统计量相关小题 题干 总体X 有一些样本X1 X2 X3 解法 注意 S的分母是n 1 接下来练习套公式 例1 直接背公式 例2 解 除X S n外有其
  • The illustrated Transformer 笔记

    The illustrated Transformer Transformer是一种使用Attention机制类提升模型训练的速度的模型 该模型的最大优势在于其并行性良好 Transformer模型在Attention is All You
  • IDEA 方法注释 自动获取返回值和传参

    一 设置 1 添加自定义注释快捷键 2 注释内容 desciption params return returns Author junwei Date date time 点击右边的edit variables 设置函数 下面3个内容选择
  • 前端面试题梳理

    一 技术方面 60 1 实现一个元素的水平垂直居中的几种方式 2 vue中 双向绑定的原理 3 vueX的原理 4 实现一个左边固定 右边自适应的布局 5 pomise的理解 6 对浏览器兼容的理解 如何兼容低版本浏览器 7 地址栏输入一个
  • UnityEditor.BuildPlayerWindow+BuildMethodException

    unity3D安卓打包报错 UnityEditor BuildPlayerWindow BuildMethodException 61 errors at UnityEditor BuildPlayerWindow DefaultBuild
  • hive 查询输入中文乱码

    设置 home 用户 profile 文件中LANG en US UTF 8即可
  • envi5.3处理高分二号影像数据详细过程记录

    目录 一 多光谱影像处理 1 辐射定标 2 大气校正 1 需要准备一些数据 2 大气校正过程 3 正射校正 二 全色影像处理 1 辐射定标 2 正射校正 三 图像融合 1 几何配准 2 图像融合 高分二号处理流程 envi5 3的安装教程
  • C3P0的详细配置说明(com.mchange.v2.c3p0.ComboPooledDataSource)

    C3P0是一个开放源代码的JDBC连接池 它在lib目录中与Hibernate一起发布 包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象 c3p0 config gt
  • 使用Pytorch进行多卡训练

    当一块GPU不够用时 我们就需要使用多卡进行并行训练 其中多卡并行可分为数据并行和模型并行 具体区别如下图所示 由于模型并行比较少用 这里只对数据并行进行记录 对于pytorch 有两种方式可以进行数据并行 数据并行 DataParalle