~/blog/qwen38-flash-next-tp2-two-dgx-sparks

DGX Spark · part 46

[vLLM] 兩台 DGX Spark 跑 Qwen3.8-Flash-Next TP=2:51.9 tok/s(單機 41.7)

cat --toc

TL;DR

兩台 DGX Spark 用 QSFP112 直連,跑 vLLM 跨節點 TP=2。同一顆 Qwen3.8-Flash-Next NVFP4、同一組 40 題,中位速度從單機 41.7 tok/s 到 51.9,KV 池從 103 萬 token 到 213 萬。跨機那條 185 Gb/s 的線在推論時用掉 0.3%,剩下的全是餘裕 —— 單流 decode 搬的是 activation 不是權重。開工前要先把兩台的 driver 與 kernel 對齊,權重兩台各放一份完整的。

前言

要在一本電話簿裡找一個名字,兩個人各翻一半會比一個人翻完快。tensor parallel 做的就是這件事:把模型權重切成兩半,兩台機器各讀一半,湊出同一個答案。

Part 45 把 Qwen3.8-Flash-Next 的 NVFP4 配方在一台 DGX Spark 上跑到 40 題中位 41.7 tok/s。這篇幫那顆模型加上第二台機器:兩台之間拉一條 QSFP112 銅纜,起 vLLM 的跨節點 TP=2,然後量同一組題。

結論先給,兩件事:裝得下更大的模型(兩台合計 242 GiB,一台是 121),而且更快 —— decode 從 41.7 到 51.9 tok/s、KV 池從 103 萬 token 到 213 萬。跨機那條線在推論時還有 99.7% 的餘裕。


你會拿到什麼:51.9 tok/s 與兩倍的 KV 池

同一顆 checkpoint、同一組 40 題 harness、同樣關思考、同樣 concurrency 1。這表看最後一欄:

單機 TP=1兩台 TP=2
40 題中位 decode41.7 tok/s51.9 tok/s+24.5%
KV 池1,027,392 token2,128,927 token2.07 倍
TTFT0.26 s0.17 s−35%

⚠️ 兩邊的「中位」不是同一種:41.7 是單機那一發跑完 40 題的中位,51.9 是 TP=2 三發(各自的 40 題中位)再取中位。 TP=2 那三發分別是 56.6、51.7、51.9,spread 9.5%。頭條要用中位的 51.9,不是最高的 56.6 —— 三發裡挑最快的那發去算加速比,那是選擇性報告,我第一版就是這樣算成 +35.7% 的。

三題探針拆開看,加速不是均勻的:

題型單機兩台 TP=2
程式45.2561.79
英文散文28.1637.87
繁中散文15.2433.16

中文那格從 15 跳到 33,是全表最大的變化。但那不是 TP=2 的功勞 —— 兩個組態的 PLE 模式不一樣,而 MTP 的 draft head 會用到 PLE 那張表。單機那席跑 PLE_MODE=staged(表放硬碟、逐列取),兩台這席跑 PLE_MODE=none。單一變因的測試我還沒跑,所以中文那格只能算「換組態之後的觀察」,不能掛在 TP=2 帳上。


需要哪些東西:兩台各一份完整的,不是各一半

這是最容易踩的一條。vLLM 跨節點 tensor parallel 不是各放一半權重 —— 切分發生在載入之後、在張量層,每個節點都要看得到完整的模型目錄。

所以成本要這樣算:

  • 磁碟 = 模型大小 × 2。Qwen3.8-Flash-Next NVFP4 逐 blob 加總是 132,680,249,378 bytes = 124 GiB(132.7 GB),兩台就是 265 GB。
  • 記憶體預算 = 單台 × 2。兩台各量到 121 GiB MemTotal,合起來 242 GiB —— ⚠️ 那是全機共用的統一記憶體,OS 與 CPU 也吃同一池,能給模型的要另外量 available。
  • 容器 image 兩台都要有。22.2 GB 那份,worker 那台沒有的話會卡在 docker pull 十幾分鐘,而你在 head 那邊只看到它連不上。

開工前四件事要對齊:

# 1. driver 與 kernel 兩台必須同版(我們對到 580.173.02 / 6.17.0-1032-nvidia)
nvidia-smi --query-gpu=driver_version --format=csv,noheader
uname -r

# 2. image 兩台都要在,比 size 不要只比 tag
docker image inspect vllm/vllm-openai:nightly-<sha> --format '{{.Size}}'

# 3. 權重搬過去(走 CX7,換掉預設的 AES cipher)
rsync -a --info=progress2 -e "ssh -c aes128-gcm@openssh.com" \
  ~/models/qwen38-fn-nvfp4-nvidia/ <spark-2>:~/models/qwen38-fn-nvfp4-nvidia/

# 4. launcher 兩台同一份,比 sha256 不要只跑 bash -n
sha256sum ~/launch/qwen38fn-nvidia-tp2.sh

那 132.7 GB 走 CX7 用了 2 分 43 秒,813 MB/s。瓶頸是 ssh 的 AES 不是線 —— 那條線只用掉 4%。

第 4 條那個 sha256sum 是踩出來的。我第一次只在 head 改 launcher,worker 跑到 LANE must be A or B 就退出;而且只跑 bash -n 不夠 —— 網路不穩的時候 scp 可能沒把新版真的寫進去而你不會收到錯誤,這時語法檢查驗到的還是舊檔案。


怎麼接線:一個實體 port 有兩個邏輯介面

接線本身照 NVIDIA 的 connect-two-sparks playbook 走,但有三個點文件沒講清楚,而每一個都會讓你以為硬體壞了。

第一,介面要用 ibdev2netdev 回報 (Up) 的那組。 插 port 0 的話是 enp1s0f0np0enP2p1s0f0np0

第二,那兩個名字不是打錯 —— 一個實體 port 確實有兩個邏輯介面,分屬兩個 PCIe domain,兩個都要給 IP 才拿得到全頻寬。 這就是為什麼官方的量法要跑兩條 stream 再相加。只配一個介面的話你會拿到一半,然後開始懷疑線。

# /etc/netplan/40-cx7.yaml(chmod 600)
network:
  version: 2
  renderer: NetworkManager
  ethernets:
    enp1s0f0np0:
      addresses: [192.168.200.12/24]
    enP2p1s0f0np0:
      addresses: [192.168.201.12/24]

另一台換成 .13

第三,RoCEv2 要用的那個 GID index 要等 IPv4 配好才會出現。 我們這台配好之後 GID 表長這樣:0 是 link-local RoCEv1、1 是 link-local v2、2 是 IPv4 v1、3 是 IPv4 v2,所以我們用 index 3。⚠️ 3 不是通則 —— GID 表是按實際配了哪些位址長出來的(多配 IPv6 就會多幾格),要用哪一格得自己讀 GID 表、對位址家族與型別。

沒配 IPv4 之前只有 0 跟 1,這時候跑 RoCEv2 會得到:

Failed to modify QP to RTR
Unable to Connect the HCA's through the link

那句話讀起來像硬體連不上,其實是你還沒配 IP

MTU 不用動。官方自己的範例輸出也是 Mtu : 1024[B](netdev 1500)就拿到 92 Gb/s —— 我原本把 MTU 9000 當成最後一個沒轉的旋鈕,方向是錯的。

接好之後照官方的雙 stream 量法驗:

# 官方的量法是四個終端、兩組 port 同時跑,不是依序跑兩次
# server 側(對面那台)各開一個:
ib_write_bw -d rocep1s0f0   -p 12000 -D 12 --report_gbits
ib_write_bw -d roceP2p1s0f0 -p 12001 -D 12 --report_gbits
# client 側(這台)同時打過去:
ib_write_bw -d rocep1s0f0   -p 12000 -D 12 --report_gbits 192.168.200.13
ib_write_bw -d roceP2p1s0f0 -p 12001 -D 12 --report_gbits 192.168.201.13

🔴 兩條要同時在跑,依序跑兩次各自都會拿到接近單條上限的數字,加起來是假的。

我們兩條各 92.57 Gb/s,加起來 185.14 Gb/s。那個 92.57 跟官方文件 NIC1 的範例輸出是同一個數字,看到它就對了。

⚠️ 那個數字有 PCIe 的解釋,而且跟上面「兩個 domain」是同一件事:lspci -vv 看 CX7 是每條 link Speed 32GT/s, Width x4,dmesg 對其中一條印 126.028 Gb/s available PCIe bandwidth。所以單一條 stream 過不了約 126 Gb/s —— 這正是為什麼加總一定要用兩條。官方自己的範例也因此是不對稱的:92.57 + 97.28 = 189.85 Gb/s。


怎麼起 TP=2:worker 先,head 後

跑的是原作者 Kai / Tech2Wild 的 SPEED profile,我只改四個環境變數(CX7 的 IP、NCCL_IB_ADDR_RANGE--served-model-name 給兩個 id、MODEL_HOST),其餘旗標一個都沒動。

PLE_MODE=none GRAPHS=nocompile MTP=3 SEQS=6 \
CHUNK=4096 KV=fp8_e4m3 GMU=0.70 MAXLEN=262144

起的順序有講究:

# worker 那台先(--headless,它不開 API)
NODE_RANK=1 ./qwen38fn-nvidia-tp2.sh --headless

# head 那台後
NODE_RANK=0 ./qwen38fn-nvidia-tp2.sh

worker 先起、head 後起。 反過來的話 head 會找不到第二個 rank。

🔴 GRAPHS 不准開 compile Inductor 在 autotune 的時候會複製 n-gram 表,TP=2 每個 rank 多吃約 24 GiB —— 作者的腳本註解裡寫著這件事在 2026-09-05 讓兩台 Spark 餓到重開。跑 nocompile 的話 mem_avail 收在 25-27 GB,安全。

起來之後不要只看 /health:

# ① API 活著
curl -s localhost:8000/v1/models | python3 -m json.tool

# ② 真的跑一發(TP=2 少一個 rank 就服務不了,所以這一發同時驗了兩台)
curl -s localhost:8000/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"qwen38-flash-next","messages":[{"role":"user","content":"137 乘以 24 等於多少?只回數字。"}],"max_tokens":20,"temperature":0}'

# ③ KV 池真的翻倍了
docker logs <container> 2>&1 | grep 'GPU KV cache size'

第 ② 條最能確認服務是不是真的正常:TP=2 只要少一個 rank 就服務不了,所以 head 回得出答案這件事本身就證明兩台都活著。 這比分別 ping 兩台可靠 —— 我們的 Tailscale 路徑會偶爾降級到 200 ms,ping 得通不代表 rank 還在。

log 裡會有一行 distributed_init_method=tcp://192.168.200.12:29533。那是 rendezvous 的引導通道,不是資料通道 —— 看到 tcp 別以為掉回 socket 了。


進階:除錯與實驗紀錄

不讀這節不影響使用。上面那些照著做就跑得起來;這節是路上壞掉的東西、以及幾個我原本猜錯的機制。

為什麼線接好了只有 13 Gb/s?

症狀:兩台都協商 200 Gb/s、ibdev2netdev 兩個介面都 (Up)ib_write_lat 的 t_typical 是 1.99 µs(完全正常)。三個訊號全綠,但 ib_write_bw 只有 13.2-13.9 Gb/s,是線速的 6.7%。

我當時以為是線ethtool -m 報那條是 OEM 的 Q112P-400G-0.5M(我查不到這個型號的官方產品頁,只能說模組自己這樣報),不在 NVIDIA 的核准清單上(核准的是 Amphenol NJAAKK0006 跟 Luxshare LMTQF022-SD-R)。一條沒核准的銅纜在 200G 上勉強跑、靠 FEC 撐著,聽起來非常合理。

所以去查 FEC 的計數。5 秒流量期間 rx_corrected_bits_phy 增加 10,214、rx_err_lane_0/1 增加 2,209 跟 8,005 —— 看起來像找到兇手了。結果 rx_pcs_symbol_err_phy 全程是 0:所有錯誤都被 RS-FEC 修掉了。⚠️ 我原本拿應用層吞吐當分母算出 pre-FEC BER 約 1.5e-7 —— 那個分母不合法(該用實際的物理位元數),同樣的計數對著 200 Gb/s 線速是 1.0e-8 量級。要講 BER 得先抓對應的物理位元增量,我沒抓。這裡只能說錯誤全被修掉、線沒問題。

而且它對什麼都不敏感:

1QP/64KB   13.49      8QP/64KB   13.25
1QP/1MB    13.30      4QP/256KB  13.24
memlock unlimited     13.46      ← 放寬 memlock 無效

TCP 也一樣慢(iperf3 開 8 條流只有 15.9 Gb/s,零重傳),所以不是 RDMA 專屬的問題。連不經過線都慢:同一台自己 loopback 是 11.43 Gb/s

最後是看 PHY 的位元組計數才找到問題在哪。 10 秒傳輸,發送端 tx_bytes_phy 只增加 17.86 GB —— 換算 14.3 Gb/s。接收端 rx_bytes_phy 的增量跟它完全相等,而 tx_discards_phyrx_discards_phyrx_pause_ctrl_phyrx_out_of_buffer 全部是 0

沒有任何封包被丟。收到的跟送出的一樣多。那台 NIC 根本就沒把更多資料送出去。

這一刀把「線在丟東西」跟「host 沒在送」分開了 —— 這兩件事看到的現象一模一樣,兩邊都是頻寬很低。

低頻寬的兩個原因:tx_bytes_phy 增量小且 discards 為 0 代表主機沒在送;增量大且 discards 有數字代表送了但路上掉
同一個症狀分岔成兩個原因,而 PHY 的位元組計數配 discard 計數可以切開它們。實測走左邊。
看到什麼意思
tx_bytes_phy 增量小 + discards 全 0host 沒送出去
tx_bytes_phy 增量大 + discards 有數字送出去了但在路上掉

真正的 root cause 在 NVIDIA 開發者論壇(thread 363461 / 370035 / 373538):插上或重插 QSFP 之後,系統會停在降速狀態直到重新開機。論壇的實例是 13.39/13.76 → 重開後 98.01/98.01(thread 363461 第 10 樓)。⚠️ 但 thread 373538 那樓的復原明講是完全斷電不只是重開,別把兩種處置混為一談。至於 dmesg 那句 Detected insufficient power on the PCIe slot (27W),說它是 cosmetic 的是論壇使用者 mashie —— 我查不到他掛 NVIDIA 員工身分,所以那只能算一位論壇參與者的說法,不是官方承認。

我們重開之後 92.57 × 2 = 185.14。後來升級驅動再驗一次,92.56 × 2 = 185.12。

回頭看我原本的預想:我把「三個訊號全綠」當成「硬體層沒問題,所以問題在設定」,於是一路往 QP 數、訊息大小、memlock、MTU 那些旋鈕找。那個推論的漏洞是 —— ethtool 報的 200000 Mb/s 是協商結果,ib_write_lat 的 1.99 µs 是單筆延遲,兩個都不量吞吐。三個綠燈沒有一個在看我要的那件事。

netplan 的設定檔出廠就是壞的

官方的接線步驟就是寫 netplan,而兩台的 netplan generate 都直接死:

Invalid YAML: control characters are not allowed

兇手是 /etc/netplan/90-NM-2edf06d6-....yaml,715 bytes 全部是 NUL。兩台同檔名、同大小、同日期(2025-09-29,出廠),而那個 UUID 不在任何一台的 NetworkManager 連線清單裡 —— 它是個孤兒。搬到別的目錄之後才能繼續。

⚠️ 附帶一個會誤導人的:netplan get renderer 會以 UnicodeDecodeError 崩掉。那是 CLI 在印上面那句 parse 錯誤訊息的時候自己解不了碼 —— 看到那個 traceback 別以為 netplan 壞了

我早上的預想是:跨機頻寬 23 GB/s 對上本機 273 GB/s,差 12 倍,所以 TP=2 一定會被網路拖住。

那個推論的錯在比錯了東西。 該比的不是兩個頻寬的比值,是每個 step 實際要搬多少位元組

所以去量。而且不能靠「NCCL 沒印警告」—— NCCL_DEBUG=WARN 只在出問題時說話,沉默不是證據。改成量 PHY 的位元組:

99 秒推論期間,CX7 搬了 6.87 GB ≈ 0.555 Gb/s
discards 全程 0,worker 的 tx 與 head 的 rx 完全對稱
⇒ 用掉 185 Gb/s 的 0.3%

原因是單流 decode 跨機交換的是activation,不是權重。權重讀取是本機的事(每張卡讀自己那一半),跨機只要同步一個 hidden 維度的張量。兩者差了三個數量級。

這也解釋了為什麼 NCCL_CHANNELS=8(TJ Klug 的兩台配方釘 8)在我們這邊量不出差別 —— 一條只用了 0.3% 的 link,給它八個 channel 沒有東西可以搬。

有效權重頻寬是 1.41 倍,而通訊不是瓶頸

如果 decode 是純粹在讀權重、而權重剛好切一半,加速比應該接近 2。實測不是。

關掉 MTP 當代理去量有效權重頻寬,三題的比值一致到 0.2%,都落在 1.41 倍。而上面那 0.3% 排除掉的是頻寬不夠這條;collective 的固定延遲與同步成本沒有被那個數字排除,要另外做延遲剖析才算。

剩下的嫌疑有兩個:固定的每 step 開銷(kernel launch、排程、all-reduce 的固定成本),以及兩個組態之間 PLE 模式與 gpu-memory-utilization 的差異。要分辨是哪一個,還得補跑一組對照:單機 + PLE_MODE=none + 關 MTP。 那一組我還沒跑,所以這 0.59 倍現在只能說「不是網路」,不能說是什麼。

⚠️ 我曾經把一個 step 時間拆解(34 ms 讀權重 + 20 ms 其他)當成已知事實寫出來。那個拆解假設 all-reduce ≈ 0,而拿它去對 9.27 GiB/token 交叉檢查的時候,推出來的每 rank 頻寬不成立。兩個量測點解不了三個未知數,已經撤掉。

五個組態都沒比現行的快

作者的 results 目錄有 84 份 JSON、其中 69 份是完整的 40 題 harness,所以「還沒轉的旋鈕」可以直接查表不用自己跑:

組態TP=2 中位
speed_mtp4_ixs(MTP=4 + index share)55.2
ram_mirror53.7
speed_dv64k(縮減詞表 drafting)53.2
speed_async(ASYNC_SCHED=1)52.4
speed_nccl8(NCCL_CHANNELS=8)51.9
我們的 SPEED51.9 中位 / 56.6 單發最高

這五組的中位落在 51.9-55.2,而我們自己三發是 51.7-56.6、spread 9.5% —— 表裡每一個差距都小於我們自己重跑的變異,所以看不出哪一組真的比較快。(單發最高的 56.6 是我們本機的數字,不在那份公開 corpus 裡;corpus 的 TP=2 concurrency-1 最高是 55.2。)

真正有得換的是另一個方向:CONTEXT profile(PLE_MODE=mmap GRAPHS=piecewise MTP=4 SEQS=8 GMU=0.80)把 KV 池推到 587 萬 token,代價是速度掉到 35.8。要長 context 就走那條。

品質掉的那 0.046 不是 TP=2 的代價

我們的 auto_score 從單機的 0.889 掉到 TP=2 的 0.843,掉 5.2%。三發重跑都是 0.843,spread 是 0.000 —— 看起來非常確定。

我當時就認定品質真的掉了。但同一組設定重跑三次都拿到一樣的分數,只能說這個組態的結果很穩,不能說換組態之後的差距代表什麼。

把那 69 發的 auto_score 排開:

TP1 各組態   0.815 ~ 0.919
TP2 各組態   0.821 ~ 0.906
我們 TP1 0.889 · 我們 TP2 0.843

兩個範圍幾乎完全重疊。TP1 裡有一發 0.815 比我們的 TP2 還低,TP2 裡有三發 0.906 比我們的 TP1 還高。0.046 的差小於組態之間的正常變動(TP1 內部自己就橫跨 0.104)。

這兩台在辦公室,沒有 BMC

  • 實測根本沒有退路。 升級核心之後 /boot 還留著舊的 vmlinuz,但 modinfo -k <舊核心> -F version nvidiaERROR: Module nvidia not found —— 模組包已經被移掉了。從 GRUB 開回舊核心會得到一台看不到 GPU 的機器。唯一的安全網是一次只動一台。
  • Tailscale 會降級而不是斷線。 ping 通,但 RTT 從 3 ms 升到 120-200 ms、SSH 握手逾時。那是連線掉回 DERP relay 的典型特徵,不代表機器故障。分辨法是先 ssh 一台不在同一個地點的節點;繞路可以從 head 經 CX7 去操作 worker。
  • TP=2 活不過重開。 兩台各一個手動 docker run --restart no,而單機那席的 systemd unit 還是 enabled ⇒ head 一重開就自動退回單機。這個還沒修。

收穫

最花時間的不是 TP=2,是那條線。 接線到跑起來的時間裡,九成花在 13 Gb/s 那題上,而答案是重開機。

可以搬去別的地方用的是那個判別法:當你看到「頻寬很低」,先分清楚是「沒送出去」還是「送了但掉了」。 送出的位元組少而 discards 是 0,那就不是路上的問題,是源頭沒在送。這條對任何網路瓶頸都成立,不限 RDMA。

另一條是要確認服務活著就直接送一個請求,別用 ping —— 理由在上面那節。

至於第二台機器買到什麼 —— 記憶體 121 GiB 變 242 GiB、KV 池 2.07 倍、decode +24.5%,而那條線還有 99.7% 沒用到。兩件事同時成立:一台裝不下的模型現在裝得下,而且跑得更快。 想要速度翻倍的話這台不是答案,但容量、頻寬與餘裕都是實打實的。

常見問題

兩台 DGX Spark 跑 tensor parallel 會快兩倍嗎?
不會。同一顆 Qwen3.8-Flash-Next NVFP4、同一組 40 題,單機一發的 40 題中位是 41.7 tok/s,兩台 TP=2 三發的中位是 51.9 tok/s,大約 +24.5%。翻倍的是 KV 池(103 萬 token 到 213 萬),不是速度。
跨機的 tensor parallel 會不會被網路卡住?
單流 decode 不會。99 秒的推論期間,兩台之間的 ConnectX-7 只搬了 6.87 GB,約 0.555 Gb/s,佔那條 185 Gb/s link 的 0.3%。單流 decode 跨機交換的是activation,不是權重,差了三個數量級。
TP=2 的話模型權重是不是兩台各放一半?
不是。vLLM 跨節點 tensor parallel 要求每個節點都有一份完整的模型目錄,切分發生在載入之後、在張量層。所以磁碟成本是模型大小乘二,記憶體預算是單台乘二。
ConnectX-7 接好之後 ib_write_bw 只有 13 Gb/s 是線壞了嗎?
先重開機。插上或重插 QSFP 之後,系統可能停在降速狀態直到重新開機,而 ethtool 會照樣報 200000 Mb/s、RoCE port 照樣 ACTIVE、RDMA 延遲照樣正常。重開後我們兩條 stream 各 92.57 Gb/s。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。