PyTorch 分布式训练实现(DP/DDP/torchrun/多机多卡)
1、DataParallel
模型与变量必须存在于同一个设备上(CPU or GPU) pytorch使用to函数实现变量或模型的存储转移,to函数的对象要么是数据Tensor,要么是模型Module 张量不执行inplace(即 执行之后重新构建一个新的张量),模型执行inplace(执行之后不重新构建一个新的模型)
原理:当给定model时,主要实现功能是将input数据依据batch的这个维度,将数据划分到指定的设备上。其他的对象(objects)复制到每个设备上。在前向传播的过程中,module被复制到每个设备上,每个复制的副本处理一部分输入数据。在反向传播过程中,每个副本module的梯度被汇聚到原始的module上计算(一般为第0块GPU)。
举例:如果当前有4个GPU,batch_size=16,那么模型将被复制到每一个GPU上,在前向传播时,每一个gpu将分到4个batch,每个gpu独立计算依据分到的batch计算出结果的梯度,然后将梯度返回到第一个GPU上,第一个GPU再进行梯度融合、模型更新。在下一次前向传播的时候,将更新后的模型再复制给每一个GPU。
具体使用如下
### 第一步:构建模型
'''
module 需要分发的模型
device_ids 可分发的gpu,默认分发到所有看见GPU(环境变量设置的)
output_device 结果输出设备 通常设置成逻辑gpu的第一个
'''
module=your_simple_net() #你的模型
Your_Parallel_Net=torch.nn.DataParallel(module,device_ids=None,output_device=None)
### 第二步:数据迁移
inputs=inputs.to(device)
labels=labels.to(device)
#此处的device通常应为模型输出的output_device,否则无法计算loss
2、DistributedDataParallel
为什么要引入DDP(DistributedDataParallel)?
1、DP在每个训练批次(batch)中,因为模型的权重都是在 一个进程上先算出来 然后再把他们分发到每个GPU上,所以网络通信就成为了一个瓶颈,而GPU使用率也通常很低。
2、因为它在每一次的前向传播的时候把模型也复制了(即每次更新都复制一遍模型),并且单进程多线程会造成GIL contention (全局解释器锁争用) 这里进程计算权重使通信成为瓶颈造成了大量的时间浪费,因此引入了DDP。
DDP采用多进程控制多GPU,共同训练模型,一份代码会被pytorch自动分配到n个进程并在n个GPU上运行。 DDP运用Ring-Reduce通信算法在每个GPU间对梯度进行通讯,交换彼此的梯度,从而获得所有GPU的梯度。对比DP,不需要在进行模型本体的通信,因此可以加速训练。
需要注意以下几点:
1、设置DistributedSampler来打乱数据,因为一个batch被分配到了好几个进程中,要确保不同的GPU拿到的不是同一份数据。
2、要告诉每个进程自己的id,即使用哪一块GPU。
3、如果需要做BatchNormalization,需要对数据进行同步(还待研究,挖坑)
使用方式(单机多卡环境)
#启动方式,shell中运行:
python -m torch.distributed.launch \
--nnodes 1 \
--nproc_per_node=4 \
YourScript.py
# nnodes: 表示有多少个节点,可以通俗的理解为有多少台机器
# nproc_per_node 表示每个节点上有多少个进程,每个进程一般独占一块GPU
########################## 第1步 ##########################
#初始化
'''
在启动器为我们启动python脚本后,在执行过程中,启动器会将当前进程的(其实就是 GPU的)index 通过参数传递给 python,
我们可以这样获得当前进程的 index:即通过参数 local_rank 来告诉我们当前进程使用的是哪个GPU,
用于我们在每个进程中指定不同的device
'''
parse.add_argument('--local_rank',type=int)
args=parser.parse_args()
local_rank=args.local_rank
torch.cuda.set_device(local_rank)
'''
init_process_group用于初始化GPU通信方式(NCCL)和参数的获取方式(env代表通过环境变量)
gpu使用nccl最快,gloo为cpu分布式训练,mpu则需要重新编码
init_method 指定如何初始化进程组的 URL。
默认及推荐为'env://' 其他初始化方式与多机多卡有关(not sure,挖个坑)
'''
torch.distributed.init_process_group('nccl',init_method='env://')
device = torch.device(f'cuda:{args.local_rank}')
########################## 第2步 ##########################
#处理Dataloader
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset,shuffle=True)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=..., sampler=train_sampler)
#torch.utils.data.DataLoader中的shuffle应该设置为False(默认),因为打乱的任务交给了sampler
########################## 第3步 ##########################
#模型的初始化
model=torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank])
'''
使用 DistributedDataParallel 包装模型,
它能帮助我们为不同 GPU 上求得的梯度进行allreduce(即汇总不同 GPU 计算所得的梯度,并同步计算结果)。
allreduce 后不同 GPU 中模型的梯度均为 allreduce 之前各 GPU 梯度的均值。
''''
########################## 第4步 ##########################
#同DP,进行inputs、labels的设备转移
最新版本的PyTorch实现
根据PyTorch官网介绍
[ This module(torch.distributed.launch) is going to be deprecated in favor of torchrun. ]
torchrun 包含了torch.distributed.launch的所有功能,还有以下三点额外的功能:
1、worker的rank和world_size将被自动分配
2、通过重新启动所有workers来处理workers的故障
3、允许节点数目在最大最小值之间有所改变 即具备弹性
具体使用如下
# local_rank参数应当从环境变量中读取,而不是通过参数传递。
### BEFORE
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--local_rank", type=int)
args = parser.parse_args()
local_rank = args.local_rank
### NOW
import os
local_rank = int(os.environ["LOCAL_RANK"])
#运行脚本
torchrun train_script.py #除了--use_env参数,其他torch.distributed.launch所使用的参数均可使用,
#如nnodes、nproc_per_node
3、多机多卡DDP
十分重要的概念理解
group: 进程组,通常DDP的各个进程都是在同一个进程组下 world_size: 总的进程数量(原则上,一个进程占用一个GPU) rank:当前进程的序号,用于进程间通信,rank=0表示主机为master节点 local_rank:当前进程对应的GPU号
举个栗子 : 4台机器 (每台机器8张卡) 进行分布式训练。通过 init_process_group() 对进程组进行初始化。 初始化后 可以通过 get_world_size() 获取到 world size = 32。在该例中为32, 即有32个进程,其编号为0-31 通过 get_rank() 函数可以进行获取 在每台机器上,local rank均为0-8, 这是 local rank 与 rank 的区别, local rank 会对应到实际的 GPU ID 上。
三种启动方法:torch.distributed.launch / torch.multiprocessing / Slurm Workload Manager
slurm启动应该会这几天更新掉
使用方法
########################## 第1步 ##########################
#初始化
rank = int(os.environ["RANK"])
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(rank % torch.cuda.device_count())
dist.init_process_group(backend="nccl")
device = torch.device("cuda", local_rank)
########################## 第2步 ##########################
#模型定义
model = model.to(device)
model = DDP(model, device_ids=[local_rank], output_device=local_rank)
#数据集操作与DDP一致
#####运行
'''
exmaple: 2 node, 8 GPUs per node (16GPUs)
需要在两台机器上分别运行脚本
注意细节:node_rank master 为 0
机器1
>>> python -m torch.distributed.launch \
--nproc_per_node=8 \
--nnodes=2 \
--node_rank=0 \
--master_addr="master的ip" \
--master_port=xxxxx \
YourScript.py
机器2
>>> python -m torch.distributed.launch \
--nproc_per_node=8 \
--nnodes=2 \
--node_rank=1 \
--master_addr="master的ip" \
--master_port=xxxxx \
YourScript.py
'''
------ 一些更新 ------
已经很久没看分布式训练相关的内容了 ,可能有很多内容已经过时了,大家要以pytorch官方为主,英文版的pytorch tutorial内容还是不错的