3783 文字
19 分
【初心者向け】NCCLって何?マルチGPUで通信が遅いときに最初に見るところ──L4×4で環境変数を24設定試した話
この記事について

Claude(Anthropic)との共同編集により作成されました。

要約
  • NCCL(ニッケル)は NVIDIA GPU どうしの通信を担うライブラリで、PyTorch の DDP も vLLM のテンソル並列も下で使っている
  • GPU を増やすと、1 枚のときには無かった「GPU 間で計算結果を合わせる時間」が発生する。L4×4 の推論では、1 歩の約 1/3 がこの通信だった
  • NCCL は GPU のつながり方とデータの大きさを見て、経路や通信方式を自動で選ぶ
  • L4×4 で環境変数を 24 設定試したが、目標の −15% には届かず、最良でも 2〜3% だった。GPU 間の直接通信(P2P)に切り替えると 3.9〜7.6 倍遅くなった
  • 自分の結論は、既定を信じて、環境変数をいじる前に nvidia-smi topo -m と NCCL_DEBUG=INFO で経路を確かめ、通信の回数と量を減らす側を考えること

きっかけ#

Kaggle の ARC Prize 2026(ARC-AGI-2)で、NVIDIA L4 を 4 枚積んだノートブックを使って LLM の推論を回していた1。生成できるトークン数がそのままスコアに効くコンペなので、少しでも速くしたい。そこで、トークンを生成する前向き計算 1 回(以下、1 歩)の内訳を測ると、約 1/3 が GPU の計算ではなく GPU 間の通信だった。

ここで初めて NCCL をちゃんと調べた。複数 GPU で学習している人たちにとっては当たり前の話だと思う。ただ、1 枚の GPU でやってきた身には、ログに出てくる via SHM も Algo RING も意味が分からなかった。

この記事は当時の自分のような、これから複数 GPU を触る人向けの解説である。後半の実測は学習ではなく推論のものなので、そこは割り引いて読んでほしい。

NCCL は GPU どうしの通信係#

NCCL は NVIDIA Collective Communications Library の略で、「ニッケル」と読む2。名前のとおり、複数の GPU の間でデータをやり取りするための NVIDIA 製ライブラリである。

普段はこれを直接呼ばない。学習フレームワークや推論エンジンの下で動いている。

学習フレームワークや推論エンジンの下でNCCLが動き、NVLink・PCIe・InfiniBandなどの線を使い分ける図

PyTorch を使っているなら、CUDA 版を入れた時点で NCCL も使える状態になっていることが多い。Linux の PyTorch は NCCL を含んだ形でビルドされているからである3。PyTorch の指針も分かりやすく、CUDA の GPU で分散学習するなら NCCL、CPU なら Gloo を使う3。

手元の版は次のように確かめられる。

Terminal window
python -c "import torch; print(torch.cuda.nccl.version())"

なぜ GPU 間の通信が必要なのか#

GPU を 4 枚に増やしても、それぞれが勝手に計算するだけでは 1 つのモデルを動かせない。途中で結果を合わせる必要がある。学習と推論で、合わせるものが違う。

学習: 勾配を平均する#

PyTorch の DDP(DistributedDataParallel)では、GPU ごとに別のデータで勾配を計算し、それを全 GPU で平均してから重みを更新する。これで全 GPU の重みが同じ状態に保たれる4。

ここで使うのが AllReduce である。全員の値を集計して、結果を全員に配る。足し算の例なら次のようになる。

実行前: GPU0=1 GPU1=2 GPU2=3 GPU3=4
実行後: GPU0=10 GPU1=10 GPU2=10 GPU3=10

コードで NCCL が出てくるのは、ほぼ次の 1 行だけである。

import os
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
dist.init_process_group(backend="nccl") # ここで NCCL を指定する
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
model = MyModel().cuda(local_rank)
model = DDP(model, device_ids=[local_rank]) # 開始時に rank 0 の重みを全員へ配る(Broadcast)
# 学習ループの中
loss.backward() # 勾配を全員で平均する(AllReduce)
Terminal window
torchrun --nproc_per_node=4 train.py

推論: 分割した計算結果を足し合わせる#

大きな LLM は 1 枚の GPU に載らないので、重みを分割して複数の GPU に載せる。これがテンソル並列(TP)で、4 枚に分けるなら TP4 と呼ぶ。

各 GPU は層の計算の一部しか持っていないので、層の途中で全員の結果を足し合わせる必要がある。ここでも AllReduce を使う。

自分が動かしていたモデル(Qwen3.8-27B)は 64 層で、1 層につき 2 回 AllReduce が走る。1 歩ごとに 100 回以上の通信が発生するので、1 回が短くても合計は無視できなくなる。

NCCL ができること#

こうした「全員が参加する通信」を集団通信(Collective)と呼ぶ。代表的なものは次のとおりで、ほかに Reduce、ReduceScatter、AlltoAll、Gather、Scatter と、1 対 1 の Send / Recv がある2。

名前やること出てくる場面の例
AllReduce全員の値を集計し、結果を全員に配るDDP の勾配、TP の層の出力
Broadcast1 人の値を全員に配るDDP の開始時の重み
AllGather全員の値をつなげて、全員に配るTP の最後の logits(語彙ごとのスコア)

まずは AllReduce だけ覚えておけば、ログや資料はだいぶ読める。

どの経路で通信するか#

通信の速さを大きく左右するのは、GPU どうしのつながり方である。まず次のコマンドで確認できる。

Terminal window
nvidia-smi topo -m

L4×4 の Kaggle ノートブックでは、次のように出た。

GPU0 GPU1 GPU2 GPU3
GPU0 X PHB PHB PHB
GPU1 PHB X PHB PHB
GPU2 PHB PHB X PHB
GPU3 PHB PHB PHB X
PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
NV# = Connection traversing a bonded set of # NVLinks

PHB は、CPU 側のブリッジを越える PCIe 接続という意味である。ここが NV# になっていれば、GPU 専用の高速な線である NVLink でつながっている。手元の L4×4 に NVLink は無かった。

同じマシンの中では、NCCL は主に次の経路(トランスポート)を使い分ける。

SHMはCPU側のメモリを経由し、P2PはPCIeまたはNVLinkでGPUどうしが直接やり取りすることを示す図

  • SHM: CPU 側のメモリを受け渡し場所にする
  • P2P: GPU どうしが直接やり取りする。NVLink があればそれを使う

別のマシンへは、InfiniBand や RoCE、普通の IP ソケットを使う2。

手元の L4 で測ると、GPU の中のメモリのコピーは 231 GB/s、PCIe を越えて別の GPU へ送るコピーは約 10 GB/s だった。GPU の外に出た瞬間に、20 分の 1 ほどの細さになる。通信が重くなるのはこのためである。

通信のやり方も NCCL が選ぶ#

経路のほかに、NCCL はアルゴリズムとプロトコルも選んでいる。NCCL はコストモデルで所要時間を見積もり、大きさごとに組み合わせを決める5。

  • アルゴリズム: データを回す順番のこと。輪になって順に渡す Ring はピーク帯域に強く、木の形で渡す Tree は中くらいのサイズに強い
  • プロトコル: データの送り方のこと。Simple は帯域向き、LL は遅延向きで、LL128 はその中間である

自動で選ばれている様子は、ログで実際に見える。次は L4×4 での実物で、同じ AllReduce でも大きさでプロトコルが変わっている。

NCCL INFO AllReduce: 4 Bytes -> Algo RING proto LL channel{Lo..Hi}={0..0}
NCCL INFO AllReduce: 8192 Bytes -> Algo RING proto LL channel{Lo..Hi}={0..0}
NCCL INFO AllReduce: 215040 Bytes -> Algo RING proto SIMPLE channel{Lo..Hi}={0..1}

L4×4 で環境変数をいじってみた#

ここからは手元の実験記録である。先に書いたとおり、学習ではなく推論での値になる。

  • 環境: NVIDIA L4(24 GB)× 4、PCIe、vLLM 0.32 の TP4、NCCL 2.29.7

profiler で 1 歩の内訳を見ると、AllReduce が 27%、AllGather が 7% だった。AllReduce は 1 歩で 135 回、1 回の中央値は 256 µs で、1 回あたり 600 KB 前後を送っている。

通信が 1/3 なら設定で詰められるのではないかと思い、NCCL の環境変数を 24 設定試した。目標は通信時間の −15% である。

環境変数変えたもの1 歩の AllReduce(既定を 1)
なし(既定)Ring・Simple・SHM・2 channel1.00(33.3 ms)
NCCL_NTHREADS=512スレッド数0.97〜0.98(少ない本数では 1.03)
NCCL_BUFFSIZE=262144バッファを 256 KB に1.00(少ない本数では 0.95)
NCCL_MIN_NCHANNELS=8channel 数1.16
NCCL_PROTO=LL128プロトコル1.56
NCCL_PROTO=LLプロトコル1.58
NCCL_ALGO=Treeアルゴリズム1.84(AllGather はエラー)
NCCL_P2P_LEVEL=SYS経路を P2P に3.9〜7.6(別のベンチでの値)

NCCL が選んだ既定の組み合わせを、大きく超えるものは無かった。分かったことは 3 つある。

1 つ目は、設定で縮む余地が小さかったこと。最良でも 2〜3% で、しかも本数が少ないと逆に遅くなる。バッファを小さくする設定は、公式が「一部の PCIe 構成で効くことがある」と書いているとおり、少ない本数では 5% ほど縮んだ6。ただ、同時に流す本数が多い 1 歩ではほぼ差が無く、サーバーに負荷をかけて 1 MiB にしたときも +0.8% だった。

2 つ目は、直接通信のほうが遅かったこと。ホストメモリを経由する SHM より、GPU どうしが直接やり取りする P2P のほうが速そうに見える。実際は TP4 で 3.9〜7.6 倍遅くなった。同じ機の TP2 では P2P のほうがわずかに速かった(0.86 倍)ので、TP4 に固有の崩れのようだが、原因は特定できていない。少なくともこの構成では、NCCL が既定で SHM を選んだのは正解だった。

3 つ目は、効いたのは通信の量を減らす手だったこと。vLLM の use_local_argmax_reduction は、下書きトークンを選ぶときに全 GPU の logits を集めず、各 GPU の中の候補だけを集める設定である。答えの分布を変えずに 1 歩が約 2.6% 速くなり、提出の候補まで進めた。ただし、回数を減らせば何でもよいわけではない。1 サーバーの GPU を 2 枚にして TP2 × 2 組にすると、通信は減るのに生成は 0.38 倍になった。1 サーバーの KV キャッシュが足りなくなったためで、この話は vLLM と SGLang の記事 に書いた。

数字の範囲も書いておく。L4×4 の数回のセッションでの結果で、各設定は基本的に 1 回の測定である。24 設定には、既定と処理が変わらなかったものや、読み込まれたか確かめられなかったものも含む。NVLink のある機や別の版では結果が変わりうる。公式ドキュメントは 2026-11-06 時点で 2.32.3 まで進んでいる。

速くしたいときの順番#

以上を踏まえて、初めて複数 GPU を触る人に勧める順番をまとめる。

1. つながり方と経路を確かめる#

nvidia-smi topo -m でつながり方を見たら、NCCL のログで実際の経路を確かめる。

Terminal window
NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=INIT,GRAPH torchrun --nproc_per_node=4 train.py

次の行が、実際に使っている経路である。

NCCL INFO NCCL version 2.29.7+cuda13.2
NCCL INFO Channel 00 : 0[0] -> 1[1] via SHM/direct/direct

via SHM なら CPU 側のメモリ経由、via P2P なら直接である。NVLink があるのに SHM になっているなら、何かがおかしい可能性があるので、チューニングの前に調べる価値がある。

NCCL_DEBUG_SUBSYS に TUNING を足すと、前の節のように 1 回ごとの選択も見られる。ただ、通信のたびに行が出るので、ログを出したまま速さを測らないようにしたい。

2. 通信が何割かを測る#

PyTorch Profiler などで、1 歩や 1 イテレーションの内訳を見る。通信の割合が小さいなら、NCCL の話をしても速くならない。

帯域そのものが疑わしいときは、公式が案内している nvbandwidth で、ハードウェアが期待どおりの速さを出しているかを先に確認する6。一部の L4×4 は GPU 間の通信が遅い、という報告も Kaggle の discussion で見かけた。

3. 通信の回数と量を減らす#

通信の割合が大きいなら、環境変数より先に、通信の回数と 1 回の量を減らす手を探す。使っているエンジンの設定や構成の見直しがこれにあたる。

4. 環境変数は実験に留める#

環境変数を試すこと自体は悪くない。ただ、公式はデバッグ用の変数について「実験や問題の切り分けには使ってよいが、本番のコードには基本的に設定しない」としている7。

  • NCCL_PROTO は、バグが疑われるときの切り分けを除いて設定を勧めていない。LL128 は未対応の環境で有効にするとデータが壊れることがある7
  • NVIDIA のブログも、環境変数を固定して引き継ぐと NCCL の既定に二度と戻らなくなると注意している5

手元でも、NCCL_SHM_USE_CUDA_MEMCPY=1 は 90 秒待っても終わらなかった。試すなら 1 つずつ測り、採用しなかったものはスクリプトに残さないようにしたい。

つまずきやすいところ#

速い遅い以前に動かない場合は、次のあたりを見る。

  • Docker で初期化に失敗する: SHM は /dev/shm を使うので、コンテナの既定のサイズだと足りないことがある。NCCL_DEBUG=WARN で failed to extend /dev/shm/nccl-... と出たら、docker run に --shm-size=1g --ulimit memlock=-1 を足す8
  • 複数ノードで無言のまま止まる: 使うネットワークの指定が実際の配線とずれていることがある。DGX Spark 2 台をつないだときに踏んだ話は こちらの記事 に書いた

自分の結論#

NCCL の既定はかなり賢い。少なくとも PCIe の L4×4 では、環境変数を 24 設定試しても、既定を大きく超えるものは無かった。

だから、速くしたいときは環境変数をいじる前に nvidia-smi topo -m と NCCL_DEBUG=INFO で経路を確かめ、通信の回数と量を減らす側を考えること。普段は意識しなくていいが、通信が律速になったときに「何を見ればいいか」を知っているかどうかで、回り道の長さが変わると思う。

参考文献#

  1. ARC Prize https://arcprize.org/
  2. Overview of NCCL — NCCL documentation https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/overview.html
  3. Distributed communication package - torch.distributed — PyTorch documentation https://docs.pytorch.org/docs/stable/distributed.html
  4. Distributed Data Parallel — PyTorch documentation https://docs.pytorch.org/docs/stable/notes/ddp.html
  5. Understanding NCCL Tuning to Accelerate GPU-to-GPU Communication — NVIDIA Technical Blog https://developer.nvidia.com/blog/understanding-nccl-tuning-to-accelerate-gpu-to-gpu-communication/
  6. Performance and tuning — NCCL documentation https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting/performance_and_tuning.html
  7. Environment Variables — NCCL documentation https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html
  8. Runtime and MPI issues — NCCL documentation https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting/runtime_and_mpi_issues.html
【初心者向け】NCCLって何?マルチGPUで通信が遅いときに最初に見るところ──L4×4で環境変数を24設定試した話
https://yurudeep.com/posts/deeplearning/2026/20261106/
作者
ひらノルム
公開日
2026-11-06
ライセンス
CC BY-NC-SA 4.0