潍坊网站建设公司有哪些内容,巴音郭楞网站建设,构建网站空间,网站建设功能定位[深度][PyTorch] DDP系列第一篇#xff1a;入门教程 转自#xff1a;[原创][深度][PyTorch] DDP系列第一篇#xff1a;入门教程 概览
想要让你的PyTorch神经网络在多卡环境上跑得又快又好#xff1f;那你definitely需要这一篇#xff01; No one knows DDP better than I…[深度][PyTorch] DDP系列第一篇入门教程 转自[原创][深度][PyTorch] DDP系列第一篇入门教程 概览
想要让你的PyTorch神经网络在多卡环境上跑得又快又好那你definitely需要这一篇 No one knows DDP better than I do! – – MagicFrog手动狗头 本文是DDP系列三篇基本原理与入门底层实现与代码解析实战与技巧中的第一篇。本系列力求深入浅出简单易懂猴子都能看得懂误。本篇主要在下述四个方面展开描述
DDP的原理在分类上DDP属于Data Parallel。简单来讲就是通过提高batch size来增加并行度。为什么快DDP通过Ring-Reduce的数据交换方法提高了通讯效率并通过启动多个进程的方式减轻Python GIL的限制从而提高训练速度。DDP有多快 一般来说DDP都是显著地比DP快能达到略低于卡数的加速比例如四卡下加速3倍。所以其是目前最流行的多机多卡训练方法。 怎么用DDP? 有点长但是给你一个简单、完整的示例
请欢快地开始阅读吧
Quick Start
不想看原理给你一个最简单的DDP Pytorch例子
依赖
PyTorch(gpu)1.5python3.6
环境准备
推荐使用官方打好的PyTorch docker避免乱七八糟的环境问题影响心情。
# Dockerfile
# Start FROM Nvidia PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch
FROM nvcr.io/nvidia/pytorch:20.03-py3代码
单GPU代码
## main.py文件
import torch# 构造模型
model nn.Linear(10, 10).to(local_rank)# 前向传播
outputs model(torch.randn(20, 10).to(rank))
labels torch.randn(20, 10).to(rank)
loss_fn nn.MSELoss()
loss_fn(outputs, labels).backward()
# 后向传播
optimizer optim.SGD(model.parameters(), lr0.001)
optimizer.step()## Bash运行
python main.py加入DDP的代码
## main.py文件
import torch
# 新增
import torch.distributed as dist# 新增从外面得到local_rank参数
import argparse
parser argparse.ArgumentParser()
parser.add_argument(--local_rank, default-1)
FLAGS parser.parse_args()
local_rank FLAGS.local_rank# 新增DDP backend初始化
torch.cuda.set_device(local_rank)
dist.init_process_group(backendnccl) # nccl是GPU设备上最快、最推荐的后端# 构造模型
device torch.device(cuda, local_rank)
model nn.Linear(10, 10).to(device)
# 新增构造DDP model
model DDP(model, device_ids[local_rank], output_devicelocal_rank)# 前向传播
outputs model(torch.randn(20, 10).to(rank))
labels torch.randn(20, 10).to(rank)
loss_fn nn.MSELoss()
loss_fn(outputs, labels).backward()
# 后向传播
optimizer optim.SGD(model.parameters(), lr0.001)
optimizer.step()## Bash运行
# 改变使用torch.distributed.launch启动DDP模式
# 其会给main.py一个local_rank的参数。这就是之前需要新增:从外面得到local_rank参数的原因
python -m torch.distributed.launch --nproc_per_node 4 main.pyDDP的基本原理
大白话原理
假如我们有N张显卡
缓解GIL限制在DDP模式下会有N个进程被启动每个进程在一张卡上加载一个模型这些模型的参数在数值上是相同的。Ring-Reduce加速在模型训练时各个进程通过一种叫Ring-Reduce的方法与其他进程通讯交换各自的梯度从而获得所有进程的梯度实际上就是Data Parallelism各个进程用平均后的梯度更新自己的参数因为各个进程的初始参数、更新梯度是一致的所以更新后的参数也是完全相同的。
是不是很简单呢
与DP模式的不同
那么DDP对比Data ParallelDP模式有什么不同呢
DP模式是很早就出现的、单机多卡的、参数服务器架构的多卡训练模式在PyTorch即是
model torch.nn.DataParallel(model) 在DP模式中总共只有一个进程受到GIL很强限制。master节点相当于参数服务器其会向其他卡广播其参数在梯度反向传播后各卡将梯度集中到master节点master节点对搜集来的参数进行平均后更新参数再将参数统一发送到其他卡上。这种参数更新方式会导致master节点的计算任务、通讯量很重从而导致网络阻塞降低训练速度。
但是DP也有优点优点就是代码实现简单。要速度还是要方便看官可以自行选用噢。
DDP为什么能加速
本节对上面出现的几个概念进行一下介绍看完了你就知道为什么DDP这么快啦
Python GIL
GIL是个很捉急的东西如果大家有被烦过的话相信会相当清楚。如果不了解的同学可以自行百度一下噢。 这里简要介绍下其最大的特征缺点Python GIL的存在使得一个python进程只能利用一个CPU核心不适合用于计算密集型的任务。 使用多进程才能有效率利用多核的计算资源。
而DDP启动多进程训练一定程度地突破了这个限制。
Ring-Reduce梯度合并
Ring-Reduce是一种分布式程序的通讯方法。
因为提高通讯效率Ring-Reduce比DP的parameter server快。其避免了master阶段的通讯阻塞现象n个进程的耗时是o(n)。详细的介绍ring allreduce和tree allreduce的具体区别是什么
简单说明 各进程独立计算梯度。每个进程将梯度依次传递给下一个进程之后再把从上一个进程拿到的梯度传递给下一个进程。循环n次进程数量之后所有进程就可以得到全部的梯度了。可以看到每个进程只跟自己上下游两个进程进行通讯极大地缓解了参数服务器的通讯阻塞现象
并行计算 Data Parallelism 这是最常见的形式通俗来讲就是增大batch size。 平时我们看到的多卡并行就属于这种。比如DP、DDP都是。这能让我们方便地利用多卡计算资源。 能加速。 Model Parallelism 把模型放在不同GPU上计算是并行的。有可能是加速的看通讯效率。 Workload Partitioning 把模型放在不同GPU上但计算是串行的。不能加速。
如何在PyTorch中使用DDP
看到这里你应该对DDP是怎么运作的为什么能加速有了一定的了解下面就让我们学习一下怎么使用DDP吧
如何在PyTorch中使用DDPDDP模式
DDP有不同的使用模式。DDP的官方最佳实践是每一张卡对应一个单独的GPU模型也就是一个进程在下面介绍中都会默认遵循这个pattern。 举个例子我有两台机子每台8张显卡那就是2x816个进程并行数是16。
但是我们也是可以给每个进程分配多张卡的。总的来说分为以下三种情况
每个进程一张卡。这是DDP的最佳使用方法。每个进程多张卡复制模式。一个模型复制在不同卡上面每个进程都实质等同于DP模式。这样做是能跑得通的但是速度不如上一种方法一般不采用。每个进程多张卡并行模式。一个模型的不同部分分布在不同的卡上面。例如网络的前半部分在0号卡上后半部分在1号卡上。这种场景一般是因为我们的模型非常大大到一张卡都塞不下batch size 1的一个模型。
在本文中先不会讲每个进程多张卡要怎么操作免得文章过于冗长。在这里只是让你知道有这个东西用的时候再查阅文档。
如何在PyTorch中使用DDP概念
下面介绍一些PyTorch分布式编程的基础概念。
基本概念
在16张显卡16的并行数下DDP会同时启动16个进程。下面介绍一些分布式的概念。
group
即进程组。默认情况下只有一个组。这个可以先不管一直用默认的就行。
world size
表示全局的并行数简单来讲就是2x816。
# 获取world size在不同进程里都是一样的得到16
torch.distributed.get_world_size()rank
表现当前进程的序号用于进程间通讯。对于16的world sizel来说就是0,1,2,…,15。 注意rank0的进程就是master进程。
# 获取rank每个进程都有自己的序号各不相同
torch.distributed.get_rank()local_rank
又一个序号。这是每台机子上的进程的序号。机器一上有0,1,2,3,4,5,6,7机器二上也有0,1,2,3,4,5,6,7
# 获取local_rank。一般情况下你需要用这个local_rank来手动设置当前模型是跑在当前机器的哪块GPU上面的。
torch.distributed.local_rank()如何在PyTorch中使用DDP详细流程
精髓
DDP的使用非常简单因为它不需要修改你网络的配置。其精髓只有一句话
model DDP(model, device_ids[local_rank], output_devicelocal_rank)原本的model就是你的PyTorch模型新得到的model就是你的DDP模型。 最重要的是后续的模型关于前向传播、后向传播的用法和原来完全一致DDP把分布式训练的细节都隐藏起来了不需要暴露给用户非常优雅 对于有时间的人如果你想知道DDP的实现方式请看DDP第二篇进阶部分
准备工作
但是在套model DDP(model)之前我们还是需要做一番准备功夫把环境准备好的。 这里需要注意的是我们的程序虽然会在16个进程上跑起来但是它们跑的是同一份代码所以在写程序的时候要处理好不同进程的关系。
## main.py文件
import torch
import argparse# 新增1:依赖
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP# 新增2从外面得到local_rank参数在调用DDP的时候其会自动给出这个参数后面还会介绍。所以不用考虑太多照着抄就是了。
# argparse是python的一个系统库用来处理命令行调用如果不熟悉可以稍微百度一下很简单
parser argparse.ArgumentParser()
parser.add_argument(--local_rank, default-1)
FLAGS parser.parse_args()
local_rank FLAGS.local_rank# 新增3DDP backend初始化
# a.根据local_rank来设定当前使用哪块GPU
torch.cuda.set_device(local_rank)
# b.初始化DDP使用默认backend(nccl)就行。如果是CPU模型运行需要选择其他后端。
dist.init_process_group(backendnccl)# 新增4定义并把模型放置到单独的GPU上需要在调用modelDDP(model)前做哦。
# 如果要加载模型也必须在这里做哦。
device torch.device(cuda, local_rank)
model nn.Linear(10, 10).to(device)
# 可能的load模型...# 新增5之后才是初始化DDP模型
model DDP(model, device_ids[local_rank], output_devicelocal_rank)前向与后向传播
有一个很重要的概念就是数据的并行化。 我们知道DDP同时起了很多个进程但是他们用的是同一份数据那么就会有数据上的冗余性。也就是说你平时一个epoch如果是一万份数据现在就要变成1*1616万份数据了。 那么我们需要使用一个特殊的sampler来使得各个进程上的数据各不相同进而让一个epoch还是1万份数据。 幸福的是DDP也帮我们做好了
my_trainset torchvision.datasets.CIFAR10(root./data, trainTrue)
# 新增1使用DistributedSamplerDDP帮我们把细节都封装起来了。用就完事儿
# sampler的原理后面也会介绍。
train_sampler torch.utils.data.distributed.DistributedSampler(my_trainset)
# 需要注意的是这里的batch_size指的是每个进程下的batch_size。也就是说总batch_size是这里的batch_size再乘以并行数(world_size)。
trainloader torch.utils.data.DataLoader(my_trainset, batch_sizebatch_size, samplertrain_sampler)for epoch in range(num_epochs):# 新增2设置sampler的epochDistributedSampler需要这个来维持各个进程之间的相同随机数种子trainloader.sampler.set_epoch(epoch)# 后面这部分则与原来完全一致了。for data, label in trainloader:prediction model(data)loss loss_fn(prediction, label)loss.backward()optimizer optim.SGD(ddp_model.parameters(), lr0.001)optimizer.step()其他需要注意的地方
保存参数
# 1. save模型的时候和DP模式一样有一个需要注意的点保存的是model.module而不是model。
# 因为model其实是DDP model参数是被modelDDP(model)包起来的。
# 2. 我只需要在进程0上保存一次就行了避免多次保存重复的东西。
if dist.get_rank() 0:torch.save(model.module, saved_model.ckpt)理论上在没有buffer参数如BN的情况下DDP性能和单卡Gradient Accumulation性能是完全一致的。 并行度为8的DDP 等于 Gradient Accumulation Step为8的单卡 速度上DDP当然比Graident Accumulation的单卡快 但是还有加速空间。请见DDP系列第三篇实战。 如果要对齐性能需要确保喂进去的数据在DDP下和在单卡Gradient Accumulation下是一致的。 这个说起来简单但对于复杂模型可能是相当困难的。
调用方式
像我们在QuickStart里面看到的DDP模型下python源代码的调用方式和原来的不一样了。现在需要用torch.distributed.launch来启动训练。 作用 在这里我们给出分布式训练的重要参数 有多少台机器 –nnodes 当前是哪台机器 –node_rank 每台机器有多少个进程 –nproc_per_node 高级参数可以先不看多机模式才会用到 通讯的address通讯的address 实现方式 我们需要在每一台机子总共m台上都运行一次torch.distributed.launch 每个torch.distributed.launch会启动n个进程并给每个进程一个--local_ranki的参数 这就是之前需要新增从外面得到local_rank参数的原因 这样我们就得到n*m个进程world_sizen*m
单机模式
## Bash运行
# 假设我们只在一台机器上运行可用卡数是8
python -m torch.distributed.launch --nproc_per_node 8 main.py多机模式
复习一下master进程就是rank0的进程。 在使用多机模式前需要介绍两个参数 通讯的address --master_address 也就是master进程的网络地址 默认是127.0.0.1只能用于单机。 通讯的port --master_port 也就是master进程的一个端口要先确认这个端口没有被其他程序占用了哦。一般情况下用默认的就行 默认是29500
## Bash运行
# 假设我们在2台机器上运行每台可用卡数是8
# 机器1
python -m torch.distributed.launch --nnodes2 --node_rank0 --nproc_per_node 8 \--master_adderss $my_address --master_port $my_port main.py
# 机器2
python -m torch.distributed.launch --nnodes2 --node_rank1 --nproc_per_node 8 \--master_adderss $my_address --master_port $my_port main.py小技巧
# 假设我们只用4,5,6,7号卡
CUDA_VISIBLE_DEVICES4,5,6,7 python -m torch.distributed.launch --nproc_per_node 4 main.py
# 假如我们还有另外一个实验要跑也就是同时跑两个不同实验。
# 这时为避免master_port冲突我们需要指定一个新的。这里我随便敲了一个。
CUDA_VISIBLE_DEVICES4,5,6,7 python -m torch.distributed.launch --nproc_per_node 4 \--master_port 53453 main.pymp.spawn调用方式
PyTorch引入了torch.multiprocessing.spawn可以使得单卡、DDP下的外部调用一致即不用使用torch.distributed.launch。 python main.py一句话搞定DDP模式。
给一个mp.spawn的文档代码文档
下面给一个简单的demo
def demo_fn(rank, world_size):dist.init_process_group(nccl, rankrank, world_sizeworld_size)# lots of code....def run_demo(demo_fn, world_size):mp.spawn(demo_fn,args(world_size,),nprocsworld_size,joinTrue)mp.spawn与launch各有利弊请按照自己的情况选用。 按照笔者个人经验如果算法程序是提供给别人用的那么mp.spawn更方便因为不用解释launch的用法但是如果是自己使用launch更有利因为你的内部程序会更简单支持单卡、多卡DDP模式也更简单。
总结
既然看到了这里不妨点个赞/喜欢吧
在本篇中我们介绍了DDP的加速原理和基本用法。如果你能充分理解文章内容那么你可以说对DDP初步入门了可以开始改造你的算法程序来吃掉多卡训练速度提升这波红利了
在DDP系列的后面两篇中我们还会介绍DDP的底层实现方法以及DDP的一些实战。这些属于进阶的文章如果你的DDP程序运行情况不理想/没有获得速度提升或者你比较有探究精神/学习兴趣浓厚那么一定不要错过后面这两篇.
最后让我们来总结一下所有的代码这份是一份能直接跑的代码推荐收藏
################
## main.py文件
import argparse
from tqdm import tqdm
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
# 新增
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP### 1. 基础模块 ###
# 假设我们的模型是这个与DDP无关
class ToyModel(nn.Module):def __init__(self):super(ToyModel, self).__init__()self.conv1 nn.Conv2d(3, 6, 5)self.pool nn.MaxPool2d(2, 2)self.conv2 nn.Conv2d(6, 16, 5)self.fc1 nn.Linear(16 * 5 * 5, 120)self.fc2 nn.Linear(120, 84)self.fc3 nn.Linear(84, 10)def forward(self, x):x self.pool(F.relu(self.conv1(x)))x self.pool(F.relu(self.conv2(x)))x x.view(-1, 16 * 5 * 5)x F.relu(self.fc1(x))x F.relu(self.fc2(x))x self.fc3(x)return x
# 假设我们的数据是这个
def get_dataset():transform torchvision.transforms.Compose([torchvision.transforms.ToTensor(),torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])my_trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform)# DDP使用DistributedSamplerDDP帮我们把细节都封装起来了。# 用就完事儿sampler的原理第二篇中有介绍。train_sampler torch.utils.data.distributed.DistributedSampler(my_trainset)# DDP需要注意的是这里的batch_size指的是每个进程下的batch_size。# 也就是说总batch_size是这里的batch_size再乘以并行数(world_size)。trainloader torch.utils.data.DataLoader(my_trainset, batch_size16, num_workers2, samplertrain_sampler)return trainloader### 2. 初始化我们的模型、数据、各种配置 ####
# DDP从外部得到local_rank参数
parser argparse.ArgumentParser()
parser.add_argument(--local_rank, default-1, typeint)
FLAGS parser.parse_args()
local_rank FLAGS.local_rank# DDPDDP backend初始化
torch.cuda.set_device(local_rank)
dist.init_process_group(backendnccl) # nccl是GPU设备上最快、最推荐的后端# 准备数据要在DDP初始化之后进行
trainloader get_dataset()# 构造模型
model ToyModel().to(local_rank)
# DDP: Load模型要在构造DDP模型之前且只需要在master上加载就行了。
ckpt_path None
if dist.get_rank() 0 and ckpt_path is not None:model.load_state_dict(torch.load(ckpt_path))
# DDP: 构造DDP model
model DDP(model, device_ids[local_rank], output_devicelocal_rank)# DDP: 要在构造DDP model之后才能用model初始化optimizer。
optimizer torch.optim.SGD(model.parameters(), lr0.001)# 假设我们的loss是这个
loss_func nn.CrossEntropyLoss().to(local_rank)### 3. 网络训练 ###
model.train()
iterator tqdm(range(100))
for epoch in iterator:# DDP设置sampler的epoch# DistributedSampler需要这个来指定shuffle方式# 通过维持各个进程之间的相同随机数种子使不同进程能获得同样的shuffle效果。trainloader.sampler.set_epoch(epoch)# 后面这部分则与原来完全一致了。for data, label in trainloader:data, label data.to(local_rank), label.to(local_rank)optimizer.zero_grad()prediction model(data)loss loss_func(prediction, label)loss.backward()iterator.desc loss %0.3f % lossoptimizer.step()# DDP:# 1. save模型的时候和DP模式一样有一个需要注意的点保存的是model.module而不是model。# 因为model其实是DDP model参数是被modelDDP(model)包起来的。# 2. 只需要在进程0上保存一次就行了避免多次保存重复的东西。if dist.get_rank() 0:torch.save(model.module.state_dict(), %d.ckpt % epoch)################
## Bash运行
# DDP: 使用torch.distributed.launch启动DDP模式
# 使用CUDA_VISIBLE_DEVICES来决定使用哪些GPU
# CUDA_VISIBLE_DEVICES0,1 python -m torch.distributed.launch --nproc_per_node 2 main.pyCitation
很全面的知乎上的文章会飞的闲鱼Pytorch 分布式训练pytorch 官方入门https://pytorch.org/tutorials/intermediate/ddp_tutorial.htmlpytorch 官方设计笔记https://pytorch.org/docs/master/notes/ddp.html关于并行的介绍https://medium.com/esaliya/model-parallelism-in-deep-learning-is-not-what-you-think-94d2f81e82ed