我们知道 PyTorch 本身对于单机多卡提供了两种实现方式
DataParallel(DP)是基于Parameter server的算法,实现比较简单,只需在原单机单卡代码的基础上增加一行:
gpu_ids = [0, 2, 3]
model = nn.DataParallel(model, device_ids=gpu_id)
但是DP模式负载不均衡的问题比较严重,有时在模型较大的时候(比如bert-large),reducer的那张卡会多出3-4g的显存占用,并且速度也比较慢。
在 pytorch 1.0 之后,官方终于对分布式的常用方法进行了封装,支持 all-reduce,broadcast,send 和 receive 等等。通过 MPI 实现 CPU 通信,通过 NCCL 实现 GPU 通信。官方也曾经提到用 DistributedDataParallel 解决 DataParallel 速度慢,GPU 负载不均衡的问题,目前已经很成熟了~
官方建议用新的DDP(DistributedDataParallel),采用all-reduce算法,本来设计主要是为了多机多卡使用,但是单机上也能用。
使用 4 块 Tesla V100-PICE 在 ImageNet 进行了运行时间的测试,测试结果发现 Apex 的加速效果最好,但与 Horovod/Distributed 差别不大,平时可以直接使用内置的 Distributed。Dataparallel 较慢,不推荐使用。
同时 pytorch 官方文档也建议使用 DistributedDataParallel(DDP)替换 DataParallel(DP),因此本文着重介绍 DistributedDataParallel(DDP)的相关内容。
DistributedDataParallel 多卡可以分为:
由于第一种数据并行方式是最常用,并且是官方实践性能最佳的方式,因此本文相关内容主要针对数据并行方式给出。
GPU 并行计算基础处理流程:设置可见 GPU 并配置并行化参数,创建模型,通过 API 将模型并行化,将模型和数据搬到 GPU,进行前向和后向传播。
在前向传播中,会自动将 batch_size 切分后分配到可见的 GPU 上并行计算。
结束后,会有一台或其他方式收集前向传播计算结果并根据 loss 更新每块 GPU 上模型参数。
因此在多 GPU 训练时,可以:
rank
world_size【world_size = int(os.getenv(“WORLD_SIZE”, ‘1’)) 】
local_rank
管理方式
torch.cuda
torch.distributed
其他
官方建议用新的DDP,采用all-reduce算法,本来设计主要是为了多机多卡使用,但是单机上也能用。
DDP 多卡运行的本质还是通过创建多个进程来实现并行,但由于多个进程使用的是同一份代码,因此需要在代码中增加相关逻辑来指定进程与 GPU 硬件之间的关联关系。同时为了实现数据的并行化,需要为不同的进程(不同的 GPU)加载不同的数据,因此需要一个特殊的 data sampler 来实现,这个 DDP 通过 torch.utils.data.distributed.DistributedSampler
来实现。
与 DataParallel 的单进程控制多 GPU 不同,在 distributed 的帮助下,我们只需要编写一份代码,torch 就会自动将其分配给 n n n 个进程,分别在 n n n 个 GPU 上运行。
Pytorch 中分布式的基本使用流程如下:
导入必要的包
import torch.distributed as dist # 分布式训练依赖
from torch.utils.data.distributed import DistributedSampler # 分布式训练依赖
from torch.nn.parallel import DistributedDataParallel # 分布式训练依赖
在使用 distributed 包的任何其他函数之前,需要使用 init_process_group 初始化进程组,同时初始化 distributed 包。
# 分布式训练-新增: DDP backend初始化
dist.init_process_group(backend='nccl')
torch.cuda.set_device(args.local_rank) # 根据local_rank来设定当前使用哪块GPU
args.world_size = int(os.getenv("WORLD_SIZE", '1')) # 单机多卡:代表有几块GPU
args.global_rank = dist.get_rank() # 获取当前进程的序号,用于进程间通讯
如果需要进行小组内集体通信,用 new_group 创建子分组
创建分布式并行模型 DDP(model, device_ids=device_ids),find_unused_parameters参数设置为True
# 分布式训练-新增:定义并把模型放置到哪些GPU上,需要在调用`model=DDP(model)`前做
device = torch.device("cuda", args.local_rank)
model = import_module("moels." + model_name).Model().to(device) # 动态载入模型
model = DistributedDataParallel(model, device_ids=[args.local_rank],find_unused_parameters=True) # 前提是model已经.cuda() 了
为数据集创建 Sampler
# 加载数据集
train_dataset = SampleDataset(config.train_path)
val_dataset = SampleDataset(config.test_path)
test_dataset = SampleDataset(config.test_path)
# 分布式训练-新增:为数据集创建 Sampler
train_sampler = DistributedSampler(train_dataset)
val_sampler = DistributedSampler(val_dataset)
test_sampler = DistributedSampler(test_dataset)
DataLoader使用sample参数,并且shuffle一定要设置为False
# 数据迭代器;分布式训练-新增: sampler参数
train_dataloader = DataLoader(dataset=train_dataset, batch_size=config.batch_size, shuffle=False, collate_fn=collate_fn, drop_last=True, sampler=train_sampler)
val_dataloader = DataLoader(dataset=val_dataset, batch_size=config.batch_size, shuffle=False, collate_fn=collate_fn, drop_last=True, sampler=val_sampler)
test_dataloader = DataLoader(dataset=test_dataset, batch_size=config.batch_size, shuffle=False, collate_fn=collate_fn, drop_last=True, sampler=test_sampler)
在 API 层面,pytorch 为我们提供了 torch.distributed.launch 启动器,用于在命令行分布式地执行 python 文件。使用启动工具 torch.distributed.launch 在每个主机上执行一次脚本,开始训练
CUDA_VISIBLE_DEVICES=0,1,2,3 python -m torch.distributed.launch --nproc_per_node=4 main.py
(pytorch) ninjia@ailian-Super-Server:~$ nvidia-smi
Sat May 21 13:42:18 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 455.23.05 Driver Version: 455.23.05 CUDA Version: 11.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 GeForce RTX 208... On | 00000000:18:00.0 Off | N/A |
| 28% 55C P2 225W / 250W | 9929MiB / 11019MiB | 90% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
| 1 GeForce RTX 208... On | 00000000:3B:00.0 Off | N/A |
| 30% 58C P2 214W / 250W | 9307MiB / 11016MiB | 88% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
| 2 GeForce RTX 208... On | 00000000:86:00.0 Off | N/A |
| 29% 55C P2 166W / 250W | 10123MiB / 11019MiB | 86% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
| 3 GeForce RTX 208... On | 00000000:AF:00.0 Off | N/A |
| 30% 57C P2 123W / 250W | 9643MiB / 11019MiB | 88% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 94633 G /usr/lib/xorg/Xorg 4MiB |
| 0 N/A N/A 94707 G /usr/bin/gnome-shell 0MiB |
| 0 N/A N/A 869045 C ...3/envs/pytorch/bin/python 9921MiB |
| 0 N/A N/A 869046 C ...3/envs/pytorch/bin/python 0MiB |
| 0 N/A N/A 869047 C ...3/envs/pytorch/bin/python 0MiB |
| 0 N/A N/A 869048 C ...3/envs/pytorch/bin/python 0MiB |
| 1 N/A N/A 94633 G /usr/lib/xorg/Xorg 9MiB |
| 1 N/A N/A 94707 G /usr/bin/gnome-shell 3MiB |
| 1 N/A N/A 869045 C ...3/envs/pytorch/bin/python 0MiB |
| 1 N/A N/A 869046 C ...3/envs/pytorch/bin/python 9289MiB |
| 1 N/A N/A 869047 C ...3/envs/pytorch/bin/python 0MiB |
| 1 N/A N/A 869048 C ...3/envs/pytorch/bin/python 0MiB |
| 2 N/A N/A 94633 G /usr/lib/xorg/Xorg 4MiB |
| 2 N/A N/A 94707 G /usr/bin/gnome-shell 0MiB |
| 2 N/A N/A 869045 C ...3/envs/pytorch/bin/python 0MiB |
| 2 N/A N<