VRAM 溢出、MoE Offload 與調校方法:為什麼「Experts 全放 CPU 最快」不是通則¶
狀態:已發布
領域:AI/Local LLM/效能調校
理解確認:Kent 已於 2026-08-26 明確確認理解核心概念
內容核准:Kent 已於 2026-08-26 核准本文發布
測試條件:12GB VRAM 消費級顯卡、6P+8E 混合架構 CPU、32GB DDR4-3200 雙通道、35B-A3B MoE(Q4)、llama.cpp、2026-08
這篇文章回答什麼問題?
網路上有實測指出「把 MoE 的 Experts 全部放回 CPU,比放進 GPU 更快」,且曲線單調遞減。本文先建立權重與 KV cache 的分工原理,再用同一套方法在 12GB 顯卡上測出相反的結果,說明那條曲線其實是 VRAM 溢出的症狀,並整理一套可複製的調校與判讀流程。
VRAM 溢出不會報錯,而且從生成速度幾乎看不出來
顯卡驅動在 VRAM 不足時通常不會中斷,而是把超出部分放到系統 RAM,經 PCIe 存取,速度掉到約 1/10。實測中它讓 Prefill 掉 85%,但 Decode 只掉 23%。只看 tokens/s 會完全錯過這個問題。
先看結論¶
權重在哪裡,計算就在哪裡
→ Experts 在 RAM 就由 CPU 算,權重不會過 PCIe
GPU 與 CPU 是串行不是並行
→ T_混合 = T_gpu + T_cpu + 同步開銷
→ 只要塞得下,全 GPU 一定最快
唯一該讓權重待在 RAM 的理由
→ VRAM 裝不下
「Experts 全放 CPU 最快」
→ 是「無論如何切分都會溢出」時的正確操作,不是普遍規律
一、原理:權重與 KV cache 的分工¶
1.1 權重在哪裡,計算就在哪裡¶
llama.cpp 的切分單位是 layer。指定給 GPU 的層權重常駐 VRAM 由 GPU 計算;留在 RAM 的層由 CPU 計算。每個 Token 兩邊只交換 hidden state——一個幾 KB 的小向量。
權重從頭到尾都不跨 PCIe。
為什麼不「權重留 RAM、搬去 GPU 算」:
| 方案 | 每 Token 約 1GB Experts 權重的耗時 |
|---|---|
| 搬過 PCIe 4.0 x16 給 GPU 算(約 32 GB/s) | 約 31 ms |
| CPU 直接用 DDR4 算(實測約 40 GB/s) | 約 25 ms |
| CPU 直接用 DDR5 算(約 80 GB/s) | 約 12 ms |
MoE 的 router 每個 Token 會選到不同 Experts,搬過去幾乎沒有快取價值。搬運時間大於計算時間,GPU 全程等待。
1.2 KV cache 是什麼¶
常見誤解是「KV cache 不參與計算」。它參與得很深,只是不是被訓練出來的東西。
| 權重 | KV cache | |
|---|---|---|
| 來源 | 訓練時學到 | 推論時當場算出 |
| 生命週期 | 永久 | 該輪對話用完即丟 |
| 內容 | 語言與知識的規律 | 這段對話目前說了什麼 |
| 隨 Context | 不變 | 線性增長 |
| 每 Token 讀取 | 是 | 是 |
| 每 Token 寫入 | 否 | 是(append) |
Attention 需要每個舊 Token 的 K 與 V,而它們由權重投影而來:
x_i 算完就不再變,輸入與權重都不變,K_i/V_i 也永遠不變。與其每次重算(O(n²)),不如算一次存起來——這就是 KV cache,本質是用記憶體換計算的快取。
1.3 兩者的交匯點只有兩個¶
┌─────────────────────────────┐
輸入 Token ────────►│ Embedding(權重) │
└──────────────┬──────────────┘
▼
RMSNorm(權重)
▼
┏━━━━━━━━━━━━━━━━━ ATTENTION 區塊 ━━━━━━━━━━━━━━━━━┓
┃ x ──► W_q ──► q ┃ 權重
┃ ├─► W_k ──► k ─┐ ┃ 單獨
┃ └─► W_v ──► v ─┤ ┃ 作用
┃ │ ┃
┃ ╭─────────────────▼───────────────────────────╮ ┃
┃ │ ★交匯點 1:WRITE │ ┃
┃ │ append k,v ──► KV cache 尾端 │ ┃
┃ ╰─────────────────┬───────────────────────────╯ ┃
┃ ╭─────────────────▼───────────────────────────╮ ┃
┃ │ ★交匯點 2:READ │ ┃
┃ │ q · K_all ──► softmax ──► · V_all │ ┃
┃ │ (讀取整塊 cache,此處無權重參與) │ ┃
┃ ╰─────────────────┬───────────────────────────╯ ┃
┃ W_o(權重) ┃
┗━━━━━━━━━━━━━━━━━━━━┬━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
▼ + 殘差連接
┏━━━━━━━━━━━━━━━ MoE 區塊 ━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Router(權重)──► 從 256 個中選 8 個 ┃
┃ expert_a expert_b ... shared_expert ┃ 純權重
┃ ▼ ┃ 不碰 cache
┃ 加權求和 ┃
┗━━━━━━━━━━━━━━━━━━━━┬━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
▼(進入下一層)
★交匯點 2 完全沒有權重參與。 q · Kᵀ → softmax → · V 的運算元全是當場算出來的東西,沒有任何學習來的參數。這是 KV cache 不算「模型的一部分」卻又必須參與計算的原因。
MoE 區塊從頭到尾不碰 KV cache——這是架構上的天然隔離,不是設定出來的。
1.4 所以最佳切分是這樣¶
┌─── GPU(VRAM)─────────────────────────────┐
│ W_q W_k W_v W_o ──┐ │
│ ├─► ★1 寫入 │
│ KV cache ◄────────┘ │
│ └──────────────► ★2 讀取 │
│ Router 權重/部分 Experts 層 │
└────────────────┬───────────────────────────┘
│ hidden state 幾 KB ⇅ PCIe
┌────────────────▼───────────────────────────┐
│─── CPU(RAM)──────────────────────────────│
│ 其餘 Experts 權重 │
│ (這裡完全沒有 KV cache) │
└────────────────────────────────────────────┘
兩者成本結構完全不同:
| 每 Token 讀取量 | 隨 Context | 性質 | |
|---|---|---|---|
| Experts 權重 | 稀疏,只碰 9/256 | 不變 | 固定成本 |
| KV cache | 全額讀取 | 線性增長 | 變動成本 |
把變動成本留在高頻寬的 VRAM、固定成本丟給 RAM。
1.5 GPU 與 CPU 是串行,不是並行¶
常見的想法是「分散負載讓兩邊分擔壓力」。但 llama.cpp 的混合執行是串行的:
總時間是相加,不是取最大值:
因為 T_cpu > 0 恆成立,只要塞得下,全 GPU 一定最快。把層搬去 CPU 不會「分擔壓力」,只會「追加時間」。
記住這句話
唯一該讓權重待在 RAM 的理由,永遠只有一個:VRAM 裝不下。這是理解下一節那條詭異曲線的鑰匙。
二、實測¶
2.1 先從 config 算清楚記憶體需求¶
不要憑感覺,直接讀模型的 config.json:
| 參數 | 值 |
|---|---|
num_hidden_layers |
40 |
layer_types |
30 linear_attention + 10 full_attention |
hidden_size / head_dim |
2048 / 256 |
num_key_value_heads |
2(GQA) |
num_experts / num_experts_per_tok |
256 / 8(+1 shared) |
moe_intermediate_size |
512 |
max_position_embeddings |
262144 |
這是混合架構:40 層裡只有 10 層是完整 attention,其餘 30 層走線性注意力(狀態固定大小,不隨 Context 增長)。
| Context | KV cache |
|---|---|
| 32K | 0.62 GiB |
| 128K | 2.50 GiB |
| 256K(原生上限) | 5.00 GiB |
12GB 顯卡跑滿 256K 也只要 5GB。 對照傳統 40 層全 attention 的模型,同條件會是 4 倍。
由此得到第一個實務結論:這個架構不需要 KV cache 量化。省下的空間換不到一層 Experts,卻要付精度代價。
Experts 才是空間大戶:
所以該問的不是「Context 能開多長」,而是「12GB 能塞下幾層 Experts」。
2.2 基準線與第一次掃描¶
以「全部 Experts 在 CPU」為起點,逐步往 GPU 收(表中為留在 CPU 的層數):
| CPU 上的 Experts 層數 | Decode(tok/s) | Prefill |
|---|---|---|
| 40(全部) | 10.45 | 269.6 |
| 32 | 11.84 | 235.6 |
| 27 | 11.59 | 252.5 |
| 23 | 15.13 | 255.5 |
曲線方向與網路上的說法相反,收回 GPU 反而快 45%。
2.3 插曲:背景程式吃掉的 VRAM¶
檢查時發現閒置桌面環境已佔用 1.7 GB VRAM——聊天軟體、瀏覽器、遊戲平台、顯卡 overlay,共 20 幾個程序。
關閉後重測同一配置:
| 關閉前 | 關閉後 | |
|---|---|---|
| Decode | 15.13 | 18.66(+23%) |
| Prefill | 255.5 | 540.2(+111%) |
Prefill 直接翻倍。 這是本地 LLM 最便宜的一次效能提升。
2.4 第二次掃描:斷崖¶
| CPU 上的 Experts 層數 | Prefill | Decode |
|---|---|---|
| 23 | 540.2 | 18.66 |
| 21 | 577.9 | 21.20 |
| 19 | 83.8 ⚠️ | 16.28 |
| 17 | 84.5 ⚠️ | 14.46 |
Prefill 從 578 掉到 84——7 倍的斷崖式下跌。 不是漸進衰減,是撞牆。21 與 19 之間就是這張卡的物理邊界,多塞 2 層(約 1 GB)就撐爆。
致命的檢測盲點
同一組對照中,Decode 只從 21.20 掉到 16.28(-23%),Prefill 卻掉了 85%。 只看 Decode 幾乎察覺不到問題。 Prefill 是批次運算,對頻寬極度敏感,溢出的懲罰在這裡放大最明顯。
2.5 為什麼溢出不會報錯¶
顯卡驅動(Windows 的 WDDM 模式尤其明顯)在 VRAM 不足時不會直接 OOM 中斷,而是把超出部分放到「共享 GPU 記憶體」,也就是系統 RAM。此時 GPU 每次存取都要走 PCIe,速度掉到約 1/10。
症狀是:明明「塞進去了」,卻慢到不如純 CPU。
回頭看那個「Experts 全放 CPU 最快」的實測——原作者提到他最終配置的 VRAM 使用量是 11.7GB,幾乎用滿 12GB。那麼當他嘗試把約一半 Experts(約 11GB)額外塞進 GPU 時,需求瞬間超過 20GB。
12GB 的卡裝不下,但驅動沒報錯。
所以那條漂亮的單調遞減曲線,測的不是「GPU 與 CPU 誰算得快」,而是溢出程度的曲線——收回 GPU 的 Experts 越多,溢出越嚴重,越慢。
在必然溢出的前提下,「乾脆不要讓 GPU 碰 Experts」確實是最好的選擇。但這是特定條件下的解。
2.6 第三次驗證:短 Context 的最佳值會在長 Context 崩掉¶
Benchmark 預設多以短 Context 執行,但實際使用要 32K,KV cache 會多吃約 0.5GB——而 21 距離斷崖只剩 1GB 餘裕。
| Context 深度 | 21 層在 CPU | 22 層在 CPU |
|---|---|---|
| 0 | 536 | 565 |
| 8K | 295 ⚠️ | 582 |
| 32K | 58 ☠️ | 538 ✅ |
21 在 8K 就開始崩,到 32K 只剩 58,掉了 9 倍。 而兩者的 Decode 全程都是 19–20 tok/s,完全看不出差別。
若只看 Decode 就定案會選 21,然後在實際使用時發現「聊久了首字要等好幾秒」卻找不出原因——而且是漸進式惡化,聊越久越慢。
教訓
Benchmark 的條件必須貼近實際使用。短 Context 測出來的最佳值,在長 Context 下可能是最差的。
2.7 第四次掃描:混合架構 CPU 的執行緒數¶
測試 CPU 為 6 P-core + 8 E-core(14 實體核心 / 20 邏輯執行緒)。
一般建議是「只用 P-core」,理由是 E-core 缺少進階 SIMD、單核吞吐低,而 llama.cpp 會同步等待所有執行緒。實測推翻了這個假設:
| 執行緒數 | Prefill | Decode |
|---|---|---|
| 6(僅 P-core) | 580.7 ± 40.9 | 19.63 ± 1.48 |
| 8 | 559.2 | 18.69 |
| 10 | 544.3 | 18.23 |
| 14(實體核心) | 542.2 ± 75.0 | 22.73 ± 1.31 |
| 20(含超執行緒) | 532.1 ± 93.9 | 16.81 ± 3.66 |
- E-core 有幫助:14 比 6 快 16%。MoE 的 Experts 是破碎小矩陣運算,本來就填不滿 P-core 的 SIMD 管線,「多核心分擔記憶體延遲」效益更大。
- 超執行緒有害:20 比 6 還慢,標準差飆到 ±3.66(其他配置 ±1.3~1.5)。HT 邏輯執行緒與實體核心共用 L1/L2,在本來就 cache 不友善的破碎存取上加劇爭用。
- 趨勢非單調:6→10 遞減、14 回升、20 崩潰,8 與 10 是最糟交界點。
結論:設實體核心數,不是 P-core 數,也不是邏輯執行緒數。混合架構一定要實測。
2.8 最終成績¶
| 階段 | Decode |
|---|---|
| 起點(全 CPU、背景程式未關) | 10.45 |
| 修正後端 + 關閉背景程式 | 18.66 |
| 找到正確的 Experts 切分 | 19.90 |
| 修正執行緒數 | 22.73 |
提升 117%,且 Prefill 在 32K Context 下維持 538 tok/s,不隨對話長度衰減。
最後那個「不衰減」正是 §2.1 的架構特性——30 層線性注意力讓 KV cache 幾乎不隨 Context 增長。這是混合架構模型在小 VRAM 上的最大優勢。
三、可複製的調校方法¶
3.1 先確認後端是對的¶
最容易白費力氣的地方。安裝工具通常依序偵測 CUDA → Vulkan → CPU,若未安裝 CUDA Toolkit 會默默退回 Vulkan,在 NVIDIA 卡上通常慢 20~40%。
在錯誤的後端上調參,前面所有努力都會失真。
3.2 清空 VRAM 再測¶
零成本的 20~100% 效能提升。測試前後都該檢查顯卡佔用。
3.3 四步流程¶
1. 粗掃:大範圍找方向
Experts-on-CPU = 40, 32, 27, 23
2. 細掃:找峰值
Experts-on-CPU = 23, 21, 19, 17
3. 長 Context 驗證 ← 最關鍵,別跳過
對峰值與峰值+1,測 depth 0 / 8K / 32K
4. 執行緒掃描
threads = 6, 14, 20,重複次數提高到 10
3.4 判讀原則¶
看 Prefill,不要只看 Decode。
| 曲線形狀 | 意義 | 對策 |
|---|---|---|
| 單調遞減(越往 GPU 收越慢) | 全程都在溢出 | Experts 全放 CPU |
| 中間出現峰值 | 找到真正的甜蜜點 | 取峰值 |
| Prefill 斷崖式下跌 | 撞到 VRAM 邊界 | 取斷崖前一階,再往回退 1 層 |
最終值務必比峰值保守一層。 壓在斷崖邊緣的配置,對話中途突然變慢 9 倍,體感遠比少 5% 速度糟糕。
3.5 使用中的自我診斷¶
不用重跑 benchmark:
如果「聊越久,開始回答前等越久」——那就是 VRAM 溢出了。
Decode 速度可能正常,但 TTFT 會越拖越長。把 Experts 往 CPU 多丟 1~2 層即可。
四、結語:靜默降級¶
回到開頭的問題:「Experts 全放 CPU 最快」是真的嗎?
在那位作者的條件下是真的,但原因不是他以為的那樣。
他歸因於「Experts 擠掉了 Attention 與 KV cache 的頻寬位置」。真正的機制是:他的模型無論如何切分都會溢出,而溢出後的 GPU 存取比 CPU 直接運算還慢。換一個量化等級、換一個正確的後端、清掉背景程式的 VRAM,同樣的方法測出來就是相反的曲線。
這件事最大的啟發不是某個參數該設多少:
當你看到一個違反常識的實測結果時,先問「是不是有什麼東西壞了而沒有報錯」,再問「是不是我的常識錯了」。
VRAM 溢出不報錯、只是變慢,而且從最直觀的指標幾乎看不出來——這種靜默降級是本地 LLM 領域最常見的效能殺手。它讓很多人得出了正確的操作結論,卻建立在錯誤的因果解釋上。
而錯誤的因果解釋,換一個環境就會失效。
檢查清單¶
安裝後
- [ ] 確認後端是 CUDA 而非 Vulkan
- [ ] 確認工具版本夠新(新架構的支援通常是一路修過來的)
調校前
- [ ] 關閉所有吃 VRAM 的背景程式
- [ ] 記下 CPU 的實體核心數
- [ ] 從
config.json算出 KV cache 大小,決定要不要量化(多半不需要)
調校中
- [ ] 粗掃 → 細掃 → 長 Context 驗證 → 執行緒掃描
- [ ] 每一步都看 Prefill,不要只看 Decode
- [ ] 最終值比峰值保守 1 層
使用中
- [ ] 若「聊越久首字越慢」→ 溢出了,往 CPU 多丟 1~2 層
相關文章¶
- MoE 與 Dense:容量、計算與 Offload:Total/Active Parameters 與 Offload 的容量估算基礎
- 量化跨硬體選擇:量化格式如何影響容量與頻寬
- DFlash 解碼加速:另一條對抗記憶體頻寬瓶頸的路徑
範圍界線¶
本文數據來自 2026-08 單一機器的實測,硬體、模型與 runtime 版本變化快速。方法論(四步流程與判讀原則)可通用,但具體參數值不應直接套用——請在自己的硬體上重跑掃描。