Pytorch的模型加速方法:Dataparallel (DP) 和 DataparallelDistributedparallel (DDP)
- 2021 年 7 月 16 日
- 筆記
Dataparallel 和 DataparallelDistributed 的區別
一、Dataparallel(DP)
1.1 Dartaparallel 的使用方式
Dataparallel 的使用方式比較簡單,只需要一句話即可: net = nn.Dataparallel(net, device_ids, output_device)
其中,net
就是自己定義的網絡實例,device_ids
就是需要使用的顯卡列表,output_device
表示參數輸出結果的設備,默認情況下 output_device = device_ids[0]
。因此在使用時經常發現第一塊卡所佔用的顯存會多一些。
1.2 Dataparallel 的基本原理
Dataparallel是數據分離型,其具體做法是:在前向傳播過程中,輸入數據會被分成多個子部分送到不同的 device
中進行計算,而網絡模型則是在每個 device
上都拷貝一份,即:輸入的 batch
是平均分配到每個 device
中去,而網絡模型需要拷貝到每個 device
中。在反向傳播過程中,每個副本積累的梯度會被累加到原始模塊中,未指明 output_device
的情況下會在 device_ids[0]
上進行運算,更新好以後把權重分發到其餘卡。
1.3 Dataparallel 的注意事項
運行DataParallel模塊之前,並行化模塊必須在device_ids [0]
上具有其參數和緩衝區。在執行DataParallel之前,會首先把其模型的參數放在device_ids[0]
上。舉個例子,服務器是八卡的服務器,剛好前面序號是0的卡被別人佔用着,於是你只能用其他的卡來,比如你用2和3號卡,如果你直接指定 device_ids=[2, 3]
的話會出現模型初始化錯誤,類似於module沒有複製到在 device_ids[0]
上去。那麼你需要在運行train之前需要添加如下兩句話指定程序可見的devices,如下:
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3"
當添加這兩行代碼後,那麼 device_ids[0]
默認的就是第2號卡,你的模型也會初始化在第2號卡上了,而不會佔用第0號卡了。設置上面兩行代碼後,那麼對這個程序而言可見的只有2和3號卡,和其他的卡沒有關係,這是物理上的號卡,邏輯上來說其實是對應0和1號卡,即 device_ids[0]
對應的就是第2號卡,device_ids[1]
對應的就是第3號卡。(當然你要保證上面這兩行代碼需要定義在下面兩行代碼之前:
device_ids = [0, 1]
net = torch.nn.DataParallel(net, device_ids=device_ids)
1.4 Dataparallel 的優缺點
Dataparallel 的優點就是使用起來非常簡單,能夠使用多卡的顯存來處理數據。然而其缺點是:會造成負載不均衡的情況,成為限制模型訓練速度的瓶頸。
二、DataparallelDistributed(DDP)
2.1 DDP 的基本原理
DataparallelDistributed 在每次迭代中,操作系統會為每個GPU創建一個進程,每個進程具有自己的 optimizer
,並獨立完成所有的優化步驟,進程內與一般的訓練無異。在各進程梯度計算完成之後,各進程需要將梯度進行匯總平均,然後再由 rank=0
的進程,將其 broadcast
到所有進程。各進程用該梯度來更新參數。由於各進程中的模型,初始參數一致 (初始時刻進行一次 broadcast
),而每次用於更新參數的梯度也一致,因此,各進程的模型參數始終保持一致。而在 DataParallel
中,全程維護一個 optimizer
,對各 GPU
上梯度進行求和,而在主 GPU
進行參數更新,之後再將模型參數 broadcast
到其他 GPU
。相較於 DataParallel
,torch.distributed
傳輸的數據量更少,因此速度更快,效率更高。
2.2 DDP的使用方式
DDP使用起來比DP要麻煩一些,具體想要了解其中原理的可以參考下面幾篇文章:
//blog.csdn.net/laizi_laizi/article/details/115299263
DataParallel & DistributedDataParallel分佈式訓練 – 知乎 (zhihu.com)
最後,參考上述文章,整理出來了下面一份可以直接跑的代碼,由於個人環境不同,可能在個別環境出現不適配的情況,可以參考上述文章進行修改。
################
## main.py文件
import argparse
from tqdm import tqdm
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
# 新增:
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
### 1. 基礎模塊 ###
# 假設我們的模型是這個,與DDP無關
class ToyModel(nn.Module):
def __init__(self):
super(ToyModel, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
# 假設我們的數據是這個
def get_dataset():
transform = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
my_trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
# DDP:使用DistributedSampler,DDP幫我們把細節都封裝起來了。
# 用,就完事兒!sampler的原理,第二篇中有介紹。
train_sampler = torch.utils.data.distributed.DistributedSampler(my_trainset)
# DDP:需要注意的是,這裡的batch_size指的是每個進程下的batch_size。
# 也就是說,總batch_size是這裡的batch_size再乘以並行數(world_size)。
trainloader = torch.utils.data.DataLoader(my_trainset,
batch_size=16, num_workers=2, sampler=train_sampler)
return trainloader
### 2. 初始化我們的模型、數據、各種配置 ####
# DDP:從外部得到local_rank參數
parser = argparse.ArgumentParser()
parser.add_argument("--local_rank", default=-1, type=int)
FLAGS = parser.parse_args()
local_rank = FLAGS.local_rank
# DDP:DDP backend初始化
torch.cuda.set_device(local_rank)
dist.init_process_group(backend='nccl') # nccl是GPU設備上最快、最推薦的後端
# 準備數據,要在DDP初始化之後進行
trainloader = get_dataset()
# 構造模型
model = ToyModel().to(local_rank)
# DDP: Load模型要在構造DDP模型之前,且只需要在master上加載就行了。
ckpt_path = None
if dist.get_rank() == 0 and ckpt_path is not None:
model.load_state_dict(torch.load(ckpt_path))
# DDP: 構造DDP model
model = DDP(model, device_ids=[local_rank], output_device=local_rank)
# DDP: 要在構造DDP model之後,才能用model初始化optimizer。
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
# 假設我們的loss是這個
loss_func = nn.CrossEntropyLoss().to(local_rank)
### 3. 網絡訓練 ###
model.train()
iterator = tqdm(range(100))
for epoch in iterator:
# DDP:設置sampler的epoch,
# DistributedSampler需要這個來指定shuffle方式,
# 通過維持各個進程之間的相同隨機數種子使不同進程能獲得同樣的shuffle效果。
trainloader.sampler.set_epoch(epoch)
# 後面這部分,則與原來完全一致了。
for data, label in trainloader:
data, label = data.to(local_rank), label.to(local_rank)
optimizer.zero_grad()
prediction = model(data)
loss = loss_func(prediction, label)
loss.backward()
iterator.desc = "loss = %0.3f" % loss
optimizer.step()
# DDP:
# 1. save模型的時候,和DP模式一樣,有一個需要注意的點:保存的是model.module而不是model。
# 因為model其實是DDP model,參數是被`model=DDP(model)`包起來的。
# 2. 只需要在進程0上保存一次就行了,避免多次保存重複的東西。
if dist.get_rank() == 0:
torch.save(model.module.state_dict(), "%d.ckpt" % epoch)
################
## Bash運行
# DDP: 使用torch.distributed.launch啟動DDP模式
# 使用CUDA_VISIBLE_DEVICES,來決定使用哪些GPU
# CUDA_VISIBLE_DEVICES="0,1" python -m torch.distributed.launch --nproc_per_node 2 main.py
三、總結
總之Dataparellel和Distribution都是模型訓練加速的一種方法。Dataparallel (支持單機多卡),但是速度慢(主要原因是它採用parameter server 模式,一張主卡作為reducer,負載不均衡,主卡成為訓練瓶頸),在主GPU上進行梯度計算和更新,再將參數給其他gpu。而DDP則使用多線程進行加速,訓練速度得到了明顯的提升,但是代碼修改起來比較麻煩,需要不斷試錯積累經驗。