3 個月前,我用同一張 RTX 4070 寫過這篇驗證文章。結論是:「35B 的 MoE 模型,只要你等得起,就能跑。」當時測得的速度是 10.6 tok/s。能跑是能跑,但拿來聊天時,速度慢到讓人手指都會停住。
上週,我用同一張 GPU、同一個模型重新測了一次,結果跑出了 34.6 tok/s。硬體完全沒有改變一毫米。改掉的只有推論引擎,以及僅僅一個 flag。
最奇怪的是,在這個過程中,我得出了與自己常識完全相反的結果:「把專家模組放上 GPU 越多,反而越慢。」這不是「加大 VRAM 就會更快」的故事。本文就是這段驗證紀錄。
先說清楚這一點,不然後面的內容會像飄在空中。
這次的模型是 Qwen3.5-35B-A3B。總參數量是 34.66B。照常理來看,就算做 Q4 量化,也會接近 20GB,12GB 的 VRAM 根本不可能塞得下。3 個月前的我還在納悶:「連 9B(密集模型)都還沒到實用範圍,為什麼 35B 反而能跑?」
答案就在 A3B 這一段。它是 Mixture of Experts(MoE),128 個專家中每個 token 只會啟用 8 個,大約只有 3.3B 的量會被實際活化。總量雖然是 35B,但每個 token 真正運作的只有 3B 等級。
這種「只用其中一部分」的特性,後面會變得非常關鍵。
如果你只是想先把本地 LLM 跑起來,第一個通常會用的就是 Ollama。我也是從這裡開始的。只要執行 ollama run qwen3.5:35b-a3b,Ollama 就會自動幫你做分配:能放進 GPU 的就放進去,剩下的丟給 CPU。
在清空 VRAM(停止其他程序)之後重新測量,結果如下。
| 項目 | 值 |
|---|---|
| 生成速度 | 12.2 tok/s |
| 自動分割 | 58% CPU / 42% GPU |
| Context | 4096 |
| VRAM 使用量 | 11.4GB |
雖然比 3 個月前的 10.6 tok/s 稍微提升了一些,但那只是因為 Ollama 版本升級了,趨勢本身沒有變。這裡最值得注意的是分割比例。Ollama 只把模型的 42% 放到 GPU 上,即使 VRAM 已經用了 11.4GB 也是如此。
也就是說,Ollama 做的是很直覺的判斷:「VRAM 滿了,那剩下的就放 CPU。」這很聰明,但它並不知道 MoE 的結構。問題的突破口就在這裡。
--cpu-moeOllama 底層其實是 llama.cpp,但專家模組的配置沒辦法透過 Ollama 細緻地控制。所以我改成直接編譯 llama.cpp 來用。
# 1. 啟用 CUDA 編譯(需要 cmake;若沒有,可用 pip install cmake 安裝)
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j
# 2. 取得 GGUF(unsloth 版 Q4_K_M,約 22GB)
wget https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-Q4_K_M.gguf
這裡用到的是 --n-cpu-moe N(簡寫 -ncmoe)。它的意思是「把前 N 層的專家放到 CPU 上」,N 越大,越多專家會被移到 CPU。這次模型總共有 48 層,所以 -ncmoe 48 就代表所有專家都放到 CPU。
我一開始很單純地以為:「只要 VRAM 還容得下,專家留在 GPU 上應該會更快,所以 N 越小越好。」結果完全相反。
我固定使用 -ngl 99(指定所有層都放 GPU),然後把 -ncmoe 從 48 掃到 24,測量生成速度。
| n-cpu-moe | 專家配置 | 生成速度 tok/s |
|---|---|---|
| 48 | 全部 CPU | 34.60 |
| 44 | 幾乎全 CPU | 27.19 |
| 40 | 幾乎全 CPU | 16.88 |
| 36 | 半半 | 15.29 |
| 32 | 半半 | 14.06 |
| 28 | 半半 | 12.85 |
| 24 | 半 GPU | 11.71 |
這是很漂亮的右肩下滑。把專家搬到 GPU 越多(也就是 N 越小),速度就單調下降。作為對照,沒有 -ncmoe 的純 -ngl 99 是 12.94 tok/s,和 Ollama 的自動分配幾乎一樣。
因此排序大致如下:
3 個月前我寫的「等得起的話可以跑」那個速度,靠一個 flag 就進入了實用範圍。-ncmoe 24(也就是專家有一半放進 GPU)竟然是最慢的,這點非常耐人尋味。
看到結果後我也抓破頭,後來把理由拆開來想,才終於理解。關鍵在於要把「瓶頸是什麼」拆成不同部件來看。
Attention 和 KV cache 是記憶體頻寬受限的。資料讀寫速度會直接影響表現,所以放在頻寬可達 500GB/s 等級的 VRAM 上,速度就會大幅提升。另一方面,專家模組在每個 token 只會碰到約 3.3B,而且是稀疏的,20GB 的全部內容本來就不可能塞進 12GB。
如果這時把專家模組半吊子地塞進 VRAM,就會搶走原本最吃頻寬的 Attention 和 KV cache 的位置。結果就是,本來最想加速的部分被擠出去,整體反而更慢。也就是說,把「吃頻寬的 Attention 全部放 GPU、稀疏計算的專家全部放 CPU」做乾淨切分,才是最快的。
當目標變成「把 VRAM 塞滿」時,反而會吃虧。GPU 不是萬能收納箱,而是應該拿來放那些真的能吃到頻寬優勢的東西——我只是很長一段時間都忘了這點。
光看步驟好像很簡單,但我其實繞了幾次遠路。這裡記下來,免得別人重蹈覆轍。
1. 跑 benchmark 前先把 VRAM 清空。
第一次測量時,明明沒有開 Ollama,VRAM 卻已經被吃掉 9.4GB。兇手是另一個叫 Salad、會出租 GPU 的應用程式。剩下 2.8GB 的情況下,不管怎麼測都會失真。請先看一下 nvidia-smi,確認不是自己的模型以外的東西在吃 VRAM。
2. 不要試圖直接把 Ollama 的模型丟給 llama.cpp。
Ollama 下載的 GGUF 會以 blob 形式放在系統區域,可能會因為權限問題而無法直接讀取。若要考慮重現性,直接從 HuggingFace 另外下載相同的 Q4_K_M GGUF,反而是最快也最可靠的做法。
3. 不要用 llama-cli 的原始 completion 直接測,否則可能跑不完。
Qwen3.5 是會輸出思考(thinking)內容的模型,如果不套 chat template 直接裸跑,它會一直思考下去,生成可能根本不會停。速度測試建議使用 llama-bench,或透過 llama-server 搭配 chat template 來跑。我就曾經因此讓 GPU 空轉了 5 分鐘。
最後,在 4070 上把 Qwen 35B 跑到實用速度的啟動方式就是這樣:
./build/bin/llama-server \
-m Qwen3.5-35B-A3B-Q4_K_M.gguf \
-ngl 99 \
--cpu-moe \
-c 8192
# -ngl 99 : 指定全部層都放到 GPU
# --cpu-moe : 只把專家模組放回 CPU(等同 -ncmoe 48)
這樣的配置下,VRAM 使用量約 11.7GB,幾乎把 12GB 用滿,但仍然塞得下。若要處理更長的上下文、而 VRAM 開始吃緊,可以把 KV cache 量化成 -ctk q8_0 -ctv q8_0,這樣 KV 所需的 VRAM 大約能減半,而速度幾乎不受影響,還能把上下文拉長。
用 3 行總結這次驗證的重點:
--cpu-moe 可以拉到 34.6 tok/s接下來,我打算試試剛出的 Qwen3.6 世代,以及用同樣的方法測其他 MoE 模型(像 gpt-oss 系列)能跑到什麼程度。
話說,你的 GPU VRAM 現在都拿去做什麼了呢?我在這次驗證中,直到測量前一刻才發現有 9GB 被另一個應用程式(GPU 出租的 Salad)吃掉。只要在 benchmark 前先看一次 nvidia-smi,就能少走一段冤枉路。