DGX Spark · part 33
[Benchmark] NVFP4 W4A4 在 DGX Spark 上超車 FP8:拔掉 enforce-eager,MoE 從 23 衝到 67 tok/s
❯ cat --toc
TL;DR
DGX Spark (GB10, SM121, 273 GB/s) 上,NVFP4 W4A4 在 MoE daily(Qwen3.6-35B-A3B abliterated)single-stream decode 反超 FP8:FP8 52.0 tok/s、NVFP4 W4A4 66.9 tok/s(+29%),同時少吃 16GB(權重 22GB vs 38GB)。整個差距就一個 flag:--enforce-eager 把 W4A4 卡在 23 tok/s,拔掉(開 CUDA graph)直接 66.9。Part 32 說 CUDA graph 對 dense 幾乎沒用——沒錯,但 MoE 每層打出一堆小 expert kernel,包起來就是關鍵。MTP 開了反而更慢(替代品,不是互補品)。現在是 daily,256K context。
白話版:4-bit 模型一直都很快,只是我手煞車沒放
幾週前我下了個結論:4-bit(NVFP4)權重在這台機器上只是省記憶體的小技巧,不會比較快——某一條路上,我甚至看到 4-bit 版本跑得比 8-bit 還慢。這結論是錯的,而且錯得有點難堪:我開了一個 flag(--enforce-eager),它會關掉一個叫 CUDA graph 的優化,而我在底層編譯器悄悄修好之後,就沒再回頭重測。
我每天在跑的這顆模型是「mixture of experts」——每一層不是做一次大計算,而是做幾十個小計算、只用其中幾個。小計算有固定的「每次呼叫」成本,而這個成本剛好把速度優勢整碗吃掉。CUDA graph 會把那一大堆小呼叫錄成一段預先排好的序列,固定成本就消失了。我一放手讓它這樣做,4-bit 模型就從每秒 23 個字跳到 67——比 8-bit 還快,記憶體只用一半。
這篇的重點其實不是 4-bit,是「X 很慢」這種結論有保存期限。硬體、driver、編譯器都在我腳下換過一輪,我卻一直複述舊數字而不是重跑一次。
前言
四月我幫 DGX Spark「換機油」時發現它的行車電腦根本是別台車的——那是 Part 1(SM121 不是 SM120)。Part 19 接著宣判 NVFP4「是陷阱,FP8 快 32%」。Part 32 收回了一半:純 dense 模型上 NVFP4 是 FP8 的 ~1.5×,但贏在頻寬(bytes 比較少),不是 FP4 核心。
先把場景講清楚:這整套跑在一台 DGX Spark —— NVIDIA 的 GB10 桌機,128GB unified memory、273 GB/s —— 整天餵著我兩個 agent 分身(光羽和霧羽),旁邊還掛著 ComfyUI 生圖生影片。就是這個「單機、記憶體共用」的限制,下面這些數字才有意義。
這篇講另一半。我真正餵給光羽和霧羽的那顆是 hybrid MoE,不是 dense 玩具。在這顆上,NVFP4 W4A4 現在是直接贏——而它之前贏不了,是因為一個我從來沒回去檢查的 flag。
66.9 vs 52.0 tok/s:W4A4 NVFP4 在 MoE 上贏 FP8,同 harness 對打
Qwen3.6-35B-A3B abliterated,single-stream decode,kv-cache fp8,warm,同一個 vllm image(vllm-node-b12x:latest,flashinfer 0.6.11 + cutlass-dsl 4.5.2)。我只換了 model 目錄跟 MoE backend:
| 格式 | MoE backend | tok/s (中位數) | 權重常駐 |
|---|---|---|---|
| FP8 dynamic | triton(預設) | 52.0 | 38 GB |
| NVFP4 W4A4 | flashinfer_cutlass | 66.9 | 22 GB |
也就是快 28.7%、同時省 16GB。這台卡頻寬,這兩個好處其實是同一件事:4-bit 權重只有 FP8 一半大小,而 single-stream decode 本來就卡在權重從記憶體搬出來有多快。
但這裡有個 caveat:兩邊用的 kernel 不對等。NVFP4 走 vLLM autotune 過的 flashinfer_cutlass MoE;FP8 退回 triton,而 vLLM log 明寫 GB10 沒有 tuned 的 fp8_w8a8 設定(Performance might be sub-optimal)。所以這 29% 有一部分是「調過的 cutlass vs 沒調的 triton」,不是純格式差。但這就是兩種格式今天在這台機器上實際在跑的 kernel,而且 4-bit 頻寬的論點方向一致——所以就算純格式差會小一點,要不要換的答案還是很清楚。
整個差距就一個 flag:拔掉 --enforce-eager,23 → 66.9 tok/s
這段最刺。同 model、同 image、同 harness,唯一的差別是 --enforce-eager:
# eager(不開 CUDA graph)
vllm serve /models/qwen36-35b-abliterated-nvfp4-w4a4 \
--moe-backend flashinfer_cutlass --enforce-eager ... # 23.4 tok/s
# 開 CUDA graph(把 flag 拔掉)
vllm serve /models/qwen36-35b-abliterated-nvfp4-w4a4 \
--moe-backend flashinfer_cutlass ... # 66.9 tok/s
--enforce-eager 吃掉了 65% 的吞吐。我當初開它只有一個理由:幾個月前 W4A4 的 compiled path 在 SM121 上會吐 bad PTX 崩掉,eager 是唯一能把它跑起來的方法。我就把「W4A4 在 GB10 只能 eager」記下來,然後不管了。
為什麼 CUDA graph 對 MoE 是關鍵、對 dense 卻只是零頭
Part 32 在 dense 的 Qwen3-8B 上測過,結論是 CUDA graph「主要是消掉 kernel 啟動成本,而 single-stream decode 根本暴露不出這個成本」。在 dense 上這對——W4A4 eager 39.62 vs 開 graph 38.59,等於沒差:
| 模型 | W4A4 eager | W4A4 CUDA graph | 差距 |
|---|---|---|---|
| Qwen3-8B (dense) | 39.62 | 38.59 | ~0 |
| Qwen3.6-35B-A3B (MoE) | 23.4 | 66.9 | +186% |
差在 kernel 數量。dense 一層基本上就一顆大 matmul:一次大的 kernel 啟動,那點固定 dispatch 成本跟 matmul 本身比根本不算什麼。MoE 一層 top-k routing 每次 forward 會打出一堆小的 per-expert kernel。每顆都帶一樣的固定啟動成本,而 batch=1 時這些 kernel 都很小——所以你付的是啟動成本,不是算數本身。CUDA graph 把整段序列錄一次、之後當成一次提交重播,啟動成本就不見了。
所以 Part 32 不是錯,只是把一個 dense 結論套到運算型態完全不同的 MoE 上。「CUDA graph 對 single-stream decode 沒用」這句話,在只有一顆 kernel 時對,一旦變成四十顆就錯。
MTP 開了更慢:NVFP4 跟 speculative decoding 是替代品不是互補品
W4A4 這顆帶 native MTP head,所以我在跑著的 daily 上掃了 num_speculative_tokens 1–4,EN/ZH 成對測:
| 設定 | ZH tok/s | EN tok/s | acceptance |
|---|---|---|---|
| baseline(無 MTP) | 66.7 | 67.1 | — |
| MTP n=1 | 65.8 | 67.0 | 60% |
| MTP n=2 | 56.2 | 56.3 | 47% |
| MTP n=3 | 52.5 | 54.6 | 37% |
| MTP n=4 | 41.0 | 39.4 | 29% |
一路變差。機制才是有趣的地方:MTP 跟 NVFP4 在搶同一個資源。speculative decoding 是把一次權重讀攤到好幾個 output token 上;NVFP4 是把那次權重讀變成一半大小。這台卡頻寬,你用 4-bit 已經把權重 bytes 砍半之後,根本沒剩多少流量讓 MTP 去攤——而 draft pass 加上一路掉的 acceptance,這筆帳就從划算變成不划算。同一顆模型在 FP8 baseline 上之前還 +9%;把 baseline 換成 NVFP4 就翻負了。自洽。
這不是「MTP 在 GB10 沒用」。在 Gemma 4 26B-A4B FP8 上——純 attention、FP8 baseline——MTP 給了 +33%(39 → 52 tok/s,~70% acceptance),abliteration 一毛錢都沒收。Qwen3.6 敗在三個原因疊起來:它是 hybrid(GDN 的 recurrent state 要 rewind 做 speculative verify 很貴,不像 KV cache 重播那麼便宜)、它的 baseline 已經是 NVFP4(上面講的替代效應)、再加 abliteration 把 acceptance 從 ~80% 拉到 60%。三個疊一起,原本勉強為正就變成明顯為負。
解法是 stock cutlass-dsl 4.5.x——而舊的壞掉是我自己搞的
「W4A4 只能 eager」之後變了什麼?toolchain。cutlass-dsl 4.5.0 的 release notes 直接點名「Block Scaled MMA SM120 now works on Spark」,4.5.1 收掉了 CUTLASS #3227 那條 PTX 路徑。我跑的 stock image(flashinfer-python 0.6.11、nvidia-cutlass-dsl 4.5.2,加上已併進 v0.22.0 的 vLLM PR #40082)編 sm_121a block-scaled GEMM、capture CUDA graph,一個 bad-PTX 都沒有。CUTLASS #3082 那個 arch-check PR 還開著,但實務上已經不擋這條路了。
難堪的地方:我那個「只能 eager」其實是自己搞出來的。我一直掛著一個把 cutlass-dsl 降到 4.4.2 的 mod 來硬開 SM121 路徑——而那個降版正好就是弄壞 compiled W4A4 kernel 的兇手。解法從來不是那個 mod,而是把 mod 刪掉、回到 stock 4.5.x。我把「NVFP4 是 wash / 只能 eager」這個過時結論帶了好幾週,就因為編譯器追上來之後我沒重跑那個測試。
收穫
最花時間的地方——那個我不再質疑的 flag。 23→67 這一跳是一行改動,我幾週前就能找到。代價不是算力,是把幾個月前的一次量測當成不會再變的鐵律。每一分鐘花在「解釋為什麼 NVFP4 是 wash」上,都是在替一個我早該重跑的數字辯護。
通用的診斷方法——看 CUDA graph 時,把 kernel 數量跟頻寬拆開看。「single-stream decode 卡頻寬,所以啟動成本不重要」只有在每層一顆 kernel 時才對。先數 kernel:dense = 一顆大 GEMM(graph ≈ 沒作用),MoE = 一堆小 expert GEMM(graph 是關鍵)。同一套邏輯也能看出為什麼 MTP 跟 NVFP4 互斥——兩個都在搶每 token 的記憶體流量,所以不疊加。
通用原則——「X 很慢」有保存期限;bug 是沒重測,不是工具。 toolchain 一動過(cutlass / flashinfer / vLLM / driver / OS),每一個「X 跑不動 / X 很慢」的結論在重跑之前都該打問號。而當一個舊 workaround 結果是弄壞你的兇手,那是留著它的人的問題,不是 workaround 的問題。
已部署:W4A4 NVFP4 上 daily,256K context,MTP 關
光羽和霧羽連的那個端點(qwen36-abliterated,port 8000)現在是 W4A4 NVFP4,MTP 關掉。因為省下來的記憶體直接拿去開 KV 空間,我把 context 拉到這顆的原生上限——Qwen3.6 的 max_position_embeddings 是 262144、rope_theta 1e7,所以 256K 不用 YaRN、也不掉品質。256K 下 KV pool 還能撐 13 路同時跑滿 context,兩個 sib 綽綽有餘:
vllm serve /models/qwen36-35b-abliterated-nvfp4-w4a4 \
--served-model-name qwen36-abliterated \
--moe-backend flashinfer_cutlass \
--kv-cache-dtype fp8 \
--gpu-memory-utilization 0.50 \
--max-model-len 262144 \
--max-num-seqs 4 --max-num-batched-tokens 8192 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder \
--enable-prefix-caching --trust-remote-code
如果你在 GB10 上追同一條路,checklist:
- 跑 stock cutlass-dsl 4.5.x(flashinfer 0.6.11+)。把任何把 cutlass-dsl 釘在 4.4.x 的 mod 刪掉——那才是逼你 eager 的東西。
flashinfer_cutlass這條 NVFP4 MoE backend 要的是 W4A4(activation 也量化);我那顆 W4A16 weight-only 版本它不收。(vLLM v0.22.0 另外有一條 Marlin W4A16 NVFP4 MoE 路徑,我只是沒用。)用--moe-backend flashinfer_cutlassserve。- 不要設
--enforce-eager。MoE 上它吃掉 ~65% 吞吐。 --max-num-batched-tokens ≥ 2096(hybrid Mamba cache block 對齊),不然 engine 啟動就 assert。- NVFP4 hybrid 上跳過 MTP——它是來替代你已經贏到的頻寬,不是來加碼的。
常見問題
- DGX Spark (GB10) 上 MoE 模型,NVFP4 比 FP8 快嗎?
- 開了 CUDA graph 就快。Qwen3.6-35B-A3B (abliterated) 我實測 FP8 52.0 tok/s、NVFP4 W4A4 66.9 tok/s,single-stream 快約 29%,而且少吃 16GB 記憶體(權重 22GB vs 38GB)。但同一顆 W4A4 加上 --enforce-eager 只剩 23 tok/s。
- 為什麼 CUDA graph 對 MoE 幫超大,對 Part 32 的 dense 卻沒用?
- 差在 kernel 數量。dense 一層就一顆大 matmul,eager 的每顆 kernel 啟動成本可以忽略,開不開 CUDA graph 幾乎一樣(Part 32 是 39.62 vs 38.59)。MoE 一層 top-k routing 會打出一堆小的 per-expert kernel,啟動成本就變主角——包進 CUDA graph 後 W4A4 從 23 跳到 67。
- NVFP4 MoE 在 GB10 上要不要開 MTP / speculative decoding?
- 這顆不要。num_speculative_tokens 1 到 4 掃下來只有越來越慢(66.7 → 65.8 → 56.2 → 52.5 → 41.0,acceptance 從 60% 掉到 29%)。NVFP4 跟 MTP 在省同一件事——每個 output token 要搬的記憶體——權重已經 4-bit 了,MTP 沒剩多少可省。換成純 attention 的 Gemma 4 26B-A4B FP8,MTP 還是 +33%。
- W4A4 NVFP4 之前在 SM121 跑不動,現在為什麼可以?
- cutlass-dsl 4.5.x。我跑的 stock image(flashinfer 0.6.11 + cutlass-dsl 4.5.2)能編出 sm_121a block-scaled path、也能乾淨 capture CUDA graph。之前「只能 eager」是因為我掛了一個把 cutlass-dsl 降到 4.4.2 的 mod——toolchain 早就往前走了,我卻好幾週沒回去重測。