PyTorch 可以通过 torch.nn.DataParallel 直接切分数据并行在单机多卡上,实践证明这个接口并行力度并不尽如人意,主要问题在于数据在 master 上处理然后下发到其他 slaver 上训练,而且由于 GIL 的存在只有计算是并行的。torch.distributed 提供了更好的接口和并行方式,搭配多进程接口 torch.multiprocessing 可以提供更加高效的并行训练。
GIL含义解释
torch.distributed 可以通过 torch.distributed.launch 启动多卡训练,也可以使用 torch.multiprocessing 手动提交多进程并行。
我们分别介绍torch.distributed.launch 和 torch.multiprocessing
(1)torch.distributed.launch
(2)torch.multiprocessing
torch.distributed 提供了和通用分布式系统常见的类似概念。
In the single-machine synchronous case, torch.distributed or the torch.nn.parallel.DistributedDataParallel() wrapper may still have advantages over other approaches to data-parallelism, including torch.nn.DataParallel():也就是说orch.distributed 和 the torch.nn.parallel.DistributedDataParallel() wrapper更有效率,具体原因见:
DISTRIBUTED COMMUNICATION PACKAGE - TORCH.DISTRIBUTED
初始化操作一般在程序刚开始的时候进行
在调用任何其他方法之前,需要使用torch.distributed.init_process_group()函数对包进行初始化。这会阻塞所有进程,直到所有进程都已连接。
torch.distributed.init_process_group(backend, init_method=None, timeout=datetime.timedelta(0, 1800), world_size=-1, rank=-1, store=None, group_name='')
两种初始化方式
1.Specify store, rank, and world_size explicitly.
2.Specify init_method (a URL string) which indicates where/how to discover peers. Optionally specify rank and world_size, or encode all required parameters in the URL and omit them.
If neither is specified, init_method is assumed to be “env://”.
backend (str or Backend) – 。根据构建时配置,有效值包括mpi、gloo和nccl。该字段应该以小写字符串的形式给出(例如,“gloo”),它也可以通过后端属性访问(例如,back . gloo)。如果在每台机器上使用nccl后端多个进程,那么每个进程必须独占访问它使用的每个GPU,因为在进程之间共享GPU可能会导致死锁。
根据官网的介绍, 如果是使用cpu的分布式计算, 建议使用gloo, 因为表中可以看到 gloo对cpu的支持是最好的, 然后如果使用gpu进行分布式计算, 建议使用nccl, 实际测试中我也感觉到, 当使用gpu的时候, nccl的效率是高于gloo的. 根据博客和官网的态度, 好像都不怎么推荐在多gpu的时候使用mpi
对于后端选择好了之后, 我们需要设置一下网络接口, 因为多个主机之间肯定是使用网络进行交换, 那肯定就涉及到ip之类的, 对于nccl和gloo一般会自己寻找网络接口, 但是某些时候, 比如我测试用的服务器, 不知道是系统有点古老, 还是网卡比较多, 需要自己手动设置. 设置的方法也比较简单, 在Python的代码中, 使用下面的代码进行设置就行:
import os
# 以下二选一, 第一个是使用gloo后端需要设置的, 第二个是使用nccl需要设置的
os.environ['GLOO_SOCKET_IFNAME'] = 'eth0'
os.environ['NCCL_SOCKET_IFNAME'] = 'eth0'
我们怎么知道自己的网络接口呢, 打开命令行, 然后输入ifconfig, 然后找到那个带自己ip地址的就是了, 我见过的一般就是em0, eth0, esp2s0之类的, 当然具体的根据你自己的填写. 如果没装ifconfig, 输入命令会报错, 但是根据报错提示安装一个就行了.
init_method (str, optional) –指定如何初始化进程组的URL。如果没有指定init_method或store,Default是“env://”。与store相互排斥。
初始化init_method的方法有两种, 一种是使用TCP进行初始化, 另外一种是使用共享文件系统进行初始化:
使用TCP初始化:
import torch.distributed as dist
dist.init_process_group(backend, init_method='tcp://10.1.1.20:23456',
rank=rank, world_size=world_size)
注意这里使用格式为tcp://ip:端口号, 首先ip地址是你的主节点的ip地址, 也就是rank参数为0的那个主机的ip地址, 然后再选择一个空闲的端口号, 这样就可以初始化init_method了.
使用共享文件系统初始化
好像看到有些人并不推荐这种方法, 因为这个方法好像比TCP初始化要没法, 搞不好和你硬盘的格式还有关系, 特别是window的硬盘格式和Ubuntu的还不一样, 我没有测试这个方法, 看代码:
import torch.distributed as dist
dist.init_process_group(backend, init_method='file:///mnt/nfs/sharedfile',
rank=rank, world_size=world_size)
根据官网介绍, 要注意提供的共享文件一开始应该是不存在的, 但是这个方法又不会在自己执行结束删除文件, 所以下次再进行初始化的时候, 需要手动删除上次的文件, 所以比较麻烦, 而且官网给了一堆警告, 再次说明了这个方法不如TCP初始化的简单.
world_size (int, optional) -参与作业的进程数。如果指定了store,则必须指定它。
rank (int, optional) – Rank of the current process (it should be a number between 0 and world_size-1). Required if store is specified.
表示进程序号,用于进程间通信,可以用于表示进程的优先级。一般设置 rank=0 的主机为 master 节点。
store (Store, optional) – (int, optional) –Key/value store accessible to all workers, used to exchange connection/address information. Mutually exclusive with init_method.
timeout (timedelta, optional) –默认值为30分钟。这适用于gloo后端。对于nccl,只有当环境变量NCCL_BLOCKING_WAIT或NCCL_ASYNC_ERROR_HANDLING设置为1时,这才适用。当设置了NCCL_BLOCKING_WAIT时,这时进程将阻塞并等待集合在抛出异常之前完成的持续时间。当设置了NCCL_ASYNC_ERROR_HANDLING时,这时集合将异步中止并且进程将崩溃的持续时间。NCCL_BLOCKING_WAIT将向用户提供可以捕获和处理的错误,但由于其阻塞性质,它有性能开销。另一方面,NCCL_ASYNC_ERROR_HANDLING的性能开销很少,但在出现错误时会使进程崩溃。这是因为CUDA执行是异步的,并且继续执行用户代码不再安全,因为失败的异步NCCL操作可能导致后续的CUDA操作在损坏的数据上运行。应该只设置这两个环境变量中的一个。
group_name (str, optional, deprecated) – 组名
local_rank:进程内 GPU 编号,非显式参数,由 torch.distributed.launch 内部指定。比方说, rank=3,local_rank=0 表示第 3 个进程内的第 1 块 GPU。
注意初始化rank和world_size
你需要确保, 不同机器的rank值不同, 但是主机的rank必须为0, 而且使用init_method的ip一定是rank为0的主机, 其次world_size是你的进程数量, 你不能随便设置这个数值,它的值一般设置为每个节点的gpu卡个数乘以节点个数。.
初始化中一些需要注意的地方
首先是代码的统一性, 所有的节点上面的代码, 建议完全一样, 不然有可能会出现一些问题, 其次, 这些初始化的参数强烈建议通过argparse模块(命令行参数的形式)输入, 不建议写死在代码中, 也不建议使用pycharm之类的IDE进行代码的运行, 强烈建议使用命令行直接运行.
多机的启动方式可以是直接传递参数并在代码内部解析环境变量,或者通过torch.distributed.launch来启动,两者在格式上面有一定的区别,总之要保证代码与启动方式对应。
例如使用下面的命令运行代码distributed.py:
在代码上添加如下:
torch.multiprocessing.set_start_method('spawn')
待验证的命令行
python distributed.py -bk nccl -im tcp://10.123.225.1:12345 -rank 0 -world_size 2
上面的代码是在主节点上运行, 所以设置rank为0, 同时设置了使用两个主机, 在从节点运行的时候, 输入的代码是下面这样:
这里的rank其实是node_rank,指的是节点编号或者是机器编号,具体概念见rank local_rank node node_rank等的概念
ython distributed.py -bk nccl -im tcp://10.10.10.1:12345 -rank 1 -world_size 2
待验证的命令行(torch.distributed.launch启动)
Node 1: (IP: 192.168.1.1, and has a free port: 1234)
>>> python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE
--nnodes=2 --node_rank=0 --master_addr="192.168.1.1"
--master_port=1234 YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3
and all other arguments of your training script)
Node 2:
>>> python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE
--nnodes=2 --node_rank=1 --master_addr="192.168.1.1"
--master_port=1234 YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3
and all other arguments of your training script)
一定要注意的是, 只能修改rank的值, 其他的值一律不得修改, 否则程序就卡死了初始化到这里也就结束了.
在torch.distributed当中提供了一个用于启动的程序torch.distributed.launch,此帮助程序可用于为每个节点启动多个进程以进行分布式训练,它在每个训练节点上产生多个分布式训练进程。这个工具可以用作CPU或者GPU,如果被用于GPU,每个GPU产生一个进程进行训练。
该工具既可以用来做单节点多GPU训练,也可用于多节点多GPU训练。如果是单节点多GPU,将会在单个GPU上运行一个分布式进程,据称可以非常好地改进单节点训练性能。如果用于多节点分布式训练,则通过在每个节点上产生多个进程来获得更好的多节点分布式训练性能。如果有Infiniband接口则加速比会更高。
在单节点分布式训练或多节点分布式训练的两种情况下,该工具将为每个节点启动给定数量的进程(–nproc_per_node)。如果用于GPU培训,则此数字需要小于或等于当前系统上的GPU数量(nproc_per_node),并且每个进程将在从GPU 0到GPU(nproc_per_node - 1)的单个GPU上运行。
Launch utility
因为是用torch.distributed.launch 启动,故只需要设置rank的初始值(一个节点一个初始值)这种解释对不对??。
注意:讨论下torch.distributed.init_process_group 中的word_size,让其等于torch.cuda.device_count(),那就是一个GPU一个进程,属于单机多进程,一个进程占用一个GPU。设置为1时,一个进程占用了多个GPU,我觉得这种解释是有问题的
结论:用torch.distributed.launch启动时是通过nproc_per_node=5来指定进程数量的,那么在torch.distributed.init_process_group中指定其值是不是不管用了。指定为1或者torch.cuda.device_count()都是可以的,不影响进程的总数??
单进程多卡
os.environ['CUDA_VISIBLE_DEVICES'] = "0,1,2,3,4"
torch.distributed.init_process_group(backend='nccl', init_method='tcp://localhost:23456', rank=0, world_size=1)
data_set = torchvision.datasets.MNIST('~/DATA/', train=True, transform=trans, target_transform=None, download=True)
train_sampler = torch.utils.data.distributed.DistributedSampler(data_set)
data_loader_train = torch.utils.data.DataLoader(dataset=data_set, batch_size=256, sampler=train_sampler, num_workers=16, pin_memory=True)
net = torchvision.models.resnet101(num_classes=10)
net.conv1 = torch.nn.Conv1d(1, 64, (7, 7), (2, 2), (3, 3), bias=False)
net = net.cuda()
# net中不需要指定设备!
net = torch.nn.parallel.DistributedDataParallel(net)
运行:
python python demo.py --arg1 --arg2 --arg3
其他都一样
单机多进程多卡(一个进程占用一个GPU)
parser.add_argument("--local_rank", type=int) # 增加local_rank
torch.cuda.set_device(args.local_rank)
os.environ['CUDA_VISIBLE_DEVICES'] = "0,1,2,3,4"
torch.distributed.init_process_group(backend='nccl', init_method='tcp://localhost:23456', rank=0, world_size=torch.cuda.device_count())或者
dist.init_process_group("nccl", init_method='env://') # init_method方式修改
data_set = torchvision.datasets.MNIST('~/DATA/',train=True, transform=trans, target_transform=None, download=True)
data_loader_train = torch.utils.data.DataLoader(dataset=data_set, batch_size=256,sampler=train_sampler, num_workers=16,pin_memory=True)
net = torchvision.models.resnet101(num_classes=10)
net = net.cuda()
# DDP 输出方式修改:
net = torch.nn.parallel.DistributedDataParallel(net, device_ids=[args.local_rank],output_device=args.local_rank)
criterion = torch.nn.CrossEntropyLoss()
opt = torch.optim.Adam(net.parameters(), lr=0.001)
for epoch in range(1):
for i, data in enumerate(data_loader_train):
images, labels = data
# 要将数据送入指定的对应的gpu中
images.to(args.local_rank, non_blocking=True)
labels.to(args.local_rank, non_blocking=True)
opt.zero_grad()
outputs = net(images)
loss = criterion(outputs, labels)
loss.backward()
opt.step()
if i % 10 == 0:
print("loss: {}".format(loss.item()))
启动:
python -m torch.distributed.launch --nproc_per_node=8
--nnodes=1 --node_rank=0 --master_addr="192.168.1.1"
--master_port=12355 MNIST.py
注: 这里如果使用了argparse, 一定要在参数里面加上–local_rank, 否则运行还是会出错的。它是非显式参数,在使用torch.distributed.launch 启动时,会产生–local_rank参数。因此在参数里面包含这个代码就行,不需要赋值,也不需要在命令行中对其赋值parser.add_argument("--local_rank",type=int)
这一参数的作用是为各个进程分配rank号,因此可以直接使用这个local_rank参数作为torch.distributed.init_process_group()当中的参数rank,同时也可以作为model = DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank)
**注:**还需要注意的是, 如果使用这句代码, 直接在pycharm或者别的编辑器中,是没法正常运行的, 因为这个需要在shell的命令行中运行, 如果想要正确执行这段代码, 假设这段代码的名字是main.py,用一下命令行:
python -m torch.distributed.launch --nproc_per_node=5 main.py
以下试验会报错:
torch.distributed.init_process_group(backend="nccl")
model = DistributedDataParallel(model) # device_ids will include all GPU devices by default
2.多机多卡-多进程训练(每个进程一个gpu)
步骤:
import torch
torch.multiprocessing.set_start_method('spawn')
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel
import os
os.environ['SLURM_NTASKS'] #可用作world size
os.environ['SLURM_NODEID'] #node id
os.environ['SLURM_PROCID'] #可用作全局rank
os.environ['SLURM_LOCALID'] #local_rank
os.environ['SLURM_STEP_NODELIST'] #从中取得一个ip作为
通讯ip
def dist_init(host_addr, rank, local_rank, world_size, port=23456):
host_addr_full = 'tcp://' + host_addr + ':' + str(port)
torch.distributed.init_process_group("nccl", init_method=host_addr_full,rank=rank, world_size=world_size)
num_gpus = torch.cuda.device_count()
torch.cuda.set_device(local_rank)
assert torch.distributed.is_initialized()
rank = int(os.environ['SLURM_PROCID'])
local_rank = int(os.environ['SLURM_LOCALID'])
world_size = int(os.environ['SLURM_NTASKS'])
# get_ip函数自己写一下 不同服务器这个字符串形式不一样
# 保证所有task拿到的是同一个ip就成
ip = get_ip(os.environ['SLURM_STEP_NODELIST'])
dist_init(ip, rank, local_rank, world_size)
# 接下来是写dataset和dataloader,这个网上有很多教程
# 我这给的也只是个形式,按自己需求写好就ok
dataset = your_dataset() #主要是把这写好
datasampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
dataloader = DataLoader(dataset, batch_size=batch_size_per_gpu, sampler=source_sampler)
model = your_model() #也是按自己的模型写
if torch.cuda.device_count() > 1:
print("Let's use", torch.cuda.device_count(), "GPUs!")
model = DistributedDataPrallel(model.cuda(), device_ids=[local_rank])
#srun指定-n 进程总数以及 --ntasks-per-node 每个节点进程数,这样就可以通过os.environ获得每个进程的节点ip信息,全局rank以及local rank
# 这里是3台机器,每台机器8张卡的样子, slrum指令写这样:
srun -n24 --gres=gpu:8 --ntasks-per-node=8 python train.py
PyTorch分布式训练基础–DDP使用
DDP中有关于多机多卡
上面这两个链接可探讨关于使用DDP实现多机多卡训练的步骤,对照使用。
第一个里面有好几种情况。单进程多卡,单机多进程多卡,不同的启动方式。
PyTorch 多进程分布式训练实战
这篇中使用的启动方式mp.spawn,而且只用了torch.distributed实现分布式训练,没有用DDP,其中spawn启动方式结合和第一篇中的spawn启动方式,代码写作相结合实现无DDP的多分布式训练。
Pytoch分布式多机多卡的启动方式详解
Distribution is all you need
在[Distribution is all you need]中实现了以下几种方式的多卡训练:
1.nn.DataParallel 简单方便的 nn.DataParallel
2.torch.distributed 使用 torch.distributed 加速并行训练
3.torch.multiprocessing 使用 torch.multiprocessing 取代启动器
4.apex 使用 apex 再加速
5.horovod horovod 的优雅实现
6.slurm GPU 集群上的分布式
7.补充:分布式 evaluation
我在机器中测试了2确实可行,其他并没有。
最复杂的实现方法应该是不需要nn.parallel.DistributedDataParallel()
采用:这上面的方法,但不全
mp.spawn启动方式代替launch启动方式的最全代码