PyTorch 分布式训练DDP 单机多卡快速上手

PyTorch 分布式训练DDP 单机多卡快速上手

本文旨在帮助新人快速上手最有效的 PyTorch 单机多卡训练,对于 PyTorch 分布式训练的理论介绍、多方案对比,本文不做详细介绍,有兴趣的读者可参考:

[分布式训练] 单机多卡的正确打开方式:理论基础

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

DP与DDP

我们知道 PyTorch 本身对于单机多卡提供了两种实现方式

  • DataParallel(DP):Parameter Server模式,一张卡位reducer,实现也超级简单,一行代码。
  • DistributedDataParallel(DDP):All-Reduce模式,本意是用来分布式训练,但是也可用于单机多卡。

DataParallel是基于Parameter server的算法,实现比较简单,只需在原单机单卡代码的基础上增加一行:

model = nn.DataParallel(model, device_ids=config.gpu_id)

但是其负载不均衡的问题比较严重,有时在模型较大的时候(比如bert-large),reducer的那张卡会多出3-4g的显存占用。

并且速度也比较慢:

在这里插入图片描述

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

DistributedDataParallel

官方建议用新的DDP,采用all-reduce算法,本来设计主要是为了多机多卡使用,但是单机上也能用。

首先明确几个概念:

  • rank

    多机多卡:代表某一台机器

    单机多卡:代表某一块GPU

  • world_size

    多机多卡:代表有几台机器

    单机多卡:代表有几块GPU

  • local_rank

    多机多卡:代表某一块GPU的编号

    单机多卡:代表某一块GPU的编号

单机单卡训练代码

我们先给出一个单机单卡训练代码的 demo,简单地跑一下数据流。麻雀虽小,五脏俱全。这个 demo 包含了我们平时深度学习训练过程中的完整步骤。包括模型、数据集的定义与实例化,损失函数,优化器的定义,梯度清零、梯度反传,优化器迭代更新以及训练日志的打印。

接下来我们将会使用 PyTorch 提供的 DistributedDataParallel 把这个单机单卡的训练过程改装为单机多卡并行训练。

import torch
import torch.nn as nn
from torch.optim import SGD
from torch.autograd import Variable
from torch.utils.data import Dataset, DataLoader
import os
import argparseparser = argparse.ArgumentParser()
parser.add_argument('--gpu_id', type=str, default='0,2')
parser.add_argument('--batchSize', type=int, default=32)
parser.add_argument('--epochs', type=int, default=5)
parser.add_argument('--dataset-size', type=int, default=128)
parser.add_argument('--num-classes', type=int, default=10)
config = parser.parse_args()os.environ['CUDA_VISIBLE_DEVICES'] = config.gpu_id# 定义一个随机数据集,随机生成样本
class RandomDataset(Dataset):def __init__(self, dataset_size, image_size=32):images = torch.randn(dataset_size, 3, image_size, image_size)labels = torch.zeros(dataset_size, dtype=int)self.data = list(zip(images, labels))def __getitem__(self, index):return self.data[index]def __len__(self):return len(self.data)# 定义模型,简单的一层卷积加一层全连接softmax
class Model(nn.Module):def __init__(self, num_classes):super(Model, self).__init__()self.conv2d = nn.Conv2d(3, 16, 3)self.fc = nn.Linear(30*30*16, num_classes)self.softmax = nn.Softmax(dim=1)def forward(self, x):batch_size = x.shape[0]x = self.conv2d(x)x = x.reshape(batch_size, -1)x = self.fc(x)out = self.softmax(x)return out# 实例化模型、数据集、加载器和优化器
model = Model(config.num_classes)
dataset = RandomDataset(config.dataset_size)
loader = DataLoader(dataset, batch_size=config.batchSize, shuffle=True)
loss_func = nn.CrossEntropyLoss()if torch.cuda.is_available():model.cuda()
optimizer = SGD(model.parameters(), lr=0.1, momentum=0.9)# 若使用DP,仅需一行
# if torch.cuda.device_count > 1: model = nn.DataParallel(model)
# 我们不用DP,而将用DDP# 开始训练
for epoch in range(config.epochs):for step, (images, labels) in enumerate(loader):if torch.cuda.is_available(): images = images.cuda()labels = labels.cuda()preds = model(images)loss = loss_func(preds, labels)optimizer.zero_grad()loss.backward()optimizer.step()print(f'Step: {step}, Loss: {loss.item()}')print(f'Epoch {epoch} Finished !')

训练日志输出为:

Step: 0, Loss: 1.4611507654190063
Step: 1, Loss: 1.4611507654190063
...
Step: 7, Loss: 1.4611507654190063
Epoch 0 Finished !
...

修改代码

使用 PyTorch 提供的 DistributedDataParallel 将单机单卡的训练代码为单机多卡的并行训练代码需要以下几个步骤:

  1. 初始化

    torch.distributed.init_process_group(backend="nccl")
    local_rank = torch.distributed.get_rank()
    torch.cuda.set_device(local_rank)
    device = torch.device("cuda", local_rank)
    
  2. 设置模型并行

    model=torch.nn.parallel.DistributedDataParallel(model)
    
  3. 设置数据并行

    from torch.utils.data.distributed import DistributedSampler
    sampler = DistributedSampler(dataset) # 这个sampler会自动分配数据到各个gpu上
    loader = DataLoader(dataset, batch_size=batch_size, sampler=sampler)
    

在上面单机单卡的代码合适位置中增添这么几行即可。

DDP多卡训练的启动

另外需要注意的是,单机多卡的启动与平时的 python ddp_demo.py 也不一样,需要:

python -m torch.distributed.launch --nproc_per_node 2  ddp_demo.py --batchSize 64 --epochs 10 --gpu_id 1,2
# 或 torchrun  --nproc_per_node=2 ddp_demo.py --batchSize 64 --epochs 10

其中 --nproc_per_node 是我们要使用的显卡数量。argparse 的参数加在后面即可。

注意 --local_rank 不是由我们手动指定。

DDP多卡训练的日志输出

还有,由于 DDP 多卡训练是多进程进行的,每个进程都会打印一遍日志输出。即会出现类似这种输出:

Step: 0, Loss: 1.4611507654190063
Step: 0, Loss: 1.4611507654190063
Step: 1, Loss: 1.4611507654190063
Step: 1, Loss: 1.4611507654190063
...
Step: 7, Loss: 1.4611507654190063
Step: 7, Loss: 1.4611507654190063
Epoch 0 Finished !
Epoch 0 Finished !
...

因此在训练验证过程中的日志记录输出也要注意:

在启动器启动python脚本后,在执行过程中,启动器会将当前进程的 index 通过参数传递给 python,我们可以这样获得当前进程的 index:即通过命令行参数 --local_rank 来告诉我们当前进程使用的是哪个GPU,用于我们在每个进程中指定不同的device(也有其他的方式来获取当前进程)。进程可以简单理解为运行一个代码,分布式训练采用多GPU多进程的方式,即每个进程都要独立运行一份训练代码,由此,为每个GPU分配一个进程进行分布式训练。通常不需要在每个进程中都有日志或其他信息(模型权重等)的输出,即可以通过--local_rank来指定打印日志或其他信息(模型权重等)的进程。

查看当前设备

我们可以在训练循环里加上一行,来查看当前进程时在哪个GPU上进行计算的:

print(f"data: {images.device}, model: {next(model.parameters()).device}")

注意这里我们的 model.parameters() 实际上是 Python 中的一个生成器,因此,需要用 next() 方法来取得其中一个,查看其所在设备。

部分输出:

Epoch 0 Finished !
data: cuda:0, model: cuda:0
data: cuda:1, model: cuda:1
...
data: cuda:0, model: cuda:0
data: cuda:1, model: cuda:1
data: cuda:0, model: cuda:0
data: cuda:1, model: cuda:1
Epoch 1 Finished !
...

可以看到,在我们的DDP多进程单机多卡训练中,在两个设备上都会有训练数据和模型的分布,并且也都会打印出来。

附录:完整DDP训练代码

# ddp_demo.py
import torch
import torch.nn as nn
from torch.optim import SGD
from torch.autograd import Variable
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
import os
import argparse# 定义一个随机数据集
class RandomDataset(Dataset):def __init__(self, dataset_size, image_size=32):images = torch.randn(dataset_size, 3, image_size, image_size)labels = torch.zeros(dataset_size, dtype=int)self.data = list(zip(images, labels))def __getitem__(self, index):return self.data[index]def __len__(self):return len(self.data)# 定义模型
class Model(nn.Module):def __init__(self, num_classes):super(Model, self).__init__()self.conv2d = nn.Conv2d(3, 16, 3)self.fc = nn.Linear(30*30*16, num_classes)self.softmax = nn.Softmax(dim=1)def forward(self, x):batch_size = x.shape[0]x = self.conv2d(x)x = x.reshape(batch_size, -1)x = self.fc(x)out = self.softmax(x)return outparser = argparse.ArgumentParser()
parser.add_argument('--gpu_id', type=str, default='0,2')
parser.add_argument('--batchSize', type=int, default=64)
parser.add_argument('--epochs', type=int, default=5)
parser.add_argument('--dataset-size', type=int, default=1024)
parser.add_argument('--num-classes', type=int, default=10)
config = parser.parse_args()os.environ['CUDA_VISIBLE_DEVICES'] = config.gpu_id
torch.distributed.init_process_group(backend="nccl")local_rank = torch.distributed.get_rank()
torch.cuda.set_device(local_rank)
device = torch.device("cuda", local_rank)# 实例化模型、数据集和加载器loader
model = Model(config.num_classes)dataset = RandomDataset(config.dataset_size)
sampler = DistributedSampler(dataset) # 这个sampler会自动分配数据到各个gpu上
loader = DataLoader(dataset, batch_size=config.batchSize, sampler=sampler)# loader = DataLoader(dataset, batch_size=config.batchSize, shuffle=True)
loss_func = nn.CrossEntropyLoss()if torch.cuda.is_available():model.cuda()
model = torch.nn.parallel.DistributedDataParallel(model)
optimizer = SGD(model.parameters(), lr=0.1, momentum=0.9)# 开始训练
for epoch in range(config.epochs):for step, (images, labels) in enumerate(loader):if torch.cuda.is_available(): images = images.cuda()labels = labels.cuda()preds = model(images)# print(f"data: {images.device}, model: {next(model.parameters()).device}")loss = loss_func(preds, labels)optimizer.zero_grad()loss.backward()optimizer.step()print(f'Step: {step}, Loss: {loss.item()}')print(f'Epoch {epoch} Finished !')

启动训练(以两张卡为例):

torchrun  --nproc_per_node=2 ddp_demo.py --batchSize 64 --epochs 10

Ref:

https://blog.csdn.net/weixin_44966641/article/details/121015241

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/532629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux free 命令详解

Linux free 命令详解 free 命令用来查看系统中已用的和可用的内存。 命令选项及输出简介 关于各种命令的功能和命令选项,还是推荐英语比较好的同学直接看手册 RTFM:man free。这里简单总结一下一些重点: 功能及输出简介 free 命令显示系…

CTF web题 wp:

1.签到题 火狐F12查看源码,发现注释: 一次base64解码出flag 2.Encode 在这里插入图片描述 和第一题界面一样?? 轻车熟路f12: 发现编码: 格式看上去是base64,连续两次base64后,观…

【深度学习】深入理解Batch Normalization批归一化

【深度学习】深入理解Batch Normalization批归一化 转自:https://www.cnblogs.com/guoyaohua/p/8724433.html 这几天面试经常被问到BN层的原理,虽然回答上来了,但还是感觉答得不是很好,今天仔细研究了一下Batch Normalization的原…

ThinkPHP V5 漏洞利用

ThinkPHP 5漏洞简介 ThinkPHP官方2018年12月9日发布重要的安全更新,修复了一个严重的远程代码执行漏洞。该更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本…

Vim 重复操作的宏录制

Vim 重复操作的宏录制 转自:https://www.cnblogs.com/ini_always/archive/2011/09/21/2184446.html 在编辑某个文件的时候,可能会出现需要对某种特定的操作进行许多次的情况,以编辑下面的文件为例: ; ;This is a sample config…

Vim 进阶1

Vim 进阶1 所有你觉得简单重复,可以自动化实现的操作,都是可以自动化实现的。 Vim光标移动拾遗 w:下一个单词的开头,e:下一个单词的结尾,b:上一个单词的开头, 0:行首…

攻防世界web题ics-06(爆破id值)

打开界面:嚯!这花里胡哨 点来点去只有报表中心有回显: 发现url中id等于1,sql注入尝试无果, burp工具爆破id 对id的值进行爆破 burp报ERROR的话这是个bug,先点击Hex后点decimal手动刷新就可以使用 强行总…

crontab用法与实例

crontab用法与实例 本文基于 ubuntu 18.04 在Linux系统的实际使用中,可能会经常碰到让系统在某个特定时间执行某些任务的情况,比如定时采集服务器的状态信息、负载状况;定时执行某些任务/脚本来对远端进行数据采集等。这里将介绍下crontab的配…

手工sql注入常规总结

1.发现注入点 2.报数据库 先用单引号(也尝试双引号)闭合前面的语句,使注入的语句能够执行, 数字 0 :匹配字段,还有 11 12 等等都可以使用,有些网站会有过滤处理,建议采用 1%2b12 1%2b1>1 绕…

Systemd入门教程:命令篇

Systemd入门教程:命令篇 转自:http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html 作者: 阮一峰 日期: 2016年3月 7日 Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数…

【CVE-2018-12613】phpmyadmin 4.8.1 远程文件包含漏洞复现

**环境:**http://62.234.56.138:8080/server_databases.php 官网下载phpmyadmin 4.8.1 源码:index.php文件中 函数含义: targer非空targer是否位字符串不能以index为开头,即过滤了index值不能出现在blacklist内,即…

Systemd 入门教程:实战篇

Systemd 入门教程:实战篇 转自:https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html 作者: 阮一峰 日期: 2016年3月 8日 上一篇文章,我介绍了 Systemd 的主要命令,今天介绍如何使…

关于ubuntu自定义service服务时找不到/usr/lib/systemd/system目录的问题

关于ubuntu自定义service服务时找不到/usr/lib/systemd/system目录的问题 问题 我们知道在 systemd 取代了 init 而成为广大 Linux 系统中 PID 为1的守护进程之后,Linux 中的服务(service)主要有 systemd 命令组来实现。在大多数发行版 Lin…

攻防世界web2(逆向加密算法)

打开网页有如下代码&#xff1a; <?php $miwen"a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";function encode($str){$_ostrrev($str);// echo $_o;for($_00;$_0<strlen($_o);$_0){$_csubstr($_o,$_0,1);$__ord($_c)1;$_cchr($__);$_$_.$…

ctags 基本使用方法

ctags 基本使用方法 简介 ctags&#xff08;Generate tag files for source code&#xff09;是vim下方便代码阅读的工具。尽管ctags也可以支持其它编辑器&#xff0c;但是它正式支持的只有 Vim。并且 Vim 中已经默认安装了 ctags&#xff0c;它可以帮助程序员很容易地浏览源…

vimrc配置文件

vimrc配置文件 转自&#xff1a;https://www.ruanyifeng.com/blog/2018/09/vimrc.html Vim 是最重要的编辑器之一&#xff0c;主要有下面几个优点。 可以不使用鼠标&#xff0c;完全用键盘操作。系统资源占用小&#xff0c;打开大文件毫无压力。键盘命令变成肌肉记忆以后&am…

CTFHUB 《请求方式》 http请求,curl命令总结

打开网页&#xff1a; 思路一&#xff1a; 根据题目&#xff0c;应该是向网页发送get方式请求&#xff0c;但并没有具体规定要发送什么&#xff0c;尝试get发送参数后&#xff0c;都没有返回网页&#xff0c;emmm’…好像不是我想的那种套路 思路二&#xff1a; 网上找到思路…

Vim进阶2 map映射

Vim进阶2 map映射 简介 map是一个 vim 中的一些列映射命令&#xff0c;将常用的很长的命令映射到一个新的功能键上。map是Vim强大的一个重要原因&#xff0c;可以自定义各种快捷键&#xff0c;用起来自然得心应手。 map系列命令格式 格式 以 map 命令为例&#xff0c;它的…

CTFHUB 《基础认证》:burp使用,basic请求了解

题目简介&#xff1a;在HTTP中&#xff0c;基本认证&#xff08;英语&#xff1a;Basic access authentication&#xff09;是允许http用户代理&#xff08;如&#xff1a;网页浏览器&#xff09;在请求时&#xff0c;提供 用户名 和 密码 的一种方式。详情请查看 https://zh.w…

信息量、熵、交叉熵、KL散度、JS散度杂谈及代码实现

信息量、熵、交叉熵、KL散度、JS散度杂谈及代码实现 信息量 任何事件都会承载着一定的信息量&#xff0c;包括已经发生的事件和未发生的事件&#xff0c;只是它们承载的信息量会有所不同。如昨天下雨这个已知事件&#xff0c;因为已经发生&#xff0c;既定事实&#xff0c;那…