Pytorch使用資料並行,單機多卡

fnangle發表於2020-05-14

pytorch的並行分為模型並行、資料並行

左側模型並行:是網路太大,一張卡存不了,那麼拆分,然後進行模型並行訓練。

右側資料並行:多個顯示卡同時採用資料訓練網路的副本。

 

一、模型並行

 

二、資料並行

資料並行的操作要求我們將資料劃分成多份,然後傳送給多個 GPU 進行並行的計算。

注意:多卡訓練要考慮通訊開銷的,是個trade off的過程,不見得四塊卡一定比兩塊卡快多少,可能是訓練到四塊卡的時候通訊開銷已經佔了大頭

下面是一個簡單的示例。要實現資料並行,第一個方法是採用 nn.parallel 中的幾個函式,分別實現的功能如下所示:

  1. 複製(Replicate):將模型拷貝到多個 GPU 上;

  2. 分發(Scatter):將輸入資料根據其第一個維度(通常就是 batch 大小)劃分多份,並傳送到多個 GPU 上;

  3. 收集(Gather):從多個 GPU 上傳送回來的資料,再次連線回一起;

  4. 並行的應用(parallel_apply):將第三步得到的分散式的輸入資料應用到第一步中拷貝的多個模型上。

  5. 實現程式碼如下
# Replicate module to devices in device_ids
replicas = nn.parallel.replicate(module, device_ids)
# Distribute input to devices in device_ids
inputs = nn.parallel.scatter(input, device_ids)
# Apply the models to corresponding inputs
outputs = nn.parallel.parallel_apply(replicas, inputs)
# Gather result from all devices to output_device
result = nn.parallel.gather(outputs, output_device)  

  6.事實上PyTorch也提供了簡單的函式,只用幾行程式碼可實現簡單高效的並行GPU計算。

  ①nn.parallel.data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None)

  ②class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

  可見二者的引數十分相似,通過device_ids引數可以指定在哪些GPU上進行優化,output_device指定輸出到哪個GPU上。唯一的不同就在於前者直接利用多GPU平行計算得出結果,而後者則返回一個新的module,能夠自動在多GPU上進行並行加速。

# method 1
new_net= nn.DataParallel(net, device_ids=[0, 1])
output= new_net(input)
# method 2
output= nn.parallel.data_parallel(new_net, input, device_ids=[0, 1])

 

資料並行torch.nn.DataParallel

PyTorch 中實現資料並行的操作可以通過使用 torch.nn.DataParallel

一、並行處理機制

DataParallel,是將輸入一個 batch 的資料均分成多份,分別送到對應的 GPU 進行計算,各 個 GPU 得到的梯度累加。與 Module 相關的所有資料也都會以淺複製的方式複製多份。每個 GPU 將針對各自的輸入資料獨立進行 forward 計算,在 backward 時,每個卡上的梯度會彙總到原始的 module 上,再用反向傳播更新單個 GPU 上的模型引數,再將更新後的模型引數複製到剩餘指定的 GPU 中,以此來實現並行。

DataParallel會將定義的網路模型引數預設放在GPU 0上,所以dataparallel實質是可以看做把訓練引數從GPU拷貝到其他的GPU同時訓練,這樣會導致記憶體和GPU使用率出現很嚴重的負載不均衡現象,即GPU 0的使用記憶體和使用率會大大超出其他顯示卡的使用記憶體,因為在這裡GPU0作為master來進行梯度的彙總和模型的更新,再將計算任務下發給其他GPU,所以他的記憶體和使用率會比其他的高。

(圖源知乎)

 

 二、使用程式碼

注意我這裡的程式碼時一個文字分類的,模型叫TextCNN

1.單gpu(用做對比)

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"

#在訓練函式和測試函式中,有兩個地方要判斷是否用cuda,將模型和資料搬到gpu上去
model = TextCNN(args)
if args.cuda: 
  model.cuda()
。。。

for batch_idx, (data, target) in enumerate(train_loader):  
  if args.cuda:
    data, target = data.cuda(), target.cuda()

2.多gpu,DataParallel使用

#device_ids = [0,1,2,3]
如果不設定好要使用的device_ids的話, 程式會自動找到這個機器上面可以用的所有的顯示卡用於訓練。
如果想要限制使用的顯示卡數,怎麼辦呢?
在程式碼最前面使用:
os.environ['CUDA_VISIBLE_DEVICES'] == '0,5'
或者
CUDA_VISIBLE_DEVICES=1,2,3 python
# 限制程式碼能看到的GPU個數,這裡表示指定只使用實際的0號和5號GPU
# 注意:這裡的賦值必須是字串,list會報錯

————————————————下面是重點

if args.cuda:
    model =  model.cuda()    #這裡將模型複製到gpu
if len(device_ids)>1:
  model = nn.DataParallel(model)

  
#when train and test
data = data.cuda(non_blocking=True)
target = target.cuda(non_blocking=True)

稍微完整一點:

# 這裡要 model.cuda()
model = nn.DataParallel(model.cuda(), device_ids=gpus, output_device=gpus[0])

for epoch in range(100):
   for batch_idx, (data, target) in enumerate(train_loader):
      # 這裡要 images/target.cuda()
      images = images.cuda(non_blocking=True)
      target = target.cuda(non_blocking=True)
      ...
      output = model(images)
      loss = criterion(output, target)
      ...
      optimizer.zero_grad()
      loss.backward()
      optimizer.step()

 

3.cuda()函式解釋

.cuda()函式返回一個儲存在CUDA記憶體中的複製,其中device可以指定cuda裝置。 但如果此storage物件早已在CUDA記憶體中儲存,並且其所在的裝置編號與cuda()函式傳入的device引數一致,則不會發生複製操作,返回原物件。

cuda()函式的引數資訊:

  • device (int) – 指定的GPU裝置id. 預設為當前裝置,即 torch.cuda.current_device()的返回值。

  • non_blocking (bool) – 如果此引數被設定為True, 並且此物件的資源儲存在固定記憶體上(pinned memory),那麼此cuda()函式產生的複製將與host端的原storage物件保持同步。否則此引數不起作用。

  

分散式資料並行 torch.nn.parallel.DistributedDataParallel

一、並行處理機制

DistributedDataParallel,支援 all-reduce,broadcast,send 和 receive 等等。通過 MPI 實現 CPU 通訊,通過 NCCL 實現 GPU 通訊。可以用於單機多卡也可用於多機多卡, 官方也曾經提到用 DistributedDataParallel 解決 DataParallel 速度慢,GPU 負載不均衡的問題。

效果比DataParallel好太多!!!torch.distributed相對於torch.nn.DataParalle 是一個底層的API,所以我們要修改我們的程式碼,使其能夠獨立的在機器(節點)中執行。

與 DataParallel 的單程式控制多 GPU 不同,在 distributed 的幫助下,我們只需要編寫一份程式碼,torch 就會自動將其分配給n個程式,分別在 n 個 GPU 上執行。

 

相比DataParallel優勢如下:

1.每個程式對應一個獨立的訓練過程,且只對梯度等少量資料進行資訊交換。

在每次迭代中,每個程式具有自己的 optimizer ,並獨立完成所有的優化步驟,程式內與一般的訓練無異。

在各程式梯度計算完成之後,各程式需要將梯度進行彙總平均,然後再由 rank=0 的程式,將其 broadcast 到所有程式。之後,各程式用該梯度來獨立的更新引數。

而 DataParallel是梯度彙總到gpu0,反向傳播更新引數,再廣播引數給其他的gpu

由於各程式中的模型,初始引數一致 (初始時刻進行一次 broadcast),而每次用於更新引數的梯度也一致,因此,各程式的模型引數始終保持一致。

而在 DataParallel 中,全程維護一個 optimizer,對各 GPU 上梯度進行求和,而在主 GPU 進行引數更新,之後再將模型引數 broadcast 到其他 GPU

相較於 DataParalleltorch.distributed 傳輸的資料量更少,因此速度更快,效率更高。

2.每個程式包含獨立的直譯器和 GIL。

一般使用的Python直譯器CPython:是用C語言實現Pyhon,是目前應用最廣泛的直譯器。全域性鎖使Python在多執行緒效能上表現不佳,全域性直譯器鎖(Global Interpreter Lock)是Python用於同步執行緒的工具,使得任何時刻僅有一個執行緒在執行。

由於每個程式擁有獨立的直譯器和 GIL,消除了來自單個 Python 程式中的多個執行執行緒,模型副本或 GPU 的額外直譯器開銷和 GIL-thrashing ,因此可以減少直譯器和 GIL 使用衝突。這對於嚴重依賴 Python runtimemodels 而言,比如說包含 RNN 層或大量小元件的 models 而言,這尤為重要。

 

分散式幾個概念:

  • group:

    即程式組。預設情況下,只有一個組,一個 job 即為一個組,也即一個 world。

    當需要進行更加精細的通訊時,可以通過 new_group 介面,使用 word 的子集,建立新組,用於集體通訊等。

  • world size :

    表示全域性程式個數。

  • rank:

    表示程式序號,用於程式間通訊,表徵程式優先順序。rank = 0 的主機為 master 節點。

  • local_rank:

    程式內,GPU 編號,非顯式引數,由 torch.distributed.launch 內部指定。比方說, rank = 3,local_rank = 0 表示第 3 個程式內的第 1 塊 GPU。

二、使用流程

Pytorch 中分散式的基本使用流程如下:

  1. 在使用 distributed 包的任何其他函式之前,需要使用 init_process_group 初始化程式組,同時初始化 distributed 包。

  2. 如果需要進行小組內集體通訊,用 new_group 建立子分組

  3. 建立分散式並行模型 DDP(model, device_ids=device_ids)

  4. 為資料集建立 Sampler

  5. 使用啟動工具 torch.distributed.launch 在每個主機上執行一次指令碼,開始訓練

  6. 使用 destory_process_group() 銷燬程式組

三、使用程式碼

1. 新增引數  --local_rank
#每個程式分配一個 local_rank 引數,表示當前程式在當前主機上的編號。例如:rank=2, local_rank=0 表示第 3 個節點上的第 1 個程式。
# 這個引數是torch.distributed.launch傳遞過來的,我們設定位置引數來接受,local_rank代表當前程式程式使用的GPU標號
parser = argparse.ArgumentParser()
parser.add_argument('--local_rank', default=-1, type=int,
                    help='node rank for distributed training')
args = parser.parse_args()
print(args.local_rank))

2.初始化使用nccl後端
dist.init_process_group(backend='nccl') 
# When using a single GPU per process and per
# DistributedDataParallel, we need to divide the batch size
# ourselves based on the total number of GPUs we have
device_ids=[1,3]
ngpus_per_node=len(device_ids)
args.batch_size = int(args.batch_size / ngpus_per_node)
#ps 檢查nccl是否可用
#torch.distributed.is_nccl_available ()

3.使用DistributedSampler
#別忘了設定pin_memory=true
#使用 DistributedSampler 對資料集進行劃分。它能幫助我們將每個 batch 劃分成幾個 partition,在當前程式中只需要獲取和 rank 對應的那個 partition 進行訓練

train_dataset = MyDataset(train_filelist, train_labellist, args.sentence_max_size, embedding, word2id)
train_sampler = t.utils.data.distributed.DistributedSampler(train_dataset)
train_dataloader = DataLoader(train_dataset, 
                                  pin_memory=true,
                                shuffle=(train_sampler is None),
                                batch_size=args.batch_size, 
                                num_workers=args.workers,
                                sampler=train_sampler    )
#DataLoader:num_workers這個引數決定了有幾個程式來處理data loading。0意味著所有的資料都會被load進主程式

#注意 testset不用sampler

4.分散式訓練
#使用 DistributedDataParallel 包裝模型,它能幫助我們為不同 GPU 上求得的梯度進行 all reduce(即彙總不同 GPU 計算所得的梯度,並同步計算結果)。
#all reduce 後不同 GPU 中模型的梯度均為 all reduce 之前各 GPU 梯度的均值. 注意find_unused_parameters引數!
net = textCNN(args,vectors=t.FloatTensor(wvmodel.vectors)) if args.cuda: # net.cuda(device_ids[0]) net.cuda() if len(device_ids)>1: net=torch.nn.parallel.DistributedDataParallel(net,find_unused_parameters=True)
5.最後,把資料和模型載入到當前程式使用的 GPU 中,正常進行正反向傳播: for batch_idx, (data, target) in enumerate(train_loader): if args.cuda: data, target = data.cuda(), target.cuda() output = net(images) loss = criterion(output, target) ... optimizer.zero_grad() loss.backward() optimizer.step() 6.在使用時,命令列呼叫 torch.distributed.launch 啟動器啟動: #pytorch 為我們提供了 torch.distributed.launch 啟動器,用於在命令列分散式地執行 python 檔案。 #--nproc_per_node引數指定為當前主機建立的程式數。一般設定為=NUM_GPUS_YOU_HAVE當前主機的 GPU 數量,每個程式獨立執行訓練指令碼。 #這裡是單機多卡,所以node=1,就是一臺主機,一臺主機上--nproc_per_node個程式 CUDA_VISIBLE_DEVICES=0,1,2,3 python -m torch.distributed.launch --nproc_per_node=4 main.py

四、一些解釋

1、啟動輔助工具 Launch utility

啟動實用程式torch.distributed.launch,此幫助程式可用於為每個節點啟動多個程式以進行分散式訓練,它在每個訓練節點上產生多個分散式訓練程式。

這個工具可以用作CPU或者GPU,如果被用於GPU,每個GPU產生一個程式Process。該工具既可以用來做單節點多GPU訓練,也可用於多節點多GPU訓練。如果是單節點多GPU,將會在單個GPU上執行一個分散式程式,據稱可以非常好地改進單節點訓練效能。如果用於多節點分散式訓練,則通過在每個節點上產生多個程式來獲得更好的多節點分散式訓練效能。如果有Infiniband介面則加速比會更高。

在 單節點分散式訓練 或 多節點分散式訓練 的兩種情況下,該工具將為每個節點啟動給定數量的程式(--nproc_per_node)。如果用於GPU培訓,則此數字需要小於或等於當前系統上的GPU數量(nproc_per_node),並且每個程式將在從GPU 0到GPU(nproc_per_node - 1)的單個GPU上執行。

 

2、NCCL 後端

NCCL 的全稱為 Nvidia 聚合通訊庫(NVIDIA Collective Communications Library),是一個可以實現多個 GPU、多個結點間聚合通訊的庫,在 PCIe、Nvlink、InfiniBand 上可以實現較高的通訊速度。

NCCL 高度優化和相容了 MPI,並且可以感知 GPU 的拓撲,促進多 GPU 多節點的加速,最大化 GPU 內的頻寬利用率,所以深度學習框架的研究員可以利用 NCCL 的這個優勢,在多個結點內或者跨界點間可以充分利用所有可利用的 GPU。

NCCL 對 CPU 和 GPU 均有較好支援,且 torch.distributed 對其也提供了原生支援。

對於每臺主機均使用多程式的情況,使用 NCCL 可以獲得最大化的效能。每個程式內,不許對其使用的 GPUs 具有獨佔權。若程式之間共享 GPUs 資源,則可能導致 deadlocks。

 

3、DistributedSampler

原型

torch.utils.data.distributed.DistributedSampler(dataset, num_replicas=None, rank=None)

引數

  • dataset

進行取樣的資料集

  • num_replicas

分散式訓練中,參與訓練的程式數

  • rank

當前程式的 rank 序號(必須位於分散式訓練中)

說明

對資料集進行取樣,使之劃分為幾個子集,不同 GPU 讀取的資料應該是不一樣的。

一般與 DistributedDataParallel 配合使用。此時,每個程式可以傳遞一個 DistributedSampler 例項作為一個 Dataloader sampler,並載入原始資料集的一個子集作為該程式的輸入。

Dataparallel 中,資料被直接劃分到多個 GPU 上,資料傳輸會極大的影響效率。相比之下,在 DistributedDataParallel 使用 sampler 可以為每個程式劃分一部分資料集,並避免不同程式之間資料重複。

注意:在 DataParallel 中,batch size 設定必須為單卡的 n 倍,但是在 DistributedDataParallel 內,batch size 設定於單卡一樣即可。

 

兩種方法的使用情況,負載和訓練時間

一、DataParallel

PID相同的是一個程式

在兩張GPU上,本人用了3種不同batchsize跑的結果如下,

batch=64(藍色),主卡3678MB,副卡1833MB

batch=128(黑色),主卡3741MB,副卡1863MB

batch=256(紅色),主卡3821MB,副卡1925MB

 總體看來,主卡,預設是指定的卡中序號排在第一個的,記憶體使用情況是副卡的2倍,負載不太均衡

 

 batch=512時,訓練時間和準確率

 

 

二、DistributedDataParallel

 

其中PID為113673、113674的是我的程式,用了兩張卡,總體有兩個程式,可以看到,負載相對很均衡了。 

 

batch=500時,訓練時間和準確率:

 

相比DataParallel,訓練時間縮減了好幾倍!推薦大家使用分散式資料並行

 

-------------------

參考:https://blog.csdn.net/zwqjoy/article/details/89415933

https://www.ctolib.com/tczhangzhi-pytorch-distributed.html

 

相關文章