目录
1. 前言
最近开始上手进行分布式训练,于是在网上开始搜各种教程和资料,看到了许多写得很好的文章,但是有些文章又不是特别的全面,于是决定对这些文章进行归纳和总结,方便后来者的学习和查阅。这里友情提醒一下,大家查资料的时候不要仅仅局限于百度,有时候在百度上查半天,不如去官方文档或者google搜一下,立马就能看到很多优质的解答。
2.分布式训练的分类
关于分布式训练的分类,网上的分类方法五花八门,目前看的这么多文章中最为清晰的要属这篇文章了大模型LLM之分布式训练,简单来讲,分布式训练方法可以分为以下几类:
- 数据并行
- 模型并行(包含
流水线并行
和张量并行
) - 混合并行
关于分布式训练分类大家也可以去看一下Hugging Face上的一篇文章Efficient Training on Multiple GPUs,它的分类方式和我们上面提到的有些不同。
接下来的内容主要来自这篇文章:分布式并行训练(DP、DDP、DeepSpeed、Accelerate、Trainer)
3.不并行(单机单卡)
判断GPU是否可用:device = "cuda" if torch.cuda.is_available() else "cpu"
模型拷贝:model.to(device)
数据拷贝:data.to(device)
模型加载:torch.load(xx.pt, map_location=device)
模型保存:torch.save(model, "model.pt")
4. 数据并行 DP和DDP
Pytorch提供的分布式数据并行有两种Data Parallel(DP)和Distributed Data Parallel(DDP) 目前pytorch官网更推荐大家使用的是DDP模式,详见官方文档Data Parallel 和官方文档Distributed Data Parallel
4.1 异同点
DP 和DDP在工作模式上有着共同点和差异,这里博采众长,做一下梳理和总结:
-
共同点:都是
数据并行
,都是即插即用 -
不同点:
- DP:仅支持单机多卡, 显存负载不均衡(device[0]负载大),不支持Apex混合精度训练,GPU利用不均衡,因Python GIL争用影响 仅支持单进程多线程,训练速率低。
- DDP:支持单机多卡&多机多卡,数据分配均衡,支持Apex混合精度训练,借助All-Reduce的数据交换方式提高GPU的通讯效率,支持多进程,训练速率高,
如果模型特别大,DDP也可以和模型并行一起工作,这个在官方教程中有提到
关于DP和DDP的区别还可以看一下这篇有深度的文章:Distributed data parallel training using Pytorch on AWS
4.2 原理
DP原理
- 将模型权重从主卡(默认GPU:0)复制到所有GPU上,输入的bacth大小的数据会分散到各个GPU上(每张卡上输入batch / 4,假设有四张卡)。
- 之后独自在各张卡上执行前向过程,每张卡的输出结果会被聚合到主卡上,然后在主卡上计算总的损失,总损失是一个标量,一般可以求平均值,再通过损失函数求导,得到损失的梯度,分发到各个GPU上。
- 在每个GPU根据链式法则继续计算各个参数的梯度,再将所有设备上的梯度回传到主卡上进行梯度累加并求梯度均值。在主卡上通过平均梯度更新模型权重。最后将更新后的模型参数 广播 到其余 GPU 中进行下一轮的前向传播。
DDP 原理
DDP背后的实现是依靠Ring-AllReduce 具体细节可以参考这两篇文章:
Pytorch 分布式训练DDP(torch.distributed)详解-原理-代码
分布式训练(中)——DP、DDP模式
4.3 DP 实现(单机多卡)
在Pytorch中torch.nn.DataParallel()
包含三个参数:
module:参数传入具体的模型,比如ResNet,Unet等
device_ids:传入gpu编号例如:[0,1,2,3],默认为None即使用所有GPU
output_device:指定中心设备(参数服务器),用于汇总梯度的 GPU 是哪个,默认device_ids[0]
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2" #设置可见的gpu
net=Unet() #初始化一个模型
device_ids=[2,0,1] #gpu编号,这里故意写成2,0,1而不是0,1,2是为了方便大家理解特殊请情况
net.to(device_ids[0]) #设置中心设备,注意是device_ids[0] 这一行不能少
net = torch.nn.DataParallel(net, device_ids=[2, 0, 1])
batch_data.to(device_ids[0]) #
output = net(batch_data)
模型的加载和保存,为了和单机单卡保持一致
,可以这样写:
保存
if isinstance(net,torch.nn.DataParallel):
torch.save(net.module.state_dict(), "model.pth")
else:
torch.save(net.state_dict(), "model.pth")
加载
if isinstance(self.model, torch.nn.DataParallel):
net.module.load_state_dict(torch.load("model.pth"), strict=False)
else:
net.load_state_dict(torch.load("model.pth"), strict=False)
注:
- os.environ[“CUDA_VISIBLE_DEVICES”] = “0,1,2” 设置可见的GPU
- 在上述的代码中
net.to(device_ids[0])
设置的是中心设备,这一行不能少,这里device_ids=[2,0,1]是为了方便大家理解device_ids[0]不代表一定是cuda:0。 - torch.nn.DataParallel() 是对模型进行包裹
- 关于使用中的一些详细的细节可以看这两篇博客torch.nn.DataParallel使用细节和pytorch GPU torch.nn.DataParallel (DP)多卡并行
4.4 DDP 实现(单机多卡,多机分布式)
DDP相比于DP要复杂得多,这里首先推荐官网的教程,教程很详细,一定要认真看,官方的教程看明白了再看接下来的内容!!!
官方教程 Tutorials
官方API DistributedDataParallel
4.4.1 DDP 基本概念
WORLD:
- world 表示包含所有进程的组(所有gpu的集合)。
- 每个进程通常对应一个 GPU, world 中的进程可以相互通信,这使得使用分布式数据并行(Distributed Data Parallel, DDP)进行训练成为可能。
WORLD_SIZE:
- world_size 表示分布式训练环境中的总进程数/gpu数。
- 每个进程都会被分配一个唯一的标识符(rank),从 0 到 world_size-1。
RANK:
- rank 是分配给world中每个进程的唯一标识符,用于标识每个进程在分布式训练中的角色。
LOCAL RANK:
- local rank是分配个单个node中每个进程的标识符,world中可能有多个node。每个进程的local_rank都不一样。 判断master GPU:if local_rank == 0:
NODE:
- node 可以理解为一个服务器,代表着物理设备上的一个实体。
- 在多机分布式训练中,每台机器被视为一个节点,节点之间需要进行通信。例如,如果有2 个node/server,每个 node/server/machine 各有4张卡(4 gpus)。total_world_size = 2(节点数) * 4(每个节点的 GPU 数量)= 8, rank 的取值范围为 [0, 1, 2, 3, 4, 5, 6, 7], local_rank 的取值范围为 [0, 1, 2, 3],[0, 1, 2, 3] 分别对应着不同的节点上的进程。
这里可以参考这篇博客中的例子,如图所示,有三个node,每个node有4个GPU(则每个node会有四个进程,一个进程对应一个GPU)
4.4.2 DDP之单机多卡
Pytorch支持的分布式框架的后端主要有 Gloo、MPI 和 NCCL 三种。PyTorch 官方的规则是:如果是分布式 GPU 训练,优先推荐 NCCL:如果是分布式 CPU 训练,优先推荐 Gloo,其次是 MPI。
三种方式:torch.distributed.launch,torch.multiprocessing,torchrun
4.4.2.1 torch.distributed.launch(淘汰)和 torchrun (推荐)
python -m torch.distributed.launch 参数列表1 main.py argsparse参数列表
torchrun 参数列表1 main.py argsparse参数列表
关于这两个命令官方有具体说明官方文档
torch.distributed.launch 具体有什么作用?
- 从torch.distributed.run中获取parser(一般通过
argsparse.ArgumentParser()
创建),并再添加一个–use-env(–use_env)参数,代表从环境变量中获取LOCAL RANK值 (argsparse参数列表将不再有–local-rank这个参数)。取而代之,可以在main.py中通过os.environ[‘LOCAL_RANK’]获得。 - 从torch.distributed.run中获取run函数,执行run(args),这个args就是参数列表1获取的参数。
- 可见,
torch.distributed.launch
与torchrun
的区别仅在于local rank的获取方式,launch有两种,而torchrun只能通过os.environ[‘LOCAL_RANK’]。这是因为pytorch官方虽然说torch.distributed.launch将会被弃用,因为torchrun增加了一些新功能,但实际上他们把torchrun和torch.distributed.launch合并了。
在main.py中可获取的环境变量,来自官方文档
分布式训练模板代码
https://zhuanlan.zhihu.com/p/561218626
【深度学习实战(27)】「分布式训练」DDP单机多卡并行指南
import os
import torch.distributed as dist
import argparse
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torchvision.models import resnet18
import torch
import torchvision
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'
def train(args):
local_rank=int(os.environ['LOCAL_RANK'])
world_size=int(os.environ['WORLD_SIZE'])
print("world_size:", world_size)
print("local_rank:", local_rank)
# dist.init_process_group(backend='nccl', init_method=args.init_method, rank=local_rank, world_size=world_size)
dist.init_process_group(backend='nccl' ,rank=local_rank, world_size=world_size)
# ......
transform=transforms.Compose([transforms.Resize((args.image_size,args.image_size)),transforms.ToTensor()])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_sampler = torch.utils.data.distributed.DistributedSampler(trainset)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=args.batch_size, sampler=train_sampler)
# ......
net = resnet18()
net.to(f'cuda:{local_rank}')
net = torch.nn.parallel.DistributedDataParallel(net, device_ids=[local_rank],find_unused_parameters=False)
net=torch.nn.SyncBatchNorm.convert_sync_batchnorm(net)
for epoch in range(1,args.epochs+1):
train_sampler.set_epoch(epoch)
for batch_idx, (inputs, targets) in enumerate(trainloader):
inputs, targets = inputs.to(f'cuda:{local_rank}'), targets.to(f'cuda:{local_rank}')
# outputs = net(inputs)
# print(outputs.device)
rank = dist.get_rank()
print(f"Start running basic DDP example on rank {rank}.")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='PyTorch distributed training on cifar-10')
parser.add_argument('--image_size', type=int, default=224, help='image size')
parser.add_argument('--batch_size', type=int, default=8, help='batch size')
parser.add_argument('--epochs', type=int, default=10, help='number of epochs')
# parser.add_argument('--init_method', type=str, default='tcp://127.0.0.1:23456', help='init method')
args = parser.parse_args()
train(args)
torchrun --nnodes=1 --nproc_per_node=2 main.py
4.4.2.2 torch.multiprocessing
Multi-GPU Training in PyTorch with Code (Part 3): Distributed Data Parallel
在训练过程中难免会遇到一些问题,这时候需要调试,可以参考这篇文章:pycharm进行torchrun的调试
4.4.4 DDP之多机分布式
一般情况下不会用到这种情况所以,我们这里不做讲解,感兴趣的可以看这篇博客:PyTorch分布式训练基础–DDP使用
参考文献
大模型LLM之分布式训练
A Survey of Large Language Models
Pytorch 分布式训练DDP(torch.distributed)详解-原理-代码
Efficient Training on Multiple GPUs
分布式训练(中)——DP、DDP模式
torch.nn.DataParallel使用细节
pytorch GPU torch.nn.DataParallel (DP)多卡并行
DistributedDataParallel
Tutorials
pycharm进行torchrun的调试
PyTorch分布式训练基础–DDP使用
Distributed data parallel training using Pytorch on AWS
torchrun
DDP
分布式并行训练(DP、DDP、DeepSpeed、Accelerate、Trainer)!!!!!!!
Pytorch多卡训练
分布式训练(上)——rank local_rank node