©作者 | 伟大是熬出来的

单位 | 同济大学

研究方向 | 机器阅读理解

前言

因为课题组发的卡还没有下来,先向导师问了实验室的两张卡借用。之前都是单卡训练模型,正好在这个机会实践以下单机多卡训练模型的方法。关于 DDP 网上有很多资料,但都比较零碎(有些博客的代码甚至没办法 run),Pytorch 给出的官方文档看起来也比较吃力。因此这篇文章的主要目的是梳理一下笔者学习过程中认为比较好的资料,用通俗的语言介绍一下 DDP 的原理,最后给出使用 DDP 的模板以及一份详细的运行案例。

当代研究生应当掌握的并行训练方法(单机多卡):

https://zhuanlan.zhihu.com/p/98535650

这篇文章中介绍了目前常用的并行训练方法。其中,nn.Dataparallel 的使用最简单,只需要使用 Dataparallel 包装模型,再设置一些参数就可以实现。参数中需要指定参与训练的 GPU,device_ids=gpus;汇总梯度的 GPU,output_device=gpus[0]。

model = nn.DataParallel(model.cuda(), device_ids=gpus, output_device=gpus[0])

nn.Dataparallel 方法实际上是使用单进程将模型和数据加载到多个 GPU 上,控制数据在 GPU 之间流动,协同不同的 GPU 上的模型进行并行训练。这篇文章 [8] 中提到 nn.Dataparallel 方法的弊端:在训练的过程中,每个 batch 的模型权重是在一个进程上计算出来之后,再分发到每个 GPU 上。

这会导致负载不均衡的问题,可能第一个 GPU(12GB)占用了 10GB,剩余 GPU 却只使用了 4GB。因为在数据并行的时候,loss 会在第一个 GPU 上相加计算,更新好以后把权重分发到其余卡。这就造成了第一个 GPU 的负载远大于其他显卡。

nn.DistributedDataParallel 原理

与 nn.Dataparallel 使用单进程控制多个 GPU 不同, nn.DistributedDataParallel 为每个 GPU 都创建一个进程。这些 GPU 可以位于同一个结点上(单机多卡),也可以分布在多个节点上(多机多卡)。每个进程都执行相同的任务,每个进程都与其他进程进行通信。

另外一点不同是,只有梯度会在进程(GPU)之间传播。以单机多卡举例,假设我们有三张卡并行训练,那么在每个 epoch 中,数据集会被划分成三份给三个 GPU,每个 GPU 使用自己的 minibatch 数据做自己的前向计算,然后梯度在 GPU 之间全部约简。在反向传播结束的时候,每个 GPU 都有平均的梯度,确保模型权值保持同步(synchronized)。

运行模板

这一小节以官方文档给出的 demo 作为例子,介绍 DDP 的使用模板以及运行流程:

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

首先我们导入库文件,其中 torch.multiprocessing 用于创建进程,后面会详细介绍。

import os
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
import torch.multiprocessing as mpfrom torch.nn.parallel import DistributedDataParallel as DDP

前文提到,DDP 模型对于每个 GPU 都会创建一个单独的进程管理。在程序并发执行的过程中,进程之间需要同步和通信。因此我们需要一个方法管理进程组,这个方法需要知道如何找到进程 0。也要知道进程组中同步了多少个进程。init_process_group 方法能够实现上述功能,其中参数的含义解释如下:

  • backend:使用的后端。包括 mpi, gloo, 和 nccl。根据官方文档的介绍,nccl 是运行速度最快的,因此大多设置为这个

  • rank:当前进程的等级。在 DDP 管理的进程组中,每个独立的进程需要知道自己在所有进程中的阶序,我们称为 rank

  • world_size:在 DDP 管理的进程组中,每个独立的进程还需要知道进程组中管理进程的数量,我们称为 world_size

下面 setup 以及 cleanup 分别实现了进程组的设置以及销毁。

ef setup(rank, world_size):os.environ['MASTER_ADDR'] = 'localhost'os.environ['MASTER_PORT'] = '12355'# initialize the process groupdist.init_process_group("nccl", rank=rank, world_size=world_size)def cleanup():dist.destroy_process_group()

本例中我们训练一个简单的网络结构 ToyModel

class ToyModel(nn.Module):def __init__(self):super(ToyModel, self).__init__()self.net1 = nn.Linear(10, 10)self.relu = nn.ReLU()self.net2 = nn.Linear(10, 5)def forward(self, x):return self.net2(self.relu(self.net1(x)))

下面是模型训练部分的模板。数据和模型加载到当前进程使用的 GPU 中,正常进行正反向传播,需要注意以下几点:

  • 每个进程都需要复制一份模型以及数据。我们需要根据前文提到的 rank 和 world_size 两个参数初始化进程组。这样进程之间才能相互通信。使用我们前文定义的 setup() 方法实现;

  • model = ToyModel().to(rank) 这条语句将我们的模型移动到对应的 GPU中, rank 参数作为进程之间的阶序,可以理解为当前进程 index。由于每个进程都管理自己的 GPU,因此通过阶序可以索引到对应的 GPU;

  • ddp_model = DDP(model, device_ids=[rank])这条语句包装了我们的模型;

  • 其他与 pytorch 中训练模型的模板相同,最后一点需要注意的是,在我们将 tensor 移动到 GPU 的时候,同样需要使用 rank 索引,代码中体现在第 14 行。

def demo_basic(rank, world_size):print(f"Running basic DDP example on rank {rank}.")setup(rank, world_size)# create model and move it to GPU with id rankmodel = ToyModel().to(rank)ddp_model = DDP(model, device_ids=[rank])loss_fn = nn.MSELoss()optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)optimizer.zero_grad()outputs = ddp_model(torch.randn(20, 10))labels = torch.randn(20, 5).to(rank)loss_fn(outputs, labels).backward()optimizer.step()cleanup()

最后是启动器的介绍。DDP 的启动有两种方式,分别对应不同的代码。

  • torch.distributed.launch 启动器,用于在命令行分布式地执行 python 文件。在执行过程中,启动器会将当前进程的(其实就是 GPU 的)index 通过参数传递给 python。在使用的时候执行语句CUDA_VISIBLE_DEVICES=0,1,2,3 python -m torch.distributed.launch --nproc_per_node=4 main.py 调用启动器 torch.distributed.launch。

  • 本例中使用的是第二种方法 torch.multiprocessing.spawn。使用时,只需要调用 torch.multiprocessing.spawn,torch.multiprocessing 就会帮助我们自动创建进程。

如下面的代码所示,spawn 开启了 world_size 个进程,每个进程执行 demo_fn 并向其中传入 local_rank(当前进程 index)作为参数。这里需要结合前文 demo_basic 的定义来看。args 中的 world_size 对应 demo_basic 的 world_size 参数;mp.spawn 中 nprocs 则是创建进程的数量;至于demo_basic 中的 rank 参数,应当是框架内部实现了索引机制因此不需要我们显示对应(笔者自己的理解)。

def run_demo(demo_fn, world_size):mp.spawn(demo_fn,args=(world_size,),nprocs=world_size,join=True)

完整代码如下:

import os
import sys
import tempfile
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
import torch.multiprocessing as mpfrom torch.nn.parallel import DistributedDataParallel as DDP# On Windows platform, the torch.distributed package only
# supports Gloo backend, FileStore and TcpStore.
# For FileStore, set init_method parameter in init_process_group
# to a local file. Example as follow:
# init_method="file:///f:/libtmp/some_file"
# dist.init_process_group(
#    "gloo",
#    rank=rank,
#    init_method=init_method,
#    world_size=world_size)
# For TcpStore, same way as on Linux.def setup(rank, world_size):os.environ['MASTER_ADDR'] = 'localhost'os.environ['MASTER_PORT'] = '12355'# initialize the process groupdist.init_process_group("nccl", rank=rank, world_size=world_size)def cleanup():dist.destroy_process_group()class ToyModel(nn.Module):def __init__(self):super(ToyModel, self).__init__()self.net1 = nn.Linear(10, 10)self.relu = nn.ReLU()self.net2 = nn.Linear(10, 5)def forward(self, x):return self.net2(self.relu(self.net1(x)))def demo_basic(rank, world_size):print(f"Running basic DDP example on rank {rank}.")setup(rank, world_size)# create model and move it to GPU with id rankmodel = ToyModel().to(rank)ddp_model = DDP(model, device_ids=[rank])loss_fn = nn.MSELoss()optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)optimizer.zero_grad()outputs = ddp_model(torch.randn(20, 10))labels = torch.randn(20, 5).to(rank)loss_fn(outputs, labels).backward()optimizer.step()cleanup()def run_demo(demo_fn, world_size):mp.spawn(demo_fn,args=(world_size,),nprocs=world_size,join=True)if __name__ == "__main__":n_gpus = torch.cuda.device_count()assert n_gpus >= 2, f"Requires at least 2 GPUs to run, but got {n_gpus}"world_size = n_gpusrun_demo(demo_basic, world_size)

这份代码可以直接运行,输入结果如下,笔者使用两张卡,因此对应的 rank 分别是 0 和 1:

Running basic DDP  example on Rank 1
Running basic DDP  example on Rank 0

在官网给的这份 demo 之外,其实还有一点需要注意。我们使用 pytorch 处理数据集创建 dataloader 的过程中,需要使用 DistributedSampler 采样器。我们已经知道,每个进程都会拷贝一份模型和数据的副本,但是在并行计算的过程中,单个进程只会处理自己的 minibatch 的数据。

假设我们使用五个 GPU 并行,其对应的五个进程都有模型和数据的副本,我们假设训练集有一万条数据,那么在单个 epoch 中每个进程实际上只需要使用两千条数据训练,之后进行梯度整合。那么进程如何知道自己需要处理哪些数据呢?这是 DistributedSampler 的功能。

关于 DistributedSampler 具体做了什么,可以参考文献 7。

在此基础上,笔者根据官网的 demo 总结了一份使用 DDP 进行多卡并行加速模型的模板,读者在使用过程中根据需要进行简单更改即可使用:

import os
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
import torch.multiprocessing as mpfrom torch.nn.parallel import DistributedDataParallel as DDPdef setup(rank, world_size):os.environ['MASTER_ADDR'] = 'localhost'os.environ['MASTER_PORT'] = '12355'# initialize the process groupdist.init_process_group("nccl", rank=rank, world_size=world_size)def run(demo_fn, world_size):setup(rank, world_size)torch.manual_seed(18)torch.cuda.manual_seed_all(18)torch.backends.cudnn.deterministic = Truetorch.cuda.set_device(rank) # 这里设置 device ,后面可以直接使用 data.cuda(),否则需要指定 ranktrain_dataset = ...train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=..., sampler=train_sampler)model = ...model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[rank])optimizer = optim.SGD(model.parameters())for epoch in range(100):train_sampler.set_epoch(epoch)for batch_idx, (data, target) in enumerate(train_loader):data = data.cuda()target = target.cuda()...output = model(images)loss = criterion(output, target)...optimizer.zero_grad()loss.backward()optimizer.step()if __name__ == "__main__":n_gpus = torch.cuda.device_count()assert n_gpus >= 2, f"Requires at least 2 GPUs to run, but got {n_gpus}"world_size = n_gpusmp.spawn(run,args=(world_size,),nprocs=world_size,join=True)

总结一下,使用 DDP 进行多卡并行加速模型的重点:

  • init_process_group 函数管理进程组

  • 在创建 Dataloader 的过程中,需要使用 DistributedSampler 采样器

  • 正反向传播之前需要将数据以及模型移动到对应 GPU,通过参数 rank 进行索引,还要将模型使用 DistributedDataParallel 进行包装

  • 在每个 epoch 开始之前,需要使用 train_sampler.set_epoch(epoch)为 train_sampler 指定 epoch,这样做可以使每个 epoch 划分给不同进程的  minibatch 不同,从而在整个训练过程中,不同的进程有机会接触到更多的训练数据

  • 使用启动器进行启动。不同启动器对应不同的代码。torch.distributed.launch 通过命令行的方法执行,torch.multiprocessing.spawn 则可以直接运行程序。

最后,笔者使用上述模板实现了一份基于 Roberta 的文本分类任务,使用 DDP 进行单机双卡并行加速,运行的结果如下,有感兴趣的读者再放代码吧,这里展示一下运行结果:

参考文献

注:下述文献大多使用 torch.distributed.launch 启动器执行程序

[1] 当代研究生应当掌握的并行训练方法(单机多卡):

https://zhuanlan.zhihu.com/p/98535650

[2] PyTorch Parallel Training(单机多卡并行、混合精度、同步BN训练指南文档):https://zhuanlan.zhihu.com/p/145427849

[3] pytorch多卡分布式训练简要分析:https://zhuanlan.zhihu.com/p/159404316

[4] Pytorch中的Distributed Data Parallel与混合精度训练(Apex):https://zhuanlan.zhihu.com/p/105755472

[5] PyTorch分布式训练基础--DDP使用:https://zhuanlan.zhihu.com/p/358974461

[6] 使用PyTorch编写分布式应用程序:https://www.jianshu.com/p/be9f8b90a1b8?utm_campaign=hugo&utm_medium=reader_share&utm_content=note&utm_source=weixin-friends

[7] DistributedSampler 具体做了什么:https://blog.csdn.net/searobbers_duck/article/details/115299691?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link

[8] Pytorch的nn.DataParallel:https://zhuanlan.zhihu.com/p/102697821

更多阅读

#投 稿 通 道#

 让你的文字被更多人看到 

如何才能让更多的优质内容以更短路径到达读者群体,缩短读者寻找优质内容的成本呢?答案就是:你不认识的人。

总有一些你不认识的人,知道你想知道的东西。PaperWeekly 或许可以成为一座桥梁,促使不同背景、不同方向的学者和学术灵感相互碰撞,迸发出更多的可能性。

PaperWeekly 鼓励高校实验室或个人,在我们的平台上分享各类优质内容,可以是最新论文解读,也可以是学术热点剖析科研心得竞赛经验讲解等。我们的目的只有一个,让知识真正流动起来。

PyTorch多卡分布式训练:DistributedDataParallel (DDP) 简要分析相关推荐

  1. multi task训练torch_Pytorch多机多卡分布式训练

    被这东西刁难两天了,终于想办法解决掉了,来造福下人民群众. 关于Pytorch分布训练的话,大家一开始接触的往往是DataParallel,这个wrapper能够很方便的使用多张卡,而且将进程控制在一 ...

  2. PyTorch 分布式训练 (DP/DDP/torchrun/多机多卡) <笔记总结>

    1.DataParallel device = torch.device("cuda" if torch.cuda.is_available() else "cpu&qu ...

  3. pytorch多GPU分布式训练代码编写

    本文主要讲述单机单卡.单机多卡的简单使用方法: 文章目录 单机单卡 单机多卡 DP DDP 单机单卡 单机单卡就是一台机器上只有一张卡,是最简单的训练方式 对于单机单卡,我们所需要做的就是把模型和数据 ...

  4. PyTorch:DistributedDataParallel(DDP)学习

    鉴于Transformer盛行,导致模型训练已经ImageNet打底了,因此这里得学点分布式训练的知识,不然大数据集都训练不起来,本文是根据参考文献的一些归纳总结和自我理解,在学之前我们先看一下GPU ...

  5. DistributedDataParallel(DDP)Pytorch 分布式训练示例及注意事项

    现在pytorch主流的分布式训练库是DistributedDataParallel,它比Dataparallel库要快,而且前者能实现多机多卡后者只能单机多卡.本文是在单机多卡的环境下执行的分布式训 ...

  6. 简单介绍pytorch中分布式训练DDP使用 (结合实例,快速入门)

    文章目录 DDP原理 pytorch中DDP使用 相关的概念 使用流程 如何启动 torch.distributed.launch spawn调用方式 针对实例voxceleb_trainer多卡介绍 ...

  7. 新手手册:Pytorch分布式训练

    文 | 花花@机器学习算法与自然语言处理 单位 | SenseTime 算法研究员 目录 0X01 分布式并行训练概述 0X02 Pytorch分布式数据并行 0X03 手把手渐进式实战 A. 单机单 ...

  8. Pytorch - 分布式训练极简体验

    由于工作需要,最近在补充分布式训练方面的知识.经过一番理论学习后仍觉得意犹未尽,很多知识点无法准确get到(例如:分布式原语scatter.all reduce等代码层面应该是什么样的,ring al ...

  9. PyTorch 源码解读之分布式训练了解一下?

    来源丨商汤学术   编辑丨极市平台 本文由浅入深讲解 torch.distributed 这一并行计算包的概念,实现细节和应用方式,并带大家快速入门 PyTorch 分布式训练. 0 前言 由于大规模 ...

最新文章

  1. Linux C编程--进程介绍3--进程终止和等待
  2. java cst gmt_“CST”和“GMT”时间的区别?
  3. MVC3快速搭建Web应用(二)
  4. java变量数据类型_java变量与数据类型
  5. mysql in 命令
  6. 永磁无刷电机及其驱动技术_「技术」某种车型后驱动桥装配工艺及其工装的设计...
  7. java 3dm_3DM游戏运行库合集安装包v3.0
  8. win11 windows 服务打开word 另存为pdf
  9. 关于LNode 和* LinkList
  10. 计算机排线知识,宏利工程师为您讲解笔记本电脑触摸板软排线FFC的知识点[宏利]...
  11. 【H5即时通讯系统PHP源码】支持嵌入+单聊+群聊+可单独封装APP
  12. oracle字符中不包含字母,oracle中字母A或B是否包含在字符串中
  13. SpringMVC进阶
  14. 退款java_APP支付 + 退款(JAVA实现)
  15. html+css:自定义鼠标指针图案
  16. 利用python画空间分布图
  17. 惠普笔记本恢复出厂系统
  18. 2020CCPC绵阳D.Defuse the Bombs(二分)
  19. Android-银联支付开发
  20. 2020年再见,2021年你好!

热门文章

  1. linux实验串行端口程序设计,Linux下串口编程心得(转)
  2. 飞桨框架2.0RC新增模型保存、加载方案,与用户场景完美匹配,更全面、更易用
  3. python中二维数组如何按索引找元素_按索引或坐标访问二维数组中的元素
  4. 腾讯爬虫python_Python爬虫,爬取腾讯漫画实战
  5. oracle 10g体系结构及安全管理
  6. Python有了concurrent的话mutiprocessing和threading还有存在的意义吗?
  7. dede调用当前顶级栏目名称、ID、url方法
  8. Linux 下比较文件内容并相同部分、不同部分
  9. java消息推送与接收
  10. Javascript权威指南学习笔记一:数据类型