Claude Media
Claude Code torchrun NCCLのtimeoutを追う — 最初に落ちたランクの見つけ方

Claude Code torchrun NCCLのtimeoutを追う — 最初に落ちたランクの見つけ方

torchrunの分散学習がNCCL timeoutで落ちる、または止まったまま動かないとき、ランクごとのログ分離とNCCL_DEBUG=INFOの読み方、2プロセスの再現コードをClaude Codeに任せる手順をまとめます。

torchrunの分散学習がNCCL timeoutで落ちたとき、スタックトレースに出ているのは「待たされた側」のランクです。止まった原因を作ったランクは、例外を出さずに別の場所で止まっています。Claude Codeに任せるなら、全ランクのログをランク名つきでファイルに分け、NCCL_DEBUG=INFOの出力を一緒に読ませ、最後に2プロセスの再現コードまで作らせる流れが扱いやすい構成です。

この記事は、その流れをtorchrunのログオプション、NCCLの環境変数、claude -pの呼び出し方の順に並べます。

例外を出したランクが原因とは限らない

分散学習の集団通信(all_reduceなど)は、全ランクが同じ順序で呼び出して初めて完了します。1つのランクが呼び出しに到達しなければ、残りのランクは待ち続けます。

torch.distributed.init_process_groupのtimeoutは、NCCLバックエンドの既定が10分です。この時間を過ぎると集団通信は非同期に中断され、プロセスがクラッシュします。CUDAの実行は非同期のため、失敗したNCCL操作の後でユーザーコードを続けると壊れたデータの上で計算が進みかねない、というのが公式の説明です。

つまり、timeoutで落ちるのは待っていたランクです。monitored_barrierの説明にも同じ構造が書かれています。あるランクがバリアに到達しない(たとえばハングしている)と、ほかの全ランクがバリアで失敗します。

くらべる

ログの読み方を分ける

待たされた側

例外を出したランク

timeoutの例外とスタックトレースが残ります。「どの集団通信を待っていたか」までは分かります。

止めた側の候補

例外を出さないランク

別の集団通信で待っている、データ読み込みで止まっている、すでに異常終了している、のいずれかです。ログの最終行と時刻から探します。

したがって調査の目標は「timeoutの例外を読むこと」ではなく、「全ランクのログの最終行を横に並べて、最初に止まったランクを探すこと」になります。

ランクごとのログをファイルに分ける

標準の設定では、全ワーカーの標準出力と標準エラーがコンソールにそのまま混ざります。torchrunのドキュメントには、混ざった出力ではどのランクか区別できない、と書かれています。

--tee 3を付けると、両方のストリームをログファイルに書き出しつつコンソールにも流せます。3は標準出力と標準エラーの両方、1は標準出力のみ、2は標準エラーのみを指します。--redirectsは同じ書式で、コンソールには流さずファイルだけに書きます。ファイルの置き場所は--log-dirで決めます。

行の先頭にホスト名とランクを付けるには、--log-line-prefix-templateを使います。使えるマクロは${role_name}、${local_rank}、${rank}、${hostname}で、複数ノードのジョブで問題のホストを特定するのに役立ちます。

NCCL_DEBUG=INFO NCCL_DEBUG_FILE=logs/nccl.%h.%p.log \
torchrun --nnodes 2 --nproc-per-node 8 \
  --tee 3 --log-dir logs \
  --log-line-prefix-template "${hostname}:${rank}: " \
  train.py

NCCL_DEBUG_FILEのファイル名には、%hでホスト名、%pでプロセスIDを埋め込めます。公式は、ファイル名がジョブ内のプロセス間で一意でないと出力が失われたり壊れたりすると注意しています。全ランクが同じ名前に書くと上書きし合うため、必ず%hと%pを入れます。

もう1つ、Python側に目印を置く方法があります。エントリポイントをtorch.distributed.elastic.multiprocessing.errors.recordで装飾すると、捕捉されなかった例外がトレースつきで、環境変数TORCHELASTIC_ERROR_FILEが指すファイルに書かれます。親プロセスはこの環境変数を各子プロセスに設定し、全ワーカーのエラーファイルを集めたうえで、タイムスタンプが最小のもの、つまり最初のエラーを伝播します。

落とし穴

最初のエラーは「最初に止まった」とは限らない

recordが伝播するのは、例外として記録された最初のエラーです。例外を出さずに止まったランクは、エラーファイルに現れません。エラーファイルは手がかりの1つにとどめ、ログの最終行と突き合わせます。

NCCL_DEBUG=INFOの出力で見る3点

NCCL_DEBUGの値はVERSION、WARN、INFO、TRACEなどで、INFOはデバッグ情報を出します。NCCL 2.32以降にはATTNもあり、WARNに加えて設定の上書きや非同期イベントの通知などを出します。ただし古いNCCLはATTNを認識せず、ログが出なくなります。バージョンが混在する環境ではNCCL_DEBUG=WARN NCCL_DEBUG_LEVELS=ATTNと書くよう、公式が案内しています。

INFOは量が多いので、NCCL_DEBUG_SUBSYSで絞れます。既定はINIT,BOOTSTRAP,ENVで、NET(ネットワーク)、COLL(集団通信)、P2P、GRAPH(トポロジ検出)などを足せます。通信経路の確認が目的ならINIT,NETから始めるのが現実的です。

読むべき点は次の3つです。

  1. 使われた通信経路: InfiniBand/RoCEか、IPソケットか。想定と違うなら、インターフェース指定(NCCL_SOCKET_IFNAME、NCCL_IB_HCA)が効いていない可能性があります
  2. ランクごとの差: 初期化のログがあるランクだけ途中で終わっていないか。全ランクで同じ経路・同じバージョンが選ばれているか
  3. タイムアウト設定: IBのタイムアウト(NCCL_IB_TIMEOUT)が、ネットワークの規模に対して妥当か

切り分けの実験として、NCCL_IB_DISABLE=1を付けるとIB/RoCEの転送が使われず、IPソケットなどの別の転送へフォールバックします。IBに起因する問題かどうかを見分ける手がかりになります。本番設定に入れる前に、切り分けの実験として使います。

3種類のtimeoutを取り違えない

名前に「timeout」が付く設定は層が違います。どの層の話かを最初に決めないと、数値を変えても症状が変わりません。

層設定公式に書かれた挙動
PyTorchの集団通信設定init_process_group(timeout=...)公式に書かれた挙動NCCLの既定は10分。超えると非同期に中断してプロセスがクラッシュ
NCCLのIB設定NCCL_IB_TIMEOUT公式に書かれた挙動4.096µs × 2^値。待機の合計はNCCL_IB_RETRY_CNTとの積
Claude Code設定Bashツールのタイムアウト公式に書かれた挙動Claude Codeから学習を起動したときだけ関係する。コマンドの実行時間に上限がある

NCCL_IB_TIMEOUTの説明では、値を大きくするとネットワーク規模の大きい環境で、ibv_poll_cqのエラー12のような失敗が減ることがあるとされています。値の目安は表で示されており、たとえば17なら536.9 ms、18なら1.1秒です。

3行目のClaude Code側は、学習そのものを起動させるときの話です。トレーニングを走らせっぱなしにするなら、BASH_DEFAULT_TIMEOUT_MSでBashコマンドのタイムアウトを変更する記事の設定か、バックグラウンドが時間制限で止まる原因の確認が先です。調査を始める前に、学習ジョブはClaude Codeの外のターミナルやジョブスケジューラで走らせ、ログだけを読ませる構成にすると、この層を考えなくて済みます。

Claude Codeにログを読ませる

ログがファイルに分かれていれば、claude -p(非対話モード)で読ませられます。非対話モードは標準入力を読めるので、ログをパイプで渡して答えを標準出力に受けられます。

claude -p "logs/配下の各ランクのログを読み、最後に出力した時刻が \
最も早いランクと、その直前のNCCLのログ行を挙げて。原因の推測は \
根拠の行番号を付けて、確実な点と推測を分けて書いて" \
  --allowedTools "Read" --max-turns 8

ポイントは次の3つです。

  • 読み取りだけを許可する: --allowedTools "Read"で、ログの閲覧だけを事前承認します。ログを直すツールは要りません。claude -pでは承認が必要な呼び出しは拒否されるため、許可は明示します
  • ターン数に上限を置く: --max-turnsはprintモード専用の上限で、超えるとエラー終了します。大量のログをあてもなく往復する事故を避けられます
  • 入力が10MBを超えるなら、ファイルパスで渡す: パイプした標準入力は10MBが上限です。超えると非ゼロのステータスで終了します。上限を超える場合は、内容をファイルに書き出して、プロンプトにはパスを書きます

ログが大きすぎて止まる場合の絞り方は、claude -pにログを渡すと10MB上限で止まるにまとめています。

CIのような無人環境で使うなら、--bareを付けると、ホストのフックやプラグイン、CLAUDE.mdを読み込まずに起動できます。ただしbareモードはサブスクリプションのログインを使わず、ANTHROPIC_API_KEYの設定が前提です。

依頼文には、事実と推測を分けて書かせる指示を入れておくと便利です。NCCLのログに書かれた通信経路は事実として読めますが、「このIBスイッチが原因」のような断定は、ログだけでは裏付けにくいためです。

2プロセスの最小構成で再現させる

原因の候補が絞れたら、本番の学習コードから切り離した最小構成で再現させます。PyTorchの分散デバッグの章には、2プロセスでランク間の入力形状を食い違わせる例があります。NCCLバックエンドなら通常はハングする状況です。

Claude Codeには次のような依頼をします。

学習コードの集団通信のうち、止まった箇所の直前の呼び出しだけを抜き出して、2プロセスで動くスクリプトにして。片方のランクの入力だけ形を変えて、TORCH_DISTRIBUTED_DEBUG=DETAILで実行したときのエラーメッセージを確認して

生成されるスクリプトは、公式の例に沿えば次のような形になります。

import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
 
def worker(rank):
    dist.init_process_group("nccl", rank=rank, world_size=2)
    torch.cuda.set_device(rank)
    # rank 0 と rank 1 で入力の長さを変え、不一致を意図的に作る
    tensor = torch.randn(10 if rank == 0 else 20).cuda()
    dist.all_reduce(tensor)
    torch.cuda.synchronize(device=rank)
 
if __name__ == "__main__":
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "29501"
    os.environ["TORCH_CPP_LOG_LEVEL"] = "INFO"
    os.environ["TORCH_DISTRIBUTED_DEBUG"] = "DETAIL"
    mp.spawn(worker, nprocs=2, args=())

TORCH_DISTRIBUTED_DEBUG=DETAILにすると、集団通信の呼び出しごとに整合性の確認が走ります。まずmonitored_barrierで全ランクが未完了の集団通信を終えたことを確かめ、止まっているランクを報告します。続いて、集団通信の種類とテンソルの形がランク間で一致するかを調べます。不一致なら、ハングの代わりに、形の食い違いを示すエラー報告つきでクラッシュします。この例を実行すると、ランク0のログにError when verifying shape tensors for collective ALLREDUCEという趣旨のエラーが出ると、公式は示しています。

形が食い違う以外の原因(あるランクがバリアに到達しない、など)を試すなら、torch.distributed.monitored_barrierにタイムアウトを付けて呼びます。到達しなかったランクが報告されます。wait_all_ranks=Trueを付けると、ランク0は最初の失敗で止まらず、応答しなかった全ランクを列挙します。公式の注意として、これはデバッグ用で、性能への影響があります。

スクリプトができたら、本番と同じNCCL_DEBUG=INFOで2プロセスを動かし、INIT,NETのログが本番と同じ経路を示すかを比べます。経路が違うなら、再現したのは別の問題です。

作業を任せる前に決めておくこと

  • GPUを使うコマンドの実行は人が確認する: 再現スクリプトの実行は、GPUを占有します。共有クラスタでは、実行前に承認を挟む運用が無難です
  • NCCL_DEBUGのログは大きい: INFOは出力が多いため、NCCL_DEBUG_SUBSYSで絞ってから読ませます
  • 設定変更は1回に1つ: タイムアウト値を変えるときは、init_process_groupのtimeoutかNCCL_IB_TIMEOUTのどちらか一方だけを変えて再実行させます。両方変えると、どちらが効いたか分かりません

学習コードの改造をClaude Codeに任せる前の検証の考え方は、PyTorchモデルをONNXに書き出し出力の一致をテストで検証すると共通しています。合否を読めるコマンドを先に決めておくと、手戻りが減ります。

まとめ

NCCL timeoutの調査は、例外を読むより先に、全ランクのログの最終行を並べることから始まります。torchrun --tee 3 --log-dirと--log-line-prefix-templateでログを分け、NCCL_DEBUG=INFOを%h.%pつきのファイルに出し、claude -pには読み取り権限だけを渡します。原因の候補が出たら、2プロセスの再現コードとTORCH_DISTRIBUTED_DEBUG=DETAILで確かめます。timeoutの数値を変えるのは、その後です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. docs.pytorch.org/docs/2.14/elastic/run.html
  3. docs.pytorch.org/docs/2.14/elastic/errors.html
  4. docs.pytorch.org/docs/2.14/distributed.html
  5. docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html
  6. docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting.html
この記事を共有:XはてブLinkedIn