TL;DR
-
2台のDGX Sparkの最大の価値は「速くなる」ことではなく、1台に収まらないサイズのモデルをロードできること。約228.5GBの
GLM-5.2 UD-IQ1_MGGUFを、llama.cppのRPCで2台に分散ロードし、生成まで確認できた - 実はこの2台連結、2026年1月にも公式手順(当時TRT-LLM 1.2.0rc6)で挑戦し、MPI疎通の段階で敗退している。本記事は約5ヶ月越しの再挑戦であり、当時なぜ失敗したのかの分析も含める
- 2台をつなぐCX7直結リンクの実測帯域は TCPで約106Gbps、RDMAで約109Gbps(単一)/約185Gbps(2ポート合算)。「200GbE」の実体は100Gbps級MAC×2の束である。なおCX7はNVLinkではない — 混同しやすい両者の違いもコラムで整理する
- サービング測定では、228.5GBモデルで mean TTFT約3.2秒(c=1)。サーバ側
--parallelを同時リクエスト数に合わせて上げると、スループットは4並列で+87.7%、TTFTはほぼ横ばいのままスケールした。--parallel 1のまま多重リクエストを投げると「キューイングの見せかけ並列」になりTTFTが7.5倍悪化する — ここは実際に踏んだ罠として詳述する - llama.cpp最新masterの RPC over RDMA をTCPとA/B比較したところ、ワークロード次第で prompt処理+45%、実効スループット+47% の改善を確認。ただし改善幅はモデル・プロンプト長・並列度で大きく変動し、GPU計算が支配的になるほど縮小する(並列c=2では+3.1%まで縮む)
- RDMAトランスポートには 停止順序に起因するクライアント側クラッシュ(条件を特定し、回避手順の有効性まで実証済み、後述)と、「未設定ならTCP」ではない自動ネゴシエーション仕様という2つの落とし穴がある。運用のデフォルトは当面TCPを推奨する
- NVIDIA公式playbookに近いTensorRT-LLM経路(
Qwen3-235B-A22B-FP4)は、RoCE directではなくSocket fallback構成で成功した
2026年1月の敗北
この2台連結、最初の挑戦は2026年1月で完敗でした。
当時の目標は今回と同じくNVIDIA公式の「Run on two Sparks」相当、Qwen3-235B-A22B-FP4 を tp=2 で動かすことでした。しかし最終目標どころか、その手前の最小疎通 — 「コンテナ内の mpirun がリモート側でもコンテナ内の orted を起動し、hostname が2ノード分返る」 — を通せないまま、複数セッションにわたる手作業の試行錯誤の末に中断しました。
約5ヶ月後に当時の記録を読み返すと、敗因は4つに整理できます。そして悔しいことに、正解の一歩手前までは行っていたことも分かりました。
敗因1: 公式ドキュメントの過渡期に正面から突っ込んだ。 NVIDIAは2026年1月2日に「Run on two Sparks」workflowを刷新し(Docker Swarm + compose方式 → 各ノードで docker run + primary側から mpirun 方式)、コンテナも1.2.0rc6へ更新していました。挑戦したのはまさにその直後で、Web上には新旧両方式の情報が混在。参照するドキュメントのインデックス時点によって「公式手順」の中身が食い違い、相談していたAIアシスタント同士の見解まで割れる始末で(片方は2025年11月時点のSwarm方式、もう片方は刷新後のdocker run方式を「公式」と主張)、別々の公式手順を交互に追いかけて時間を溶かしました。手順書を見たら、まずLast Updatedの日付を確認すべきでした。
敗因2: 「罠」だと思って無効化したPort 2233が、実は公式機構の一部だった。 TRT-LLMコンテナにはSSHをPort 2233へ向ける設定が入っており、当初「SSHが通らない原因」に見えたため、これを無効化して通常のport 22 — つまりホストOS行きのSSH — に戻して作業を続けました。しかしこのPort 2233こそ、公式 trtllm-mn-entrypoint.sh がコンテナ内に立てるsshdへの入口です。障害物に見えたものが実は正規ルートの一部で、それを外したことが次の敗因3を構造的に固定化していました。
敗因3(本質): リモートへのSSHがホストOSに着地し、ortedがコンテナ外の環境で起動していた。 primary側コンテナ内の mpirun はリモートへSSHして orted を起動しますが、そのSSHがリモートのホストOSに着地すると、ortedはコンテナ外(または誤ったOPAL_PREFIX)で起動され、orte_session_dir failed や /opt/hpcx/ompi/bin/orted: No such file or directory で死にます。実は1月の時点でこの「着地点のズレ」仮説には自力で到達しており、SSHの着地先をリモートコンテナ内へ強制する自作rshラッパ(ssh → docker exec)まで作っていました。診断は正しかったのです。ただ、手作りラッパではコンテナ内のMPI環境(prefix、session_dir、ユーザー文脈)を完全には再現しきれず、引き継ぎメモに「公式のentrypoint / port 2233 / sshdの流儀に戻すべきか」という正解候補を書き残したまま、検証する前に力尽きました。「SSHは通っている」と「MPIが期待する場所にSSHが通っている」は別物 — この一文に当時の数週間が要約されます。
敗因4: 状態管理の混乱と、試行の定量記録の欠如。 リトライを重ねるうちに trtllm / trtllm-multinode / trtllm-mn-models と別名のコンテナが混在しました(最終的に統一して引き継ぎメモに明記したので回復はしています)。より痛かったのは前述の通り試行単位の記録がないことで、「この条件は試したか?」を記憶に頼ることになり、同じ袋小路を何度か往復した可能性を否定できません。
検証環境
2台のDGX SparkをCX7 (ConnectX-7) のQSFPポートで直結しました。

DGX Spark同士の直結に使えるケーブルは対応型番が限られており、現在、入手が困難です。海外通販サイトにもなく、メーカーサイトで探す感じです。
| 役割 | ホスト | 管理LAN | CX7 link-local | GPU |
|---|---|---|---|---|
| ローカル / primary | NODE_A | <NODE_A_MGMT_IP> |
<NODE_A_CX7_IP> |
NVIDIA GB10 |
| リモート / worker | NODE_B | <NODE_B_MGMT_IP> |
<NODE_B_CX7_IP> |
NVIDIA GB10 |
- OS: Ubuntu 24.04.3 LTS / Kernel 6.14系 (NVIDIA公式)
- CUDA 13.0.88 / Driver 580.126.09
- llama.cpp: TCP baseline
version 9743 (c57607016)、RDMA検証build 10068 (571d0d540) - 記事内のホスト名・IPは自環境に読み替えてください
各ノードのGB10は、llama.cppログ上おおむね122GiB級のメモリを持つデバイスとして見えていました。DGX SparkではNVMLや nvidia-smi の見え方が実行環境によって揺れることがあるため、本記事ではログ上のデバイス認識と実際のロード結果を重視します。
CX7のインターフェース名は2台で非対称でした(片方 enp1s0f1np1、もう片方 enp1s0f0np0)。この非対称性は後述のNCCL/UCX設定やRDMAデバイス指定のあちこちに効いてくるので、最初に ip a / ibv_devinfo で両ノードの対応表を作っておくことを勧めます。
link-local (169.254.x.x): DHCPサーバなしで直結リンクに自動付与されるアドレス帯です。2台のDGX SparkをQSFPケーブルで直結すると、CX7側インターフェースにこの帯域のIPが付きます。本記事の <NODE_*_CX7_IP> はこの帯域の値です。
まず土管の実力を測る: CX7直結リンクの生帯域
分散推論の前に、リンクそのものの帯域をTCP (iperf3) とRDMA (perftest) の両方で実測しました。
RDMA / RoCEv2: RDMA (Remote Direct Memory Access) はCPU・OSカーネルを介さずNIC同士が相手のメモリを直接読み書きする転送方式、RoCEv2はそれをEthernet上で行う規格です。TCPよりレイテンシが低くCPU負荷も小さいため、GPU間通信が頻発する分散推論で効果が期待できます。
| 測定 | 値 |
|---|---|
| iperf3 TCP 単一ストリーム | 29.2 Gbps (順方向) / 45.1 Gbps (逆方向) |
| iperf3 TCP 並列8 | 106.4 Gbps |
| ib_write_bw RDMA 単一セッション | 109.1 Gbps |
| ib_write_bw RDMA 2ポート合算 | 185.1 Gbps |
| 参考: 管理LAN経由 | 2.35 Gbps |
ここで注目は「200GbE」を謳うリンクなのにTCPでもRDMA単一でも110Gbps付近で頭打ちになる点です。RDMAで2セッションを別ポートに張ると185Gbpsまで伸びたことから、このリンクの実体は100Gbps級MAC×2の束であり、単一フローは片側100Gbps分しか使えない、という構造が見えてきます。カタログの「200GbE」をそのまま単一フローの上限と思い込むと見積もりを誤ります。
TCP単一ストリームが29〜45Gbpsに留まるのは受信側CPUのパケット処理が律速になっているためで(iperf3のCPU使用率から確認)、MTU 1500・ジャンボフレーム未使用という条件も効いています。llama.cpp RPCは実質単一コネクションなので、TCP構成のRPCが使える帯域は実質この「単一ストリーム相当」の領域である点は、後のRDMA比較を読む上での伏線になります。
コラム: CX7はNVLinkではない
2台連結の話をすると必ず出る質問が「それはNVLinkで繋がっているのか?」です。答えはNoで、ここを混同すると2台構成への期待値を大きく見誤るので、整理しておきます。
ConnectX-7とNVLinkは別物です。 ConnectX-7は旧Mellanox由来のネットワークカード(NIC)の製品ラインで、Ethernet/InfiniBandの上でRDMAを話す「ネットワーク」。NVLinkはGPU同士(やCPU-GPU間)をメモリセマンティクスで直結する専用インターコネクトです。混同が起きやすいのには理由があります。NVIDIAがMellanoxを買収して両方が「NVIDIAのインターコネクト」になったこと。DGX H100やGB200 NVL72ではノード内=NVLink/NVSwitch、ノード間=ConnectXと両方を積んでいて役割分担が外から見えにくいこと。そしてNVLinkにも外部ケーブル版(NVLink Network)があり、見た目がNICのケーブルと紛らわしいことです。
さらにややこしいことに、DGX SparkにはNVLinkも載っています。ただし場所が違う。GB10チップの内部でGrace CPUとBlackwell GPUを結んでいるのがNVLink-C2C(キャッシュコヒーレント接続)で、これが128GB統合メモリの正体です。つまりSparkは「箱の中はNVLink、箱の間はCX7」。本記事の2台連結で使ったのは後者だけです。
| NVLink (+NVSwitch) | CX7 RDMA (RoCEv2) | |
|---|---|---|
| 正体 | GPU間直結インターコネクト | ネットワーク(NIC + Ethernet) |
| セマンティクス | load/store。GPUが相手のメモリをメモリ命令で直接読み書き(P2P)、世代によりキャッシュコヒーレント | verbs。明示的な転送要求をNICが片側DMAで実行。メモリ命令ではない |
| 帯域の桁 | NVLink 5世代で1.8TB/s/GPU、NVLink-C2Cで600GB/s級 | 今回実測13〜23GB/s(106〜185Gbps)。約2桁差 |
| レイテンシ | 数百ns級 | µs級 |
| ソフトからの見え方 | 別デバイスだが、P2P/UVAで相手のVRAMをポインタで触れる | ネットワークの向こう側。触るには転送が要る |
そして本題、「NVLinkなら今回やったようなモデル分割は不要なのか?」— 正確な答えは、分割自体はNVLinkでも必要です。8×H100のDGXで70Bを動かす場合も、Tensor Parallelで各GPUに重み行列を分割して配置します。NVLinkがあっても複数GPUが自動的に1つの巨大メモリ空間へ融合されるわけではありません(P2Pで相手のメモリをポインタ参照することは可能ですが、帯域・レイテンシはローカルHBMより不利なので、実用フレームワークは重みを明示的に分割配置します)。
NVLinkが変えるのは、分割の有無ではなく「分割に伴う通信のコスト」です。 TPは毎トークン・全レイヤーでallreduceが走る通信ヘビーな分割方式ですが、1.8TB/s+ns級レイテンシの上ではこれがほぼ誤差になり、TPが素直にスケールします。一方CX7の13GB/s+µs級では同じallreduceが死活問題になる — まさに本記事のTRT-LLM tp=2経路がRoCEで転び、Socket fallbackでも8〜12 tok/s級に留まった世界です。だからこの記事の結論(通信要求の緩いllama.cpp RPC-split方式がSparkでは確実)は、こう言い換えられます: NVLinkは「分割を不要にする」技術ではなく「細かい分割(TP)を実用にする」技術であり、それがない2台のSparkでは、粗い分割(層の分散配置)を選ぶのが合理的。
なおCX7側にも、NICがGPUメモリへ直接DMAするGPUDirect RDMAという「近縁」の仕組みはあり、llama.cpp RPC over RDMAで得たprompt処理+45%(後述)はこの方向の恩恵です。ただそれでもload/storeのコヒーレンスではなく、帯域の2桁差も埋まりません。箱の間の物理を変えない限り、「2台で1台のGPUのように」はならない — これが2台連結の期待値を正しく設定する出発点です。
最終結果サマリ
GLM-5.2 GGUF + llama.cpp RPC
| 項目 | 値 |
|---|---|
| モデル | unsloth/GLM-5.2-GGUF |
| 量子化 | UD-IQ1_M |
| ファイル合計 |
228,492,966,624 bytes (約228.5GB) |
| 起動方式 | local llama-server + remote rpc-server
|
| RPC | TCP, --rpc <REMOTE_CX7_IP>:50052
|
| listenまで | 約6分18秒、health OK 約6分30秒 |
| local GPU使用量 | 約 110161 MiB
|
| remote GPU使用量 | 約 104798 MiB
|
短い /completion decode |
約 8.21 tok/s
|
| サービング mean TTFT (c=1) | 約 3.2秒
|
| サービング output tok/s (4並列) |
13.74 tok/s (c=1比 +87.7%) |
ローカルとリモートの両方に100GB級ずつ載っており、1台では物理的に不可能な228.5GB級モデルが2台構成でロードできたことを示しています。先行して約206GBの GLM-5.1 UD-IQ1_M でも同構成で成功しています(listen約5分19秒、decode約8.26 tok/s)。
Qwen3-235B-A22B-FP4 + TensorRT-LLM
公式playbook系ルート
| 項目 | 値 |
|---|---|
| モデル | nvidia/Qwen3-235B-A22B-FP4 |
| Runtime | TensorRT-LLM 1.3.0rc13、--tp_size 2
|
| 成功した通信構成 | NCCL Socket + OpenMPI TCP + UCX TCP (Socket fallback) |
| decode平均 | 約 11.97 tok/s (3回平均) |
| listen平均 | 約 149.7s (3回平均) |
1月に敗退した公式2台ターゲットへの再挑戦です。RoCE/NCCL directは今回も動かせませんでしたが、Socket fallbackでの安定稼働まで到達しました(詳細は後述)。
最短で動作確認する手順: GLM-5.2 GGUF + llama.cpp RPC TCP
まずはTCP構成の手順です。GLM-5.1でもモデル名とshard名を読み替えればほぼ同じ流れです。
RPC (llama.cppのRPC機能): リモートマシンのGPUを、ネットワーク越しに「ローカルの追加GPUデバイス」として見せる仕組みです。リモート側で
rpc-serverを起動し、ローカルのllama-serverに--rpc <IP>:<PORT>を渡すと、モデルの層を2台のGPUに分散して載せられます。
前提:
-
llama.cppはローカル・リモート両方でビルド済み (GGML_CUDA=ON,GGML_RPC=ON) - モデルはローカル側にのみ置く(リモートは
rpc-serverだけでよい)
llama.cppをRPC付きでビルド
git clone https://github.com/ggml-org/llama.cpp llama.cpp-latest
cd llama.cpp-latest
cmake -B build-rpc-cuda-tcp \
-DGGML_CUDA=ON \
-DGGML_RPC=ON \
-DGGML_RPC_RDMA=OFF \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-rpc-cuda-tcp --config Release -j
GGML_RPC_RDMA=OFF を明示しているのは意図的です。ONのままlibibverbsが見つかる環境でビルドすると、後述する「自動ネゴシエーション」によって知らないうちにRDMAで動くことがあるためです。まずTCPで確実に動かしたい段階では、ビルド時点で切っておくのが安全です。
バージョン確認:
./llama.cpp-latest/build-rpc-cuda-tcp/bin/llama-server --version
# version: 9743 (c57607016)
# built with GNU 13.3.0 for Linux aarch64
GLM-5.2 UD-IQ1_Mをダウンロード
unsloth/GLM-5.2-GGUF の UD-IQ1_M、6 shard・合計 228,492,966,624 bytes です。
GGUF / UD-IQ1_M: GGUFはllama.cpp系のモデルファイル形式、
UD-IQ1_MはUnsloth Dynamicの1bit級超低ビット量子化です。GLM-5.2はこの量子化で約228.5GBまで縮んでいます(1bit級まで削ってなおこのサイズ、とも言えます)。
mkdir -p /models/glm-5.2-ud-iq1_m
hf download unsloth/GLM-5.2-GGUF \
--include 'UD-IQ1_M/*.gguf' \
--local-dir /models/glm-5.2-ud-iq1_m
今回の実測ではダウンロードに約2時間35分かかりました。
remote rpc-serverを先に起動する
起動順序は重要です: (1) remote rpc-server 起動 → (2) listen確認 → (3) local llama-server 起動。
ssh <NODE_B_MGMT_IP> \
"cd /path/to/workdir; \
setsid nohup ./llama.cpp-latest/build-rpc-cuda-tcp/bin/rpc-server \
-H <NODE_B_CX7_IP> \
-p 50052 \
> logs/llamacpp_rpc_server_glm52.log 2>&1 < /dev/null &"
ssh <NODE_B_MGMT_IP> "ss -lntp | grep ':50052'"
local llama-serverを起動する
/path/to/llama.cpp-latest/build-rpc-cuda-tcp/bin/llama-server \
--model /models/glm-5.2-ud-iq1_m/GLM-5.2-UD-IQ1_M-00001-of-00006.gguf \
--alias glm-5.2-ud-iq1m-rpc \
--host 127.0.0.1 \
--port 8092 \
--ctx-size 2048 \
--parallel 1 \
--n-gpu-layers 999 \
--tensor-split 1,1 \
--rpc <NODE_B_CX7_IP>:50052
--tensor-split 1,1 がローカルGPUとリモートGPUへの層の割り振り比率です。ここでは動作確認用に --ctx-size 2048 --parallel 1 としていますが、複数リクエストを同時にさばくなら後述の「同時リクエスト性能」の節を参照して両方を引き上げてください。起動ログでローカルCUDAとremote RPCの両方が見えていることを確認します。
CUDA0 : NVIDIA GB10 (...)
RPC0 : <NODE_B_CX7_IP>:50052 (...)
GLM-5.2では以下のような警告が出ましたが、最終的にロード・生成できました。GLM-5.2 GGUFとllama.cppビルドの互換性上の注意点として扱ってください。
special_eot_id is not in special_eog_ids
model has unused tensor blk.78.... -- ignoring
ロード中の /health は503を返し、最終的にHTTP 200になります。実測ではhealth OKまで起動後約6分30秒でした。
短い生成テスト
curl -s http://127.0.0.1:8092/completion \
-H 'Content-Type: application/json' \
-d '{
"prompt":"日本語で一文だけ返してください。GLM-5.2の2ノードRPC確認です。",
"n_predict":32,
"temperature":0.2
}'
実測: prompt throughput 13.44 tok/s、decode throughput 8.21 tok/s。nvidia-smi 上でlocal 110161 MiB / remote 104798 MiBが載っていました。
prompt eval (pp) と decode (tg): pp(prompt eval)は入力プロンプトを読み込む処理の速度で、まとめて並列計算できるため通信・演算効率の影響が出やすい指標。tg(decode)は1トークンずつ生成する速度で、体感の「文章が流れる速さ」に相当します。以降のA/Bでは主にこの2つを見ます。
停止: 「クライアント完全終了→サーバ停止」の順序を必ず守る
停止は起動の逆順、かつローカルのllama-serverプロセスが完全に消えたことを確認してから、リモートのrpc-serverを止めます。
# 1. local llama-serverを止め、PIDの消滅まで待つ
kill <LLAMA_SERVER_PID>
while kill -0 <LLAMA_SERVER_PID> 2>/dev/null; do sleep 1; done
# 2. その後でremote rpc-serverを止める
ssh <NODE_B_MGMT_IP> "kill <RPC_SERVER_PID>"
TCP構成ではこの順序を破っても即座に問題にはなりませんが、後述のRDMA構成ではこの順序を破ると確定的にクラッシュする条件があり、トランスポートによらず同じ停止手順に統一しておくのが安全です。
もうひとつ実地で得た教訓として、「APIポートが閉じたこと」をクライアント終了の判定に使ってはいけません。ポートのcloseと、リモートバッファ解放を含む終了処理の完了(プロセスの消滅)の間にはタイムラグがあります。自動化スクリプトでは上記のようにPIDの消滅を待ってください。
TensorRT-LLMでQwen3-235Bを動かす
NVIDIA公式playbookに近い経路、過去に敗退したルートへの再挑戦です。コンテナ(TensorRT-LLM 1.3.0rc13)を両ノードで起動し、コンテナ間MPIで trtllm-llmapi-launch trtllm-serve を --tp_size 2 実行します。
1月に越えられなかったMPI疎通は、今回は次の組み合わせで通りました。
-
公式
trtllm-mn-entrypoint.shを使う。1月の敗因2・3の答え合わせです。このスクリプトがコンテナ内にsshd(専用ポート)とMPI環境を構成することで、「SSHがホストOSに着地しortedがコンテナ外で起動する」問題が構造的に消えます。1月に自作rshラッパで再現しようとしていたものの正式実装であり、「罠」に見えたPort 2233はこの機構の入口でした - コンテナ世代の更新(1.2.0rc6 → 1.3.0rc13)と、それに追随した公式手順の安定化
- serve前のpreflight(両コンテナ内の
nvidia-smi/torch.cuda.is_available()/pynvml.nvmlInit())をゲート化し、「GPUが見えていないまま謎の失敗を追う」時間を排除
そのうえで、通信構成は結論から言うとRoCE/NCCL directは動かせず、以下のSocket fallback構成で成功しました。
export MASTER_ADDR=<NODE_A_CX7_IP>
export MASTER_PORT=29555
export NCCL_IB_DISABLE=1
export NCCL_SOCKET_IFNAME=enp1s0f
export NCCL_SOCKET_FAMILY=AF_INET
export OMPI_MCA_pml=ob1
export OMPI_MCA_btl=tcp,self
export OMPI_MCA_btl_tcp_if_include=169.254.0.0/16
export UCX_TLS=tcp,self
export UCX_NET_DEVICES=all
ポイント:
-
NCCL_IB_DISABLE=1でNCCLをIB/RoCEではなくSocketへ寄せる - 2台でCX7のinterface名が非対称のため、
NCCL_SOCKET_IFNAMEはprefix指定(enp1s0f)、OpenMPIはCIDR指定(169.254.0.0/16)、UCXはallと、片方のノードにしか存在しない名前に固定しない指定方法を選ぶ
remote container内のNVML/CUDA可視性は環境変数で一時的に壊れることがあり、壊れていた場合はremote containerのrestartで復旧しました。
serve・API確認の結果、ISL=2048/OSL=128/BS=1相当の簡易ベンチで3回平均 decode約 11.97 tok/s、listenまで平均約149.7秒でした。
RoCE/NCCL directが落ちる点
NCCL単体の2 rank all_reduce smokeは成功する(NCCL INFO Using network IB)のに、TensorRT-LLM serveのallreduce registration pathで毎回落ちます。
NCCL WARN Call to ibv_reg_mr_iova2 failed with error Bad address
ncclUtils.cpp:511: unhandled system error
NCCL_NET_GDR_LEVEL=LOC、NCCL_DMABUF_ENABLE=0、rc14、小型Llamaモデルへの変更いずれも効果なし(いずれも複数回再現)。「NCCL smokeが通る」ことは「TRT-LLM serveが動く」ことの必要条件であっても十分条件ではない、というのが教訓です。RoCE directは現時点では採用を見送り、公式の修正や明確な回避策を待つ判断にしました。
RPC over RDMA検証: TCPとのA/B比較
llama.cpp最新master(build 10068)にはRPCのRDMA (RoCEv2) トランスポートが入っています。TCP baseline(build 9743)とA/B比較しました。
A/B比較: 比較したい条件(ここではトランスポート = TCP / RDMA)だけを変え、それ以外(モデル・量子化・プロンプト・ビルドフラグ・測定手順)をすべて固定して交互に測る方法です。後述のとおりllama.cppのRDMAは「何もしなければ自動でRDMA」仕様のため、TCP側が本当にTCPで走ったことをログで確認する(陰性確認)ところまでやって初めてA/Bが成立します。
A/Bを成立させるための前提: 自動ネゴシエーションの罠
最初に落とし穴の方から書きます。llama.cppのRDMAトランスポートは、libibverbs付きでビルドされていると、環境変数 GGML_RDMA_DEV を何も設定しなくても、接続に使うIPアドレスに対応するRoCEデバイスが見つかれば自動的にRDMAへ切り替わります。「未設定ならTCP」という直感は成り立ちません。
つまり、RDMA対応ビルドで素朴に「TCP vs RDMA」を測ると、両方ともRDMAで走っていて差が出ないという無意味な比較になり得ます。今回は以下で対処しました。
- TCP側: 実在しないデバイス名
GGML_RDMA_DEV=disabledを指定してRDMA probeを確実に失敗させ、TCPフォールバックさせる - 検証:
GGML_RPC_DEBUG=1でremote側ログにRDMA probed: dev=...が出ていないことを全runで確認(陰性確認)。なおこのINFOログはremote (rpc-server) 側にしか出ず、client側ログでは判定できない点にも注意
トランスポートは自動判定に任せず、RDMAで動かすときもTCPで動かすときも常に明示指定し、ログで裏を取る。これが今回のA/B設計の土台です(この仕様とREADME記載のギャップはupstreamへのドキュメント改善提案としてまとめました)。
測定条件
- 同一ハード・同一OS・同一cmakeフラグで、トランスポートのみ変更
- GPT-OSS-120B (MXFP4, 約60GiB) と GLM-5.2 (UD-IQ1_M, 約228.5GB) の2モデル
- 短文生成・長文prompt(2048トークン級)を各3回測定して平均。さらに GLM-5.2 は日本語知識QA 1,119問のベンチマーク(早稲田大+ヤフー製 JCommonsenseQA を筆者の定点観測用に固定したもの、以下JCQ)を全問通しで耐久実行(soak)。
結果1: GPT-OSS-120B (各3回平均)
| 指標 | TCP | RDMA | 差分 |
|---|---|---|---|
| load時間 | 64.0秒 | 80.0秒 | RDMAが+25%遅い |
| 短文decode | 49.36 tok/s | 56.97 tok/s | +15% |
| 長文prompt eval | 2174.5 tok/s | 3163.3 tok/s | +45% |
| 長文decode | 48.37 tok/s | 56.67 tok/s | +17% |
prompt evalの+45%が最大の改善で、通信レイテンシがボトルネックになりやすい処理ほどRDMAが効く、という素直な結果です。一方loadはRDMAの方が遅く、RDMAが常に勝つわけではありません。
結果2: GLM-5.2
| 指標 | TCP | RDMA | 差分 |
|---|---|---|---|
| load時間(平均) | 364秒 | 364秒 | ほぼ同値 |
| JCQ accuracy (1,119問) | 97.86% | 97.5% | -0.36pt (誤差範囲) |
| JCQ 実効速度 | 10.8 tok/s | 15.9 tok/s | +47.2% |
| 長文prompt eval | 171.85 tok/s | 178.29 tok/s | +3.75% |
| 長文decode | 8.06 tok/s | 8.21 tok/s | +1.86% |
興味深いのは改善幅の振れ方です。短文主体のJCQ実測では+47.2%と大きく改善する一方、同じGLM-5.2でも長文promptでは+3.75%/+1.86%とわずかです。GLM-5.2は228GB級の巨大MoEでGPU計算時間そのものが支配的になるため、長いprompt処理では通信オーバーヘッド削減の相対効果が薄まる、と解釈しています。GPT-OSS-120B(60GiB級)の長文ppが+45%だったこととあわせて、RDMAの効果は「モデルサイズ×プロンプト長×生成長×並列度」の組み合わせで大きく変動する、が今回の結論です(並列度の影響は次章で実測します)。自分のワークロードで測らずに一般化はできません。
なお、既知のRPCデッドロック問題(llama.cpp #24813)は今回の環境ではGLM-5.2ロード3/3で非再現でした。accuracyがTCP/RDMAで一致していることは、トランスポート変更が生成品質に影響していないことの確認でもあります。
落とし穴: シャットダウン時のクライアント側クラッシュ(条件特定・回避策実証済み)
RDMA構成で検証中、停止時にクライアント(llama-server)側が以下でクラッシュする事象に遭遇しました。
ggml-rpc.cpp: Remote RPC server crashed or returned malformed response
→ ggml_abort() (ggml_backend_rpc_buffer_free_buffer 起因)
当初は「シャットダウン時にほぼ確実に発生する」ように見えたのですが、gdbでバックトレースを取りつつ条件を切り分けた結果、正確なトリガーは次の通りでした。
クライアントが正常終了処理(リモートバッファのRPC経由解放)を行っている最中に、リモートのrpc-serverプロセスが先に(または同時に)終了させられると発生する。
- クライアントkill直後、間を置かずサーバもkill(レース条件): 11/11でクラッシュ
- クライアントの終了(PID消滅)を待ってからサーバをkill、またはサーバを止めない: 0/4で非再現
TCPトランスポートの同等コードパスは接続断を recv()==0 としてグレースフルに処理するため、同じ操作をしてもクラッシュしません(6/6で正常終了)。つまりこれはRDMAトランスポート固有の、接続断エラーハンドリング欠如によるもので、再現手順・バックトレースを添えてupstreamへの報告を準備しています。
回避策は前述の停止手順そのもの、「クライアントのPID消滅を確認してからサーバを止める」です。ここで注意したいのは、自作の停止スクリプトが「APIポートのclose」を終了判定に使っていると、この回避策を実装したつもりでもレースを踏むことです(実際に一度踏みました)。ポートが閉じた後もバッファ解放処理は続いているため、判定は必ずPIDで行ってください。この点は後日、PID消滅確認方式に改修した停止スクリプトでRDMA構成の起動・測定・停止を再実施し、クラッシュ0件で完走することを確認済みです(旧スクリプト=ポートclose判定では同条件で再現していたため、対照としても成立しています)。
運用判断: それでもデフォルトはTCP
性能ゲートとしては「ppまたはloadで+10%以上」「soakで品質劣化なし」を大きくクリアしており、RDMAは十分実用域です。それでも当面の運用デフォルトをTCPにしたのは、クラッシュ回避が停止順序という運用規律に依存するためです。起動・停止を自動で繰り返す構成では、スクリプトの実装ミスひとつでレースを踏みます。upstreamでエラーハンドリングが修正されたら、RDMAをデフォルトに昇格させる予定です。
逆に言えば、対話的に使う・停止手順を統制できる環境で、prompt処理の重いワークロードなら、RDMAを使う価値は今でも十分あります。
同時リクエスト性能: TTFTと「真の並列」
ここまでの数字は主に単発リクエストの生成速度でした。実際にAPIサーバとして使うなら、「初回応答までの待ち時間(TTFT)」と「複数リクエストを同時にさばいたときの挙動」が重要です。vllm bench serve(vLLMに同梱のサービングベンチマーククライアント)を、llama-serverのOpenAI互換 /v1/chat/completions に向けて測定しました。データセットはShareGPT(実会話由来の多様な長さのプロンプト)、各run 50リクエスト(全run 50/50成功・失敗0)、--max-concurrency で同時リクエスト数(c)を制御します。
TTFT (Time To First Token): リクエスト送信から最初のトークンが返るまでの時間で、ユーザーの「待たされ感」を最も直接的に表します。プロンプト処理時間+スケジューリング待ちを含み、tok/sが良くてもTTFTが悪ければ対話用途では使いものになりません。
ITL (Inter-Token Latency): 生成中のトークン間隔で、decode速度の逆数に相当し、ストリーミング表示の滑らかさを表します。並列度を上げるとスループットは伸びる一方でITLは悪化する、というトレードオフが本節で観測できます。
まず踏んだ罠: --parallel 1 のままc=2を投げると「見せかけの並列」になる
--parallelとスロット:--parallel Nはサーバが同時に処理できるリクエスト数(スロット数)です。--ctx-sizeはスロット間で分割されるため、並列度を上げるならctxも一緒に引き上げる必要があります。ここが1に固定されたまま多重リクエストを受けると、2本目以降はキューで待たされる——というのが以下で踏んだ罠です。
サーバを --parallel 1(スロット1)のまま起動してc=2を投げた結果がこれです。
| 構成 | output tok/s | mean TTFT |
|---|---|---|
| p1, c=1 | 7.32 | 3175ms |
| p1, c=2 | 8.21 | 23909ms |
throughputは+12%しか伸びない一方、TTFTは7.5倍に悪化しました。スロットが1つしかないため、2本目のリクエストは1本目の生成完了までキューで待たされます。ITL(生成速度そのもの)がc=1/c=2でほぼ同じ(119ms前後)であることが、「並列処理ではなくキューイングである」ことの証拠です。ベンチマークでクライアント側の同時数だけ上げてサーバ側スロットを上げ忘れると、この「見せかけの並列」を測ってしまいます。
--parallel を一致させた真の並列スイープ
サーバ側 --parallel をcに一致させ、ctx-sizeもスロット分割を考慮して引き上げて再測定しました(p4はctx 8192、メモリはlocal約110GB/remote約105GBでOOMなし)。
| 構成 (GLM-5.2, TCP) | output tok/s | total tok/s | mean TTFT | median TTFT | mean ITL |
|---|---|---|---|---|---|
| p1, c=1 | 7.32 | 16.64 | 3175ms | 2762ms | 119ms |
| p2, c=2 | 10.86 | 24.15 | 3603ms | 3077ms | 165ms |
| p4, c=4 | 13.74 | 30.80 | 3853ms | 3714ms | 266ms |
読みどころは3つあります。
- スループットはp4で+87.7%(7.32→13.74 tok/s)。4並列までは素直にスケールし、この構成のGPU/RPC帯域にまだ余裕があることを示します。
- TTFTはほぼ横ばい(3.2→3.9秒)。キューイング構成の23.9秒と比べれば、並列スロットを正しく確保することでTTFT悪化は実効的に解消できます。228.5GBモデルの初回応答が同時4リクエスト下でも4秒弱、というのは対話用途に現実的な水準です。
- ITLは並列度に応じて悪化(119→165→266ms)。GPUの計算リソースを時分割で共有するためで、1ユーザーあたりのストリーミング速度は遅くなります。総スループットと個別体感のトレードオフとして設計時に織り込む必要があります。
RDMAは並列時にどうなるか
同条件(p2, c=2)でTCPとRDMAを比較しました。
| 指標 | TCP p2-c2 | RDMA p2-c2 | 差分 |
|---|---|---|---|
| output tok/s | 10.86 | 11.20 | +3.1% |
| mean TTFT | 3603ms | 3572ms | -0.9% |
| median TTFT | 3077ms | 2794ms | -9.2% |
| mean ITL | 165ms | 159ms | -3.8% |
RDMAの優位が、c=2では+3.1%に縮小しました。並列処理ではGPU計算負荷が相対的に増すため、通信オーバーヘッド削減というRDMAの利点の寄与度が下がる — 前章の「GPU計算が支配的になるほどRDMA効果は薄まる」という観察と整合する追加証拠です。median TTFTの-9.2%は残っており、対話レイテンシ重視ならRDMAの価値は並列時にもゼロにはなりません。
参考: GPT-OSS-120Bの2ノードサービング
同じ枠組みでGPT-OSS-120B(2ノードTCP)も測りました。
| 構成 | output tok/s | mean TTFT | mean ITL |
|---|---|---|---|
| p1, c=1 | 45.46 | 414ms | 20.5ms |
| p2, c=2 | 56.24 (+23.7%) | 631ms | 33.1ms |
GLM-5.2の並列スケーリング(+48.4% @p2)と比べて伸び幅が小さいのは、GPT-OSS-120Bは元々decodeが速く(GLM-5.2の5〜6倍)GPU計算がボトルネックになりやすいためと考えられます。並列化の効き方もモデル依存です。なおGPT-OSS-120Bは約60GiB級で1台にも載るため、「あえて2台で動かすと1台と比べて何を失うか」の対比は今後の課題に残しています(2ノード側の条件は再現可能な形で記録済み)。
GLM-5.2とは / なぜ選んだか
GLM-5.2 は智譜(Z.ai / zai-org)が公開した、long-horizon タスク(長時間・多ステップ作業)向けのフラッグシップモデル。総パラメータは 753B、MoE 構成で、HuggingFace 上のアーキ識別子は glm_moe_dsa(MoE + sparse attention 系)。前世代 GLM-5.1 からのアップデートの主眼は、long-horizon 能力を 1M トークンの長コンテキスト上で安定して出せるようになったこと。ライセンスは MIT(地域制限なし)。
なお本記事で使う UD-IQ1_M は、この 753B を1bit級まで削って約228.5GBにしたもの。「1台に載らない」という本記事の出発点は、753BをフルにすればBF16で1.4TB級というサイズ感から来ている。
このモデルを選んだ理由は以下:
- 現行の大きなモデルで、GGUF量子化が複数公開されている
-
UD-IQ1_Mが約228.5GBで、1台(実効110GiB強)には絶対に収まらないが2台なら収まる絶妙なサイズ - ローカルのllama.cppに
GLM_DSA系の実装が入っていた - 先行のGLM-5.1成功結果(約206GB)と比較しやすい
公式のGLM-5.2モデルカードでは、ローカル推論フレームワークとしてSGLang、vLLM、Transformers、KTransformers、Unslothなどが挙げられています。llama.cpp/GGUFはコミュニティ提供の経路として扱うのが妥当です。
まとめ
2026年1月の最初の挑戦は、MPIの最小疎通を通せずに終わりました。ただし当時の記録を読み返すと、敗因の診断(SSHのホスト着地)も正解候補(公式entrypointの流儀)も手元にあり、あと一歩の状態で力尽きていたことが分かります。約5ヶ月後の再挑戦では、2つの有効なルートに到達できました。
1つ目はNVIDIA公式playbookに近い Qwen3-235B-A22B-FP4 + TensorRT-LLMで、1月の敗因だったMPI疎通は公式entrypointスクリプトとpreflightの徹底で解決し、RoCE/NCCL directではなくSocket fallback + OpenMPI TCP + UCX TCPが実用的なルートでした(decode平均約 11.97 tok/s)。
2つ目は GLM-5.2 UD-IQ1_M + llama.cpp RPCで、約228.5GBのGGUFを2台に分散ロードし、listen約6分18秒、decode約 8.21 tok/s で生成できました。サービングとしてもmean TTFT約3.2秒(c=1)、--parallel を正しく設定すれば4並列で+87.7%のスループット(TTFTほぼ横ばい)までスケールします。さらにRPC over RDMAを使えば、ワークロードによってはprompt処理+45%、実効+47%まで伸びる一方、GPU計算が支配的な条件(巨大モデルの長文処理や並列時)では数%まで縮むことも確認しました。停止順序のレース条件と自動ネゴシエーションの仕様という2つの落とし穴があるため、当面の運用デフォルトはTCPを推奨します。
2台のDGX Sparkは、単純な高速化装置というよりも、1台では載らないモデルを現実的に試すための容量拡張として価値がある。NVLinkのない箱の間では細かい分割(TP)は割に合わず、粗い分割(RPC split)を選ぶのが合理的 — それでもGLM-5.2の約228.5GB GGUFがロードでき、同時4リクエストまで実用的なレイテンシでさばけたことで、「2台つなぐ意味」はかなり明確になりました。1月に通らなかった構成が、正しい機構の理解と記録の規律でここまで進む、というのがもう一つの結論です。
参考リンク
- NVIDIA DGX Spark TRT-LLM playbook
- NVIDIA DGX Spark stacked Sparks / TRT-LLM
- NVIDIA NCCL environment variables
- NVIDIA NVLink / NVLink-C2C
- NVIDIA/dgx-spark-playbooks issue #3
- TensorRT-LLM issue #6366
- llama.cpp RPC
- llama.cpp issue #24813
- vLLM (bench serve)
- ShareGPT dataset (ShareGPT_V3_unfiltered_cleaned_split)
- GLM-5.2 / GLM-5.2-GGUF
- GLM-5.1 / GLM-5.1-GGUF
- gpt-oss-120b
