マルチGPU NCCLが遅い原因をNVLink・P2P・nccl-testsで突き止める

GPUを2枚に増やした。学習スクリプトもDDPに書き換えた。なのに、速度は1.3倍にしかならない。2倍を期待していたのに——そう頭を抱えた経験が一度はあるだろう。

原因の大半は計算ではなく、GPU間の通信経路にある。NVLinkが効いていない、PCIe経由のP2Pが落とし穴にはまっている、あるいはコンシューマGPUの制限に引っかかっている。どれも、見た目には分からない。

この記事では、その経路を次の観点から診断していく。

  • なぜ枚数ぶん速くならないのか、仕組みを理解する
  • 自分の環境の通信経路を nvidia-sminccl-tests で診断する
  • ボトルネックを特定して、打ち手を判断する

前提として、単一GPUでの学習とPyTorch DDPを一度は触ったことがあるレベルを想定している。NCCLそのものの知識は要らない。

なぜマルチGPUは「枚数ぶん」速くならないのか

学習の1ステップを分解すると、大きく2つの処理に分かれる。

  • 計算: forwardとbackward。各GPUが自分の担当データでパラメータの勾配を求める
  • 通信: GPU間の勾配同期。全GPUの勾配を揃えてから、同じ方向にパラメータを更新する

GPUを増やすと、計算は分散されて減る。データ並列なら、バッチを4分割すれば1枚あたりの計算は約4分の1だ。ここまでは直感どおり。

問題は通信のほうだ。GPUが増えるほど、同期すべき相手が増える。勾配のサイズはモデルパラメータ数で決まるため、GPUを4枚にしても各ステップで交換するデータ量は減らない。参加者が増えたぶん、調整の手間は増える。

この構図はアムダールの法則そのものだ。並列化できる計算部分をいくら速くしても、並列化できない通信部分が全体を律速する。計算が速くなっても通信が追いつかなければ、そこで頭打ちになる。

この通信を担うのが NCCL(NVIDIA Collective Communications Library、「ニックル」と読む)だ。GPU間で「全員の勾配を足し合わせて平均する」といった集合通信を、ハードウェアの経路に合わせて最適化して実行する。マルチGPU NCCLの診断では、この経路の実態を把握することが出発点になる。

診断は次の6つの観点で進める。

  1. NCCLが使う集合通信プリミティブ(DDPとFSDPで何が違うか)
  2. Ring AllReduceの通信量がなぜ理論限界に近づくのか
  3. NVLinkとPCIeの帯域差、そして nvidia-smi topo -m の読み方
  4. GPUDirect P2Pが効かない3つの落とし穴
  5. nccl-tests による定量診断(algbwとbusbw)
  6. 環境変数と診断フロー

NCCLと集合通信プリミティブの基礎(DDP vs FSDP)

NCCLが提供する操作は「集合通信プリミティブ」と呼ばれる。AllReduce、ReduceScatter、AllGatherなどがそれだ。DDPとFSDPはこのプリミティブの使い方が根本的に違う。ここを押さえると、後の診断が格段に読みやすくなる。

DDPはAllReduceで勾配を同期する

DDP(Distributed Data Parallel)は、全GPUが同じモデルの完全なコピーを持つ。各GPUは別々のデータでforwardとbackwardを実行し、それぞれ勾配を計算する。

各GPUの勾配はバラバラの値だ。同じパラメータ更新をするには、全GPUの勾配を平均して揃える必要がある。ここで使うのが AllReduce だ。「全員の値を集約し、その結果を全員が受け取る」操作で、勾配を足し合わせてGPU数で割れば平均が出る。完了後、全GPUが同一の勾配を持つため、モデルのコピーは常に一致したまま進む。

FSDPはReduceScatter+AllGatherに分かれる

FSDP(Fully Sharded Data Parallel)は、メモリ節約のためにアプローチを変える。パラメータ・勾配・オプティマイザ状態をGPU間でシャーディング(分割保持)し、各GPUは全体の一部分だけを持つ。

このため通信が2種類に分かれる。

forwardの前には、計算に必要な完全なパラメータを一時的に集める。各GPUが自分のシャードを出し合い、全員が完全なパラメータを得る——これが AllGather だ。計算が終われば、また各自のシャードだけを残してメモリを解放する。

backwardの後には、勾配を集約する。ただしDDPのように全勾配を全員に配るのではなく、各GPUが「自分が担当するシャードぶんの集約結果」だけ受け取れば足りる——これが ReduceScatter だ。集約(Reduce)と分配(Scatter)を同時に行う。

実は AllReduce は、内部的にはReduceScatterとAllGatherの2段で構成されている。FSDPはこの2段を別々のタイミングに分けて使っている、と捉えると整理しやすい。

手法 通信プリミティブ 各GPUが保持するデータ 通信タイミング
DDP AllReduce 全パラメータのコピー backward後(勾配)
FSDP ReduceScatter + AllGather パラメータのシャード backward後 + forward前

注目すべきはFSDPが通信回数を増やすという点だ。メモリは節約できるが、forward前のAllGatherが余分に走る。FSDPで思ったほど速くないとき、この追加通信が経路の遅さで増幅されている可能性がある。通信経路の診断が効いてくるのは、まさにこういう場面だ。

Ring AllReduceの仕組みと通信量 2(N-1)/N の根拠

AllReduceの実装にはいくつかの方式があるが、NCCLの主力は Ring AllReduce だ。なぜこれが帯域効率に優れるのか、通信量の式から理解しておく。診断時に「この帯域なら正常か」を判断するための基礎になる。

Ring AllReduceの2フェーズ

N個のGPUをリング状(円環状)に並べる。各GPUは隣のGPUにだけデータを送る。GPU0 → GPU1 → GPU2 → GPU3 → GPU0、という具合だ。

AllReduceは2つのフェーズで進む。

Reduce-Scatterフェーズ(N-1ステップ)

まず、各GPUが持つテンソル(総サイズK)をN個のチャンクに分割する。N=4なら4分割だ。各ステップで、各GPUは1つのチャンクを隣へ送り、届いたチャンクを自分の同じ位置に足し込む(Reduce)。これをN-1回繰り返すと、各GPUは「あるチャンク1個ぶんの、全GPU分の合計」を持つ状態になる。

N=4 のリング。各GPUのテンソルを a/b/c/d の4チャンクに分割。
Reduce-Scatter 完了後、各GPUは1チャンクぶんの全GPU合計を持つ:

GPU0: [ a の全合計 ]  b        c        d
GPU1:  a       [ b の全合計 ]  c        d
GPU2:  a        b       [ c の全合計 ]  d
GPU3:  a        b        c       [ d の全合計 ]

AllGatherフェーズ(N-1ステップ)

次に、集約済みのチャンクをリング上で順に伝播させる。各ステップで隣へ送り、受け取ったものをそのまま保持する(足し算ではなく上書き)。これもN-1回で完了し、全GPUが完全なAllReduce結果を持つ。

AllGather 完了後、全GPUが完全な結果を持つ:

GPU0: [全合計 a][全合計 b][全合計 c][全合計 d]
GPU1: [全合計 a][全合計 b][全合計 c][全合計 d]
GPU2: [全合計 a][全合計 b][全合計 c][全合計 d]
GPU3: [全合計 a][全合計 b][全合計 c][全合計 d]

2(N-1)/N の導出

各GPUが送受信するデータ量を数える。チャンク1個のサイズはK/Nだ。

Reduce-ScatterフェーズではN-1ステップ、毎回K/Nを送る。送信量は (N-1) × (K/N)。AllGatherフェーズも同じ。2フェーズを合計すると:

2 × (N-1) × (K/N) = 2(N-1)/N × K

この係数 2(N-1)/N がRing AllReduceの特徴を表す数字だ。

GPU数(N) 2(N-1)/N 理論の最大値(2倍)に対する比
2 1.00 50%
4 1.50 75%
8 1.75 87.5%
16 1.875 93.75%
→ 2.00 100%

Nが増えるほど係数は2に近づく。つまり転送するデータ量がGPU数にほぼ依存しなくなる。リング上の全リンクが常にフル稼働するため、ハードウェアの帯域を無駄なく使い切る。Ring AllReduceが「帯域幅最適(bandwidth-optimal)」と呼ばれるのは、この性質を指している。

Tree AllReduceとの使い分け

Ringには弱点もある。リングを1周する都合上、レイテンシがO(N)で増える。GPUが何百個と並ぶ大規模クラスタで小さなメッセージを送るときには、この遅延が無視できなくなる。

NCCLは2.4(2019年)で Double Binary Tree AllReduce を導入した。2本の相補的な二分木を組み合わせてレイテンシをO(log N)に抑える方式で、ランク数が多くてメッセージが小さい場面で有利だ。数百MiB以上の大きなメッセージではRingのほうが帯域効率が良く、両者は得意領域が違う。

NCCLはメッセージサイズとランク数からコストモデルで判断し、RingとTreeを自動選択する。手動で固定したい場合は環境変数で指定できる。

NCCL_ALGO=Ring   # Ring に固定
NCCL_ALGO=Tree   # Tree に固定
※ RingとTreeの自動選択の閾値は、NCCLバージョン・ランク数・メッセージサイズに依存して動的に決定される。固定の境界値は公式に明記されていない(NCCL 2.30.7時点)。「何バイトを超えるとRing」といった数値は環境ごとに変わるため、鵜呑みにしないこと。

NVLinkとPCIe:通信経路の帯域差を理解する

アルゴリズムの話はここまで。次は物理層、つまりデータが実際に流れる配線の話だ。同じAllReduceでも、流れる経路がNVLinkかPCIeかで速度は桁違いになる。

インターコネクト 双方向合計帯域 備考
NVLink 3.0(A100) 600 GB/s
NVLink 4.0(H100) 900 GB/s NVSwitch 3基搭載時は 3.6 TB/s
PCIe Gen4 x16 64 GB/s
PCIe Gen5 x16 128 GB/s
※ いずれも双方向合計値。実効帯域はプロトコルオーバーヘッドなどにより、これを下回る場合がある。

数字を並べると差は一目瞭然だ。H100のNVLink 4.0は900 GB/s、対してPCIe Gen5は128 GB/s。約7倍の開きがある。

マルチGPUをPCIe接続だけで組んだ場合、GPU間通信はこの細いPCIeを通る。計算がいくら速いH100でも、勾配同期が64〜128 GB/sで律速されれば、枚数ぶんのスケールは出ない。冒頭の「2枚で1.3倍」は、まさにこのPCIeボトルネックで起きる典型例だ。

nvidia-smi topo -m でトポロジを読む

自分の環境のGPUがどう繋がっているかは、次のコマンドで確認できる。

nvidia-smi topo -m

出力はGPU間の接続マトリクスだ。簡略化した例を示す。

GPU0 GPU1 GPU2 GPU3 CPU Affinity
GPU0 X NV4 NV4 SYS 0-23
GPU1 NV4 X SYS NV4 0-23
GPU2 NV4 SYS X NV4 24-47
GPU3 SYS NV4 NV4 X 24-47

各セルの記号が、そのGPU同士の接続経路を表す。

記号 意味
NV# NVLinkで接続(数字はリンク本数)
PIX PCIeブリッジを最大1つ経由
PXB ホストブリッジを経由せず、複数のPCIeブリッジを経由
PHB PCIeホストブリッジ(CPU)を経由
NODE 同一NUMAノード内のインターコネクトを経由
SYS PCIeおよびNUMAノード間インターコネクト(QPI/UPI等)を横断

NV# が出ていればNVLink直結で、GPUDirect P2Pによる高速転送が期待できる。上の例ならGPU0-GPU1はNV4で繋がっているので速い。

逆に SYS は最も遅い経路だ。PCIeを通り、さらにCPUソケット間のリンク(NUMA跨ぎ)まで越える。上の例ではGPU0-GPU3がSYSなので、この2枚の間で大量に通信するとボトルネックになる。

「NVLinkで繋がっていると思っていたらSYSだった」と判明することは珍しくない。診断の最初の一手として、まずこのコマンドを叩く価値がある。

GPUDirect P2Pが効かない3つの落とし穴

NVLinkやPCIeで物理的に繋がっていても、GPUDirect P2P(GPU同士がCPUを介さず直接メモリをやり取りする機能)が有効でなければ速度は出ない。ここが最もハマりやすい。

P2Pが有効になる条件

GPUDirect P2Pが働くには、いくつかの条件が揃う必要がある。

  • NVLinkで直結されている、または同一のPCIeルートコンプレックス配下にある
  • ACS(Access Control Services)が無効化されている
  • IOMMUが無効、またはパススルーモードになっている

どれか1つでも崩れると、P2Pは無効になる。そしてNCCLは黙ってフォールバック経路に切り替えるため、エラーは出ない。速度だけが落ちる——これが厄介な点だ。

ACSがP2Pを壊す仕組みとNCCLログでの確認手順

ACSはPCIeのアクセス制御機能で、デバイス間のトランザクションを監視・制御する。仮想化のセキュリティ用途で有効になっていることが多い。

ACSが有効だと、GPU間のP2Pトランザクションが一度PCIeルートコンプレックス(CPU側)を経由するよう強制される。GPU同士が物理的に近くにあっても、わざわざCPUまで往復させられる。直接転送の恩恵は消える。

P2Pが無効になると、NCCLは SHM(共有ホストメモリ)経由にフォールバックする。GPU → ホストメモリ → GPUという経路だ。PCIeを2回通るうえメモリコピーが挟まるため、明確に遅くなる。

実際にどの経路が使われているかはNCCLのログで確認できる。

NCCL_DEBUG=INFO python your_train_script.py 2>&1 | grep -E "NCCL|P2P|Ring|Tree|SHM"

ログに p2p が出ていればP2Pが有効だ。SHM が出ていればフォールバック中で、本来の速度が出ていない。ここが診断の核心になる。

ACSの状態はLinux側からも確認できる。

# ACSが有効なデバイスを探す(root権限またはsudoが必要)
sudo lspci -vvv | grep -i "ACSCtl"

出力の ACSCtl 行で、各制御ビットが +(有効)か -(無効)かを読む。有効になっているものが多ければ、P2Pを妨げている可能性がある。

※ ACSの無効化はBIOSおよびカーネル設定に依存する。サーバの構成やセキュリティ要件によっては無効化が許されない場合もある。本番環境へ適用する前に、必ず検証環境で挙動と影響を確認すること。

コンシューマGPU(RTX系)のP2P非対応問題

3つめの落とし穴は、GPUそのものの制限だ。

RTX 30系(Ampere)以降、NVIDIAはGeForce系GPUに対しドライバレベルでP2Pをブロックしている。製品ラインの差別化が理由とされる。GeForce系でマルチGPUを組んでも、GPU間通信はSHM経由になりやすい。RTX 20系(Turing)が、P2Pに対応した最後のコンシューマ世代とされる。

つまりRTX 4090を2枚並べても、勾配同期はSHMを通る。データセンター向けGPUのような直結転送は期待できず、枚数を増やしても通信で頭打ちになる。

※ コンシューマGPUのP2P対応可否は、GPU世代・ドライバ・プラットフォームに依存する。世代の境界も将来変わりうる。自分の環境では必ず NCCL_DEBUG=INFO のログでSHMフォールバックの有無を確認すること。「RTXだから絶対に出ない」と決めつけず、実測で判断する。

診断手順は前述のNCCLログ確認と同じだ。NCCL_DEBUG=INFO を付けて実行し、SHM が出ているかを見る。出ていれば、それがそのGPUの上限であり、ソフトウェア設定では解消できないと判断する。

nccl-testsで定量診断する(algbw vs busbw)

経路が分かったら、次は数字で性能を測る。NVIDIA公式の nccl-tests を使うと、AllReduceなどの実効帯域を計測できる。「遅い気がする」を「○○ GB/sしか出ていない」に変えるステップだ。

nccl-testsのビルドと実行

# ビルド(NCCLとCUDAのパスは環境に合わせること)
git clone https://github.com/NVIDIA/nccl-tests.git
cd nccl-tests
make MPI=0 CUDA_HOME=/usr/local/cuda NCCL_HOME=/usr/local/nccl

# 実行例(GPU 4枚でAllReduce, 8B〜256MBの範囲)
./build/all_reduce_perf -b 8 -e 256M -f 2 -g 4

主要なオプション:

  • -b: 計測する最小メッセージサイズ(ここでは8バイト)
  • -e: 最大メッセージサイズ(256MB)
  • -f: サイズの刻み倍率(2なら2倍ずつ増やす)
  • -g: 使用するGPU数

小さいサイズはレイテンシ、大きいサイズは帯域の傾向を見るのに使う。ボトルネック診断では、大きいサイズ(数百MB)の値が参考になる。

algbwとbusbwの違い・補正係数の導出

実行すると、サイズごとに次のような行が並ぶ(-g 4での例)。

# out-of-place in-place
# size count type redop root time algbw busbw error time algbw busbw error
268435456 67108864 float sum -1 5.43 49.43 74.14 N/A 5.41 49.62 74.43 N/A

注目する列は algbwbusbw の2つ。単位はGB/sだ。

algbw(algorithm bandwidth) は「データサイズ ÷ 時間」で出した値だ。直感的だが、欠点がある。ランク数(GPU数)が増えると、同じハードウェアでも値の上限がずれる。AllReduceは内部で 2(N-1)/N 倍のデータを流すため、algbwだけ見ても異なるGPU数同士を公平に比較できない。

busbw(bus bandwidth) はこの差を補正した値だ。AllReduceの場合:

busbw = algbw × 2(N-1)/N

N=4の場合、補正係数は 2×3/4 = 1.5 だ。上の出力例なら 49.43 × 1.5 ≈ 74.14 GB/s となる(上記の出力値と整合する)。ReduceScatterやAllGatherの場合は補正係数が (N-1)/N になる(フェーズが片方だけのため)。

busbwの利点は、ランク数によらず同じハードウェアなら同じ値に収束することだ。だから busbwはNVLinkやPCIeの理論帯域と直接比較できる。性能評価には必ずbusbwを使う。algbwを見て「GPUを増やしたら帯域が下がった」と誤解しないこと。

busbwをハードウェア理論値と突き合わせる

測ったbusbwを、トポロジで判明したインターコネクトの理論帯域と照らし合わせる。判断の目安:

  • busbwがNVLink理論帯域の 70〜80%以上 → 通常の範囲。経路は正常に効いている
  • busbwがPCIe理論帯域を大きく下回る → P2P不全・SHMフォールバックを疑う。NCCLログを確認する
  • busbwがNVLink理論帯域の 30%以下 → トポロジ・ACS・ドライバを疑う。nvidia-smi topo -m とログを見直す

たとえばNVLink 4.0(900 GB/s)の環境でbusbwが700 GB/s前後なら妥当な値だ。NVLinkで繋がっているはずなのに60 GB/s台しか出ていなければ、PCIe相当の速度しか出ていないことになり、P2Pが効いていない疑いが濃い。数字とハードウェアの理論値を並べると、異常がはっきり見える。

主要NCCL環境変数と診断フロー

診断と切り分けに使う環境変数と、ここまでの内容を1本の手順にまとめる。

押さえるべき環境変数

変数名 設定値 効果
NCCL_DEBUG INFO アルゴリズム選択・トポロジ検出のログを出力。診断の第一歩
NCCL_P2P_DISABLE 1 NVLink/PCIe経由のP2Pを無効化 → SHMフォールバックさせる(テスト用)
NCCL_ALGO Ring または Tree アルゴリズムを固定(デフォルトはNCCLが自動選択)
NCCL_IB_DISABLE 1 InfiniBand/RoCEを無効化 → IPソケットにフォールバック
NCCL_SOCKET_IFNAME eth0 等 マルチノード時に使用するIPインターフェースを指定

NCCL_P2P_DISABLE=1 は切り分けに使える。これを付けた状態と付けない状態でbusbwを測り、差が小さければそもそもP2Pが効いていなかった、と分かる。差が大きければP2Pは効いている証拠だ。

診断フローチャート

1. トポロジを確認する

nvidia-smi topo -m を実行し、GPU間の接続を把握する。NV#(NVLink)で繋がっているか、SYS(NUMA跨ぎ)かを最初に確認する。

2. NCCLログで経路を確認する

NCCL_DEBUG=INFO を付けて学習スクリプトを実行し、ログを見る。P2P / SHM / Ring / Treeのどれが使われているかを読む。SHM が出ていればフォールバック中だ。

3. busbwを実測する

nccl-testsall_reduce_perf を走らせ、大きいメッセージサイズでのbusbwを計測する。

4. 理論帯域と比較する

実測busbwを、トポロジで判明したインターコネクト(NVLink / PCIe)の理論帯域と突き合わせる。正常範囲か、明らかに低いかを判断する。

5. P2P不全を切り分ける

帯域が低い場合は、ACSの状態(sudo lspci -vvv | grep ACSCtl)、BIOS設定、コンシューマGPUのP2P制限を順に確認する。NCCL_P2P_DISABLE の有無で差が出るかも見る。

まとめ

マルチGPU NCCLのスケールは、計算の速さよりも通信経路で決まる。2枚で1.3倍しか出ないとき、足を引っ張っているのはGPUの演算性能ではなく、勾配同期が流れる配線とその設定であることが多い。

診断は5ステップで進められる。

  • nvidia-smi topo -m でトポロジを確認する(NV# か SYS か)
  • NCCL_DEBUG=INFO でログを取り、P2P / SHM / Ring / Treeを読む
  • nccl-tests でbusbwを実測する
  • busbwをNVLink・PCIeの理論帯域と比較する
  • 低ければACS・BIOS・コンシューマGPU制限を確認する

通信経路の正常性が確認できたら、次はシャーディング設定や通信オーバーラップの調整に進める段階だ。FSDPのチューニング詳細は別記事で扱う([内部リンク: FSDPの実践チューニング(予定)])。複数ノードにまたがるNCCL設定、InfiniBandやRoCEの構成については稿を改めて掘り下げる([内部リンク: マルチノードNCCLのInfiniBand設定(予定)])。

まずは1枚の nvidia-smi topo -m から。自分のGPUが本当に直結しているのか、そこから確かめてほしい。

コメント

タイトルとURLをコピーしました