[TOC]
## Data Parallelism (數據並行) 與 Model Parallelism (模型並行)概述
以下以**兩張GPU卡**,做跨卡通信為例。
### Data Parallelism (數據並行)
#### 步驟概述
1. 單一完整模型複製兩份; 於每張GPU卡各放置一份。(下圖的範例是模型有六層; 直接將該模型從GPU0初始化後, 模型架構和權重直接複製一份到GPU1)。
2. 將一個批次的資料, 分散成兩個平均等分的小批次。例如 $X_{batch} = \{ {x^{(1)}, x^{(2)}, ..., x^{(N)}} \}$可拆分成 $X_{batch}^1 = \{ {x^{(1)}, x^{(2)}, ..., x^{(N/2)}} \}$ 與 $X_{batch}^2 = \{ {x^{(1+N/2)}, x^{(2+N/2)}, ..., x^{(N)}} \}$。
3. 各卡片,拿自己所得到的批次資料,進行正向傳遞後,再進行倒傳遞。接著,各卡可分別得到各模型權重的部分梯度。例如權重$w_1$於卡片A得到梯度 $g_{w_1}^{(1)}$, 於卡片B 得到梯度$g_{w_2}^{(2)}$。
4. 模型需要完整梯度資訊才可更新權重。將各卡片上面的梯度做`allreduce`運算 (跨卡通信),使得每張卡片都有完整的權重梯度。這個步驟結束後,卡A上面的 $g_{w_1}^{(1)}$或是卡B上面的 $g_{w_1}^{(2)}$均會被置換成$g_{w_1}(=g_{w_1}^{(1)}+g_{w_1}^{(2)})$。
5. 每張卡片上面的模型上面的所有權重都已經得到該權重的完整梯度了。接著,讓優化器(例如梯度下降法)於每張卡片去更新權重梯度,完成一個Iteration的模型優化。

#### 效果
1. 增速: 如果跨卡通信成本非常低,那麼有可能做到線性增速。例如1卡片跑10秒, 2卡片則有可能5-6秒即可完成。(速度快將近一倍)。
以下是常用的跨卡通信實作Data Parallelism的框架(Horovod)的官方數據:

前提是要有高速網路介面才能真正做到跨節點GPU通信加速。如果網速是不夠快的(10G或1G), 我們嘗試過,卡片即使再多,也只會變得更慢 (例如單卡片跑10秒, 分散於兩個節點的雙卡片變成跑15秒)。
2. 缺點: 若模型太肥,單張卡片記憶體放不下,則此法不可使用。
### Model Parallelism (模型並行)
#### 步驟概述
1. 單一完整模型分散成前後兩段; 於每張GPU卡各放置一段 (下圖的範例是模型有六層; 前三層放置於GPU0, 後三層放置於GPU1)。
2. 只有模型做分散; 資料不做分散。自行寫正傳遞程式碼;將模型網路層分散至不同GPU卡。後續倒傳遞以及模型優化,則主要讓AI框架(PyTorch或TensorFlow)執行。

#### 範例程式碼
```python=
# 正傳遞
import torch
import torch.nn as nn
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.part1 = nn.Linear(10, 20).to('cuda:0')
self.part2 = nn.Linear(20, 30).to('cuda:1')
def forward(self, x):
x = self.part1(x)
return self.part2(x.to('cuda:1'))
```
```python=
# 倒傳遞以及優化
model = Model()
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for inputs, targets in dataloader:
inputs = inputs.to('cuda:0')
targets = targets.to('cuda:1')
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
```
更多範例也許可參考:
* https://zhuanlan.zhihu.com/p/432907816
#### 效果
1. 不能計算變成可計算: 因為可將模型分拆到不同卡片,卡片記憶體需求降低。適合將單卡放不下的大模型拆到多卡去存放。
2. 無加速訓練效果,且相當耗時: 例如, 於正傳遞過程,輸入的張量要過完卡A, 卡B,卡C, ..等,直至最後一張卡, 才能得到模型誤差。除了中間跨卡傳遞張量要通信,有一定程度的通信成本之外; 當卡A在運算時,卡B,卡C, ..等卡片都在閒置。因此無法發揮顯卡大規模平行運算加速的淺力。
#### 一些可能會遇到的問題
1. 跨卡通信於單節點內可能已經有一定成本 (例如: NVIDIA 將部分Geforce卡閹割掉P2P功能,所以速度會再更慢)。更不用說,一次只能跑一顆GPU; 有一堆GPU在閒置作業的話,會缺乏加速效果。另,跨節點還有網速問題。網速要升級到Infiniband路由器和網卡,是一筆不小開銷 (或可買台初階伺服器)。
2. Model Parallelism從單節點拓展至多節點,會需要額外使用諸如PyTorch的[RPC框架](https://zhuanlan.zhihu.com/p/497350730)。必須學習一下RPC框架的用法。
### 另闢蹊徑
有些伺服器的設計是CPU <-> GPU之間有高速通道可以通信。例如Apple M1, M2晶片架構的Unified Memory設計; 伺服器等級的話, 則例如[IBM AC922伺服器](https://www.ithome.com.tw/review/122206)。高速通道可確保若卡片放不下模型,可以將模型權重部分動態搬移至系統記憶體 (舉例來說, 伺服器上面, 若記憶體插滿, 通常可達2TB)。如果要這樣做:
* 硬體上: CPU <-> GPU通信必須要要足夠快。IBM伺服器的設計是,Arm CPU和NVIDIA GPU之間有NVLINK傳輸介面,所以可以超級快。
* 軟體上: 如何能更加有效率的搬移張量至GPU做訓練; 有效率的緩存暫時用不到的張量至系統記憶體。這是工程性問題,可以是一個小碩士論文題目去克服。IBM多年前推出[LMS技術](https://github.com/IBM/tensorflow-large-model-support),讓大模型訓練,即使要跨CPU<-> GPU, 還是能足夠快。如果是有軟體工程或是超算加速背景,也許可以考慮從該層面著手。
## 叢集排程器
架設叢集,可以透過Slurm或Kubernetes (K8s)。叢集的好處是帶有排程器,可將任務做排程。壞處是使用方式較複雜,運維成本也較高。
- 對於使用者:
- 建議參考國網中心這兩種功能說明([Slurm](https://man.twcc.ai/@twccdocs/doc-twnia2-main-zh/https%3A%2F%2Fman.twcc.ai%2F%40twccdocs%2Fguide-twnia2-slurm-intro-zh))([K8s容器](https://docs.twcc.ai/docs/concepts-tutorials/twcc/ccs)),進而對於排程器的使用有些概念。排程器因為可以排程多使用者的任務,有助於最大化伺服器使用率。
- 對於管理者:
- 必須熟悉Slurm或K8s叢集環境維護與建制。本公司僅可協助初階K8s環境建置。通常建立叢集,必須支出一定程度的維護成本(買帶UI介面的管理軟體+少許人力來方便管理; 又或是聘請一些有相關經驗的IT人員來管理。)
## 帶有排程器的叢集使用&管理軟體
叢集若帶有軟體(網頁介面)去:
1. 協助管理者維護叢集
2. 協助使用者送出工作任務至排程器
應該會方便不少。本公司有自行開發的K8s單節點管理軟體; 亦有和合作廠商搭機協同出售單/多節點管理軟體。歡迎洽詢。