Pytorch 分散式訓練 DistributedDataParallel — 實作篇

李謦伊
謦伊的閱讀筆記
10 min readMar 19, 2022

上一篇文章: Pytorch 分散式訓練 DistributedDataParallel — 概念篇 有介紹分散式訓練的概念,本文將要來進行 Pytorch DistributedDataParallel 實作。

首先來介紹一下分散式的基礎概念

  • group: 指進程組,默認為一組。
  • backend: 指進程使用的通訊後端,Pytorch 支援 mpi、gloo、nccl,若是使用 Nvidia GPU 推薦使用 nccl。

關於後端的詳細資訊可由官方文檔 DISTRIBUTED COMMUNICATION PACKAGE — TORCH.DISTRIBUTED 查看。

  • world_size: 指進程組中的進程數量。

若使用單台機器多 GPU ,world_size 表示使用的 GPU 數量

若使用多台機器多 GPU ,world_size 表示使用的機器數量

  • rank: 指當前進程的序號,用於進程間的通訊,rank=0 表示為 master。

若使用單台機器多 GPU ,rank表示當前正在使用的某個 GPU

若使用多台機器多 GPU ,rank表示當前正在使用的某台機器

  • local_rank: 指每台機器上的進程序號。

📚 rank vs local_rank

假設目前使用兩台機器,每台機器有 3 個 GPU,每個 GPU 使用一個進程 (DDP 的最佳使用方式是每一個 GPU 使用一個進程)。

機器一的 rank 序號為 0, 1, 2;機器二的 rank 序號為 3, 4, 5

機器一的 local_rank 序號為 0, 1, 2;機器二的 local_rank 序號為 0, 1, 2

接下來就來開始實作啦~ 先 import 需要的 library,我的 pytorch 版本為 1.9.0、torchvision 版本為 0.10.0。

import torch
import torch.distributed as dist
from torch.utils.data.distributed import DistributedSampler

設置 local_rank argparse 參數

在啟動分散式訓練時,需要在命令行使用 torch.distributed.launch 啟動器,該啟動器會將當前進程的序號 (若每個 GPU 使用一個進程,也是指 GPU 序號) 通過 local_rank 參數傳給 python 檔。

parser = argparse.ArgumentParser()
parser.add_argument("--local_rank", default=0, type=int)
args = parser.parse_args()

初始化 DDP

設置進程通訊的後端為 nccl

可參考官方參數設置: https://pytorch.org/docs/stable/distributed.html#torch.distributed.init_process_group

dist.init_process_group(backend='nccl')

同步所有進程

dist.barrier()

獲取當前進程組內的所有進程數

world_size = dist.get_world_size()

數據平行化

將數據集劃分給每一個進程,避免進程間的數據重複

train_sampler = DistributedSampler(train_dataset)
valid_sampler = DistributedSampler(valid_dataset)
  • DistributedSampler 分配方式

假設使用 2 個 GPU 且數據集分為 0~4 份,首先將數據集進行 shuffle 後分為兩份,不足的部分由第一份數據補 (第 5 份),最後再以間隔方式分配數據至不同的 GPU 中。

接著再將 sampler 傳入 DataLoader

DataLoader 官方參數設置: https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader

train_loader = DataLoader(train_dataset, sampler=train_sampler, batch_size=256, pin_memory=False, prefetch_factor=2, num_workers=4)valid_loader = DataLoader(valid_dataset, sampler=valid_sampler, batch_size=256, pin_memory=False, prefetch_factor=2, num_workers=4)

模型

假設模型已經 load,直接使用 .to(device),將模型分配至 CUDA

device = torch.device("cuda", args.local_rank)
model = model.to(device)

使用 SyncBN 優化各自進程中數據集的 BN 計算

model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model)

使用 DistributedDataParallel 將模型包裝起來

 model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank)

set_epoch

在每一次迭代前,通過 set_epoch 來設置一個 random seed,這樣每次運行才會將每個 epoch 的數據都 shuffle,並且讓每個 GPU 拿到的數據不同。

train_sampler.set_epoch(epoch)
valid_sampler.set_epoch(epoch)

啟動分散式訓練

這部分有兩種寫法: torch.distributed.launch、torchrun

1. torch.distributed.launch

*參數說明

nproc_per_node: 每台機器有多少進程

nnodes: 總共使用多少台機器

node_rank: 當前進程位於哪台機器

master_addr: master 進程的網路地址

master_port: master 進程的端口,要先確認該端口是否被其他進程佔用

可參考官方設置: https://pytorch.org/docs/stable/distributed.html#launch-utility

  • 運行一台機器多 GPU

CUDA_VISIBLE_DEVICES 參數為選擇要使用哪一台 GPU,0,1 表示使用第 0、1 序號的 GPU

$ CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 ddp_example.py
  • 運行多台機器多 GPU

假設 IP 為 192.168.1.1、free port 為 1234

# 第一台機器
$ CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 --nnodes=2 --node_rank=0 --master_addr=”192.168.1.1" --master_port=1234 ddp_example.py
# 第二台機器
$ CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 --nnodes=2 --node_rank=0 --master_addr=”192.168.1.1" --master_port=1234 ddp_example.py
  • 運行一台機器多 GPU,但執行兩個不同實驗
$ CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 ddp_example.py# 使用不同 master_port 避免衝突
$ CUDA_VISIBLE_DEVICES=2,3 python -m torch.distributed.launch --nproc_per_node=2 --master_port 9999 ddp_example.py

2. torchrun

Pytorch 提供了新的指令替代 torch.distributed.launch,參數的使用方式可參考官方文件: https://pytorch.org/docs/stable/elastic/run.html#launcher-api

  • 運行一台機器多 GPU
$ CUDA_VISIBLE_DEVICES=0,1 torchrun --standalone --nnodes=1 --nproc_per_node=2 ddp_example.py
  • 運行多台機器多 GPU

參數 nnodes 為使用機器序號的區間、rdzv_id 為唯一的 job id、rdzv_endpoint 為 backend 所執行的 endpoint,格式為 <host>:<port> ,替代原本的 master_addr 及 master_port。

$ torchrun --nnodes=1:4 --nproc_per_node=$NUM_TRAINERS --rdzv_id=$JOB_ID --rdzv_backend=c10d --rdzv_endpoint=$HOST_NODE_ADDR ddp_example.py

完整的 Code 可參考我的 Github:

執行以下指令進行訓練

$ CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 ddp_example.py

顯示結果如下

最後,總結 Pytorch 分散式訓練的基本流程:

  • 設置 local_rank 參數並使用 init_process_group 進行初始化
  • 使用 DistributedSampler 劃分數據集
  • 將模型分配至 CUDA 並設置 SyncBN 及 DistributedDataParallel
  • 將 train、vaild Sampler 設置 set_epoch
  • 在 sever 上執行指令開始訓練

--

--