~/blog/ds4-0731-on-dgx-spark-howto

DeepSeek-V4-Flash on DGX Spark · part 11

[DeepSeek-V4-Flash] 把前沿級開源模型搬進自己家:在 DGX Spark 上把 0731 跑起來

cat --toc

TL;DR

DGX Spark(GB10)跑 DeepSeek-V4-Flash-0731 比塞進單張顯卡簡單得多:統一記憶體不用分堆,-ncmoe 那整組旗標直接跳過,你只要確認 91GB 的權重加 KV 塞得進 121 GiB。實測 decode 17.5–17.9 tok/s、context 256K、process 吃 94,252 MiB、冷啟 406 秒。而同一份 GGUF、同一個 llama.cpp commit,它只比一張二手 2080 Ti 快 7%

封面:一台銀色小型工作站,機殼裡是一整塊連續發亮的記憶體區塊,沒有從中間切開;旁邊放著兩根標著 12 與 13.2 的接頭,其中一根接錯了位置。DeepSeek-V4-Flash-0731 跑在 DGX Spark。

前言

搬家到大房子的好處不是東西變少,是你不用再決定哪個箱子要塞進閣樓。全部擺得下,就不用分類。

上一篇我把同一顆模型塞進一張 22G 的二手顯卡,整篇的重點是怎麼決定哪些權重上卡、哪些丟一般記憶體。這次換成 DGX Spark。因為它是統一記憶體,那個問題根本不用處理。

所以這篇會短很多。後半確實有一個坑花掉我三個小時,不過先說清楚:那是我自己把環境搞髒的,不是 DGX Spark 會對你做的事。你的機器要是只裝一套 CUDA,恭喜,那節可以直接跳過——我當初要是也這麼乾淨就好了。

為什麼 DGX Spark 上這件事簡單:沒有「搬上去」這個動作

上一篇整篇在談一條分界線:哪些權重放 VRAM、哪些放一般記憶體。那條線之所以存在,是因為顯卡記憶體跟系統記憶體是兩塊分開的東西,中間隔著 PCIe。

GB10 不是這樣。它是統一記憶體——CPU 跟 GPU 共用同一塊 121 GiB,沒有「從這邊搬到那邊」這回事。

後果很直接:

  • -ncmoe / --n-cpu-moe 這一整組旗標跳過,不用算、不用掃描、不用找邊界。
  • 你只需要回答一個問題:91GB 的權重,加上 KV cache 跟計算緩衝區,塞不塞得進 121 GiB?
  • 答案是塞得下。實測 process 佔 94,252 MiB,載完系統還剩約 24 GiB。

上一篇那條 (VRAM − 11) ÷ 2 的算式在這裡沒有用武之地,這是好事。

我自己挖的坑:機器上有兩套 CUDA,而我只講了一半

這台機器我前前後後裝過不少東西,結果上面有兩套 CUDA,而預設路徑指向的是舊的那套:

which -a nvcc
# /usr/bin/nvcc
nvcc --version | tail -2
# Cuda compilation tools, release 12.0, V12.0.140

ls -d /usr/local/cuda*
# /usr/local/cuda  /usr/local/cuda-13  /usr/local/cuda-13.2
/usr/local/cuda/bin/nvcc --version | tail -2
# Cuda compilation tools, release 13.2, V13.2.78

/usr/bin/nvcc 是 12.0,/usr/local/cuda 指向 13.2。PATH 預設吃到的是舊的那個。

我第一次編的時候有指定編譯器,自以為處理掉了:

cmake -B build -DGGML_CUDA=ON \
      -DCMAKE_CUDA_ARCHITECTURES=121 \
      -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.2/bin/nvcc   # ← 只講了編譯器

編譯全過,零錯誤。模型也載得起來。然後第一次送 request 就死了。

那個崩潰長什麼樣子

這是當時的原文,一個字都沒改:

/home/coolthor/llama.cpp-0801/ggml/src/ggml-cuda/ggml-cuda.cu:106: CUDA error
ggml_cuda_compute_forward: SOFT_MAX failed
CUDA error: invalid argument
  current device: 0, in function ggml_cuda_compute_forward at ggml-cuda.cu:2374

SOFT_MAX failedinvalid argument。這兩個詞加起來,任何人的第一個念頭都是模型或參數有問題——我也是。我去懷疑了 GGUF 檔、懷疑了 context 長度、懷疑了 0731 的新架構 llama.cpp 支援不完整。

真正指認兇手的是堆疊裡的檔案路徑:

#3  ggml_cuda_error(...) from /home/coolthor/llama.cpp-0801/build/bin/libggml-cuda.so.0

問題出在那個 .so 連到了什麼:

ldd build/bin/libggml-cuda.so | grep -E 'cudart|cublas'
# libcudart.so.12 => /lib/aarch64-linux-gnu/libcudart.so.12     ← 12!

用 13.2 的編譯器編出來的東西,跑起來連到 12 的 runtime。 CMake 照我說的用了新編譯器,但去找 runtime 函式庫的時候,自己在系統目錄裡找到了舊的那套。

為什麼是「載得進去、推論才炸」?載入階段只是配記憶體跟搬權重,那些 API 兩版之間相容;真正跑到 kernel 的時候,新編譯器產生的呼叫慣例對不上舊 runtime,才在第一個 SOFT_MAX 上碎掉。版本不對,error 卻噴在一個完全無關的地方,所以才這麼難抓。

CUDA 版本錯配的四步:用 CUDA 13.2 的編譯器編(零錯誤)→ 編出來的檔卻連到 CUDA 12 的 runtime(沒人會提醒你)→ 模型 91GB 載得進去都正常 → 第一次推論 SOFT_MAX failed,而錯誤訊息指向模型不是工具鏈;解法是 -DCUDAToolkit_ROOT 再用 ldd 確認

正確的編法:把 toolkit 路徑也講死,然後用 ldd

差別只有一行:

git clone https://github.com/ggml-org/llama.cpp ~/llama.cpp-0801
cd ~/llama.cpp-0801
git checkout b10217          # 我用的版本,commit ddd4ec1

cmake -B build-cuda132-sm121 -DGGML_CUDA=ON \
      -DCMAKE_CUDA_ARCHITECTURES=121 \
      -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.2/bin/nvcc \
      -DCUDAToolkit_ROOT=/usr/local/cuda-13.2 \
      -DCMAKE_BUILD_TYPE=Release
cmake --build build-cuda132-sm121 --config Release -j$(nproc)

-DCUDAToolkit_ROOT 就是那一行。CMAKE_CUDA_ARCHITECTURES=121 是 GB10 的 compute capability(sm_121),跟消費級顯卡不一樣,別抄 86 或 89。

編完先驗,不要直接跑:

ldd build-cuda132-sm121/bin/libggml-cuda.so | grep -E 'cudart|cublas'
# libcudart.so.13   => /usr/local/cuda/targets/sbsa-linux/lib/libcudart.so.13
# libcublas.so.13   => /usr/local/cuda/targets/sbsa-linux/lib/libcublas.so.13
# libcublasLt.so.13 => /usr/local/cuda/targets/sbsa-linux/lib/libcublasLt.so.13

.so.13,而且路徑在 /usr/local/cuda 底下。這一行 ldd 可以幫你省掉三個小時,因為另一條路的錯誤訊息根本不會讓你想到這裡。

一句話記起來:能編、能載都不算驗過版本;編完一定要跑 ldd

下載:跟上一篇同一份檔案

模型部分跟上一篇完全一樣,同一個 repo、同一份量化,不重講細節:

export HF_HUB_ENABLE_HF_TRANSFER=1
hf download unsloth/DeepSeek-V4-Flash-0731-GGUF \
  --include "UD-Q2_K_XL/*" \
  --local-dir ~/models/ds4-0731-ud-q2-k-xl

這份模型切成三個 shard、共 91GB,第一個只有 5MB 且 n_tensors = 0(正常,不是壞檔),-m 指它就好。

服務設定:三個旗標是拿來擋 OOM 的

這是我跑著的那份:

# ~/.config/systemd/user/llama-ds4-0731-8000.service
[Unit]
Description=DeepSeek V4 Flash 0731 via llama.cpp on port 8000
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/coolthor/llama.cpp-0801
ExecStart=/home/coolthor/llama.cpp-0801/build-cuda132-sm121/bin/llama-server \
  -m /home/coolthor/models/ds4-0731-ud-q2-k-xl/UD-Q2_K_XL/DeepSeek-V4-Flash-0731-UD-Q2_K_XL-00001-of-00003.gguf \
  -ngl all --fit off -c 262144 \
  -np 1 -ctxcp 2 -cram 2048 -b 1024 -ub 256 --no-warmup \
  --host 0.0.0.0 --port 8000 \
  --alias deepseek-v4-pro --reasoning-format deepseek
Restart=on-failure
RestartSec=10
TimeoutStopSec=180

[Install]
WantedBy=default.target

中間那排短旗標是這台機器的重點,值得逐個講:

-np 1 -ctxcp 2 -cram 2048壓住 llama-server 自己的快取用量。它預設會開多個 slot、留 32 份 context checkpoint、把 prompt 快取養到 8GB——在一般機器上這些吃的是主記憶體,無傷大雅;在統一記憶體的機器上,它們跟模型權重搶的是同一塊 121 GiB。不限制的話,送一個長 prompt 就可能讓整台直接 OOM。

-b 1024 -ub 256 縮小 prefill 的計算緩衝區,同樣是為了留餘裕。

--fit off 關掉自動配適。開著的時候 llama.cpp 會替你沒指定的參數挑值好讓東西塞得下;你明寫的 -c 它不會動。關掉是為了讓每個數值都是我自己決定的。

-ngl all 把所有層都交給 GPU。在統一記憶體上這不像獨顯那樣是「搬過去」,但仍然要明講。

context 開 256K 而不是 1M,理由是餘裕

這顆模型原生支援到 1,048,576,而我 production 設 262,144。

1M 我測過,載得起來,冷啟 416 秒。但 121 GiB 是 CPU 跟 GPU 共用的,KV 養大之後,留給作業系統跟其他服務的記憶體就會變緊。256K 載完還剩約 24 GiB,那個餘裕讓我晚上不用擔心。

121 GiB 裡餘裕在哪裡:兩個槽的模型權重都是約 92 GB,差別在被 context 撐大的那塊 KV cache。左邊是我在跑的 -c 262144,KV 還小,上面留著約 24 GiB 的餘裕給作業系統跟其他服務;右邊 -c 1048576 的 KV cache 長高之後,餘裕只剩薄薄一條

另外要修正我原本的一個誤判:我曾經從 rope.scaling.factor = 16.0original_context_length = 65536 推論「1M 是硬外推、沒訓練過」,那是錯的——DeepSeek 的技術報告說 V4 的預訓練長度一路推到 1M。最後只剩一個 caveat:長 context 的品質我沒量過。

驗收:載入要等七分鐘,別以為它掛了

冷啟實測 406 秒。這段時間 /health 會一直回 503,那是正常的:

# 1. 還在載 vs 好了
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/health
# 503 = 還在載入(要等,別重啟);200 = 可以用了

# 2. 參數有沒有被偷偷改掉
curl -s http://localhost:8000/props | python3 -m json.tool | grep -E 'n_ctx|build'
# n_ctx 262144 / build b1-ddd4ec1

# 3. 載的是哪顆
curl -s http://localhost:8000/v1/models | python3 -m json.tool | grep -E 'n_params|n_ctx_train'
# n_params 284334567511 / n_ctx_train 1048576

⚠️ 看到 503 不要重啟。重啟只會讓它從頭再讀 91GB,再等七分鐘。

記憶體佔用可以這樣看(GB10 的 nvidia-smi --query-gpu=memory.used 會回 [N/A],統一記憶體的怪癖):

nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader
# 1142691, 94252 MiB

誠實對照:比那張二手顯卡快 7%

同一份 GGUF、同一個 llama.cpp commit、同一種量化,兩台機器對跑:

機器contextdecode
DGX Spark(GB10,121 GiB 統一記憶體)256K17.5–17.9 tok/s
單張改裝 2080 Ti 22G + EPYC 74021M16.4–16.5 tok/s

差約 7%。

這個數字要怎麼看,取決於你想買什麼。如果你要的是吐字速度,7% 不值那個價差。DGX Spark 的優勢其實在另外兩件事:121 GiB 的空間(下一顆更大的模型還塞得下,那張 22G 的卡就不一定了),以及不用分堆的省事——上一篇有一整節在教怎麼算 -ncmoe,這篇連提都不用提。

⚠️ 邊界講清楚:這是兩台實際部署的機器對跑,不是受控的硬體 A/B。兩邊 context 設定不同(256K 對 1M),不過上一篇量過這顆模型的 decode 幾乎不隨 context 變,所以這個差距我認為是真的。這只涵蓋單一 workload、單流、短輸入;流量型態一換,結論可能就不一樣。

收尾

DGX Spark 上裝這顆模型,真正要做的判斷只有一個:編譯的時候把 CUDA 路徑講死,然後用 ldd 確認它真的連對了。 其餘的不用管,因為 CPU 跟 GPU 共用同一塊記憶體。

而那個坑真正值得記的不是「CUDA 有兩套」,是它怎麼發作:編得過、載得進去、第一次推論才炸,而錯誤訊息指向 softmax。 下次再看到「載入正常、一跑就 CUDA error」,先去 ldd 一下,別急著怪模型。


這個系列的其他文章:

常見問題

DGX Spark 上跑 DeepSeek-V4-Flash-0731 要不要調 expert offload?
不用。GB10 是統一記憶體,CPU 跟 GPU 共用同一塊 121 GiB,沒有「搬去顯卡」這個動作,所以 `-ncmoe` 這類旗標整組跳過。你只要確認 91GB 的權重加上 KV cache 塞得進那 121 GiB 就好,實測 process 吃 94,252 MiB。
為什麼模型載得進去,一推論就 CUDA error?
多半是編譯器跟 runtime 不同版,而這通常是自己的機器裝過多套 CUDA 才會有的問題——只裝一套的話不會遇到。我這台的 PATH 預設 `nvcc` 是 12.0、另外又裝了 13.2;CMake 用了我指定的編譯器,卻自己去系統目錄找到舊的 runtime。症狀是載入完全正常、第一次推論才丟 `SOFT_MAX failed / CUDA error: invalid argument`,看起來完全不像版本問題。
DGX Spark 比一張二手 2080 Ti 快多少?
同一份 GGUF、同一個 llama.cpp、同一種量化下,我量到 DGX Spark decode 17.5–17.9 tok/s,單張改裝 2080 Ti 22G 配 EPYC 是 16.4–16.5 tok/s,差約 7%。DGX Spark 真正買到的不是速度,是「不用分堆」的省事和 121 GiB 的空間。
context 該開 256K 還是 1M?
我 production 選 256K。1M 也載得起來(冷啟 416 秒),但 121 GiB 是 CPU 跟 GPU 共用的,長 context 把 KV 養大之後,留給系統的記憶體就會變緊。256K 載完還剩約 24 GiB,這個餘裕讓我晚上睡得著。

接著讀

不想錯過新文章?

訂閱我確保不漏接!

隨時一鍵退訂。