跳轉到

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,而它們由權重投影而來:

K_i = W_k · x_i
V_i = W_v · x_i

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 的混合執行是串行的:

時間軸 →
GPU: [算層 0-22] ────等待──── [算下一 Token...]
CPU: ────等待──── [算層 23-39]

總時間是相加,不是取最大值:

T_混合 = T_gpu + T_cpu + 同步開銷

因為 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 增長)。

每 Token = 2(K,V) × 2(kv_heads) × 256(head_dim) × 10(層) = 10,240 元素
fp16 → 20 KiB / Token
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 才是空間大戶:

單一 expert = 3 × 2048 × 512 = 3.15M 參數
每層 256 個 experts = 0.805B 參數 → Q4 約 483 MB
全 40 層 ≈ 19.3 GB

所以該問的不是「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
  1. E-core 有幫助:14 比 6 快 16%。MoE 的 Experts 是破碎小矩陣運算,本來就填不滿 P-core 的 SIMD 管線,「多核心分擔記憶體延遲」效益更大。
  2. 超執行緒有害:20 比 6 還慢,標準差飆到 ±3.66(其他配置 ±1.3~1.5)。HT 邏輯執行緒與實體核心共用 L1/L2,在本來就 cache 不友善的破碎存取上加劇爭用。
  3. 趨勢非單調: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 層

相關文章

範圍界線

本文數據來自 2026-08 單一機器的實測,硬體、模型與 runtime 版本變化快速。方法論(四步流程與判讀原則)可通用,但具體參數值不應直接套用——請在自己的硬體上重跑掃描。