複数GPUで学習するときのDP(Data Parallel)とDDP(Distributed Data Parallel)の違い

PyTorch とかで複数GPUを使って学習する場合に DP と DDP って何が違うのか毎回調べてたのでまとめました。

本記事は下記記事やドキュメントをさらに噛み砕いたものとなっています。
特に一つ目の記事は少し前の記事ですがわかりやすいのでおすすめです。

- [Distributed data parallel training using Pytorch on AWS – Telesens](https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/)
- [DataParallel — PyTorch 1\.9\.0](https://pytorch.org/docs/1.9.0/generated/torch.nn.DataParallel.html) 
- [Distributed Data Parallel — PyTorch 1\.9\.0](https://pytorch.org/docs/1.9.0/notes/ddp.html)

# Q. DP と DDP どっち使えばよいの？

A. DDP 使え（DDPの方が速いし PyTorch のドキュメントでも DDP が推奨されてるから）

# 結論: 学習時の DP と DDP の違い

- 複数GPUがある想定で、`GPU:0` とそれ以外の GPU を `他GPU` とします
- GPU間通信（データの送信）が起きる場所を☆ + **太字**で表します
- 雑に言うと DP の場合1回のパラメータ更新に対し GPU:0 を起点とした GPU 間通信（データ送信）回数が DDP より多いです
    - そのため `DP の方が遅くなりやすい = DDP の方が速くなりやすい` です

| 項目 | DP | DDP |
|---|:--|:--|
| Dataloader | GPU:0 のみ所有 | 各GPUで所有(この時点でDatasetを分割済)
| batch の分割(mini batch作成) | GPU:0 で行われる | なし（Dataloader 作成の時点で分割されている） |
| mini batch のGPU間共有 | ☆ **GPU:0 から他GPUに送られる** | なし（Dataloader 作成の時点で分割されている） |
| モデルパラメータの共有 | ☆ **GPU:0 でパラメータを更新する度他GPUに送られる** | 初回のみ行われる |
| forward pass 計算 | 各GPUで行われる | 各GPUで行われる |
| loss 計算 | ☆ **他GPUからGPU:0 にoutputsを送信して GPU:0 で行われる** | 各GPUで行われる |
| 勾配計算 | ☆ **GPU:0 から他GPUにlossを送信して各GPUで計算する** | 各GPUで行われる |
| モデルパラメータ更新 | ☆ **他GPUから GPU:0 に勾配値を送信して GPU:0 でパラメータ更新される** | ☆ **各GPU間で勾配を送信して集計後勾配値を得て、それを使って各GPUで更新する** |

# 1. Single GPU での学習時の挙動について

ここでは DP と DDP の違いを説明するために、通常の Single GPU での学習の要点を記載します。

- Dataloader を一つ用意する
- 下記のように1batchあたりの学習を行う

```python
for batch in Dataloader
    inputs, targets = batch
    outputs = Model(inputs)  # Run forward pass = forward pass 計算
    loss = Loss(outputs, targets)  # Loss 計算
    loss.backward()  # backward pass 計算と勾配計算
    Model.parameter.update()  # Model のパラメータ更新
```



# 2. DP と DDP の学習時の挙動

図は [Distributed data parallel training using Pytorch on AWS – Telesens](https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/) より引用。

## 2.1 DP (Data Parallel)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33573/bbfe08df-f5fa-2280-2ffb-9ce4f1b1af6e.png" width="900">

ここでは`GPU0` と GPU0 以外のGPUを `他GPU` として説明します。
☆ + **太字** の行はGPU間通信が発生している場所を表します。

DP の場合1回のモデルパラメータ更新に対して複数回 GPU 間通信が発生していることがわかります。


1.  `GPU0` にディスクからデータを読み込み
1.  ☆ **`GPU0` から `他GPU` に対し `batch` を分割して送信する**
1.  ☆ **最新のモデル(のパラメータ) を `GPU0` から `他GPU` に送信する**
1.  各GPUで forward pass 計算
    1.  `outputs = Model(inputs)`
1.  ☆ **`他GPU` から `GPU0` に outputs を集約して loss を計算する**
    1.  loss の計算は分割前の batch で計算される
1.  ☆ **loss を `GPU0` から `他GPU` に送信して各 GPU でモデルの各パラメータの勾配を計算する**
    1. `他GPU` でも勾配を計算するのは、各 GPU で `inputs` が異なるため forward pass も変わりうる可能性があるため
1. ☆ **`他GPU` から `GPU0` に各パラメータを送信して集計する**
1. `GPU0` でモデルのパラメータを更新する


## 2.2 DDP (Distributed Data Parallel)

図は [Distributed data parallel training using Pytorch on AWS – Telesens](https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/) より引用。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33573/aa3bbe2a-314f-8bb6-6c76-8708ddb57caf.png" width="900">

☆ + **太字** の行はGPU間通信が発生している場所を表します。

DDP の場合1回のモデルパラメータ更新に対して1度のみGPU間通信を行えば良いです(勾配値の共有)
(正確にはあるGPUでは `全GPU数 - 1` 回だけ通信する)

DDP の方が DP よりGPU間の通信（データ送信）回数や量が少ないため、基本的に `DDP の方が速いです`。

1. 各GPUに対し予め Dataloader を用意する
    1. 各Dataloader でデータが重複しないようにする
1. 各GPU 担当の Dataloader がディスクからデータを読み込む
1. 各GPUで forward pass 計算
    1. `outputs = Model(inputs)`
1. ☆ **各GPUでLossを計算し、モデルの各パラメータの勾配を計算してGPU間で勾配を集計する**
    1. この時点で各GPUで同じ勾配値を持つ
    1. このとき `GPU0` を介さないデータ送信も起きる(e.g. GPU1 と GPU2 間)
    1. 例えば全 GPU が 3 つの場合、あるGPUで計算した勾配値の 1/3 を他 GPU に向けて送れば良い
1. 各GPU でパラメータ更新
    1. 初めに一度だけ同じパラメータのモデルを共有し、後に共有される勾配値が同じ値であればパラメータ更新後も同じパラメータのモデルとなる

# Reference

- [Distributed data parallel training using Pytorch on AWS – Telesens](https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/)