3 個月前,我用同一張 RTX 4070 寫過這篇驗證文章。結論是:「35B 的 MoE 模型,只要你等得起,就能跑。」當時測得的速度是 10.6 tok/s。能跑是能跑,但拿來聊天時,速度慢到讓人手指都會停住。

上週,我用同一張 GPU、同一個模型重新測了一次,結果跑出了 34.6 tok/s。硬體完全沒有改變一毫米。改掉的只有推論引擎,以及僅僅一個 flag。

最奇怪的是,在這個過程中,我得出了與自己常識完全相反的結果:「把專家模組放上 GPU 越多,反而越慢。」這不是「加大 VRAM 就會更快」的故事。本文就是這段驗證紀錄。

前提:為什麼 12GB 的 GPU 能跑 35B

先說清楚這一點,不然後面的內容會像飄在空中。

這次的模型是 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 等級。

這種「只用其中一部分」的特性,後面會變得非常關鍵。

起點:Ollama 的自動 Offload

如果你只是想先把本地 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 的結構。問題的突破口就在這裡。

轉換:llama.cpp 的 --cpu-moe

Ollama 底層其實是 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 的自動分配幾乎一樣。

因此排序大致如下:

  • Ollama 自動 / 素の llama.cpp:約 12 tok/s
  • 把所有專家退回 CPU:34.6 tok/s(2.8 倍)

3 個月前我寫的「等得起的話可以跑」那個速度,靠一個 flag 就進入了實用範圍。-ncmoe 24(也就是專家有一半放進 GPU)竟然是最慢的,這點非常耐人尋味。

為什麼放到 GPU 反而會變慢

看到結果後我也抓破頭,後來把理由拆開來想,才終於理解。關鍵在於要把「瓶頸是什麼」拆成不同部件來看。

Attention 和 KV cache 是記憶體頻寬受限的。資料讀寫速度會直接影響表現,所以放在頻寬可達 500GB/s 等級的 VRAM 上,速度就會大幅提升。另一方面,專家模組在每個 token 只會碰到約 3.3B,而且是稀疏的,20GB 的全部內容本來就不可能塞進 12GB。

如果這時把專家模組半吊子地塞進 VRAM,就會搶走原本最吃頻寬的 Attention 和 KV cache 的位置。結果就是,本來最想加速的部分被擠出去,整體反而更慢。也就是說,把「吃頻寬的 Attention 全部放 GPU、稀疏計算的專家全部放 CPU」做乾淨切分,才是最快的。

當目標變成「把 VRAM 塞滿」時,反而會吃虧。GPU 不是萬能收納箱,而是應該拿來放那些真的能吃到頻寬優勢的東西——我只是很長一段時間都忘了這點。

卡關處:我踩過的 3 個雷

光看步驟好像很簡單,但我其實繞了幾次遠路。這裡記下來,免得別人重蹈覆轍。

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 行總結這次驗證的重點:

  • 4070 上的 35B-A3B 不只停留在「12 tok/s」;用 --cpu-moe 可以拉到 34.6 tok/s
  • 專家模組不要放 GPU,全部丟 CPU 才最快(放越多越慢)
  • 原因是要把 VRAM 留給受頻寬限制的 Attention

接下來,我打算試試剛出的 Qwen3.6 世代,以及用同樣的方法測其他 MoE 模型(像 gpt-oss 系列)能跑到什麼程度。

話說,你的 GPU VRAM 現在都拿去做什麼了呢?我在這次驗證中,直到測量前一刻才發現有 9GB 被另一個應用程式(GPU 出租的 Salad)吃掉。只要在 benchmark 前先看一次 nvidia-smi,就能少走一段冤枉路。


原文出處:https://qiita.com/kenimo49/items/dff3c8a2a0ee563ca16f


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝16   ❤️1
488
🥈
我愛JS
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登