
從今天起除了推論引擎實測外,還有安裝相關軟體與工具的教學,在這些測試工作開跑前,先把測試方法論定下來,這樣每一篇都遵守相同的 SOP,資料之間才能較客觀地去比較。
測量什麼
LLM 推論的效能拆成兩段去測試,也就是大家常看到的 prefill(處理輸入提示詞)與 decode(逐 token 生成)。Day 2 文章我有提過這兩段的瓶頸完全不同,prefill 吃算力,decode 吃記憶體頻寬,混在一起報一個平均值是行銷術語,我比較在意的是工程測試數字。
怎麼量
以固定模型(gpt-oss-120b,昨天上線的量尺)、固定量化(MXFP4)、固定提示詞長度(512、2048、8192 token 三段,由 llama-bench 合成固定長度的 token 序列,因此最僅幾篇的各引擎測試都會對齊同一組長度)、固定生成長度(128 token)、每組跑三次取中位數與標準差。
機器狀態固定為無其他負載、模型已完成載入、且 GPU 已經暖機的熱狀態。
誠實原則
所有測試資料都是這台機器、這個環境、這個軟體版本的結果。如果你的環境,測試時跑出不同數字是正常的,方法論可複現比數字本身重要。
今天的主角是 llama.cpp,單用戶場景的基準線。
| 項目 | 值 |
|---|---|
| llama.cpp | 0.1.2-dev,commit 07822bd(2026-08-20),無 tag |
| 建置方式 | 原始碼編譯,CUDA 後端,GGML_NATIVE 自動解析架構為 sm_121a |
| 編譯器 | GCC 13.3.0 / nvcc 13.0.88,Linux aarch64 |
| 模型 | gpt-oss-120b-MXFP4.gguf,59.02 GiB 單一檔案,116.83 B 參數,36 層 128 experts |
| 模型來源 | /mnt/nas/AIModels/gguf/gpt-oss-120b-mxfp4/,NFSv4.1 掛載的 NAS(來源與 sha256 見 Day 7) |
| 硬體 | DGX Spark(GB10, sm_121),128 GiB 統一記憶體 |
| 驅動與 CUDA | 580.173.02 / CUDA 13.0(見 Day 4 環境整備) |
先講兩個可能會碰到的情況。
第一,模型是單一檔案,不是分片。 ggml-org 發布的 gpt-oss-120b GGUF 只有一個 59.02 GiB 的檔案,沒有 00001-of-0000N。網路上很多指令範例寫成分片是抄別的模型來的。
第二,官方 README 那行 cmake 指令在這台機器上會直接失敗。
cmake -B build -DGGML_CUDA=ON
失敗訊息長這樣:
/usr/include/aarch64-linux-gnu/bits/math-vector.h(96): error: identifier "__Float32x4_t" is undefined
5 errors detected in the compilation of "CMakeCUDACompilerId.cu".
-- Configuring incomplete, errors occurred!
看起來像是 glibc 或 ARM 的 SVE 型別出問題,其實不是,答案其實是:
Compiler: /usr/bin/nvcc
這台機器上有兩個 nvcc
一個是 DGX OS 內建的 CUDA 13.0(/usr/local/cuda/bin/nvcc),另一個是 apt 的 nvidia-cuda-toolkit 套件裝的 CUDA 12.0(/usr/bin/nvcc)。即使 PATH 裡 /usr/local/cuda/bin 排在前面、which nvcc 也回答 13.0,CMake 的 enable_language(CUDA) 還是挑了 /usr/bin/nvcc。而 CUDA 12.0 的 nvcc 前端看不懂 Ubuntu 24.04 glibc 標頭裡的 ARM SVE 向量型別,於是連 CMake 用來認編譯器的那個一行 .cu 測試檔都編不過。
指定正確的 nvcc 就好:
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc
cmake --build build --config Release -j
架構旗標不必給。 llama.cpp 在 GGML_NATIVE=ON(預設)時會把 CMAKE_CUDA_ARCHITECTURES 設成 native,在這台機器上自動解析成 sm_121a,cuobjdump 可以驗證。
整包編完 2 分 49 秒(20 核心,CPU 使用率 1537%),零錯誤。編譯完成後 llama-bench、llama-server、llama-cli 都在 build/bin 底下。

llama.cpp 內建的 llama-bench 就是為這件事而生的,直接量不同提示詞長度下的 prefill 與 decode:
./build/bin/llama-bench \
-m /mnt/nas/AIModels/gguf/gpt-oss-120b-mxfp4/gpt-oss-120b-MXFP4.gguf \
-p 8192,512,2048,8192 \
-n 128 \
-ngl 999 \
-r 3
參數對應方法論,-p 三檔提示詞長度、-n 生成 128 token、-r 每組三次。
眼尖的會發現 -p 開頭多了一個 8192。那是暖機用的,先來看測試結果:
| 測項 | 提示詞長度 | 結果(tokens/s) |
|---|---|---|
| pp8192(暖機,丟棄) | 8192 | 1873.35 ± 7.81 |
| prefill(pp512) | 512 | 1860.77 ± 48.78 |
| prefill(pp2048) | 2048 | 1906.63 ± 11.70 |
| prefill(pp8192) | 8192 | 1851.84 ± 36.53 |
| decode(tg128) | 短上下文 | 60.59 ± 0.22 |

三檔提示詞長度的 prefill 幾乎是同一個數字,從 512 到 8192 差距只有 2.9%,而且沒有隨長度單調下降,代表在這個長度區間,瓶頸還不是注意力,而是那 36 層 MoE 的矩陣乘法,而矩陣乘法的效率跟一次餵幾個 token 有關,512 個 token 其實還沒把 GPU 餵飽。
decode 的成績則不錯,測試跑到 60.59 tok/s,標準差 0.22,三次跑出 60.4、60.6、60.8。這種穩定度 decode 可能是被快取影響了,這件事跟 Day 9 那邊的 prefix cache 是同一類問題,容後述。
測試之後除了看成績外,也與理論值做一下比較。在系統中,decode 階段每生成一個 token,就要把該 token 用到的權重從記憶體完整讀一遍。gpt-oss-120b 是 MoE 架構,128 個專家每個 token 只啟用 4 個,所以實際搬動的是啟用參數的權重量:
理論 decode 上限 ≈ 記憶體頻寬 ÷ 每 token 需讀取的權重位元組數
分子是 GB10 的 273 GB/s,而分母才是麻煩的地方,不少人在社群討論時,測試在這裡會算錯。
常見的算法是「約 5B 啟用參數 × 4 bit = 2.5 GB」。這個數字的問題在於,MXFP4 每個區塊還帶一個縮放係數,不是乾淨的 4 bit,而且 MoE 的每一次前向傳播,不管路由選了誰,所有非專家層都要整份讀過,那些層在這顆模型裡是 Q8_0 和 F32,不是 MXFP4。
所以把 GGUF 的張量表檢視一遍,用程式可以量不同層是專家層、是什麼量化型別、實際占幾個位元組這樣。
| 角色 | 位元組 | |
|---|---|---|
| dense(注意力、norm、router、lm_head) | 1,685,668,608 | 1.570 GiB,每個 token 全讀 |
expert(ffn_*_exps) |
61,073,326,080 | 56.879 GiB,每個 token 只讀 4/128 |
| token_embd(查表,不計) | 615,329,280 | 0.573 GiB |
每 token 讀取量 = 1.570 GiB + 56.879 GiB × (4/128) = 3.347 GiB = 3.594 GB
啟用參數量算出來是 5.13 B,也就是我們在社群看到會有人說講的「約 5B」。但有效位元寬是 5.60 bit,不是 4 bit,所以真正的分母比常見估算大了 40%。
理論上限 = 273 GB/s ÷ 3.594 GB = 75.96 tok/s
實測 = 60.59 tok/s
達成率 = 79.8%
換句話說,decode 實際跑出了 217.8 GB/s 的有效頻寬。
79.8% 這個數字是有意義的,原本預期落在五到七成,那是過往這類測試結果常見的區間。而本次測試之所以偏高,是因為分母算對了,如果照一般「5B × 4 bit」的粗估,理論上限會被算成 106.4 tok/s,達成率就變成 56.9%,剛好落進那個常見區間,然後你會以為自己的推論引擎還有四成空間可以壓榨,但實際上沒空間。剩下的 20% 是排程、KV cache 讀取、kernel 啟動與那 273 GB/s 本身就達不到的部分,llama.cpp 在單請求 decode 上已經到了極限,很難再擠出更多空間來。
基準線出來後,逐一測試常見調校設定值的實際效果,每次只動一個變因。
-fa)| fa | pp2048 | pp8192 | tg128 |
|---|---|---|---|
| off | 1342.67 ± 8.55 | 860.71 ± 2.42 | 57.52 ± 0.30 |
| on | 1915.90 ± 5.97 | 1893.91 ± 7.62 | 60.79 ± 0.21 |
| 差距 | +42.7% | +120.0% | +5.7% |
這是四組實驗裡差距最大的一組,它解釋了前面那個prefill 不隨長度衰減的現象。decode 也快了 5.7%。decode 每步只處理一個 token,注意力的計算量很小,但它仍然要把 KV cache 讀一遍,Flash Attention 少搬幾趟就是省。
-b 與 -ub)-b 是邏輯批次,-ub 是實際送進 GPU 的微批次。-ub 決定一切,-b 沒有作用。
這個設定值得代價是計算緩衝區變大,吃掉一些本來可以給 KV cache 的記憶體。在 128 GiB 統一記憶體、模型已經占掉 59 GiB 的機器上,這個取捨要自己拿捏。但如果你的場景是長提示詞(RAG、整份文件摘要),-ub 2048 大概是這台機器上最值得改的一個設定。
Day 9 換上 vLLM,它是個多人使用下效能較佳的推論引擎,有 NVIDIA 為 Spark 維護的官方容器版本,也有社群版本等可以用,重點會是聚焦在單請求效能與 llama.cpp 的差距,以及並發請求下的總吞吐變化。今天的測試成績就是明天的對照組。
我們 Day 9 見囉。
Day 1|為什麼 2026 年是地端 AI 部署元年:系列規劃與硬體總覽
Day 2|DGX Spark GB10 深度解析:128GB 統一記憶體到底解決了什麼問題
Day 3|網路與儲存規劃:10GbE 骨幹、雙 Spark 直連與 NFS 集中模型庫
Day 4|Day 4|開箱之後:DGX OS 初始環境建置與 CUDA、Docker 生態確認
Day 5|NFS 模型庫實戰:下載工具、權限設計與版本管理
Day 6|llama.cpp、vLLM、TensorRT-LLM 、Ollama、DS4 與 Unsloth 的定位與取捨
Day 7|第一個模型上線:gpt-oss-120b 從模型庫到 API 的完整流程
Day 8|llama.cpp 實測:先定量尺與 SOP
Day 9|vLLM 實測:官方容器、並發吞吐曲線,與剛出爐的新模型 Ornith 1.5