- Authors
- Name
- 1. マルチGPU学習が必要な理由
- 2. NCCL: GPU通信の中核
- 3. NVLink vs PCIe: GPU相互接続帯域幅の比較
- 4. 分散学習のパラダイム: データ / モデル / パイプライン並列化
- 5. PyTorch DDP(DistributedDataParallel)公式ドキュメントの分析
- 6. PyTorch FSDP(Fully Sharded Data Parallel)公式ドキュメントの分析
- 7. DeepSpeed ZeRO Stage 1/2/3の比較
- 8. HuggingFace Accelerate: 統一された分散学習インターフェース
- 9. nvidia-smiモニタリングとGPU利用率の最適化
- 10. 実践的なトラブルシューティング: OOM、NCCLタイムアウト、通信ボトルネック
- 11. まとめ: どの戦略を選ぶべきか
- 参考文献
- クイズ
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ベースの通信グループが作成されます。
3. NVLink vs PCIe: GPU相互接続帯域幅の比較
マルチGPU学習のパフォーマンスは、GPU相互接続の帯域幅に大きく影響されます。GPUの相互接続方式は、大きくPCIeとNVLinkに分けられます。
PCIe(Peripheral Component Interconnect Express)
PCIeは、GPU、SSD、NICなど様々なデバイスを接続する汎用インターフェースです。主要世代別の帯域幅:
| 世代 | 片方向帯域幅 (x16) | 双方向帯域幅 (x16) |
|---|---|---|
| PCIe 4.0 | 32 GB/s | 64 GB/s |
| PCIe 5.0 | 64 GB/s | 128 GB/s |
| PCIe 6.0 | 128 GB/s | 256 GB/s |
PCIe通信では、GPU間のデータがCPU/チップセットを経由するため、追加のホップとレイテンシが発生します。
NVLink
NVLinkは、NVIDIAの高速GPU専用インターコネクトで、GPU間の直接通信を提供します。CPUを経由せずにGPU間でデータが直接交換されるため、レイテンシが大幅に削減され、帯域幅が大幅に向上します。
| 世代 | GPU | 帯域幅(双方向) |
|---|---|---|
| NVLink第3世代 | A100 | 600 GB/s |
| NVLink第4世代 | H100 | 900 GB/s |
| NVLink第5世代 | B200 | 1,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は以下のように動作します:
- 初期化: rank 0のモデル状態がすべてのプロセスに
broadcastされ、同一の初期状態から開始します。 - 順伝播: 各プロセスが独立して自身のデータシャードに対して順伝播を実行します。
- 逆伝播: 逆伝播中に、勾配がバケット単位で
AllReduceにより同期されます。バケットサイズはbucket_cap_mbパラメータで調整可能です(デフォルト: 25 MB)。 - オプティマイザステップ: すべてのプロセスが同一の同期済み勾配でオプティマイザステップを実行し、モデルパラメータが同一に保たれます。
重要なのは、勾配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)論文に着想を得ています。コアとなるアイデアは:
- シャーディング状態: 通常、各GPUはパラメータの一部(シャード)のみを保持。
- 順伝播: レイヤーの計算が必要な時、
AllGatherですべてのシャードを収集し完全なパラメータを再構築、計算を実行した後、再シャーディング。 - 逆伝播: 同様に
AllGatherでパラメータを再構築して勾配を計算し、ReduceScatterで勾配を分散。 - オプティマイザステップ: 各GPUが自身が担当するシャードに対してのみオプティマイザステップを実行。
FSDP1 vs FSDP2
PyTorch公式ドキュメントによると、FSDP1は非推奨であり、FSDP2が推奨されています。主な違い:
| 側面 | FSDP1 | FSDP2 |
|---|---|---|
| シャーディング方式 | フラットパラメータシャーディング | パラメータごとのDTensorベースdim-0シャーディング |
| API | FullyShardedDataParallelラッパー | 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 1 | Stage 2 | Stage 3 |
|---|---|---|---|
| オプティマイザ状態の分割 | O | O | O |
| 勾配の分割 | X | O | O |
| パラメータの分割 | X | X | O |
| CPUオフローディング | O | O | O |
| NVMeオフローディング | X | X | O |
| 通信オーバーヘッド | 低 | 低 | 高 |
| メモリ節約 | 中程度 | 高 | 非常に高い |
| 学習速度 | 最速 | 高速 | 比較的遅い |
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は設定ファイルに基づいて適切な分散戦略を自動的に適用します。torchrunやdeepspeedランチャーを直接扱う必要がないため、実験中に分散戦略を切り替えるのが非常に便利です。
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 Utilization | GPUコア利用率 | 90%以上 |
| Memory Utilization | メモリ帯域幅利用率 | 60〜80% |
| Memory Used | 使用中のVRAM | 合計の80〜95% |
| Temperature | GPU温度 | 80度未満 |
| Power Draw | 消費電力 | TDPの80〜100% |
GPU利用率が低い場合の原因と対策
GPU利用率が低い場合(80%未満):
- DataLoaderのボトルネック:
num_workersをCPUコア数に合わせて増やし、pin_memory=Trueを設定 - バッチサイズが小さすぎる: GPUの並列計算ユニットが十分に活用されていない
- 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 Precision → Gradient Checkpointing
→ ZeRO Stage 2 → ZeRO Stage 3 → CPUオフローディング → 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ラップ後の元モデルへのアクセス | modelとddp_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公式ドキュメント
- DistributedDataParallel API Documentation - PyTorch 2.10
- Getting Started with Distributed Data Parallel - PyTorch Tutorials
- Distributed Data Parallel Internal Design - PyTorch 2.10
- What is Distributed Data Parallel (DDP) - PyTorch Tutorials
- Getting Started with FSDP2 - PyTorch Tutorials 2.11
- torch.distributed.fsdp.fully_shard - PyTorch 2.10
- FullyShardedDataParallel - PyTorch 2.10
- Introducing PyTorch FSDP API - PyTorch Blog
DeepSpeed公式ドキュメント
- Zero Redundancy Optimizer Tutorial - DeepSpeed
- ZeRO API Documentation - DeepSpeed 0.18.7
- DeepSpeed Configuration JSON - DeepSpeed
- Training Overview and Features - DeepSpeed
NVIDIA公式ドキュメント
- NVIDIA Collective Communications Library (NCCL)
- NCCL Documentation 2.29.1
- Overview of NCCL
- NVLink & NVSwitch - NVIDIA
- nvidia-smi Manual
HuggingFace公式ドキュメント
- Accelerate Documentation - HuggingFace
- Accelerate Quick Tour
- Accelerate GitHub Repository
- From PyTorch DDP to Accelerate to Trainer - HuggingFace Blog
クイズ
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に分散します。