Skip to content
Published on

マルチGPU分散学習完全ガイド: DDP、FSDP、DeepSpeed

Authors
  • Name
    Twitter

1. マルチGPU学習が必要な理由

近年の大規模言語モデル(LLM)のパラメータ数は指数関数的に増加しています。GPT-3の175B、PaLMの540B、Llama 3の405Bパラメータまで、単一GPUでの学習は物理的に不可能になっています。

モデルサイズとメモリ要件

モデルのメモリ使用量を計算すると、問題の深刻さが明確になります。FP32(32ビット浮動小数点)の場合、1つのパラメータが4バイトを占有します。したがって:

  • 7Bモデル: 7 x 10^9 x 4バイト = 約28 GB(パラメータのみ)
  • 13Bモデル: 13 x 10^9 x 4バイト = 約52 GB
  • 70Bモデル: 70 x 10^9 x 4バイト = 約280 GB

オプティマイザの状態(Adamではパラメータの2倍)、勾配(パラメータと同サイズ)、活性化値のメモリを加えると、学習時に実際に必要なメモリはパラメータサイズの約4〜8倍になります。7BモデルをFP32で学習するには、最低でも112〜224 GBのGPUメモリが必要です。

最も広く使用されているNVIDIA A100のVRAMは80 GB、H100も80 GBです。7Bモデルのフルファインチューニングでさえ、単一GPUでは困難です。これが、マルチGPU分散学習がオプションではなく必須となった理由です。

学習時間の短縮

メモリの懸念に加え、学習時間の短縮も重要な動機です。単一GPUで数週間から数ヶ月かかる学習を複数のGPUに分散することで、ほぼ線形のスピードアップが期待できます。8台のGPUを使用すれば理論的には学習時間を1/8に短縮でき、実際には通信オーバーヘッドを最小化することで90%以上のスケーリング効率を達成することも可能です。


2. NCCL: GPU通信の中核

マルチGPU学習の鍵は、GPU間の効率的な通信です。NVIDIAはこの目的のために専用の通信ライブラリ**NCCL(NVIDIA Collective Communications Library)**を提供しています。

NCCLとは

NCCLは、マルチGPUおよびマルチノード環境向けの集合通信プリミティブを最適化するライブラリで、NVIDIA GPUとネットワーキングに特化して設計されています。NCCL 2.29.1(現在の最新バージョン)では、以下の通信操作をサポートしています:

  • AllReduce: すべてのGPUのデータを合計(または平均)し、結果をすべてのGPUに配布します。DDPでの勾配同期に不可欠です。
  • AllGather: 各GPUのデータを収集し、完全なデータをすべてのGPUに配布します。FSDPで順伝播前にパラメータを再構築する際に使用されます。
  • ReduceScatter: データをリデュースし、各GPUに部分を配布します。FSDPの逆伝播で勾配を分散する際に使用されます。
  • Broadcast: 1つのGPUからすべてのGPUにデータを送信します。モデル初期化時にrank 0のウェイトを他のGPUに複製する際に使用されます。
  • Send/Recv: ポイントツーポイント通信。Pipeline Parallelismでステージ間のデータ転送に使用されます。

主要なNCCL環境変数

NCCLの問題をデバッグしたり、パフォーマンスをチューニングする際の重要な環境変数:

# NCCLデバッグログの有効化
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=ALL

# 通信インターフェースの指定(マルチノード環境で重要)
export NCCL_SOCKET_IFNAME=eth0

# InfiniBandの無効化(必要に応じて)
export NCCL_IB_DISABLE=1

# P2P(Peer-to-Peer)通信レベルの設定
export NCCL_P2P_LEVEL=NVL  # NVLinkを使用

NCCLはPyTorchのtorch.distributedパッケージでデフォルトバックエンドとして使用されます。init_process_group(backend="nccl")を呼び出すと、NCCLベースの通信グループが作成されます。


マルチGPU学習のパフォーマンスは、GPU相互接続の帯域幅に大きく影響されます。GPUの相互接続方式は、大きくPCIeNVLinkに分けられます。

PCIe(Peripheral Component Interconnect Express)

PCIeは、GPU、SSD、NICなど様々なデバイスを接続する汎用インターフェースです。主要世代別の帯域幅:

世代片方向帯域幅 (x16)双方向帯域幅 (x16)
PCIe 4.032 GB/s64 GB/s
PCIe 5.064 GB/s128 GB/s
PCIe 6.0128 GB/s256 GB/s

PCIe通信では、GPU間のデータがCPU/チップセットを経由するため、追加のホップとレイテンシが発生します。

NVLinkは、NVIDIAの高速GPU専用インターコネクトで、GPU間の直接通信を提供します。CPUを経由せずにGPU間でデータが直接交換されるため、レイテンシが大幅に削減され、帯域幅が大幅に向上します。

世代GPU帯域幅(双方向)
NVLink第3世代A100600 GB/s
NVLink第4世代H100900 GB/s
NVLink第5世代B2001,800 GB/s

**NVLink第4世代(H100)はPCIe 5.0の約7倍の帯域幅を提供します。**これにより、大規模モデルの勾配同期やパラメータAllGatherなどの通信集約的なタスクで決定的なパフォーマンス差が生まれます。

NVSwitch

DGXシステムでは、NVSwitchがノード内のすべてのGPUをフルバイセクション帯域幅で接続します。例えば、DGX H100システムでは、8台のH100 GPUがNVSwitchを介して接続され、任意のGPUペア間で900 GB/sの帯域幅が保証されます。

実践的な意味

マルチGPU学習時にNVLinkの有無に応じて戦略が異なる場合があります:

  • NVLinkあり: 通信オーバーヘッドが小さいため、DDPもFSDPも効率的に動作します。
  • PCIeのみ: 勾配圧縮やGradient Accumulationなどで通信頻度を削減することが重要です。

4. 分散学習のパラダイム: データ / モデル / パイプライン並列化

マルチGPU学習戦略は、大きく3つのパラダイムに分類されます。

データ並列化

最も基本的で広く使用されるアプローチです。同じモデルをすべてのGPUに複製し、学習データをGPU間で分割して各GPUが異なるミニバッチを処理します。順伝播/逆伝播後、各GPUで計算された勾配がAllReduceで同期され、同一のオプティマイザステップが実行されます。

GPU 0: モデルコピー + データシャード 0 → 勾配 0 ─┐
GPU 1: モデルコピー + データシャード 1 → 勾配 1 ─┤── AllReduce ──→ 平均勾配
GPU 2: モデルコピー + データシャード 2 → 勾配 2 ─┤
GPU 3: モデルコピー + データシャード 3 → 勾配 3 ─┘

利点: 実装がシンプルで、ほぼ線形のスケーリング。 欠点: すべてのGPUにモデル全体が複製されるため、モデルが単一GPUのメモリに収まる必要がある。

モデル並列化

モデルのレイヤーを複数のGPUに分散します。テンソル並列化(Tensor Parallelism)とも呼ばれ、単一レイヤー内の演算(例: 行列乗算)を複数のGPUで分担します。主にMegatron-LMで使用されるアプローチです。

GPU 0: レイヤーの重み行列の上半分  ─┐
                                     ├── 結合 → レイヤー出力
GPU 1: レイヤーの重み行列の下半分  ─┘

利点: 単一GPUのメモリを超えるレイヤーを扱える。 欠点: GPU間通信が非常に頻繁で、NVLinkレベルの高速インターコネクトが必要。

パイプライン並列化

モデルのレイヤーを複数のGPUに順次分散します。各GPUは一部のレイヤーのみを担当し、マイクロバッチがパイプラインのように流れてGPUのアイドル時間(バブル)を最小化します。

GPU 0: レイヤー 1-8    │ マイクロバッチ 1 → │ マイクロバッチ 2 → │ ...
GPU 1: レイヤー 9-16   │                    │ マイクロバッチ 1 → │ マイクロバッチ 2 → │ ...
GPU 2: レイヤー 17-24  │                    │                    │ マイクロバッチ 1 → │ ...
GPU 3: レイヤー 25-32  │                    │                    │                    │ マイクロバッチ 1 → │

利点: 各GPUがモデルの一部のみを格納するため、メモリ効率が良い。 欠点: パイプラインバブルによるGPUアイドル時間が発生。

実践での組み合わせ

現代の大規模学習では、3つのアプローチをすべて組み合わせた3D並列化を使用します。例えば、Megatron-DeepSpeedでは、ノード内でTensor Parallelism(NVLinkを活用)、ノード間でPipeline Parallelism、全体的にData Parallelismを適用します。


5. PyTorch DDP(DistributedDataParallel)公式ドキュメントの分析

PyTorch DDPは、データ並列化を実装するためのPyTorch公式モジュールです。torch.nn.DataParallelとは異なり、マルチプロセスベースで動作するため、Python GILのボトルネックが排除され、マルチノード環境をサポートしています。

DDPの内部動作

PyTorch公式ドキュメントによると、DDPは以下のように動作します:

  1. 初期化: rank 0のモデル状態がすべてのプロセスにbroadcastされ、同一の初期状態から開始します。
  2. 順伝播: 各プロセスが独立して自身のデータシャードに対して順伝播を実行します。
  3. 逆伝播: 逆伝播中に、勾配がバケット単位でAllReduceにより同期されます。バケットサイズはbucket_cap_mbパラメータで調整可能です(デフォルト: 25 MB)。
  4. オプティマイザステップ: すべてのプロセスが同一の同期済み勾配でオプティマイザステップを実行し、モデルパラメータが同一に保たれます。

重要なのは、勾配AllReduceが逆伝播の計算とオーバーラップすることです。バケット内のすべての勾配が準備できると、非同期AllReduceが即座に開始され、残りの逆伝播計算と同時に実行されます。これがDDPの高い効率を実現するコアメカニズムです。

DDPコード例

import os
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler

def setup(rank, world_size):
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    dist.init_process_group(backend="nccl", rank=rank, world_size=world_size)
    torch.cuda.set_device(rank)

def cleanup():
    dist.destroy_process_group()

def train(rank, world_size):
    setup(rank, world_size)

    # モデルの作成とDDPラッピング
    model = nn.Linear(1024, 512).to(rank)
    ddp_model = DDP(model, device_ids=[rank])

    # データセットとDataLoader(DistributedSamplerが必須)
    dataset = MyDataset()
    sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
    dataloader = DataLoader(dataset, batch_size=32, sampler=sampler)

    optimizer = optim.Adam(ddp_model.parameters(), lr=1e-3)
    loss_fn = nn.MSELoss()

    for epoch in range(10):
        sampler.set_epoch(epoch)  # 各エポックで呼び出してデータシャッフルを保証
        for batch in dataloader:
            inputs, targets = batch
            inputs = inputs.to(rank)
            targets = targets.to(rank)

            optimizer.zero_grad()
            outputs = ddp_model(inputs)
            loss = loss_fn(outputs, targets)
            loss.backward()  # ここで勾配AllReduceが自動的に実行される
            optimizer.step()

    cleanup()

# 実行: torchrun --nproc_per_node=4 train_script.py

Gradient Accumulationとno_sync()

Gradient Accumulationを使用する場合、no_sync()コンテキストマネージャで不要なAllReduceを防止できます:

accumulation_steps = 4

for i, batch in enumerate(dataloader):
    inputs, targets = batch[0].to(rank), batch[1].to(rank)

    # 最後の蓄積ステップでのみ勾配を同期
    context = ddp_model.no_sync if (i + 1) % accumulation_steps != 0 else nullcontext
    with context():
        outputs = ddp_model(inputs)
        loss = loss_fn(outputs, targets) / accumulation_steps
        loss.backward()

    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

torchrunによる実行

DDP学習スクリプトはtorchrun(またはtorch.distributed.launch)で起動します:

# 単一ノード、4GPU
torchrun --nproc_per_node=4 train.py

# マルチノード(2ノード、各8GPU)
# ノード0上:
torchrun --nproc_per_node=8 --nnodes=2 --node_rank=0 \
    --master_addr="192.168.1.1" --master_port=29500 train.py
# ノード1上:
torchrun --nproc_per_node=8 --nnodes=2 --node_rank=1 \
    --master_addr="192.168.1.1" --master_port=29500 train.py

6. PyTorch FSDP(Fully Sharded Data Parallel)公式ドキュメントの分析

DDPはすべてのGPUにモデル全体を複製するため、単一GPUのメモリに収まるモデルに限定されます。FSDPは、モデルパラメータ、勾配、オプティマイザの状態をGPU間でシャーディング(分散配置)することで、この制約を克服します。

FSDPのコア原理

FSDPは、MicrosoftのZeRO(Zero Redundancy Optimizer)論文に着想を得ています。コアとなるアイデアは:

  1. シャーディング状態: 通常、各GPUはパラメータの一部(シャード)のみを保持。
  2. 順伝播: レイヤーの計算が必要な時、AllGatherですべてのシャードを収集し完全なパラメータを再構築、計算を実行した後、再シャーディング。
  3. 逆伝播: 同様にAllGatherでパラメータを再構築して勾配を計算し、ReduceScatterで勾配を分散。
  4. オプティマイザステップ: 各GPUが自身が担当するシャードに対してのみオプティマイザステップを実行。

FSDP1 vs FSDP2

PyTorch公式ドキュメントによると、FSDP1は非推奨であり、FSDP2が推奨されています。主な違い:

側面FSDP1FSDP2
シャーディング方式フラットパラメータシャーディングパラメータごとのDTensorベースdim-0シャーディング
APIFullyShardedDataParallelラッパーfully_shard()関数型API
メモリ管理recordStreamベースrecordStreamなし、決定的なGPUメモリ
プリフェッチ限定的暗黙的/明示的プリフェッチの両方をサポート

FSDP2はtorch.chunk(dim=0)を使用して、各パラメータをデータ並列ワーカーの数でdim-0に沿って分割します。

FSDP2コード例

import torch
import torch.distributed as dist
from torch.distributed.fsdp import fully_shard, MixedPrecisionPolicy

def train_fsdp(rank, world_size):
    dist.init_process_group(backend="nccl", rank=rank, world_size=world_size)
    torch.cuda.set_device(rank)

    # Transformerモデルの作成
    model = Transformer(model_args).to(rank)

    # Mixed Precisionの設定
    mp_policy = MixedPrecisionPolicy(
        param_dtype=torch.bfloat16,    # パラメータをbfloat16で格納
        reduce_dtype=torch.float32,     # 勾配のリデュースをfloat32で実行
    )

    # 各Transformerレイヤーにfsdpを適用(レイヤーレベルのシャーディング)
    for layer in model.layers:
        fully_shard(layer, mp_policy=mp_policy)

    # トップレベルのモデルにもFSDPを適用
    fully_shard(model, mp_policy=mp_policy)

    optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

    for epoch in range(num_epochs):
        for batch in dataloader:
            inputs = batch["input_ids"].to(rank)
            labels = batch["labels"].to(rank)

            optimizer.zero_grad()
            outputs = model(inputs)
            loss = loss_fn(outputs, labels)
            loss.backward()
            optimizer.step()

    dist.destroy_process_group()

FSDPシャーディング戦略

FSDPは様々なシャーディング戦略を提供しています:

  • FULL_SHARD: パラメータ、勾配、オプティマイザの状態をシャーディング。最大のメモリ節約だが、通信オーバーヘッドも最大。(ZeRO Stage 3相当)
  • SHARD_GRAD_OP: 勾配とオプティマイザの状態のみをシャーディングし、順伝播後もパラメータを保持。適度なメモリ節約で通信が少ない。(ZeRO Stage 2相当)
  • NO_SHARD: シャーディングなし、DDPと同じ動作。(ZeRO Stage 0相当)

7. DeepSpeed ZeRO Stage 1/2/3の比較

DeepSpeedはMicrosoftが開発した分散学習ライブラリで、**ZeRO(Zero Redundancy Optimizer)**によりメモリ効率の良い学習を実現します。DeepSpeed公式ドキュメントによると、ZeROは3つのステージで構成されています。

ZeROステージの比較

ZeRO Stage 1: オプティマイザ状態の分割

オプティマイザの状態のみをGPU間で分割します。Adamオプティマイザはパラメータに加えて第1モーメント(m)と第2モーメント(v)を保持するため、これらだけの分割でも大幅なメモリ節約になります。

  • メモリ節約: FP16学習と8GPUの場合、デバイスあたりのメモリ消費を約4倍削減可能。
  • 通信オーバーヘッド: DDPと同じ(AllReduceのみ)
  • CPUオフローディング: サポート
{
  "zero_optimization": {
    "stage": 1,
    "reduce_bucket_size": 5e8
  }
}

ZeRO Stage 2: 勾配の分割

オプティマイザの状態に加え、勾配も分割します。各GPUは自身に割り当てられたオプティマイザの状態に対応する勾配のみを保持します。

  • メモリ節約: Stage 1に比べ、分散された勾配メモリ分の追加節約
  • 通信オーバーヘッド: AllReduceの代わりにReduceScatterを使用し、やや効率的
  • CPUオフローディング: サポート
{
  "zero_optimization": {
    "stage": 2,
    "allgather_partitions": true,
    "allgather_bucket_size": 2e8,
    "overlap_comm": true,
    "reduce_scatter": true,
    "reduce_bucket_size": 2e8,
    "contiguous_gradients": true
  }
}

ZeRO Stage 3: パラメータの分割

オプティマイザの状態と勾配に加え、モデルパラメータも分割します。これにより、単一GPUのメモリを超えるモデルの学習が可能になります。

  • メモリ節約: 最も劇的。すべてのモデル状態が分散。
  • 通信オーバーヘッド: 順伝播/逆伝播時に追加のAllGatherが必要で、通信量が増加
  • CPU/NVMeオフローディング: サポート(ZeRO-Infinity)
{
  "zero_optimization": {
    "stage": 3,
    "contiguous_gradients": true,
    "stage3_max_live_parameters": 1e9,
    "stage3_max_reuse_distance": 1e9,
    "stage3_prefetch_bucket_size": 1e7,
    "stage3_param_persistence_threshold": 1e5,
    "reduce_bucket_size": 1e7,
    "sub_group_size": 1e9,
    "offload_optimizer": {
      "device": "cpu",
      "pin_memory": true
    },
    "offload_param": {
      "device": "cpu",
      "pin_memory": true
    }
  }
}

ZeROステージ比較のまとめ

側面Stage 1Stage 2Stage 3
オプティマイザ状態の分割OOO
勾配の分割XOO
パラメータの分割XXO
CPUオフローディングOOO
NVMeオフローディングXXO
通信オーバーヘッド
メモリ節約中程度非常に高い
学習速度最速高速比較的遅い

DeepSpeedの完全な設定例

{
  "train_batch_size": 64,
  "gradient_accumulation_steps": 4,
  "fp16": {
    "enabled": true,
    "loss_scale": 0,
    "loss_scale_window": 1000,
    "initial_scale_power": 16,
    "hysteresis": 2,
    "min_loss_scale": 1
  },
  "zero_optimization": {
    "stage": 2,
    "overlap_comm": true,
    "contiguous_gradients": true,
    "reduce_bucket_size": 2e8,
    "allgather_bucket_size": 2e8
  },
  "optimizer": {
    "type": "AdamW",
    "params": {
      "lr": 1e-4,
      "betas": [0.9, 0.999],
      "eps": 1e-8,
      "weight_decay": 0.01
    }
  },
  "scheduler": {
    "type": "WarmupDecayLR",
    "params": {
      "warmup_min_lr": 0,
      "warmup_max_lr": 1e-4,
      "warmup_num_steps": 1000,
      "total_num_steps": 50000
    }
  }
}

実践的なステージ選択ガイド

  • モデルが単一GPUに収まる場合: ZeRO Stage 1またはDDPが最速。
  • モデルが単一GPUに収まるがバッチサイズを大きくしたい場合: ZeRO Stage 2で勾配メモリを節約。
  • モデルが単一GPUに収まらない場合: ZeRO Stage 3またはFSDPを使用。
  • GPUメモリが極端に限られている場合: ZeRO Stage 3 + CPU/NVMeオフローディング。

8. HuggingFace Accelerate: 統一された分散学習インターフェース

AccelerateはHuggingFaceが開発したライブラリで、DDP、FSDP、DeepSpeedなど様々な分散学習戦略に対する統一インターフェースを提供します。既存のPyTorch学習コードに最小限の変更を加えるだけで分散学習を実現できることが主な利点です。

Accelerateのコア概念

AccelerateはPyTorchの上に薄いラッパーを提供し、新しいフレームワークを学ぶことなく、既存の学習ループをほぼそのまま維持しながら分散学習を適用できます。API全体が単一のAcceleratorクラスを中心に構成されています。

基本的な使い方

from accelerate import Accelerator

accelerator = Accelerator()

# 既存コードからの唯一の変更
model, optimizer, dataloader, scheduler = accelerator.prepare(
    model, optimizer, dataloader, scheduler
)

for batch in dataloader:
    optimizer.zero_grad()
    outputs = model(batch["input_ids"])
    loss = loss_fn(outputs, batch["labels"])
    accelerator.backward(loss)  # loss.backward()の代わり
    optimizer.step()
    scheduler.step()

accelerate configによる分散戦略の設定

accelerate configを実行すると、対話形式で分散学習環境が設定されます:

$ accelerate config

# 質問に回答すると設定ファイルが自動生成される
# - 分散学習タイプ(マルチGPU、マルチノード、TPUなど)
# - GPU数
# - 混合精度の使用有無
# - DeepSpeed / FSDPの使用と設定

生成される設定ファイルの例(default_config.yaml):

compute_environment: LOCAL_MACHINE
distributed_type: MULTI_GPU
num_machines: 1
num_processes: 4
mixed_precision: bf16
use_cpu: false

DeepSpeedとの使用

compute_environment: LOCAL_MACHINE
distributed_type: DEEPSPEED
deepspeed_config:
  zero_stage: 2
  gradient_accumulation_steps: 4
  offload_optimizer_device: none
  offload_param_device: none
mixed_precision: bf16
num_machines: 1
num_processes: 8

FSDPとの使用

compute_environment: LOCAL_MACHINE
distributed_type: FSDP
fsdp_config:
  fsdp_sharding_strategy: FULL_SHARD
  fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
  fsdp_backward_prefetch: BACKWARD_PRE
  fsdp_state_dict_type: SHARDED_STATE_DICT
mixed_precision: bf16
num_machines: 1
num_processes: 8

学習の起動

accelerate launch train.py

Accelerateは設定ファイルに基づいて適切な分散戦略を自動的に適用します。torchrundeepspeedランチャーを直接扱う必要がないため、実験中に分散戦略を切り替えるのが非常に便利です。


9. nvidia-smiモニタリングとGPU利用率の最適化

分散学習の効率を最大化するためには、リアルタイムのGPUモニタリングが不可欠です。nvidia-smiはNVIDIAドライバに含まれるCLIユーティリティで、GPUの状態をリアルタイムでクエリできます。

基本的なモニタリングコマンド

# 基本的なGPUステータスの確認
nvidia-smi

# 1秒間隔の自動更新モニタリング
watch -n 1 nvidia-smi

# 特定のGPUのみモニタリング
nvidia-smi --id=0,1

# プロセスごとのGPU使用量モニタリング
nvidia-smi pmon -i 0 -s um -d 1

# 継続的なデバイスモニタリング(1秒間隔)
nvidia-smi dmon -d 1

主要なモニタリングメトリクス

# CSV形式で主要メトリクスを出力(スクリプトのパースに便利)
nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,utilization.memory,memory.used,memory.total,power.draw --format=csv,noheader,nounits

# 出力例:
# 0, NVIDIA A100-SXM4-80GB, 45, 98, 72, 65536, 81920, 285
# 1, NVIDIA A100-SXM4-80GB, 43, 95, 68, 61440, 81920, 278

主要メトリクスの解釈:

メトリクス説明理想的な値
GPU UtilizationGPUコア利用率90%以上
Memory Utilizationメモリ帯域幅利用率60〜80%
Memory Used使用中のVRAM合計の80〜95%
TemperatureGPU温度80度未満
Power Draw消費電力TDPの80〜100%

GPU利用率が低い場合の原因と対策

GPU利用率が低い場合(80%未満):

  1. DataLoaderのボトルネック: num_workersをCPUコア数に合わせて増やし、pin_memory=Trueを設定
  2. バッチサイズが小さすぎる: GPUの並列計算ユニットが十分に活用されていない
  3. CPU前処理のボトルネック: GPU上でデータ前処理を行う(DALIなど)か、事前処理してキャッシュ
dataloader = DataLoader(
    dataset,
    batch_size=64,
    num_workers=8,           # CPUコア数に合わせて調整
    pin_memory=True,          # GPU転送速度を向上
    prefetch_factor=2,        # 事前ロードするバッチ数
    persistent_workers=True,  # エポック間でワーカーを維持
)

Memory利用率が低い場合:

  • バッチサイズを徐々に増やし、GPUメモリ利用率を最大化します。
  • 混合精度(FP16/BF16)を使用すると、同じメモリで約2倍のバッチサイズが可能になります。

PyTorch組み込みのメモリモニタリング

import torch

# 現在のGPUメモリ使用量を確認
print(f"Allocated: {torch.cuda.memory_allocated() / 1024**3:.2f} GB")
print(f"Cached: {torch.cuda.memory_reserved() / 1024**3:.2f} GB")
print(f"Max Allocated: {torch.cuda.max_memory_allocated() / 1024**3:.2f} GB")

# メモリスナップショットの保存(詳細分析用)
torch.cuda.memory._record_memory_history()
# ... 学習コードを実行 ...
torch.cuda.memory._dump_snapshot("memory_snapshot.pickle")

10. 実践的なトラブルシューティング: OOM、NCCLタイムアウト、通信ボトルネック

マルチGPU学習で最も頻繁に発生する問題とその解決策をまとめます。

10.1 OOM(Out of Memory)エラー

症状: CUDA out of memory. Tried to allocate X MiBエラー

段階的な解決策:

# ステップ1: バッチサイズを削減
batch_size = 16  # → 8、4、2と段階的に減少を試みる

# ステップ2: Gradient Accumulationで実効バッチサイズを維持
gradient_accumulation_steps = 4  # batch_size * 4 = 実効バッチサイズ

# ステップ3: 混合精度を使用
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()

with autocast(dtype=torch.bfloat16):
    outputs = model(inputs)
    loss = loss_fn(outputs, targets)

scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

# ステップ4: Gradient Checkpointingを有効化
from torch.utils.checkpoint import checkpoint
# またはHugging Faceモデルの場合:
model.gradient_checkpointing_enable()

# ステップ5: メモリ割り当て設定の最適化
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"

メモリ不足時のエスカレーションパス:

バッチサイズ削減 → Mixed PrecisionGradient Checkpointing
ZeRO Stage 2ZeRO Stage 3CPUオフローディング → NVMeオフローディング

10.2 NCCLタイムアウトエラー

症状: Watchdog caught collective operation timeoutまたはNCCL timeoutエラー。特定のポイントで学習がハングまたはフリーズする。

一般的な原因と対策:

# 原因1: タイムアウト値が短すぎる
# 対策: タイムアウトを延長
export NCCL_BLOCKING_WAIT=1

# Pythonでタイムアウトを設定
import datetime
dist.init_process_group(
    backend="nccl",
    timeout=datetime.timedelta(minutes=30)  # デフォルトは30分
)
# 原因2: GPU P2P通信の問題
# 対策: P2Pを無効化
export NCCL_P2P_DISABLE=1

# 原因3: ネットワークインターフェースの選択ミス(マルチノード)
# 対策: 正しいネットワークインターフェースを指定
export NCCL_SOCKET_IFNAME=eth0
export GLOO_SOCKET_IFNAME=eth0

# 原因4: Docker環境での共有メモリ不足
# 対策: Docker実行時にSHMサイズを増加
# docker run --shm-size=16g ...
# デバッグ用にNCCLログを有効化
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=ALL
export TORCH_DISTRIBUTED_DEBUG=DETAIL

1つのGPUのみでOOMが発生し、他のGPUが待機する場合:

この状況はNCCLタイムアウトとして現れますが、実際の原因はOOMです。1つのGPUがOOMでクラッシュすると、残りのGPUはAllReduceの完了を待ってタイムアウトします。まずOOMを解決する必要があります。

10.3 通信ボトルネックの診断と解決

症状: GPU利用率は高いが、GPU数に比例した学習速度のスケーリングが得られない。

診断アプローチ:

import torch.autograd.profiler as profiler

with profiler.profile(
    activities=[
        profiler.ProfilerActivity.CPU,
        profiler.ProfilerActivity.CUDA,
    ],
    schedule=profiler.schedule(wait=1, warmup=1, active=3, repeat=1),
    on_trace_ready=profiler.tensorboard_trace_handler('./log/profiler'),
    record_shapes=True,
    with_stack=True,
) as prof:
    for step, batch in enumerate(dataloader):
        if step >= 7:
            break
        train_step(model, batch)
        prof.step()

対策:

# 1. 勾配圧縮の使用(DDP)
from torch.distributed.algorithms.ddp_comm_hooks import (
    default_hooks as default,
    powerSGD_hook as powerSGD,
)
ddp_model.register_comm_hook(
    state=powerSGD.PowerSGDState(process_group=None, matrix_approximation_rank=1),
    hook=powerSGD.powerSGD_hook,
)

# 2. バケットサイズの調整(DDP)
ddp_model = DDP(model, device_ids=[rank], bucket_cap_mb=50)  # デフォルト25MB

# 3. overlap_commの有効化(DeepSpeed)
# ds_config.json内:
# "zero_optimization": { "overlap_comm": true }

10.4 よくある間違いと解決策

問題原因解決策
すべてのGPUが同一データを使用DistributedSamplerが未使用DistributedSamplerを適用
毎エポック同じデータ順序sampler.set_epoch(epoch)が未呼び出し各エポックの開始時に呼び出す
モデル保存時の競合全ランクが保存を試みるif rank == 0:でrank 0のみ保存
DDPラップ後の元モデルへのアクセスmodelddp_modelの混在ddp_model.module経由でアクセス
find_unused_parametersの警告一部パラメータが順伝播で未使用DDP(..., find_unused_parameters=True)を設定

11. まとめ: どの戦略を選ぶべきか

分散学習戦略を選択するための最終的な判断フロー:

モデルが単一GPUに収まるか?
├── はい → DDP(最速かつ最もシンプル)
│         ├── バッチサイズを増やしたい → ZeRO Stage 1/2 または FSDP SHARD_GRAD_OP
│         └── 十分 → DDPだけで十分
└── いいえ → モデルが全GPUの合計メモリに収まるか?
              ├── はい → FSDP (FULL_SHARD) または ZeRO Stage 3
              │         ├── PyTorchネイティブを好むFSDP
              │         └── より多くのオプション/カスタマイズが必要 → DeepSpeed
              └── いいえ → ZeRO Stage 3 + CPU/NVMeオフローディング
                          または Pipeline Parallelism + Tensor Parallelismの組み合わせ

参考文献

PyTorch公式ドキュメント

DeepSpeed公式ドキュメント

NVIDIA公式ドキュメント

HuggingFace公式ドキュメント

クイズ

Q1: 「マルチGPU分散学習完全ガイド: DDP、FSDP、DeepSpeed」の主なトピックは何ですか?

PyTorch公式ドキュメントに基づき、DDP、FSDP、DeepSpeed ZeROなどマルチGPU分散学習のコアコンポーネントを体系的に分析し、実践的なセットアップ手順を解説します。

Q2: マルチGPU学習が必要な理由とは何ですか? 近年の大規模言語モデル(LLM)のパラメータ数は指数関数的に増加しています。GPT-3の175B、PaLMの540B、Llama 3の405Bパラメータまで、単一GPUでの学習は物理的に不可能になっています。 モデルサイズとメモリ要件 モデルのメモリ使用量を計算すると、問題の深刻さが明確になります。FP32(32ビット浮動小数点)の場合、1つのパラメータが4バイトを占有します。

Q3: NCCL: GPU通信の中核の核心的な概念を説明してください。 マルチGPU学習の鍵は、GPU間の効率的な通信です。NVIDIAはこの目的のために専用の通信ライブラリNCCL(NVIDIA Collective Communications Library)を提供しています。 NCCLとは NCCLは、マルチGPUおよびマルチノード環境向けの集合通信プリミティブを最適化するライブラリで、NVIDIA GPUとネットワーキングに特化して設計されています。

Q4: NVLink vs PCIe: GPU相互接続帯域幅の比較の主な特徴は何ですか? マルチGPU学習のパフォーマンスは、GPU相互接続の帯域幅に大きく影響されます。GPUの相互接続方式は、大きくPCIeとNVLinkに分けられます。 PCIe(Peripheral Component Interconnect Express) PCIeは、GPU、SSD、NICなど様々なデバイスを接続する汎用インターフェースです。主要世代別の帯域幅: PCIe通信では、GPU間のデータがCPU/チップセットを経由するため、追加のホップとレイテンシが発生します。

Q5: 分散学習のパラダイム: データ / モデル / パイプライン並列化はどのように機能しますか?

マルチGPU学習戦略は、大きく3つのパラダイムに分類されます。 データ並列化 最も基本的で広く使用されるアプローチです。同じモデルをすべてのGPUに複製し、学習データをGPU間で分割して各GPUが異なるミニバッチを処理します。順伝播/逆伝播後、各GPUで計算された勾配がAllReduceで同期され、同一のオプティマイザステップが実行されます。 利点: 実装がシンプルで、ほぼ線形のスケーリング。 欠点: すべてのGPUにモデル全体が複製されるため、モデルが単一GPUのメモリに収まる必要がある。 モデル並列化 モデルのレイヤーを複数のGPUに分散します。