~/blog/qwen38-flash-next-nvfp4-dgx-spark-vllm-recipe

DGX Spark · part 45

[Benchmark] Qwen3.8-Flash-Next NVFP4 在 DGX Spark 跑 41.7 tok/s,比 llama.cpp 快 78%

2026-09-0614 分鐘閱讀#dgx-spark#gb10#vllm#nvfp4English
cat --toc

TL;DR

NVIDIA 官方的 Qwen3.8-Flash-Next NVFP4 checkpoint,在一台 DGX Spark 上原本只有 17 tok/s。tonyd2wild 的配方不重編 vLLM,拿固定 hash 的 nightly image 加九個 bind-mount 上去的 overlay 檔,把 MTP 開起來、PLE 查表放硬碟、KV 用 FP8。我在 GX10 上重現:40 題中位 41.7 tok/s、散文 27.4、程式 42–45、四路並行聚合 47.2,32K prefill 1,666 tok/s,視覺與工具呼叫都正常。同一台機器的 llama.cpp 席位是 23.38。這篇前半是照著做的步驟與參數,後半是我在同一天早上算出「修好也只有 23」然後下午被 41.7 打臉的紀錄。

DGX Spark 系列 #45 封面:等距技術插畫,一台方正的深色機箱敞開著,九塊發著青綠色光的小模組正卡進機箱側面的插槽,機箱本體沒有任何改動。

有些改裝不用進廠。原廠車開回家,九個零件用卡扣裝上去,油門踩下去就是另一台車;哪天不想要了,拆掉就是原廠。這篇講的配方就是這種改法:vLLM 官方 nightly image 一個檔都不改,九個 Python 檔用 docker 的唯讀 bind mount 蓋在容器裡對應的位置,啟動參數帶對,速度從 17 變 41.7。

這是 DGX Spark 系列第 45 篇。第 43 篇把同一個模型的 llama.cpp UD-Q4_K_XL 版塞進 GB10 的 121 GB 統一記憶體,散文 20.51 tok/s,拿到的是「262K context 不用拿任何東西去換」。這篇換 runtime 不換模型:NVIDIA 自己出的 NVFP4 checkpoint,跑在 vLLM 上,速度快了七成多。配方不是我的,是 tonyd2wild 的;我做的事是在自己的機器上把它重跑一遍、把數字對上、把坑記下來。

你會拿到什麼:中位 41.7,散文 27.4,四路並行 47.2

先看「harness 中位」那一列,那是 40 題八個類別合起來的中位數,最接近日常混用的感受。散文那列是所有類別裡最慢的,因為 MTP 猜字在自由文字上接受率最低,第 5 篇在 V4-Flash 上也量到同樣的現象。

量什麼我的 GX10作者的 Spark同機 llama.cpp 席位
40 題 harness 中位(單流)41.743.923.38
散文27.429.020.51(第 43 篇)
程式42.0–45.044.3
四路並行:每流 / 聚合26.1 / 47.226.4 / 47.1
32K prefill1,666 tok/s,TTFT 17.3 s

單位都是 tok/s。llama.cpp 那格的 23.38 是我們平常探測各台機器用的通用提示詞,不是 harness,所以只能當量級參考;第 43 篇的 76.65 是 n-gram 推測在「改一個檔案」這種重複度高的工作上量到的,那類工作仍是 llama.cpp 比較快,兩邊各有各的擅長題型。

MTP 是 multi-token prediction:模型多長一顆小的「草稿頭」,一次猜好幾個 token,主模型再一口氣驗收。這個 checkpoint 的草稿頭原本在 vLLM 上載入就炸,後面會講;配方修好它之後,一步平均吐出三個多 token,單流速度才從 17 跳到 40 以上。

需要什麼

  • 一台 DGX Spark(GB10,128 GB 統一記憶體)。作者與我都是單機 TP1。
  • NVMe 空間約 155 GB:nvidia/Qwen3.8-Flash-Next-NVFP4 124 GiB(133 GB),vLLM nightly image 22.2 GB。
  • Docker 加 NVIDIA container toolkit,能跑 docker run --gpus all
  • 啟動前系統要有 25 GiB 以上的 MemAvailable。作者的 README 說每次啟動前都要 drop_caches,launcher 裡已經包了;我實測載入期間 MemAvailable 最低 25.6 GiB,32K prefill 時最低 12.9 GiB。
  • 冷啟動 13 分鐘的耐心。權重 585.81 秒,MTP 草稿頭再 69.16 秒。

PLE 是 per-layer embedding:這個模型約 180B 參數(125B 主體、51B PLE、4B MTP)裡有 51B 是一張 n-gram 查表,每個 token 只讀幾列,不是拿來乘的矩陣。配方預設 PLE_MODE=staged,表留在 NVMe,每個 forward 前先把要用的列撈進來,所以記憶體只裝剩下的 76.48 GiB。

照這個順序做

# 1. 拿配方
git clone https://github.com/tonyd2wild/Qwen3.8-Flash-Next-NVFP4-DGX-Spark ~/src/tonyd2wild-nvfp4
cd ~/src/tonyd2wild-nvfp4 && git log -1 --format='%h %ad' --date=short
# 我跑的是 d83f10c 2026-09-05

# 2. 拿 checkpoint(124 GiB / 133 GB)
hf download nvidia/Qwen3.8-Flash-Next-NVFP4 --local-dir /home/coolthor/models/qwen38-fn-nvfp4-nvidia

# 3. 拿固定 hash 的 nightly image(22.2 GB)
docker pull vllm/vllm-openai:nightly-8a728663c1c3eeace834a95f5654fa653cc1998c

# 4. overlay 檔與 launcher 放到 launcher 預期的位置
mkdir -p ~/patches && cp -r single-spark-vllm-tp1/patch ~/patches/qwen4exp-ple-mmap   # launcher 的 PATCH_DIR 預設值就是這個目錄名
cp single-spark-vllm-tp1/launch/qwen38fn-nvidia-tp1.sh ~/patches/

# 5. 起
PORT=8000 NAME=qwen38fn bash ~/patches/qwen38fn-nvidia-tp1.sh
docker logs -f qwen38fn 2>&1 | grep -m1 'Application startup complete'   # 約 13 分鐘

launcher 的預設值就是作者 2026-09-05 的單機最佳解,不用帶任何環境變數:PLE 表放硬碟用 staged gather、解碼用 CUDA graph 但不走 torch.compile、MTP 三個 token、草稿頭只在 id 最小的 65,536 個 token 裡猜(BPE 合併順序大致就是詞頻順序)、六路並行、prefill 一次 4,096 token、KV cache FP8、記憶體佔用上限 0.80、context 262,144、思考模式預設關。overlay 一共九個檔:四個是作者自己的 PLE / MTP 實作,四個是上游 PR 的原始碼直接蓋上去(PR #55375 的 PLE conv 步幅修正、PR #54846 的 QSA 路徑 FP8 KV),最後一個 modelopt.py 是上游檔加作者自己的兩處修正,讓 MTP 草稿頭找得到自己的量化設定;作者註明 sfxnz 與 MiaAI-Lab 同一天各自獨立修了同樣的洞。完整展開的 docker run 命令在文末進階段。

起來之後這樣冒煙:

curl -s http://<your-gx10-ip>:8000/v1/chat/completions -H 'content-type: application/json' -d '{
  "model": "qwen38-flash-next",
  "messages": [{"role":"user","content":"用三句話解釋什麼是 speculative decoding"}],
  "max_tokens": 200, "temperature": 0,
  "stream": true, "stream_options": {"include_usage": true}
}'

四個坑

串流模式拿不到 usage。 vLLM 的串流回應預設不帶 token 計數,要在請求裡加 stream_options: {"include_usage": true};llama-server 串流會自動附,所以從那邊搬過來的量測腳本第一次跑會算不出 tok/s。

vLLM 會檢查 model 名字,llama-server 不會。 換席位當晚我在容器 log 看到一串 404,來源是我自己筆電上兩支半年沒動的腳本,它們還在送舊模型的 id。llama-server 不管你送什麼名字都照答,所以那個錯誤藏了兩個月。換 runtime 前先把所有客戶端送的 model 名掃一遍。

--network host 直接佔 8000。 跟既有席位互斥,起新的之前先停舊的;我這次停機 33 分鐘(含 13 分鐘載入與全部驗收)。

prefix caching 刻意關著。 launcher 預設 --no-enable-prefix-caching,作者註明是因為 GDN 層開 prefix cache 會炸(他引 vLLM issue #54173,自己沒驗)。我也沒開,所以上面的數字都是無 prefix cache 的。

進階:同一天早上我算出「修好也只有 23」

不讀這節不影響使用。這節是四段紀錄,每段照「遇到什麼、原本以為怎麼解、做了什麼、結果、回頭看哪裡想錯」寫。

09-05:NVIDIA checkpoint 無 MTP 的地板是 17

遇到的問題:想知道 NVIDIA 官方 NVFP4 checkpoint 在 GX10 上到底多快。社群公開的測試有人報 36.5(jschmied 的 GB10 階梯),但那是別的 runtime 組合。

原本以為:NVFP4 權重比 UD-Q4_K_XL 小,記憶體頻寬相同,每個 token 讀得少就該跑得快,至少不會輸 llama.cpp 的 23.38。

做了什麼:自己 build 一個帶 PLE mmap 的 vLLM image,跑 batch=1、temp 0、思考關、每發獨立 nonce、max_tokens 800,散文與程式各三發。

結果:16.62–17.01 tok/s,散文與程式同速。有效權重讀取量 9.27 GiB/token 換算是 154–158 GiB/s(165–169 GB/s),約規格 273 GB/s 的六成。比 llama.cpp 席位慢 28.6%。

回頭看:「權重小就快」在這裡不成立,因為單流解碼的天花板不在權重大小,在每個 token 要走多少次記憶體(第 32 篇量過 NVFP4 快是因為壓縮,不是那顆 FP4 運算單元);而散文與程式同速這件事說明了另一件事,沒有 MTP 就沒有題型差,第 42 篇的 27B 從 12.3 到 65 全靠草稿預算,那台上散文與程式差近兩倍,全是 MTP 接受率造成的。

09-06 凌晨:MTP k=2 載入就死

遇到的問題:--speculative-config '{"method":"mtp","num_speculative_tokens":2}' 在載入權重的時候:

AttributeError: Layer mtp.layers.48.mlp.experts has no parameter 'w2_weight_scale_inv'
  for checkpoint weight 'mtp.layers.48.mlp.experts.0.down_proj.weight_scale_inv'

原本以為:記憶體或旗標問題,調一下就過。

做了什麼:讀 checkpoint 的 hf_quant_config.json 與 vLLM 的 qwen4_exp/nvidia/mtp.pymodelopt.py

結果:兩個不相干的問題疊在一起。第一,checkpoint 把 MTP 的 experts 量化成 FP8_BLOCK_SCALES(group 128),key 叫 mtp.layers.0.mlp.experts;而 vLLM 把 MTP 層編號成 mtp.layers.48,draft config 只 remap 了 ignored_layersexclude_modules,沒 remap quantized_layers,所以查不到量化設定。第二,就算 prefix 對了,modelopt.py 的 RoutedExperts 分支只認 FP8 / NVFP4 / W4A16_NVFP4 / MXFP8,整棵樹 grep 不到 FP8_BLOCK_SCALES。我估要動三個檔、加 80 到 155 行。

回頭看:這段的診斷後來被證明是對的,作者的 modelopt.py overlay 修的正是這兩處。錯的是下一段那個估算,不是這段。

09-06 早上:紙上算出「修好也只 ≈23」,然後我停了

遇到的問題:要不要為了 MTP 去修 vLLM 的 ModelOpt 解析器。

原本以為:社群回報 MTP 帶來 +35%,那是量在 26.1 上的;我的地板 17.0,乘 1.35 是 23,跟正在服役的 llama.cpp 席位 23.38 打平。修一個 80 行的 patch 換一個打平的結果,不值得。所以我決定轉向,先不修 vLLM,把 MTP 的力氣花在 llama.cpp 那邊。

做了什麼:寫下結論,沒開 ticket、沒下載、沒停席位。

結果:同一天下午,讀 tonyd2wild 的 repo。同一份 NVIDIA checkpoint,同一款硬體,他量到 eager 加 MTP3 就有 35.2,全套 43.9。晚上我在自己的機器上重跑,41.7。

回頭看:我的算式只有一個乘數,「MTP 大約 +35%」,那是別人在別的組態量到的一個數,我把它當成 MTP 的性質。實際上 MTP3 一步平均吐出三個多 token,加上草稿頭只在 65,536 個 id 裡猜(FR-Spec 的做法,作者註明是 MiaAI-Lab 先用在這個模型上)、PLE 表放硬碟省下的記憶體、不走 torch.compile 的解碼 CUDA graph,這幾個我當時一個都沒算進去。要否決一個實驗,只能用另一個實驗。

09-06 晚上:重現差 −2% 到 −12%,沒查

遇到的問題:八個類別逐一對照,我的數字全部低於作者,散文 27.4 對 29.0(−5.5%),JSON 44.3 對 49.3(−10%),摘要 31.0 對 35.4(−12%);但四路並行幾乎一樣,26.1 / 47.2 對 26.4 / 47.1。

原本以為:同 image、同 overlay、同 harness、同 checkpoint,應該在 ±3% 內。

做了什麼:只對了一次,沒有重跑。

結果:落差存在,原因沒驗。可能的原因有三個:作者的 log 提到三次斷電後重跑,他的環境可能有我不知道的差異;我的機器上 earlyoom 與其他常駐服務在跑;單流的類別數字每類只有五題,本身就有浮動。並行數字吻合,表示問題多半不在 GPU 或記憶體頻寬。

回頭看:我原本以為同一份配方就該量出同一個數,漏掉的是單流數字對排程與浮動比並行數字敏感得多。要把落差說清楚得每類至少跑三輪,那是下一張 ticket 的事。

完整參數

以下是 launcher 在全預設下真正執行的命令,IP 換成你的。overlay 的來源檔在作者 repo 的 single-spark-vllm-tp1/patch/,本機複製成 ~/patches/qwen4exp-ple-mmap/(launcher 的 PATCH_DIR 預設值)。

docker run --gpus all -d --name qwen38fn --restart no \
  --network host --ipc host --shm-size 32g --ulimit memlock=-1:-1 \
  -v /home/coolthor/models/qwen38-fn-nvfp4-nvidia:/models/qwen38fn:ro \
  -v /home/coolthor/qwen38fn-vllm-cache:/root/.cache \
  -e HF_HUB_OFFLINE=1 -e TRANSFORMERS_OFFLINE=1 -e VLLM_ENGINE_READY_TIMEOUT_S=3600 \
  -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True -e CUTE_DSL_ARCH=sm_121a \
  -e TORCH_CUDA_ARCH_LIST=12.1a -e FLASHINFER_CUDA_ARCH_LIST=12.1a -e FLASHINFER_DISABLE_VERSION_CHECK=1 \
  -e VLLM_USE_DEEP_GEMM=0 -e VLLM_USE_V2_MODEL_RUNNER=1 \
  -e QWEN4EXP_PLE_MMAP=1 -e QWEN4EXP_PLE_STAGED=1 -e QWEN4EXP_PLE_MMAP_THREADS=64 \
  -e QWEN4EXP_DRAFT_VOCAB=65536 \
  -v $P/ple_layer.py:$VP/models/qwen4_exp/nvidia/ple_layer.py:ro \
  -v $P/ple_mmap.py:$VP/models/qwen4_exp/nvidia/ops/ple_mmap.py:ro \
  -v $P/model_state.py:$VP/models/qwen4_exp/nvidia/model_state.py:ro \
  -v $P/mtp_draft_vocab.py:$VP/models/qwen4_exp/nvidia/mtp.py:ro \
  -v $P/upstream-overlays/ops_ple.py:$VP/models/qwen4_exp/nvidia/ops/ple.py:ro \
  -v $P/upstream-overlays/ops_qsa.py:$VP/models/qwen4_exp/nvidia/ops/qsa.py:ro \
  -v $P/upstream-overlays/qsa.py:$VP/models/qwen4_exp/nvidia/qsa.py:ro \
  -v $P/upstream-overlays/platforms_interface.py:$VP/platforms/interface.py:ro \
  -v $P/upstream-overlays/modelopt.py:$VP/model_executor/layers/quantization/modelopt.py:ro \
  vllm/vllm-openai:nightly-8a728663c1c3eeace834a95f5654fa653cc1998c \
    /models/qwen38fn --served-model-name qwen38-flash-next qwen3.8-flash-next \
    --host 0.0.0.0 --port 8000 --trust-remote-code \
    --quantization modelopt --tensor-parallel-size 1 \
    --max-model-len 262144 --max-num-seqs 6 --gpu-memory-utilization 0.80 --max-num-batched-tokens 4096 \
    --no-enable-flashinfer-autotune --no-enable-prefix-caching \
    --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_xml \
    --default-chat-template-kwargs '{"enable_thinking": false}' \
    --speculative-config '{"method":"mtp","num_speculative_tokens":3}' \
    --compilation-config '{"mode":0,"cudagraph_mode":"FULL_DECODE_ONLY","cudagraph_capture_sizes":[4,8,12,16,20,24]}' \
    --kv-cache-dtype fp8_e4m3
# P=/home/coolthor/patches/qwen4exp-ple-mmap   VP=/usr/local/lib/python3.12/dist-packages/vllm
# 啟動前:sync; echo 3 | sudo tee /proc/sys/vm/drop_caches

載入完的幾個數字:Model loading took 76.48 GiB;KV cache 可放 1,130,661 個 token,在 262,144 context 下是 4.31 路並行;短提示 TTFT 0.21–0.28 秒;32K 提示 28,903 個 token 進去 TTFT 17.3 秒(prefill 約 1,670 tok/s),針放在中間的 PELICAN-7 有撈到;穩態 MemAvailable 14.3–14.9 GB,swap 用了約 4 GB,earlyoom(-m 6)整晚沒動。視覺用一張 64×64 的紅色 PNG 問顏色回「Red」;工具呼叫給 get_weather schema 問台北天氣,回 {"city":"Taipei"}finish_reason=tool_calls

八個類別的完整對照(我 / 作者,tok/s):散文 27.4 / 29.0,程式 42.0 / 44.3,推理 44.6 / 45.6,JSON 44.3 / 49.3,HTML 46.8 / 47.6,敘事 27.7 / 28.5,摘要 31.0 / 35.4,格式化 30.3 / 32.4。作者另有六路並行聚合 68.8,我沒跑那一階。

同一份 checkpoint 還有一條更快的路:MiaAI-Lab 自己重新量化的 checkpoint(99 GiB,106 GB),散文單流 46.3、四路聚合 108.1。它同樣不重編 vLLM,啟動腳本從 image 裡抽出原始檔再打 patch;代價是再下 106 GB,而且配方 repo 是 AGPL-3.0(checkpoint 本身 Apache-2.0)。我的順序是先把 NVIDIA 這份處理好,那條看狀況再說。

收穫

最花時間的地方

不在跑,在早上那個算式。我算了半天,得出一個錯的結論,而推翻它只要讀一個 repo 的 README 加 13 分鐘載入。

說到底,這整件事最讓我在意的不是那 41.7,是我早上那個算式的形狀:它只裝得下我已經知道的機制,所以它的答案永遠是「沒空間了」。

常見問題

Qwen3.8-Flash-Next 的 NVFP4 版在一台 DGX Spark 上能跑多快?
用 tonyd2wild 的單機配方(vLLM nightly + overlay,MTP3、PLE 表放硬碟、FP8 KV),我在自己的 GX10 上量到 40 題 harness 中位 41.7 tok/s、散文 27.4、程式 42–45,四路並行聚合 47.2。作者同硬體的數字是 43.9 / 29.0 / 44.3 / 47.1,我的落在 −2% 到 −12% 之間。
跑這個配方要不要自己重編 vLLM?
不用。它用固定 hash 的 vLLM nightly 官方 image,再把九個 Python 檔用 docker 的唯讀 bind mount 蓋到容器裡對應的路徑上。要退回原版,不掛那九個檔就好。
128 GB 統一記憶體裝得下 124 GiB 的 checkpoint 加 262K context 嗎?
裝得下,因為 51B 參數的 PLE 查表留在 NVMe 上,需要時才讀,不進記憶體。載入完 vLLM 回報權重佔 76.48 GiB,KV cache 有 1,130,661 個 token 的空間,穩態 MemAvailable 還有 14 GB 多;冷啟動要等 13 分鐘。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。