GPUを2枚に増やした。学習スクリプトもDDPに書き換えた。なのに、速度は1.3倍にしかならない。2倍を期待していたのに——そう頭を抱えた経験が一度はあるだろう。
原因の大半は計算ではなく、GPU間の通信経路にある。NVLinkが効いていない、PCIe経由のP2Pが落とし穴にはまっている、あるいはコンシューマGPUの制限に引っかかっている。どれも、見た目には分からない。
この記事では、その経路を次の観点から診断していく。
- なぜ枚数ぶん速くならないのか、仕組みを理解する
- 自分の環境の通信経路を
nvidia-smiとnccl-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つの観点で進める。
- NCCLが使う集合通信プリミティブ(DDPとFSDPで何が違うか)
- Ring AllReduceの通信量がなぜ理論限界に近づくのか
- NVLinkとPCIeの帯域差、そして
nvidia-smi topo -mの読み方 - GPUDirect P2Pが効かない3つの落とし穴
nccl-testsによる定量診断(algbwとbusbw)- 環境変数と診断フロー
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 に固定
NVLinkとPCIe:通信経路の帯域差を理解する
アルゴリズムの話はここまで。次は物理層、つまりデータが実際に流れる配線の話だ。同じAllReduceでも、流れる経路がNVLinkかPCIeかで速度は桁違いになる。
NVLink vs 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 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を妨げている可能性がある。
コンシューマ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のような直結転送は期待できず、枚数を増やしても通信で頭打ちになる。
診断手順は前述の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での例)。
# 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
注目する列は algbw と busbw の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-tests の all_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が本当に直結しているのか、そこから確かめてほしい。


コメント