~/blog/qwen4exp-dgx-spark-262k-no-tradeoff

DGX Spark · part 43

[DGX Spark] 1770 億參數 + 262K context 一起開:Spark 上不用二選一

2026-08-2811 分鐘閱讀#dgx-spark#gb10#llama.cpp#moeEnglish
cat --toc

TL;DR

單台 DGX Spark 跑 Qwen3.8-Flash-Next(176.94B),unsloth UD-Q4_K_XL 約 104 GiB,原生 262,144 context 全開,不用在量化階和 context 之間二選一。載入 4 分 40 秒、常駐約 104 GB。改檔案類的工作配 ngram 投機解碼 76.65 tok/s,寫散文 20.51,18,092 token 的 prefill 是 366.19。關鍵設定是把那張 51B 的 n-gram 查表釘在 CPU 上,其餘全進 GPU。

DGX Spark 系列 #43 封面:等距技術插畫,一個寬淺的金屬池注滿青綠色發光液體,池中並排放著一塊實心石墨方塊與一座半透明晶格塔,兩者都還有餘裕;左側三個小杯子各自滿溢,連一塊都裝不下。

前言

行李箱塞不下的時候,你會開始算:帶厚外套就不能帶鞋,兩個都要就得換一個更大的箱子。

上個禮拜我把同一顆模型放到三張改裝 2080 Ti 上(合計 66 GB VRAM),整晚都在算那題。想跑品質好一階的 UD-Q4_K_XL?104 GiB 塞不進去,只能上 36 層、十二層掉到 CPU,吐字從 25 掉到 16.02 tok/s。想要原生 262,144 context?層數得從 48 退到 40,滿載之後 decode 剩 4.05。每一格都是拿一樣東西換另一樣。細節在那篇

這篇換一台 121 GB 統一記憶體的 DGX Spark。104 GiB 的權重直接放、262,144 直接開,沒有那一格要算。

Part 42 量過這台的頻寬天花板,這篇看它扛不扛得起一顆 1770 億參數的模型。

記憶體怎麼分配:51B 不用待在快的那一側

先看數字對不對得起來。權重 104 GiB、統一記憶體 121 GB,中間只剩十幾 GB 要塞下 KV cache——聽起來很緊。

實際上沒那麼緊,因為那 176.94B 裡有 51B 是一張 n-gram 查表(模型把常見的兩三個 token 組合記成一筆,用的時候查出來就好)。它不是一張要整片拿去相乘的權重矩陣,每個 token 只查出稀疏的幾列,所以不需要待在快的那一側

把它釘在 CPU、從 NVMe 分頁進來,GPU 那側就鬆了:

去處內容
CPU + NVMe(mmap)per_layer_token_embd,那張 51B 的查表
GPU其餘 48 層權重
KV cache(dense K/V)262,144 token 約 6 GiB

121 GB 統一記憶體的分配:51B 的 n-gram 查表用 -ot per_layer_token_embd=CPU 加 -lm mmap 放在 CPU、從 NVMe 分頁進來;48 層權重用 -ngl 999 進 GPU;262,144 token 的 KV cache 約 6 GiB。KV 這麼小是因為 48 層裡只有 12 層是全注意力,其餘 36 層是線性注意力、狀態大小固定不隨 context 長

看這張表的第三列:262K 的 dense K/V 只要約 6 GiB。 這顆 48 層裡只有 12 層是全注意力,其餘是線性注意力(狀態大小固定、不隨 context 長),所以 K/V 每個 token 只有約 24 KiB。

⚠️ 但那不是全部。在同一個 commit 裡,那 12 層每層還多一份 QSA indexer 的快取,每個 token 再多約 9 KiB。兩個加起來約 33 KiB/token,262K 大概 8.25 GiB。長 context 在這個架構上還是便宜,只是別把 6 GiB 當成全部的開銷。

直接抄這組

build/bin/llama-server \
  -m Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf \
  --alias qwen38-flash-next \
  -lm mmap -ot per_layer_token_embd=CPU \
  -ngl 999 -c 262144 -np 1 \
  --spec-type ngram-mod \
  --temp 1.0 --top-p 0.95 --top-k 20 \
  --host 127.0.0.1 --port 8199

載入約 4 分 40 秒,穩態常駐約 104 GB。

四個旗標值得單獨講:

-ot per_layer_token_embd=CPU 把那張查表釘在 CPU。-lm mmap 讓它從 NVMe 分頁進來,不是整個讀進記憶體。這兩個是上面那張表能成立的原因。

-ngl 999 其餘全上 GPU。這顆 48 層,寫 999 就是全部。

-np 1 要寫死。 同時開多個請求會撞 qwen4exp.cpp 的 indexer cache assert,直接 abort。

build 有一個預設值不會報錯,但會讓輸出整個壞掉

llama.cpp 是透過 PR #27742 加進 qwen4exp 支援的。這個 PR 在 2026-08-27 合併,我量的時候還開著,所以下面用的是合併前的 6c5afc86a

git clone https://github.com/ggml-org/llama.cpp ~/llamacpp-qwen4exp
cd ~/llamacpp-qwen4exp
git fetch origin pull/27742/head:pr27742 && git checkout pr27742

cmake -B build -DGGML_CUDA=ON \
  -DCMAKE_CUDA_ARCHITECTURES=121 \
  -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.0/bin/nvcc \
  -DGGML_NATIVE=OFF -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build --target llama-server -j 16

⚠️ -DCMAKE_CUDA_COMPILER 那行不能省。 Spark 上 /usr/local/cuda 走 alternatives、預設指向 13.2,而 PR 討論串有人做過控制變因的 A/B(同原始碼、同旗標、同 GGUF,只換這個參數):用 13.2 編出來的 binary 不報錯、不 assert、server 正常啟動,但輸出是亂碼。同一顆 binary 跑別的架構完全正常,所以一開始很難懷疑是 binary 有問題。

-DGGML_NATIVE=OFF 也是必帶的,否則 ggml-cpu 偵測到 ARM 的時候,會在 Grace 上給出 gcc 不吃的旗標。

編完先問一句再說。 因為失敗是安靜的,我用手上已有的小檔(UD-IQ1_S,68 G)先問 The capital of France is

" Paris. Given a list of countries and their capitals, answer the question..."

先問這一句,省下的是四個小時——要是下載完 104 GB 才發現 binary 在吐垃圾,那四小時就白花了。

四個數字,因為「一個數字」在這裡沒有意義

開了 ngram 投機解碼之後,同一台機器的速度會因為你在做什麼而差三倍多。先看這張表前兩列的差距:

工作decode t/s
寫散文(冷)20.51
改檔案(冷)76.65
改檔案(重複第 2 / 3 次)88.02 / 78.74
prefill(18,092 token)366.19

ngram 投機解碼不用另外養一顆小模型,它從你貼進去的 context 裡找重複的片段來草擬。你丟一份檔案要它改一行,輸出幾乎全都已經在輸入裡;你叫它寫散文,沒東西可抄,每個 token 都得老實生成。

對外要引用的是冷啟動的 76.65,不是重複之後的 88——重複之所以快,是因為 ngram 認得你上一次的答案,那不是這個設定的速度。

聰不聰明:UD-Q4_K_XL 在我的七題拿 6/7

我另外設計了七題想分辨量化階的退化:解釋投機解碼、多步年齡推理、寫一個 merge_intervals、多步算術配精確輸出格式、三重指令限制、40 到 60 行的程式、嚴格 JSON。

UD-Q4_K_XL6/7。唯一失敗的那題是程式長度——程式碼通過 py_compile、通過我寫的所有 assert,只是寫了 38 行,而我的判準要求 40 到 60 行。 那是我的尺太挑,不是模型不會。

⚠️ 這七題是在另一台機器(三張 2080 Ti)上跑同一份 UD-Q4_K_XL 權重量的,Spark 這邊沒有重跑。同樣的權重、同樣的判準,但不是同一台機器的數字,引用時要分清楚。

那台老卡跑得一樣快,只是每一格都得挑

回頭對前言那三張 2080 Ti:

DGX Spark3× 2080 Ti
跑得動的量化UD-Q4_K_XL(104 G)UD-IQ4_XS(88 G)
改檔案(冷)76.6577.56
262K 的代價沒有decode 掉到 4.05

同一顆模型在兩台機器上:三張 2080 Ti 想要 UD-Q4_K_XL 只能上 36 層、decode 掉到 16.02,想要 262K context 層數退到 40、decode 剩 4.05,取平衡用 UD-IQ4_XS 配 128K 是 23.14;DGX Spark 則是 UD-Q4_K_XL 加 262K 兩個一起開、載入 4 分 40 秒,沒有第二個選項因為不用選。改檔案冷啟動 2080 Ti 77.56 對 Spark 76.65

看這張表的第二列:吐字速度幾乎打平,76.65 對 77.56。

換句話說,這台十倍價錢的機器沒有買到速度。它買到的是第三列——2080 Ti 那邊每開一格 context、每上一階量化,都要從別的地方扣回來;這邊不用。

那個差別在你只跑一種工作的時候看不出來,在你想同時要「好一點的量化」跟「整份 codebase 的 context」的時候才會出現。


進階:三段量測紀錄

不讀這節不影響使用,上面那組指令抄走就能跑。

下載卡在 74 KB/s,關掉 Xet 之後 88 分鐘跑完

遇到什麼問題hf download 抓 104 GiB,log 預估只剩約 74 KB/s。

原本打算怎麼解:以為是家裡的網路或 HF 那邊在限速,想等離峰再抓。

所以做了什麼:先拿 HF CDN 直接做 range request 對照,量到約 4.30 MB/s。所以不是網路。

結果:問題在 huggingface_hub 1.28.0 的 Xet 傳輸層——它會做 adaptive concurrency,實測把並行從十條一路降到一條。保留已下載的 partial,加 HF_HUB_DISABLE_XET=1 重啟,三個大 shard 改走一般 HTTP 並行,1 小時 28 分 26 秒完成。

HF_HUB_DISABLE_XET=1 hf download \
  unsloth/Qwen3.8-Flash-Next-GGUF --include "*UD-Q4_K_XL*" \
  --local-dir ~/models/qwen38-flashnext-gguf/UD-Q4_K_XL

回頭看哪裡想錯了:我把「慢」預設歸給網路,因為那是最常見的原因。而分辨這兩者只要一發 range request——先跑一次繞過那個傳輸層的下載,三十秒就知道要往哪查。

載入的時候整台機器從網路上消失三分鐘

遇到什麼問題:跑 262K 的正式量測,載入到一半 SSH 斷了。

原本打算怎麼解:以為 OOM 把機器打掛,準備去看 dmesg 撿屍。

所以做了什麼:等。約三分鐘後 Tailscale 自己回來。

結果:主機沒有 reboot、dmesg 沒有任何 OOM kill 紀錄、server 的 health check 正常、模型也順利載入。整個載入 4 分 40 秒,那三分鐘落在中間。

回頭看哪裡想錯了:我把「連不上」讀成「掛了」。實際上是載入把記憶體與 I/O 吃滿的那段時間,網路那邊搶不到資源。斷線是症狀,不是死因——這台只有一個記憶體池,載入 104 GB 的時候它是真的很忙。下次先等,不要急著判死。

三次同樣的請求,第三次的輸出不一樣

遇到什麼問題:同一個改檔請求送三次,前兩次 SHA256 完全相同,第三次不同。投機解碼理論上不該改變輸出。

原本打算怎麼解:懷疑投機解碼在這個架構上不精確,準備去追 llama.cpp 那條路徑。

所以做了什麼:先看 diff。只有一處:

--- copy-1.txt
+++ copy-3.txt
@@ -1,4 +1,3 @@
-```python
 from __future__ import annotations
@@ -129,4 +128,3 @@
         return # skip other tensors
-```

結果:程式本體三次完全相同,差的只是第三次沒把 Markdown 的 code fence 包上去。而這台的 server 是照上面那組指令起的,帶著 --temp 1.0,request 本身沒有覆寫 temperature——那是取樣的隨機性

回頭看哪裡想錯了:投機解碼保證的是「分佈不變」,不是「隨機取樣之下也逐字相同」。我把兩件事混在一起,差點去追一個不存在的 bug。要真的驗它精不精確,得把 temperature 壓到 0 或固定 sampler seed——這台我還沒補測。(同一顆模型我在 2080 Ti 那台用 temperature=0 跑過三次,SHA256 完全相同,那組才是有效的一致性證據。)

環境

DGX Spark(GB10、121 GB 統一記憶體、sm_121a、aarch64、驅動 580.159.03)。llama.cpp 走 PR #277426c5afc86a,用 CUDA 13.0.88 編、CMAKE_CUDA_ARCHITECTURES=121(CMake 自己會換成 121a)。模型是 unsloth/Qwen3.8-Flash-Next-GGUFUD-Q4_K_XL,四個 shard 合計約 104 GiB。

⚠️ configure 會印出兩個版本號:The CUDA compiler identification is NVIDIA 13.0.88 是實際用來編的,Found CUDAToolkit ... 13.2.78 是 CMake 找到的 headers 路徑。看前者。

prefill 那格的數字要講清楚:API 回報 18,134 個 prompt token,其中 42 個命中前綴快取,實際評估的是 18,092 個——366.19 tok/s 是對那 18,092 個算的。

還有一件小事:第一次量 prefill 我挑的 prompt 只有 5,680 個 token,太短——固定開銷會蓋過一切,量不出 prefill。換成 18,134 個 token 重跑一次才算數。

常見問題

DGX Spark 跑得動 1770 億參數的模型嗎?
跑得動。Qwen3.8-Flash-Next 的 GGUF 是 176.94B,用 unsloth 的 UD-Q4_K_XL(四個 shard、約 104 GiB),在單台 DGX Spark 上原生 262,144 context 全開,載入約 4 分 40 秒、穩態常駐約 104 GB。實測寫散文 20.51 tok/s,改檔案類的工作配 ngram 投機解碼是 76.65 tok/s。
121 GB 統一記憶體要怎麼分配才放得下 104 GiB 的權重加 262K context?
關鍵是那張 51B 的 n-gram 查表不用進快的那一側。用 `-ot per_layer_token_embd=CPU` 把它釘在 CPU、配 `-lm mmap` 從 NVMe 分頁進來,其餘 `-ngl 999` 全上。這樣 262,144 的 KV 也放得下——這顆 48 層裡只有 12 層有全注意力,dense K/V 每 token 約 24 KiB、262K 約 6 GiB;再算上 QSA indexer 的快取,會隨 context 增加的用量合計約 8.25 GiB。
Spark 上跑這顆需要什麼特別的設定?
三件事:llama.cpp 的 `qwen4exp` 支援走 PR #27742(2026-08-27 合併,本文用的是合併前的 `6c5afc86a`);build 要明確指定 CUDA toolkit 13.0 的 nvcc,不能用 `/usr/local/cuda` 的預設 13.2;`-np 1` 要寫死,同時開多個請求會撞 indexer cache 的 assert。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。