NCCL error: unhandled cuda error が出たら ─ WSL2 + マルチGPU + vLLM で詰まった話
こんにちは!
Qualitegプロダクト開発部です!
今日は、Windows + WSL2 のマシンに RTX 4090 を2枚挿して、大規模なオープンモデルを vLLM で動かそうとしたら、NCCL の初期化で見事に詰まった話を書きます。
世の中に断片的にしか情報がなく、抜けるまでにかなり粘ったので、同じ構成で消耗している方の時間を少しでも節約できれば嬉しいです。
経緯
今回の目的は、次々と登場する最新のオープンモデル(オープンウェイトのLLM)を、手元で評価することでした。
オープンモデルは数週間単位で新しいものが出てきます。ベンチマークの数字だけでなく、自分たちのユースケースに対して実際にどう振る舞うのか——出力の質、速度、量子化したときの劣化具合、エージェント的なタスクの得手不得手——を、手を動かして確かめています
今回の環境は Windows + WSL2(Ubuntu) に RTX 4090 を2枚(各24GB)挿したマシンです。
nvidia-smi 上の CUDA Version は 12.8。
動かすのは大規模オープンモデルを AWQ(4bit) で量子化したものです。
4bitでも重みは数十GBあり1枚には載らないので、最初から tensor-parallel=2(2枚に分割して動かす)が前提でした。
サーバーとして常駐させてAPIで叩きたかったので、vLLM を選びました。OpenAI互換のエンドポイントを生やしてくれるので、評価コードはローカルもクラウドも同じインターフェースで呼べます。
ひとつ補足ですが、
40系のコンシューマGPUに NVLinkはありません。
2枚は PCIe経由 で通信します。「じゃあPCIeで直接やり取り(P2P)すればいいだけでは?」と思うかもしれません。
実際、当初はそう考えていました。ところが後述するとおり、
WSL2上からPCIe経由のGPU間P2Pを、NCCLの有効な通信経路として使うことはできませんでした。
物理的な配線やGPUのグレードではなく、WSL2のGPU仮想化レイヤーとNCCLの組み合わせに起因するもの、というのが私の理解です。ここが今回いちばんの勘所でした。
結論から言うと、ここに WSL2特有の細い道 が3つ待っていました。
本記事では「やろうとしたこと → ぶつかった問題 → どう抜けたか」を順を追って書きます。とくに2つ目の罠は、世の中のドキュメントに断片的にしか書かれておらず、抜けるまでにかなり粘りました。同じ構成で消耗している方の時間を、1日ぶんくらい節約できれば幸いです。
全体像:”tensor-parallel”とは何をしているのか
最初に、なぜ「2枚に分割」が必要なのかを整理します。
大規模なモデルは、その重み(パラメータ)だけで1枚のGPUメモリに収まりません。そこで使うのが tensor-parallel(テンソル並列、TP) です。
これは、モデル内部の主要なテンソル計算を複数のGPUに分散配置し、1回の推論の中で各GPUが自分の担当部分を計算し、結果を集めて合成する方式です。
ここで重要なのが、各GPUの計算結果を集約する「集合通信」です。たとえば行列を分割して計算した後、最後に全GPUの部分和を足し合わせる all-reduce という操作が頻繁に走ります。この集合通信を担うのが NCCL(NVIDIA Collective Communications Library) です。tensor-parallelの実効性能、そして「そもそも起動できるかどうか」は、このNCCLが正しく初期化・通信できるかにかかっています。
後述する2つ目の罠は、まさにこのNCCLの初期化でつまずいた話です。
vLLMは、起動時に「モデルをいくつのGPUに分割するか」を指定すると、内部でワーカープロセスをGPUの枚数ぶん立ち上げ、NCCLで通信グループを作り、各ワーカーに重みの担当ぶんをロードします。理屈はシンプルですが、WSL2という土俵の上では、この「NCCLで通信グループを作る」ところが鬼門でした。
罠その1:CUDA wheelの不一致で、起動すらしない
まずは素直にvLLMをインストールしました。
手元のドライバは nvidia-smi 上で CUDA Version 12.8 と表示されます。ところが、素のpipインストールで降ってきたのは CUDA 13系(cu130)向け にビルドされたバージョンでした。
ドライバ側はCUDA 12系で止まっているため、起動した瞬間に、CUDA 13のランタイム共有ライブラリ(libcudart.so.13 など)が見つからない、という主旨のエラーで、GPUを一切認識しないまま落ちます。真犯人は CUDA 13ビルド(cu130)が降ってきたこと でした。
これは、機械学習まわりでは古典的な罠です。。
教訓はシンプルで、ドライバのCUDAメジャーバージョンに合わせたビルド(wheel)を、自動解決に任せず手で固定して入れる こと。今回はドライバが CUDA 12 系なので、CUDA 12系のwheelで揃える ことを目標にしました。
ここで実用上の話を補足しておきますと、
CUDA 11以降では、同じメジャーバージョン内での minor version compatibility が用意されています。
つまり、必要な最低ドライバ要件を満たしていれば、CUDA 12.x 系のドライバ環境で、別の 12.x 系ランタイムを同梱した wheel が動くケースがあります。ただし、これは「12.x なら常に無条件で動く」という意味ではありません。NVIDIA公式も「limited feature-set」と注記しており、新しいCUDA機能やPTX JIT、ライブラリ間の依存関係によっては、より新しいドライバが必要になる場合があります。
今回の構成では、PyTorch は cu128、vLLM は当該リリースで入手できた cu129 の prebuilt wheel を直URL指定で固定し、import と起動検証に通ったため、この組み合わせを採用しました。
重要なのは、CUDA 13系(cu130)のwheelをCUDA 12系ドライバ環境に入れてしまうような、メジャーバージョンをまたぐ不一致を避けることです。マイナー差は最終的に import と起動で確認すれば良い、というのが実態に近い感覚でした。
ありがちな失敗は、「最新版を入れれば速いだろう」とバージョン指定を省くことです。すると CUDA 13向けのビルドが降ってきて、ドライバ(CUDA 12系)が追いついていない、という不一致が起きます。GPUまわりは「新しいほど良い」とは限らず、少なくともメジャーバージョンをまたぐ不一致を避けることが重要です。
検証は拍子抜けするほど簡単で、Pythonの対話環境からフレームワークの拡張モジュール(ネイティブ拡張)がimportできるかを確認するだけ。importが通れば、少なくともCUDAランタイムとの整合は取れています。逆に、ここでエラーが出るうちは、何を設定しても先に進めません。
最初にここを固めるのが鉄則です。
罠その2(本命):NCCLが「unhandled cuda error」で初期化に失敗する
はい、ここからが本番です。
モデルを2枚に分割して起動しようとすると、ワーカープロセスの初期化中に、次のような主旨のエラーで落ちます。
RuntimeError: NCCL error: unhandled cuda error
そして、これに引きずられて「エンジンコアの初期化に失敗」というエラーが連鎖し、サーバーが立ち上がりません。
厄介なのは、メッセージが「unhandled cuda error(ハンドリングされていないCUDAエラー)」という、あるあるなんですが、こういう漠然としたエラーであるという点です。何が悪いのかをこのメッセージから読み取るのは、ほぼ不可能でしょう。
まずは定番の対処を入れる
WSL2では、GPU同士の P2P(PCIeを介したピアツーピア通信) や、データセンター向けの InfiniBand 系の通信経路 が、そのままでは使えないことが多いと知られています。
そこで、まずは定番の2つを環境変数で無効化しました。
NCCL_P2P_DISABLE=1
NCCL_IB_DISABLE=1
これでNCCLが「使えない経路」を避け、共有メモリ経由などの代替経路に切り替えてくれることを期待します。多くの「WSLでマルチGPU」系の記事は、ここまでで解決、と書いています。
ところが、これだけでは直らなかった
しかし、この環境ではそれだけでは解消しませんでした。
上記2つを入れても、あいかわらず「unhandled cuda error」が出続けます。
ここで多くの情報源が途切れていて、正直、半日近く切り分けに時間を要しました。
NCCLの詳細ログ(NCCL_DEBUG=INFO)を有効化して通信のどの段階で落ちているかを追い、メモリ確保のフェーズで初期化が失敗していることまでは突き止めました。
決め手は cuMem アロケータの無効化
最終的に効いた決め手は、次の一行でした。
NCCL_CUMEM_ENABLE=0
これは、NCCLが使う cuMem(CUDAの仮想メモリ管理)アロケータを無効化 する設定です。
比較的新しいNCCLは、cuMem(CUDA Virtual Memory Management)を使ってメモリを確保・登録し、通信を効率化します。
ところが、WSL2の準仮想化されたGPU環境では、このcuMem経由のメモリ確保・登録がうまく噛み合わず、通信グループの初期化時にエラーを起こしていたようです
(NCCL_DEBUG=INFO のログで、メモリ確保・登録のフェーズで失敗していることを確認しました)。
これを無効化し、従来方式のメモリ確保にフォールバックさせることで、ようやくNCCLが通りました。
さらに、安定性のための保険として、次の2つも加えました。
- ワーカーの起動方式を
spawnに固定する
(プロセス起動の方式を明示し、フォーク由来の初期化問題を避ける) - vLLMの起動オプションで、独自実装の集合通信(custom all-reduce)を無効化し、標準のNCCL経路に寄せる
ということで、
最終的に効いた組み合わせは、以下のとおりです。
NCCL_P2P_DISABLE=1
NCCL_IB_DISABLE=1
NCCL_CUMEM_ENABLE=0 # ← これが本命の決め手
(ワーカー起動方式)spawn
(vLLM起動オプション)独自all-reduceを無効化し標準経路へ
この状態で起動すると、各ワーカーが重みの担当ぶんを順にロードし、続いて計算グラフのコンパイルと、推論を高速化するためのCUDAグラフのキャプチャが走ります。ここは数分かかるので、ログが一見止まったように見えても慌てず待つのがコツです。
最終的に、モデル一覧を返すエンドポイントが起動済みモデルを返してくれれば成功です。
これは「WSLだからこそ」なのか?
ここが、今回いちばん腑に落ちた論点です。
結論から言うと、この一連のNCCLエラーは
WSL2固有性が非常に高い
と考えています。理由は、WSL2のGPUの動き方にあります。
WSL2のGPUは、ホストのWindowsが持つGPUを 準仮想化(GPU-PV) してゲスト(Linux)に見せています。Linux側からは仮想的なGPUデバイス経由でCUDAが叩かれ、最終的にホストのドライバへ橋渡しされます。この仕組みは「Windows上のLinuxでGPUが使える」という体験を実現する素晴らしいものですが、ベアメタル(物理マシンに直接インストールしたLinux)と比べると、GPU同士の直接通信(P2P)や、低レベルなメモリ管理(cuMem)の挙動が異なる場合があります。
- P2P:物理的にPCIeやNVLinkで繋がったGPU同士が、CPUを介さず直接データをやり取りする仕組み。WSL2の仮想GPUでは、この直接経路がそのままでは成立しにくいケースがあります。
- cuMem:CUDAの仮想メモリ管理API。新しいNCCLはこれを使って通信用バッファを確保・登録するが、準仮想化されたGPUメモリ空間との相性問題が出やすい。
ここまでをまとめますと、この問題はGPUのグレードや配線では回避できない可能性が高いという点です。
今回の2枚はPCIeで繋がっていますが、ベアメタルのLinuxでは、マザーボードのPCIeトポロジやBIOS/ACS/IOMMU設定が整っていれば、PCIe経由のGPU間P2Pが有効になるケースがあります。
ところが本記事の環境では、その PCIe P2Pが WSL2ゲストからNCCLの有効な経路として使えない という状況でした。そのため、仮にNVLink付きのGPUを使ったとしても、WSL2側からその経路がNCCLの有効なP2P経路として見えなければ、同じ種類の問題に遭遇する可能性があります。
少なくとも
「NVLink付きなら必ず解決する」
「良いGPUを2枚挿せば通信は勝手にうまくいく」と考えるのはあぶなそうです。
問題は物理層ではなく、その上の 仮想化レイヤー にある可能性が高いからです(この点は環境やバージョンに依存し得るので、お使いの構成で nvidia-smi のtopologyやNCCLログを確認するのが確実です)。
裏を返せば、ベアメタルLinuxなら NCCL_CUMEM_ENABLE=0 も P2P_DISABLE も通常は不要なことが多いとおもいます
(PCIe P2PもcuMemも、適切な設定下ではおおむねそのまま機能します)
ですから「同じモデル・同じGPU・同じ枚数でも、ネイティブLinuxなら起きにくい」と言えます。
今回の苦労は、開発環境があるWindowsで楽をしようとしてWSL2という土俵を選んだことの代償だった、というわけです。
ただし、100%「WSL限定」と言い切るほど自信は正直ないです。ほか環境を全部ためしたわけではないため。
一部のコンテナ環境や、IOMMU・ACS(Access Control Services)の設定次第のベアメタル環境でも、P2Pが無効化されていたり、cuMem由来の類似事象が報告されたりします。
なので正確には「準仮想化・制約環境で顔を出しやすく、WSL2はその代表格」という整理が妥当かとおもいます。
WSLは開発体験が良いぶん、こういう低レイヤの落とし穴が時々顔を出す、と覚えておくと心の準備ができます。
罠その3:バックグラウンド起動が、セッションごと消える
NCCLを抜けて「やっと起動できた」と思ったら、実は、もうひと山ありました。
Windows側からWSLのコマンドを「バックグラウンド(末尾にアンパサンド)」で叩いて起動すると、コマンドの呼び出しが返った瞬間に、サーバープロセスごと消えてしまうのです。プロセス一覧を見ても、何も残っていません。
これはWSLのプロセスライフサイクルに起因します。
WSLのセッションは、それを起動した親プロセスが終了すると畳まれることがあり、その際に配下のプロセスを巻き込みます。nohup(ハングアップシグナルを無視させる)を付けても、セッションの畳まれ方によっては道連れになってしまう。
対処の方向性はいくつかあります。
- ターミナルを開いたまま 前景でプロセスを保持 する(評価作業中はこれで十分なことも多い)。
- WSLでsystemdを有効化している場合は、systemdのサービスとして起動 する方法もあります。ただし、WSLインスタンス自体の終了挙動やuser serviceの維持は環境設定に依存するため、実運用では実際にセッションを閉じた状態でプロセスが残るか確認しておく必要があります。
- Windows側の タスクスケジューラから
wsl.exe経由で起動 し、Windowsのプロセスツリーに紐づける。
「起動ログには成功と出ているのに、APIに繋がらない」「プロセス一覧に何も居ない」という症状が出たら、まずこれを疑うと早いです。地味ですが、ここで30分溶かす人は意外と多いんじゃないでしょうか💦
得られたもの
これらの罠を抜けた結果、RTX 4090を2枚で、大規模なオープンモデルをtensor-parallelで分割ロードし、OpenAI互換APIとして常駐させられるようになりました。
本番運用はベアメタルに任せるとして、評価のサイクルを手元開発マシンで速く回せるようになったのは個人的に大きな収穫でした。
振り返ると、3つの罠はどれも「WSL2 × マルチGPU × 量子化大規模モデル」という条件が重なったときに初めて顔を出すもので、単体のドキュメントには断片的にしか書かれていませんでした。
とくに2つ目、NCCL_CUMEM_ENABLE=0 にたどり着くまでが長く、ここを共有できるのが本記事のいちばんの価値だと思っています。「P2PとIBを切る」までは多くの記事にありますが、その先で止まっている人に届けば本望です。
検証環境
参考までに、本記事で扱った構成のバージョンを記しておきます。NCCL系の問題はバージョン依存が大きいため、同じ症状で辿り着いた方は、お使いの環境のバージョンと突き合わせて読んでください。
| 項目 | 値 |
|---|---|
| OS | Windows 11 Pro |
| WSL | 2.4.13.0(カーネル 5.15.167.4-1) |
| Ubuntu | Ubuntu 24.04.2 LTS(noble) |
| GPU | NVIDIA RTX 4090 × 2(各24GB) |
| NVIDIAドライバ | 572.83(nvidia-smi 上の CUDA Version: 12.8) |
| Python | 3.12.13 |
| vLLM | 0.21.0+cu129 |
| PyTorch | 2.11.0+cu128(torch.version.cuda = 12.8) |
| NCCL | 2.28.9(torch.cuda.nccl.version() → (2, 28, 9)) |
| 量子化 | AWQ系 INT4 weight-only(W4A16・group_size=128)。vLLM 上は compressed-tensors WNA16 を Marlin カーネルで実行 |
| tensor-parallel-size | 2 |
PyTorch が cu128、vLLM が cu129 と微妙にずれているのは誤記ではなく、罠1で説明したとおり「CUDAメジャー(12)を揃え、minor version compatibility の範囲で運用」した結果の意図的な組み合わせです。最終的には import と起動検証で動作を確認しています。
まとめ(チェックリスト)
公開後に見返すときの早見表として、設定だけを抜き出しておきます。
インストール時
- フレームワークのビルドは、ドライバのCUDAメジャーバージョンに合わせて手で固定する。素のpipインストールでは CUDA 13系(cu130)ビルドが降ってきて、CUDA 12系のドライバと不一致を起こす。CUDA 12系(cu12x)のwheelで揃えるのが解。マイナー差(cu128 と cu129 など)は CUDA minor version compatibility の範囲内で動く場合があるが、新機能・PTX・ライブラリ間依存によっては追加の制約があるため、最終的には import と起動検証で確認する。
マルチGPU起動時の環境変数
NCCL_P2P_DISABLE=1
NCCL_IB_DISABLE=1
NCCL_CUMEM_ENABLE=0
vLLM起動オプション
- ワーカー起動方式を
spawnに固定。 - 独自実装のall-reduceを無効化し、標準のNCCL経路に寄せる。
運用上の注意
- 上記のNCCL環境変数は問題回避・切り分けのための設定であり、性能面では不利になる可能性があります。本番環境やベアメタルLinuxでは、まずNCCLの自動選択に任せ、必要な場合のみ設定するのが望ましいです。
- このNCCLの一群は WSL2の準仮想化GPU固有性が高いと考えられます。ベアメタルLinuxなら(BIOS/ACS等の設定が整っていれば)通常は不要なことが多いです。
- 起動プロセスは前景で保持するか、
systemd(user service)やタスクスケジューラ等でセッションから切り離す。後者は環境依存があるので、実際にセッションを閉じた状態でプロセスが残るか確認しておくこと。バックグラウンド起動はセッションごと消えることがあります。 - 起動後はロード+コンパイルで数分かかる。モデル一覧エンドポイントが返るまで、リトライしながら機械的に待つ。
ここまでお読みいただきありがとうございました。
低レイヤの落とし穴は、抜けてしまえば一行の設定です。
けれど、その一行に辿り着くまでが長~いです。
同じ道で消耗している方の、ほんの少しの近道になりますように。
それでは、次回またお会いしましょう!