優(yōu)秀的煉丹師再也不滿足單張GPU訓(xùn)練了,總想要迭代的再快點(diǎn)阶女,batchsize再大點(diǎn)馁启。能用3D model 絕不用2D对碌,完了!內(nèi)存超了岳瞭,咋辦摊欠?急狐史, 在線等!烛恤!
為什么要使用分布式訓(xùn)練
對于懶癌星人,單卡訓(xùn)練能解決的問題怜浅,再多卡給我,我都懶得用溅潜。但是對于資深煉丹師借浊,分布式訓(xùn)練帶來的收益很大岗钩,可以加速模型的訓(xùn)練凶朗、調(diào)參的節(jié)奏、以及版本的迭代更新等~
當(dāng)你遇到以下情況,可以考慮使用分布式訓(xùn)練(使用多張卡,或者多臺服務(wù)器)
- 在理想的batc_size下,單卡訓(xùn)練 out of memory
- 使用單卡雖然能run, 但是速度非常慢,耗時(shí)。
- 數(shù)據(jù)量太大,需要利用所有資源滿足訓(xùn)練。
- 模型太大,單機(jī)內(nèi)存不足
等...
分布式訓(xùn)練有哪些方法
分布式訓(xùn)練策略按照并行方式不同,可以簡單的分為數(shù)據(jù)并行和模型并行兩種方式。原文鏈接
1?? 數(shù)據(jù)并行
數(shù)據(jù)并行是指在不同的 GPU 上都 copy 保存一份模型的副本,然后將不同的數(shù)據(jù)分配到不同的 GPU 上進(jìn)行計(jì)算跳夭,最后將所有 GPU 計(jì)算的結(jié)果進(jìn)行合并,從而達(dá)到加速模型訓(xùn)練的目的晰绎。由于數(shù)據(jù)并行會涉及到把不同 GPU 的計(jì)算結(jié)果進(jìn)行合并然后再更新模型拦赠,根據(jù)跟新方式不同,又可以分為同步更新和異步更新
2?? 模型并行
分布式訓(xùn)練中的模型并行是指將整個(gè)神經(jīng)網(wǎng)絡(luò)模型拆解分布到不同的 GPU 中拨齐,不同的 GPU 負(fù)責(zé)計(jì)算網(wǎng)絡(luò)模型中的不同部分。這通常是在網(wǎng)絡(luò)模型很大很大仰坦、單個(gè) GPU 的顯存已經(jīng)完全裝不下整體網(wǎng)絡(luò)的情況下才會采用。
基于 Pytorch 的分布式訓(xùn)練方法
在 Pytorch 中為我們提供了兩種多 GPU 的分布式訓(xùn)練方案: torch.nn.DataParallel(DP)
和 torch.nn.parallel.Distributed Data Parallel(DDP)
DP(DataParallel)
- 優(yōu)點(diǎn):修改的代碼量最少息楔,只要像這樣
model = nn.DataParallel(model)
包裹一下你的模型就行了 - 缺點(diǎn):只適用單機(jī)多卡寝贡,不適用多機(jī)多卡;性能不如DDP; DP使用單進(jìn)程钞螟,目前官方已經(jīng)不推薦兔甘。
如果覺得 DDP 很難,可以采用這種方式鳞滨。示例用法
import monai
import torch.nn as nn
import os
os.environ("CUDA_VISIBLE_DEVICES") = '0,1'
# 用哪些卡洞焙,就寫哪些,也可以在命令行運(yùn)行的時(shí)候指定可見GPU
# $: export CUDA_VISIBLE_DEVICES=0,1 python train.py
device = torch.device('cuda' if torch.cuda.is_available () else 'cpu')
model = monai.networks.nets.UNet().to(device)
model = nn.DataParallel(model)
通過兩種方式可以指定需要使用的GPU拯啦,第一種是在代碼里設(shè)置os.environ
, 第二種是在終端運(yùn)行代碼前澡匪,加一句export CUDA_VISIBLE_DEVICES=0,1
。按照自己的習(xí)慣來就行褒链。
如果需要跑來看看效果唁情,可以執(zhí)行下面的代碼,完整版
import argparse
import os
import sys
from glob import glob
import nibabel as nib
import numpy as np
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel
import monai
from monai.data import DataLoader, Dataset, create_test_image_3d, DistributedSampler
from monai.transforms import (
AsChannelFirstd,
Compose,
LoadImaged,
RandCropByPosNegLabeld,
RandRotate90d,
ScaleIntensityd,
EnsureTyped,
)
def train(args):
if not os.path.exists(args.dir):
# create 40 random image, mask paris for training
print(f"generating synthetic data to {args.dir} (this may take a while)")
os.makedirs(args.dir)
# set random seed to generate same random data for every node
np.random.seed(seed=0)
for i in range(200):
im, seg = create_test_image_3d(128, 128, 128, num_seg_classes=1, channel_dim=-1)
n = nib.Nifti1Image(im, np.eye(4))
nib.save(n, os.path.join(args.dir, f"img{i:d}.nii.gz"))
n = nib.Nifti1Image(seg, np.eye(4))
nib.save(n, os.path.join(args.dir, f"seg{i:d}.nii.gz"))
images = sorted(glob(os.path.join(args.dir, "img*.nii.gz")))
segs = sorted(glob(os.path.join(args.dir, "seg*.nii.gz")))
train_files = [{"img": img, "seg": seg} for img, seg in zip(images, segs)]
# define transforms for image and segmentation
train_transforms = Compose(
[
LoadImaged(keys=["img", "seg"]),
AsChannelFirstd(keys=["img", "seg"], channel_dim=-1),
ScaleIntensityd(keys="img"),
RandCropByPosNegLabeld(
keys=["img", "seg"], label_key="seg", spatial_size=[96, 96, 96], pos=1, neg=1, num_samples=4
),
RandRotate90d(keys=["img", "seg"], prob=0.5, spatial_axes=[0, 2]),
EnsureTyped(keys=["img", "seg"]),
]
)
# create a training data loader
train_ds = Dataset(data=train_files, transform=train_transforms)
# use batch_size=2 to load images and use RandCropByPosNegLabeld to generate 2 x 4 images for network training
train_loader = DataLoader(
train_ds,
batch_size=10,
shuffle=False,
num_workers=2,
pin_memory=True,
)
# create UNet, DiceLoss and Adam optimizer
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = monai.networks.nets.UNet(
spatial_dims=3,
in_channels=1,
out_channels=1,
channels=(16, 32, 64, 128, 256),
strides=(2, 2, 2, 2),
num_res_units=2,
).to(device)
model = torch.nn.DataParallel(model)
loss_function = monai.losses.DiceLoss(sigmoid=True).to(device)
optimizer = torch.optim.Adam(model.parameters(), 1e-3)
# start a typical PyTorch training
epoch_loss_values = list()
for epoch in range(5):
print("-" * 10)
print(f"epoch {epoch + 1}/{5}")
model.train()
epoch_loss = 0
step = 0
for batch_data in train_loader:
step += 1
inputs, labels = batch_data["img"].to(device), batch_data["seg"].to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
epoch_len = len(train_ds) // train_loader.batch_size
print(f"{step}/{epoch_len}, train_loss: {loss.item():.4f}")
epoch_loss /= step
epoch_loss_values.append(epoch_loss)
print(f"epoch {epoch + 1} average loss: {epoch_loss:.4f}")
print(f"train completed, epoch losses: {epoch_loss_values}")
torch.save(model.state_dict(), "final_model.pth")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dir", default="./testdata", type=str, help="directory to create random data")
args = parser.parse_args()
train(args=args)
if __name__ == "__main__":
import os
os.environ["CUDA_VISIBLE_DEVICES"] = '0,1'
main()
這里我們使用了2塊GPU甫匹,運(yùn)行的時(shí)候可以看下兩塊GPU的使用情況.
DDP(DistributedDataParallel)
本次重點(diǎn)介紹:??????????
與 DP 模式不同甸鸟,DDP 模式本身是為多機(jī)多卡設(shè)計(jì)的惦费,當(dāng)然在單機(jī)多卡的情況下也可以使用。DDP 采用的是 all-reduce 架構(gòu)抢韭,基本解決了 PS 架構(gòu)中通信成本與 GPU 的數(shù)量線性相關(guān)的問題薪贫。雖然在單機(jī)多卡情況下,可以使用 DP 模式刻恭,但是使用 DDP 通常會比 DP 模式快一些瞧省,因此 DDP 模式也是官方推薦大家使用的方式。
DDP為基于torch.distributed的分布式數(shù)據(jù)并行結(jié)構(gòu)鳍贾,工作機(jī)制為:在batch維度上對數(shù)據(jù)進(jìn)行分組鞍匾,將輸入的數(shù)據(jù)分配到指定的設(shè)備(GPU)上,從而將程序的模型并行化骑科。對應(yīng)的橡淑,每個(gè)GPU上會復(fù)制一個(gè)模型的副本,負(fù)責(zé)處理分配到的數(shù)據(jù)纵散,在后向傳播過程中再對每個(gè)設(shè)備上的梯度進(jìn)行平均梳码。
缺點(diǎn):代碼改動較DP多隐圾,坑較多伍掀,需要試錯(cuò)攢經(jīng)驗(yàn)
DDP的啟動方式有很多種,內(nèi)容上是統(tǒng)一的:都是啟動多進(jìn)程來完成運(yùn)算暇藏。
- torch.multiprocessing.spawn:適用于單價(jià)多卡
- torch.distributed.launch: 可用于多機(jī)或多卡蜜笤。
接下來以torch.distributed.launch
為例,只需要8步盐碱,將你的代碼改成分布式訓(xùn)練把兔。適用于單機(jī)多卡。
step 1: 初始化進(jìn)程
import torch.distributed as dist
dist.init_process_group(backend="nccl", init_method="env://")
參數(shù)解析:
在創(chuàng)建模型之前進(jìn)行初始化
- backend: 后端通信可以采用
mpi
,gloo
,andnccl
瓮顽。對于基于 GPU 的訓(xùn)練县好,建議使用nccl
以獲得最佳性能. -
init_method: 告知每個(gè)進(jìn)程如何發(fā)現(xiàn)彼此,如何使用通信后端初始化和驗(yàn)證進(jìn)程組暖混。 默認(rèn)情況下缕贡,如果未指定 init_method,PyTorch 將使用環(huán)境變量初始化方法 (env://)拣播。
圖片左邊為常規(guī)代碼晾咪,右邊為DDP修改后的代碼
step 2: 在創(chuàng)建dataloder前加一個(gè)sampler
from monai.data import DistributedSampler
# create a training data loader
train_ds = Dataset(data=train_files, transform=train_transforms)
# create a training data sampler
train_sampler = DistributedSampler(dataset=train_ds, even_divisible=True, shuffle=True)
# use batch_size=2 to load images and use RandCropByPosNegLabeld to generate 2 x 4 images for network training
train_loader = DataLoader(
train_ds,
batch_size=10,
shuffle=False,
num_workers=2,
pin_memory=True,
sampler=train_sampler,
)
與常規(guī)訓(xùn)練的區(qū)別??紅框內(nèi)為新增內(nèi)容
step 3: 設(shè)定Device
device = torch.device(f"cuda:{args.local_rank}")
torch.cuda.set_device(device)
這里涉及到的參數(shù)在后面(step 7)給出
這一步,常規(guī)的代碼也可以這樣寫贮配,但平時(shí)一般用如下方法
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
step 4: 使用DistributedDataParallel模塊包裹模型
from torch.nn.parallel import DistributedDataParallel
model = monai.networks.nets.UNet(
spatial_dims=3,
in_channels=1,
out_channels=1,
channels=(16, 32, 64, 128, 256),
strides=(2, 2, 2, 2),
num_res_units=2,
).to(device)
model = DistributedDataParallel(model, device_ids=[device], output_device=[device])
model.to(device) # 這句不能少谍倦,最好不要用model.cuda()
與常規(guī)對比
step 5: 在epoch訓(xùn)練前設(shè)置set_epoch
train_sampler.set_epoch(epoch)
在后面解釋為什么要設(shè)置set_epoch
step 6: 修改模型的保存方式
因?yàn)槊總€(gè)進(jìn)程的模型是一樣的,我們只用在某一個(gè)進(jìn)程上保存模型就行泪勒,默認(rèn)使用0進(jìn)程保存模型
if dist.get_rank() == 0:
# all processes should see same parameters as they all start from same
# random parameters and gradients are synchronized in backward passes,
# therefore, saving it in one process is sufficient
torch.save(model.state_dict(), "final_model.pth")
step 7: 在main函數(shù)里面添加local_rank參數(shù)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dir", default="./testdata", type=str, help="directory to create random data")
# must parse the command-line argument: ``--local_rank=LOCAL_PROCESS_RANK``, which will be provided by DDP
parser.add_argument("--local_rank", type=int, default=0)
args = parser.parse_args()
train(args=args)
7步已經(jīng)改完了所有代碼昼蛀,接下來copy完整代碼宴猾,嘗試跑通????
完整代碼如下
import argparse
import os
import sys
from glob import glob
import nibabel as nib
import numpy as np
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel
import monai
from monai.data import DataLoader, Dataset, create_test_image_3d, DistributedSampler
from monai.transforms import (
AsChannelFirstd,
Compose,
LoadImaged,
RandCropByPosNegLabeld,
RandRotate90d,
ScaleIntensityd,
EnsureTyped,
)
def train(args):
# disable logging for processes except 0 on every node
if args.local_rank != 0:
f = open(os.devnull, "w")
sys.stdout = sys.stderr = f
elif not os.path.exists(args.dir):
# create 40 random image, mask paris for training
print(f"generating synthetic data to {args.dir} (this may take a while)")
os.makedirs(args.dir)
# set random seed to generate same random data for every node
np.random.seed(seed=0)
for i in range(100):
im, seg = create_test_image_3d(128, 128, 128, num_seg_classes=1, channel_dim=-1)
n = nib.Nifti1Image(im, np.eye(4))
nib.save(n, os.path.join(args.dir, f"img{i:d}.nii.gz"))
n = nib.Nifti1Image(seg, np.eye(4))
nib.save(n, os.path.join(args.dir, f"seg{i:d}.nii.gz"))
# initialize the distributed training process, every GPU runs in a process
dist.init_process_group(backend="nccl", init_method="env://")
images = sorted(glob(os.path.join(args.dir, "img*.nii.gz")))
segs = sorted(glob(os.path.join(args.dir, "seg*.nii.gz")))
train_files = [{"img": img, "seg": seg} for img, seg in zip(images, segs)]
# define transforms for image and segmentation
train_transforms = Compose(
[
LoadImaged(keys=["img", "seg"]),
AsChannelFirstd(keys=["img", "seg"], channel_dim=-1),
ScaleIntensityd(keys="img"),
RandCropByPosNegLabeld(
keys=["img", "seg"], label_key="seg", spatial_size=[96, 96, 96], pos=1, neg=1, num_samples=4
),
RandRotate90d(keys=["img", "seg"], prob=0.5, spatial_axes=[0, 2]),
EnsureTyped(keys=["img", "seg"]),
]
)
# create a training data loader
train_ds = Dataset(data=train_files, transform=train_transforms)
# create a training data sampler
train_sampler = DistributedSampler(dataset=train_ds, even_divisible=True, shuffle=True)
# use batch_size=2 to load images and use RandCropByPosNegLabeld to generate 2 x 4 images for network training
train_loader = DataLoader(
train_ds,
batch_size=10,
shuffle=False,
num_workers=2,
pin_memory=True,
sampler=train_sampler,
)
# create UNet, DiceLoss and Adam optimizer
device = torch.device(f"cuda:{args.local_rank}")
torch.cuda.set_device(device)
model = monai.networks.nets.UNet(
spatial_dims=3,
in_channels=1,
out_channels=1,
channels=(16, 32, 64, 128, 256),
strides=(2, 2, 2, 2),
num_res_units=2,
).to(device)
loss_function = monai.losses.DiceLoss(sigmoid=True).to(device)
optimizer = torch.optim.Adam(model.parameters(), 1e-3)
# wrap the model with DistributedDataParallel module
model = DistributedDataParallel(model, device_ids=[device], output_device=[device])
# start a typical PyTorch training
epoch_loss_values = list()
for epoch in range(5):
print("-" * 10)
print(f"epoch {epoch + 1}/{5}")
model.train()
epoch_loss = 0
step = 0
train_sampler.set_epoch(epoch)
for batch_data in train_loader:
step += 1
inputs, labels = batch_data["img"].to(device), batch_data["seg"].to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
epoch_len = len(train_ds) // train_loader.batch_size
print(f"{step}/{epoch_len}, train_loss: {loss.item():.4f}")
epoch_loss /= step
epoch_loss_values.append(epoch_loss)
print(f"epoch {epoch + 1} average loss: {epoch_loss:.4f}")
print(f"train completed, epoch losses: {epoch_loss_values}")
if dist.get_rank() == 0:
# all processes should see same parameters as they all start from same
# random parameters and gradients are synchronized in backward passes,
# therefore, saving it in one process is sufficient
torch.save(model.state_dict(), "final_model.pth")
dist.destroy_process_group()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dir", default="./testdata", type=str, help="directory to create random data")
# must parse the command-line argument: ``--local_rank=LOCAL_PROCESS_RANK``, which will be provided by DDP
parser.add_argument("--local_rank", type=int, default=0)
args = parser.parse_args()
train(args=args)
if __name__ == "__main__":
main()
完整代碼來自 MONAI DDP教程
step 8: 終端啟動DDP訓(xùn)練
這里的啟動方式選擇torch.distributed.launch
使用該模塊啟動訓(xùn)練,分單機(jī)多卡和多機(jī)多卡
- Single-Node multi-process(單機(jī)多卡)
python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3 and all other arguments of your training script)
這里使用單機(jī)叼旋,因此節(jié)點(diǎn)nnodes=1(可以理解為幾臺服務(wù)器), nproc_per_node
參數(shù)指定節(jié)點(diǎn)上有多少塊GPU可用鳍置。例如服務(wù)器上有2塊GPU,nproc_per_node=2
送淆。假如有8塊税产,我只想用其中的4塊。則可以通過2種方式指定(如何指定GPU,詳見上文 DP(DataParallel)部分)
export CUDA_VISIBLE_DEVICES=0,1,2,3 python -m torch.distributed.launch --nproc_per_node=4 train.py -d "./testdata"
- Multi-Node multi-process(多機(jī)多卡)
python -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE --nnodes=2 --node_rank=0 --master_addr="192.168.1.1" --master_port=1234
YOUR_TRAINING_SCRIPT.py (--arg1 --arg2 --arg3 and all other arguments of your training script)
使用幾臺機(jī)器偷崩,nnodes就等于幾辟拷。另外,需要添加master_addr
和master_port
兩個(gè)參數(shù)阐斜,默認(rèn)使用第一個(gè)節(jié)點(diǎn)的IP和端口號衫冻。這兩個(gè)參數(shù)的具體使用可通過下面命令查看,該模塊的更多可選參數(shù)也可以使用如下命令查看
python -m torch.distributed.launch --help
以上就是使用DDP訓(xùn)練的全過程谒出。如果順利的話隅俘,祝?福你,you are so lucky.
以下是一些經(jīng)驗(yàn)總結(jié)笤喳,失敗的朋友繼續(xù)往下看??
注意一:使用分布式需設(shè)置set_epoch
在分布式模式下为居,需要在每個(gè)epoch開始時(shí)調(diào)用set_epoch
方法,然后再創(chuàng)建Dataloader迭代器杀狡,以使shuffle
生效蒙畴,否則,每個(gè)epoch中呜象,選擇數(shù)據(jù)的順序不會變膳凝。參考 pytorch DistributedSampler 官方代碼
正確使用姿勢
sampler = DistributedSampler(dataset) if is_distributed else None
loader = DataLoader(dataset, shuffle=(sampler is None),
sampler=sampler)
for epoch in range(start_epoch, n_epochs):
if is_distributed:
sampler.set_epoch(epoch)
train(loader)
我們可以做個(gè)實(shí)驗(yàn),驗(yàn)證一下是不是這樣恭陡。
先創(chuàng)建一個(gè)隨機(jī)data蹬音,并使用sampler進(jìn)行采樣,再送入dataloader
import torch
from torch.utils.data import Dataset, DataLoader
class RandomDataset(Dataset):
def __init__(self):
self.data = torch.randn(21, 1, 32, 32)
self.name = torch.arange(1, 22) # 這里從1開始休玩, 所有是22著淆,不是21
def __getitem__(self, idx):
return self.name[idx], self.data[idx]
def __len__(self):
return len(self.data)
torch.distributed.init_process_group(backend='nccl')
dataset = RandomDataset()
sampler = torch.utils.data.distributed.DistributedSampler(dataset, shuffle=True)
dataloader = DataLoader(dataset,
batch_size=5,
drop_last=True,
sampler=sampler)
實(shí)驗(yàn)一:不使用set_epoch
for epoch in range(3):
print(f'epoch: {epoch}')
# sampler.set_epoch(epoch) # 注釋掉這行
for i, data in enumerate(dataloader, 0):
names, _ = data
print(names)
從結(jié)果可以看到,每個(gè)epoch使用的data數(shù)據(jù)順序都是一模一樣的哥捕,并沒有shuffle
實(shí)驗(yàn)二:使用set_epoch
for epoch in range(3):
print(f'epoch: {epoch}')
sampler.set_epoch(epoch) # 注釋掉這行
for i, data in enumerate(dataloader, 0):
names, _ = data
print(names)
從圖上可以看到牧抽,使用set_epoch
后shuffle起作用了。
注意二:相關(guān)概念說明
- rank:用于表示進(jìn)程的編號/序號(在一些結(jié)構(gòu)圖中rank指的是軟節(jié)點(diǎn)遥赚,rank可以看成一個(gè)計(jì)算單位)扬舒,每一個(gè)進(jìn)程對應(yīng)了一個(gè)rank的進(jìn)程,整個(gè)分布式由許多rank完成凫佛。
- node:物理節(jié)點(diǎn)讲坎,可以是一臺機(jī)器也可以是一個(gè)容器孕惜,節(jié)點(diǎn)內(nèi)部可以有多個(gè)GPU。
- rank與local_rank: rank是指在整個(gè)分布式任務(wù)中進(jìn)程的序號晨炕;local_rank是指在一個(gè)node上進(jìn)程的相對序號衫画,local_rank在node之間相互獨(dú)立。
- nnodes瓮栗、node_rank與nproc_per_node: nnodes是指物理節(jié)點(diǎn)數(shù)量削罩,node_rank是物理節(jié)點(diǎn)的序號;nproc_per_node是指每個(gè)物理節(jié)點(diǎn)上面進(jìn)程的數(shù)量费奸。
- word size : 全局(一個(gè)分布式任務(wù))中弥激,rank的數(shù)量。
概念解析
注意三:model先to(device)再DDP
在用 DistributedDataParallel 包裹模型之前需要先將模型送到device上愿阐,也就是要先送到GPU上微服,否則會報(bào)錯(cuò)原文鏈接:AssertionError: DistributedDataParallel device_ids and output_device arguments only work with single-device GPU modules, but got device_ids [1], output_device 1, and module parameters {device(type='cpu')}.
注意四:batchsize含義有區(qū)別
對于DP而言,輸入到dataloader里面的batch_size參數(shù)指的是總的batch_size缨历,例如batch_size=30以蕴,你有兩塊GPU,則每塊GPU會吃15個(gè)sample辛孵;對于DDP而言丛肮,里面的batch_size參數(shù)指的卻是每個(gè)GPU的batch_size,例如batch_size=30觉吭,你有兩塊GPU腾供,則每塊GPU會吃30個(gè)sample,一個(gè)batch總共就吃60個(gè)sample.
關(guān)于這點(diǎn)鲜滩,使用上面的DP和DDP代碼,看下面這句的運(yùn)行結(jié)果节值,就知道了
print(f"{step}/{epoch_len}
你會發(fā)現(xiàn)徙硅,在使用DDP時(shí),假如epoch_len=40搞疗,兩塊GPU進(jìn)行訓(xùn)練嗓蘑,step最多等于20,不會等于40匿乃。
基于horovod的分布式訓(xùn)練方法
除了 Pytorch 原生提供的 DP 和 DDP 方式以外桩皿,也有很多優(yōu)秀的由第三方提供的分布式訓(xùn)練工具,其中 Horovod 就是比較常用的一款幢炸。
Horovod 是 Uber 開源的跨平臺分布式訓(xùn)練框架(horovod 名字來源于俄羅斯一種民間舞蹈泄隔,舞者手拉手站成一個(gè)圓圈跳舞,類比了 GPU 設(shè)備之間的通信模式-宛徊。
Horovod 采用 all-reduce 架構(gòu)來提高分布式設(shè)備的通信效率佛嬉。同時(shí)逻澳,Horovod 不僅支持 Pytorch,也支持 TensorFlow 等其他深度學(xué)習(xí)框架暖呕。
DDP 和 horovod是目前比較認(rèn)可的2種方法斜做。但是我在安裝horovod是失敗了,沒法進(jìn)行后面的測評啦湾揽。感興趣的參考以下鏈接