DGX Spark 4台でGLM-5.2をvLLMサーブする

DGX Spark を4台並べて、GLM-5.2 を vLLM でしばらくサーブした備忘録。

参考元: GLM-5.2-QuantTrio-TP4-DCP2-4x-DGX-Spark

ノード名は melchior / balthasar / casper / artaban

項目
ハードNVIDIA DGX Spark ×4(合計 800 W)
モデルQuantTrio/GLM-5.2-Int4-Int8Mix(405 GB / 128 shards)
並列度TP=4 / PP=1 / DCP=2(Ray不使用)
Attention backendB12X_MLA_SPARSE
KV cachefp8_ds_mla、323,712 tokens
コンテキスト163,840 / max seqs 6
投機デコードMTP k=2(draft TP=4)
APIOpenAI互換
実測 decode19.7 tok/s(実トラフィック下)

ネットワーク

DGX Spark には、

  • RJ-45(管理用)
  • ConnectX-7 の QSFP ×2(200 GbE 表記)

があるが、管理 / HTTPアクセス用とNCCLレーンを分離した。

用途経路
SSH / 管理RJ-45
NCCL 転送f1 レール 10.42.0.1-4(MTU 9000)

RAILIPS / IFNAME / HCA に 管理系 の IP を入れてはいけない。 起動はするが NCCL がとんでもなく遅い経路を掴んで低速サーブになる。

ConnectX-7

あちこちで言及されているが、癖がある。 100GbEのモジュール を使った2ノードでの実測は次の通り。

NCCL all-reduce構成直結
TCP socket (NCCL_NET=Socket)2.03GB/s
RoCE 1レーン (rocep1s0f0)12.12GB/s
RoCE 2レーン (rocep1s0f0,roceP2p1s0f0)11.89GB/s
RoCE 4レーン21.81GB/s ≒ 174 Gb/s

ConnectX-7だが、Infinibandは非対応。

ib_write_bw を使うと、2ポート線速が出る。単位が混ざってるのは許して。

NCCL all-reduce構成直結
全ペア片方向 f0レーン98.01Gb/s
4QP並列49.01Gb/s x4本 = 196Gb/s

基本の設定

MTU=9000
NCCL_NET_PLUGIN=none
NCCL_IB_MERGE_NICS=1
NCCL_IB_HCA=rocep1s0f0,roceP2p1s0f0,rocep1s0f1,roceP2p1s0f1
NCCL_IB_GID_INDEX=3

ノードの準備

  • DGX OS:最新
  • DGXのハードウェアドライバ:最新
    • 闇が深い。古いドライバを使うと、FAN=2RPMで固定されててアチアチとか、最新にするとファンが死んでチップが焼けたとか色々ある。ご調査推奨。
  • DockerとNVIDIA Container Toolkitのセットアップ
  • モデルウェイトの配置
    • 基本 melchior でダウンロードし、rsyncで他ノードへコピー。NFSとか使っても良いかもね

コンテナイメージ

イメージは vllm-dcp-glm52:e232d26 を使っている。

正直に書いておくと、melchior/balthasarcasper/artaban でイメージの中身が微妙に違う。ビルドし直した時期がずれた結果そうなった。

melchior, balthasar : vllm-dcp-glm52:e232d26 -> sha256:a43cce1c...
casper,   artaban   : vllm-dcp-glm52:e232d26 -> sha256:e65082a2...

普通なら揃えるべきだが、この構成で安定稼働している実績があるので、触らぬ神に祟りなし。


並列度の決め方(TP / PP / DCP)

ウチの環境の結論 : TP=4 / PP=1 / DCP=2

Ray は不使用

vLLM のドキュメントには、マルチノードのランタイムとして Ray のほかに multiprocessing も使える旨が書かれている。

4台程度なら Ray クラスタを立てて ray start / ray stop を回すより、素の起動のほうが取り回しが良い。落ちたときの復旧も単純になる。 過去に Ray でやっていた頃、ログが “Loading…100%” で凍るという現象に何度も遭遇した。Ray をやめてvLLMネイティブにしたところ、解消した。

PP=1

GLM-5.2 は MLA 系のアーキテクチャで、PP を入れるとパイプラインバブルとメモリ配置の両方で損をする。TP=4 で全ノードに横一列に割ってしまうほうが素直だった。

DCP=2

DCP は tp_size を割り切れる必要があり、world size は増えない(TP グループの GPU を再利用する)。TP=4 なので DCP は 2 か 4。今回は 2 にしている。

DCP を切ると速いが、待ち時間が長くなった。

うちの環境ではコーディングエージェントの利用が9割で、UX維持のため DCP=2 を採用している。

構成KV 容量実効 ctxdecodeキュー待ち発生率
DCP=2259,968〜278,272163,84024.74 tok/s4.45%
DCP off131,392131,07227.27 tok/s17.8%

Attention backend と KV キャッシュ

B12X_MLA_SPARSE

backend は B12X_MLA_SPARSE を指定している。 このスパース構造に対応した backend でないと、そもそも性能が出ない。というより、stock の vLLM には GB10(sm_121)向けの sparse-MLA 実装が当時は無かった。

関連して --hf-overridesindex_topk_pattern を渡している。

fp8_ds_mla

KV cache dtype は fp8_ds_mla

B12X_MLA_SPARSE が受け付けるのは auto / fp8 / fp8_ds_mla のみ。dtype 一覧に見える turboquant_*nvfp4 は別の attention backend 用で、ここでは通らなかった。

KV容量

KV 容量は固定値ではなく、--max-num-batched-tokens で動く。

BATCHTOKKV 容量
2048355,840
4096323,712〜327,424
8192259,968

バッチ用のワークスペースと KV が同じプールを食い合っているので、BATCHTOK を上げると KV が減る。試した結果、 4096 で利用することにした。

max-num-seqs 6 は少なく見えるが、BATCHTOK = 8192 の 259,968 tokens の KV を 163,840 の最大コンテキストで割ると、理論上そもそも1.5本しか同時に走らない。実際にはリクエストが常にフルコンテキストを使うわけではないので6本にしているが、これ以上増やすとプリエンプションが始まる。

GMU 0.89 も同じ理由。GB10 はドライバが約 12 GB 予約するので、総メモリ 121.7 GiB に対して GPU から使えるのは実質 107 GiB 前後。405 GB / 512 GB という余白の薄さでは、0.9 を超えると OOM で落ち、0.85 まで下げると KV が足りなくなる。0.89 は試行錯誤の結果で得た。

最終的にはこんな感じの設定に落ち着いた。

パラメータ
--max-model-len163,840
--max-num-seqs6
--max-num-batched-tokens4,096
--gpu-memory-utilization0.89

MTP(Multi-Token Prediction)

投機デコードとして MTP を使っている。

  • num_speculative_tokens = 2
  • draft 側も TP=4

効果は実測 tokens/step 2.54。k=2 なので理論上限は 3.0(本体1 + draft2)、そこに対して 2.54 なので、受理率はかなり良く、80%前後をキープ。

ただし、k を増やすほど通信律速がPrediction向上分を食っていく。試した組み合わせは次の通り。虫食いなのは許して

構成k=2k=4k=5k=6
DCP2, batch4096, cg823.9624.3320.61
DCP2, batch8192, cg6424.7421.08
DCP off, batch8192, cg6427.2724.15

k=2 がどのスタックでも最適。k=4 は誤差の範囲、k≥5 は明確に悪化する。ステップ時間は概ね step ≒ base + 14·k [ms] で伸びるので、k を増やすほど1ステップが重くなり、受理率の向上分がなくなる。


起動

起動は自前スクリプトに環境変数を渡す方式にしている。

RAILIPS="10.42.0.1 10.42.0.2 10.42.0.3 10.42.0.4" \
IFNAME="enp1s0f1np1" \
HCA="rocep1s0f1,roceP2p1s0f1" \
DCP=2 \
MTPK=2 \
CTX=163840 \
SEQS=6 \
BATCHTOK=4096 \
CGMAX=64 \
GMU=0.89 \
  ./deploy/glm52-dcp-launch.sh up

各変数の意味。

変数意味
RAILIPSf1レールの全ノードIP
IFNAMENCCL に使わせるインターフェース名
HCARoCE デバイス(phys_port_name を確認して選ぶ)
DCPdecode context parallel size
MTPKMTP の投機トークン数(0/1のトグルではない)
CTXmax-model-len
SEQSmax-num-seqs
BATCHTOKmax-num-batched-tokens
CGMAXCUDA Graph の最大キャプチャサイズ(SEQS×(MTPK+1) 以上に)
GMUgpu-memory-utilization

コンテナに渡している NCCL 環境変数。

NCCL_NET=IB
NCCL_IB_DISABLE=0
NCCL_IB_GID_INDEX=3
NCCL_IB_HCA=rocep1s0f1,roceP2p1s0f1
NCCL_IB_MERGE_NICS=1
NCCL_SOCKET_IFNAME=enp1s0f1np1
GLOO_SOCKET_IFNAME=enp1s0f1np1
NCCL_CROSS_NIC=1
NCCL_CUMEM_ENABLE=0
NCCL_NCHANNELS_PER_NET_PEER=4
NCCL_IB_QPS_PER_CONNECTION=4
NCCL_IGNORE_CPU_AFFINITY=1

docker run 側の引数。

  • --device /dev/infiniband:/dev/infiniband — これが無いと NCCL は黙って TCP ソケットにフォールバックするっぽい挙動なので、お守りとして。
  • --ulimit nofile=1048576:1048576 — 無いとノード間 NCCL が Call to socket failed: Too many open files で落ちる。

監視

エンドユーザーに OTEL を入れてないので、 vLLM の Metric を Prometheus + Grafana で見る。 24時間の中央値(実トラフィック下・アイドル含む)だと次のような値だった。

指標
decode20.0 tok/s(p90 で 21.6)
tokens/step2.51
step 時間125 ms
KV 使用率中央値 6.6% / 24hピーク 75.4%
待ちキュー0
サーマルスロットリング0

隔離ベンチ(他のトラフィックなし、BATCHTOK=8192)では 24.74 tok/s

ネットワーク

現在は100GbE 1レールだけで運用している。実測では次の通り。実トラフィックでは別のところに律速がありそう。

ether-speed-roce-rail.png

Decode Rate / スループット / Prefix cache

decode-rate-and-agg-thrput.png

prefix-cache-tokens.png

kv-capacity-vs-usage-tokens.png

ttft.png TTFT=40分とかなってますね……人間は寝てると思うので良いのですが……

消費電力

PDU読みの消費電力は4台合計で、7日間の実測が min 195 W / 平均 568 W / max 848 W。アイドルと全力で4倍以上ぶれるので、「◯◯W の機材」と一言で言えるものではなかった。

Nvidia-SMI読みの消費電力と、ACタップ点での4台合計の消費電力は次の通り。

pwr-comp-nvidia-smi.png

pwr-comp-pdu-4total.png


参考

See Also