nn.DataParallel()
(DP)基本概念
多卡训练原理
nn.DataParallel()
的用法torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
module
(Module
): 要放到多卡训练的模型device_ids
(list of python:int or torch.device
): 可用的 gpu 卡号output_device
(int
or torch.device
): 模型输出结果存放的卡号 (如果不指定的话,默认放在 0 卡,这也是为什么多 gpu 训练并不是负载均衡的, 一般 0 卡负载更大)dim
(int
):从哪一维度切分一个 batch 的数据,默认为 0,即从 batch 维度将数据分组后送到不同 device 上运算Example
nn.DataParallel()
的用法十分简单,加一行代码即可net = torch.nn.DataParallel(model, device_ids=[0, 1, 2]).cuda() # broadcast model parameters to all devices
output = net(input_data) # input_var can be on any device, including CPU
nn.Module
,因此使用 nn.DataParallel
后,模型需要使用 .module
来得到实际的模型和优化器:# 保存模型
torch.save(net.module.state_dict(), path)
nn.parallel.DistributedDataParallel
instead of nn.DataParallel
可能 DDP 唯一不好的地方就是相比 DP 使用起来会有些麻烦
nn.parallel.DistributedDataParallel
(DDP)torch.distributed.run
内部指定 (torch.distributed.run
是为了代替 torch.distributed.launch
的新型启动方式,但是由于是新功能, 只有最新的 torch 1.10 支持, 因此出于兼容性考虑还是建议使用 torch.distributed.launch
)。比方说,node_rank=3,local_rank=0 表示第 3 个主机内的第 1 块 GPU,因此 local_rank 对应的就是 Process 需要使用的 Device (GPU) 编号state_dict()
) broadcast 到进程组中的其他进程,然后每个 DDP 进程都会创建一个 local Reducer 来负责梯度同步。在训练过程中,每个进程从磁盘加载 batch 数据,并将它们传递到其 GPU。每个 GPU 都有自己的前向过程,完成前向传播后梯度在各个 GPUs 间进行 All-Reduce,每个 GPU 都收到其他 GPU 的梯度,从而可以独自进行反向传播和参数更新。同时,每一层的梯度不依赖于前一层,所以梯度的 All-Reduce 和后向过程同时计算,以进一步缓解网络瓶颈。在后向过程的最后,每个节点都得到了平均梯度,这样各个 GPU 中的模型参数保持同步 (回忆一下,DP 是将梯度 reduce 到主卡,在主卡上更新参数,再将参数 broadcast 给其他 GPU,这样无论是主卡的负载还是通信开销都比 DDP 大很多) (具体的实现细节可以参考 DISTRIBUTED DATA PARALLEL)distributed.init_process_group
函数来实现多进程同步通信。它需要知道 rank 0 位置以便所有进程都可以同步,以及预期的进程总数 (world_size)。每个进程都需要知道进程总数及其在进程组中的顺序,以及使用哪个 GPU. 此外,Pytorch 还提供了 torch.utils.data.DistributedSampler
来为各个进程切分数据,以保证训练数据不重叠nn.parallel.DistributedDataParallel
的用法torch.nn.parallel.DistributedDataParallel(
module, device_ids=None, output_device=None, dim=0, broadcast_buffers=True, process_group=None,
bucket_cap_mb=25, find_unused_parameters=False, check_reduction=False, gradient_as_bucket_view=False,
static_graph=False)
module
(Module
): 要放到多卡训练的模型device_ids
(list of python:int or torch.device
): CUDA devices. 1) 对于单卡训练,device_ids
可以只包含一个卡号,也可以是 None
;2) 对于多卡 / CPU 训练,device_ids
必须为 None
output_device
(int
or torch.device
): 单卡训练时模型输出结果存放的卡号。对于多卡 / CPU 训练,output_device
必须为 None
dim
(int
):从哪一维度切分一个 batch 的数据,默认为 0,即从 batch 维度将数据分组后送到不同 device 上运算导入需要的库
import os
import torch
import argparse
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
import torch.optim as optim
import torch.nn as nn
import torch.distributed as dist
from datetime import timedelta
初始化进程组、配置 Master Node (init_process_group
参数详见 torch.distributed.init_process_group)
def setup(global_rank, world_size):
torch.distributed.init_process_group(
backend="nccl",
init_method='env://', # indicates where/how to discover peers
# 'env://' means environment variable initialization
# 'file:///mnt/nfs/sharedfile' means shared file-system initialization
# 'tcp://10.1.1.20:23456' means TCP initialization
world_size=world_size,
rank=global_rank,
timeout=timedelta(seconds=5)
)
def cleanup():
dist.destroy_process_group()
定义训练过程 (定义在每一个 Process 中我们希望执行的代码)
def run_demo(args):
# get global_rank and world_size
global_rank = args.local_rank + args.node_rank * args.nproc_per_node
world_size = args.nnode * args.nproc_per_node
setup(global_rank=global_rank, world_size=world_size)
# load model to the GPU specified by local_rank
model = ToyModel().to(args.local_rank)
ddp_model = DDP(model,
device_ids=[args.local_rank],
output_device=args.local_rank)
# define loss and optimizer
loss_fn = nn.MSELoss()
optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)
# define data sampler and dataloader
train_dataset = torchvision.datasets.MNIST(
root='./data',
train=True,
transform=transforms.ToTensor(),
download=True
)
train_sampler = torch.utils.data.distributed.DistributedSampler(
train_dataset,
shuffle=True)
trainloader = DataLoader(train_dataset,
batch_size=args.batch_size,
shuffle=(train_sampler is None),
num_workers=args.workers,
sampler=train_sampler,
pin_memory=True)
for epoch in range(args.epoch):
# set epoch
train_sampler.set_epoch(epoch)
for i, data in enumerate(trainloader):
# get the inputs
inputs, labels = data
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = ddp_model(inputs)
loss = loss_fn(outputs, labels)
loss.backward()
optimizer.step()
dist.barrier()
cleanup()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--local_rank', type=int, help='automatically set by torch.distributed.launch')
parser.add_argument('--nproc_per_node', type=int, help='#gpus per node')
parser.add_argument('--nnode', type=int, help='#nodes')
parser.add_argument('--node_rank', type=int, help='node rank')
parser.add_argument('--epoch', type=int, help='#epochs to train')
parser.add_argument('--batch_size', type=int, help='batch size')
parser.add_argument('--workers', type=int, help='#workers for dataloader')
args = parser.parse_args()
run_demo(args=args)
分布式训练 (torchrun 可参考 https://pytorch.org/docs/stable/elastic/run.html#launcher-api)
python -m torch.distributed.launch \
--nnodes=1 \ # 节点的数量,通常一个节点对应一个主机,方便记忆,直接表述为主机
--nproc_per_node=2 \ # 一个节点中显卡的数量
--node_rank=0 \ # 节点的序号,从 0 开始
--master_addr="10.103.10.54" \ # master 节点的 ip 地址
--master_port=6005 \ # master 节点的 port 号,在不同的节点上 master_addr 和 master_port 的设置是一样的,用来进行通信
train.py
还要注意:在 DP 中, batch_size 设置必须为单卡的 n n n 倍, 但是在 DDP 内, batch_size 设置与单卡一样即可
torch.distributed.launch
在运行进程时会设置相应的环境变量 (对应 “env://” 的初始化方式),在程序中可以通过环境变量获取相应设置os.environ["MASTER_ADDR"]
os.environ["MASTER_PORT"]
os.environ["RANK"] # global rank (local_rank is passed into python script as arguments)
os.environ["WORLD_SIZE"]
使用 torch.distributed
的 API 进行分布式基本操作
# define tensor on GPU, count and total is the result at each GPU
t = torch.tensor([count, total], dtype=torch.float64, device='cuda')
# synchronizes all processes
dist.barrier()
# Reduces the tensor data across all machines in such a way that all get the final result.
dist.all_reduce(t, op=torch.distributed.ReduceOp.SUM,)
# Gathers tensors from the whole group in a list.
t_gather_list = [torch.zeros_like(t) for _ in range(world_size)]
dist.all_gather(t_gather_list, t)