EPM:MiniCPM-V-4.7-35B-A3B 的 8 GiB 專家分頁 CPU 推論

在 8 GiB RAM、沒有 swap 的條件下,用 CPU 跑 19.71 GiB 的 MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf。

同一個模型、同樣的 8 GiB 上限之下,未修改的 llama.cpp 連模型都載不進去 (它要配置一個 14.4 GB 的 CPU_REPACK buffer,在載入階段就失敗)。EPM 把路由 專家權重留在儲存裝置,只把當前選中的專家分頁載入受限的 CPU arena: 5.83 tok/s,峰值匿名 RSS 7799 MiB,VmSwap 全程 0 MiB。

這個 repo 收錄實作(cpp/ + patches/)、量測工具與全部證據。 不包含模型權重。

名稱說明:EPM = Expert Paging for MiniCPM。本專案不是論文中的 SDQ (Sparse Decomposed Quantization):沒有 2:4 結構化稀疏跳零計算,也不宣稱 GPU 稀疏加速。sdq/ Python package、sdq_* C++ 檔案與 SDQ_* 環境變數是 歷史 implementation identifier,保留以免破壞既有驗證工具;對外產品名稱與 執行入口是 EPM / epq-chat。


快速開始

需求:Linux x86-64、C++ 工具鏈、CMake、Git、Python 3、19.71 GB 磁碟空間。 已測 llama.cpp commit 9d7d5049804be03fce6575cc9f49c058b3c48788。

python3 demo.py --once

從頭開始時,腳本會取得本 repo 原始碼、從 openbmb/MiniCPM-V-4.7-35B-A3B-gguf 下載 GGUF、套用 patch、編譯,然後跑一句示範。模型已下載的話:

python3 demo.py --model-path /path/to/MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf \
  --once --ctx 4096 --threads 8 --n-predict 48

互動式聊天:

python3 demo.py --model-path /path/to/MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf \
  --ctx 4096 --threads 8

記憶體預算與 reserve:8 GiB 目標用 --ram-budget-mb 8192 + SDQ_RESERVE_MB=512(這個模型在 ctx=4096 / ubatch=128 下校正出來的值, 見 docs/DESIGN.md §3)。換 RAM 預算要重新校正。

此 GGUF 不含 vision projector,本路徑只支援文字推論。已驗證的 context 是 4K; 模型原生支援 262 K,但在這個 RAM 預算下沒有驗證過。


在 8 GiB / no-swap 下量到的結果

每次執行都由 tools/strict8g.py 管治:所有匿名記憶體映射被 mlock() 鎖住(被鎖的頁永遠不會被寫到 swap,所以 no-swap 是核心保證的), 實體常駐量有硬上限,超標立刻 SIGKILL,另外 RLIMIT_DATA 當第二道閘門。 strict_ok=False 的樣本一律不採用。

標配(N=9,配對交錯,提示詞中英交替)

指標 值
Decode tok/s(中位數) 5.83
單輪全距 5.19 – 6.63(抖動 25%)
Expert cache hit rate 68.68%
SSD 讀取 / decode token 214.1 MB
Arena 6.35 GiB(3714 slots)
峰值匿名 RSS 7799 MiB / 8192 MiB 上限
峰值 VmSwap 0.000 MiB
strict_ok 9/9

條件:2× Intel Xeon Platinum 8559C(AVX-512 / AMX),ctx=4096、ubatch=128、 8 threads、greedy、48 requested tokens。

這是共享主機,同一組設定的單輪抖動可達 25%。數字足以支撐「能不能跑」 與「大概多快」,不足以支撐 5% 等級的宣稱。

同一個 8 GiB 上限下的傳統 llama.cpp

路徑 結果
傳統 llama.cpp llama-completion(未套 patch) 無法載入模型
EPM epq-chat 5.83 tok/s,峰值匿名 RSS 7799 MiB,VmSwap 0

失敗點:

ggml_aligned_malloc: insufficient memory (attempted to allocate 14400.00 MB)
ggml_backend_buft_alloc_buffer_n_default: failed to allocate CPU_REPACK buffer of size 15099494400
llama_model_load: error loading model: unable to allocate CPU_REPACK buffer

對照必須在同一個上限下建立。先量「兩邊都沒記憶體限制」再比較,是把 RAM 這個變數漏掉了。

Pager budget 曲線(實體上限固定 8 GiB,只有 arena 大小在變,N=5)

Budget Arena 槽位 命中率 SSD MB/token Decode tok/s 峰值匿名 RSS VmSwap
2 GiB 0.57 GiB 332 22.72% 526.2 4.32 1842 MiB 0
3 GiB 1.53 GiB 895 50.19% 340.0 5.16 2828 MiB 0
4 GiB 2.50 GiB 1459 59.81% 275.8 5.48 3817 MiB 0
5 GiB 3.46 GiB 2023 64.88% 240.8 5.82 4805 MiB 0
6 GiB 4.43 GiB 2587 67.52% 222.2 5.38 5793 MiB 0
7 GiB 5.39 GiB 3150 68.60% 214.0 5.10 6779 MiB 0
8 GiB 6.35 GiB 3714 70.74% 198.9 5.45 7766 MiB 0

2 GiB 只有 332 個槽位。單一 token 的工作集是 320 個 layer-expert,但槽位還分兩種 尺寸:20 層用大槽位、20 層用小槽位,所以每個 token 需要的是 160 個大槽位加 160 個小槽位,而不是 320 個任一尺寸的槽位。2 GiB 之下 arena 只有 0.57 GiB, 湊不出這個組合,命中率因此崩到 22.7%(8 GiB 之下 3714 個槽位裡約四成是大槽位, 命中率回到 70.7%)。

命中率在 8 GiB 這個端點仍然在爬升(8 GiB 70.7% → 16 GiB 77.5%)→ 瓶頸是記憶體上限,不是命中率飽和。

這一欄與上面標配的命中率(68.68%)不同是正常的:這裡是 N=5、提示詞輪替不同, 標配是 N=9。兩者都受同一個 8 GiB 上限管治。

RAM 擴張曲線:4 / 8 / 16 / 64 GiB

--ram-budget-mb 從 4 GiB 拉到 64 GiB。每次執行都由 tools/strict8g.py 管治, 36/36 輪 strict_ok=True,VmSwap 全程 0。

兩點方法學說明,讀表前請先看:

  1. 這條曲線不用 mlock。 mlock() 會強制整個映射常駐 —— 對一個 60 GiB 的 arena,它會讓 RSS 直接變成 60 GiB,量到的就不是真實機器的行為。no-swap 的 核心保證保留給 8 GiB 這個部署目標(那一點另外有完整 mlock 證據,見 docs/STRICT_8GIB.md)。
  2. arena 不預先碰滿。 全部路由專家權重只有 18.16 GiB,所以預先碰觸上限就是 18.17 GiB;超出來的部分等到真的用到才 fault in。這就是為什麼 64 GiB 預算的 「峰值 RSS」會停在約 24.0 GiB 而不是 62 GiB —— 那個數字才是真實的記憶體佔用。

結構指標(槽位、命中率、SSD 讀取量、峰值 RSS)都是設定的直接函數,不受執行抖動 影響,四個點都列。吞吐另外做 N=9 配對交錯 並用 Welch t 檢定。

Budget Arena 槽位 命中率 SSD MB/token 平均 tok/s 單輪標準差 峰值 RSS VmSwap
4 GiB 2.74 GiB 1600 61.50% 275.4 5.43 ± 0.37 0.37 3963 MiB 0
8 GiB 6.36 GiB 3715 71.98% 199.1 6.33 ± 0.33 0.33 7669 MiB 0
16 GiB 13.59 GiB 7943 77.54% 157.9 6.28 ± 0.41 0.41 15076 MiB 0
64 GiB 59.89 GiB 35003 77.54% 157.6 6.32 ± 0.51 0.51 23969 MiB 0
比較 結果 Welch t p 判定
4 GiB 比 8 GiB 5.43 → 6.33(**+16.6%**) −5.44 0.0001 統計顯著
8 GiB 比 16 GiB 6.33 → 6.28(−0.8%) −0.30 0.76 無法分辨
8 GiB 比 64 GiB 6.33 → 6.32(−0.2%) −0.05 0.96 無法分辨

這條曲線的重點

  • **4 GiB → 8 GiB 是真實且顯著的提升:+16.6%**(t = −5.44,p = 0.0001)。4 GiB 時 命中率只有 61.5%、SSD/token 275.4 MB(8 GiB 的 1.4 倍),因為 arena 湊不出 「160 大 + 160 小」的工作集。
  • 8 GiB 以上再加記憶體完全沒有可量測的收益:8 vs 16(t = −0.30, p = 0.76)與 8 vs 64(t = −0.05, p = 0.96)全部無法分辨,而記憶體用量是 2 倍到 8 倍。 結構指標也說明原因:命中率在 **16 GiB 就飽和在 77.54%**,SSD/token 飽和在她 ~158 MB。
  • 真正的記憶體需求飽和在約 24.0 GiB 峰值(18.16 GiB 全部專家權重 + 1.54 GiB 非專家權重 + KV cache / compute buffer / runtime)。
  • 部署結論:8 GiB 就是對的容量。 它拿到飽和命中率的 93%(72.0% 對 77.5%), 而且是整條曲線裡 throughput 顯著優於更低設定、又與所有更高設定打平的唯一一點。

逐輪原始資料:bench/results/ram_sweep_n9.json(N=9)與 bench/results/ram_sweep.json(N=3,含 32 GiB 點)。


記憶體怎麼配置的

模型 metadata:每個 MoE 層 256 個路由專家,每個 token 選 8 個。 GGUF 21,166,255,008 B,其中路由專家權重 18.16 GiB、非專家常駐權重 1.54 GiB。

EPM 把未啟用的專家留在檔案裡,依需求把選中的專家載入記憶體 arena。關鍵實作:

  • 三段並行讀取:一個 expert 的 gate/up/down 在檔案上互不相鄰,循序讀是 3 × 延遲的鏈;改成三段同時讀之後明顯變快。
  • 大小分級槽位:這個模型有兩種 expert 大小 —— 20 層的 ffn_down_exps 是 Q6_K(2,039,808 B/expert),另外 20 層是 Q4_K(1,769,472 B/expert)。 依分佈分大小槽位,而不是全部按最大的配。
  • 需求觸發的 LRU 淘汰,加上對「本 token 使用中」的 pin 保護。
  • 讀取與計算重疊(mode C),每個 expert 算完立刻釋放槽位。
  • 跨層預取預設關閉:它是負結果(見 docs/NEGATIVE_RESULTS.md)。

Arena sizing

arena = RAM 預算 − 匿名 RSS − SDQ_RESERVE_MB
  1. 扣的是「匿名 RSS」,不是「總 RSS」。 statm 的 resident 把 mmap 進來的 模型權重頁也算成已用,但那些是乾淨的 file-backed 頁,核心會回收。
  2. RLIMIT_DATA 只當上界,不當預算來源。 它算的是匿名虛擬記憶體, 而 llama.cpp 會配置大量「映射了但永遠不會 touch」的 buffer。

SDQ_RESERVE_MB 是逐模型、逐 ctx/ubatch 校正的。本模型 ctx=4096 / ubatch=128 下,512 MiB 留 446 MiB 餘量。峰值匿名 RSS 幾乎與 reserve 成 1:1 反比。換 RAM 預算必須重新校正 —— 用固定的 512 MiB 跑 4 GiB 會超出上限。 完整校正表見 docs/DESIGN.md §3。


正確性

python3 -m sdq.verify --model <gguf> --bin <epq-chat> --fast
python3 tools/determinism_check.py --model <gguf> --bin <epq-chat> --runs 5 -n 48
層 內容 結果
1 kernel 單一 expert 前向 vs NumPy 純重算(L0 E3 / L17 E200 / L33 E7 / L39 E100) 全 PASS,max abs diff ≤ 3.7e-09
2 整層 真實路由輸入 vs NumPy,L0 / L39 L0 PASS(1.1e-08);L39 PASS(8.8e-05,落在單一 Q8_K LSB 翻轉上界 3.2e-02 之內)
2b 預取壓力 預取開 + 極小 arena 強制回收,兩種槽位配置 4/4 PASS
3 端到端 A/B vs llama.cpp 原生 mul_mat_id 在 8 GiB 上不可能執行 —— 需要整個 19.71 GiB 模型常駐

決定性:5 次執行 × 48 token × 248,144 vocab 的 logits 全部逐位元組相同。

過程中抓到並修正的問題包括一個 GGUF 標頭解析在 BOOL 陣列上錯位的 bug (qwen35moe.attention.recurrent_layers,這個混合架構模型才有)。

L39 那一組 8.8e-05 的差異被完整追查到根因,並證明是 Q8_K 量化邊界造成、 不是 pager 的錯誤:把 C++ 端自己的 SiLU 中間值餵回 NumPy 參考,輸出立刻從 7.9e-04 掉到 6.7e-08。完整證據鏈見 docs/CORRECTNESS.md §4。


這個模型特有的兩件事

相對於同系列的 Qwen3.6-35B-A3B 實作,本模型有兩個結構差異, 移植時必須重新推導、不能照抄:

  1. 兩種 expert 大小(20 層 2,039,808 B / 20 層 1,769,472 B)。槽位尺寸必須 從 metadata 推導。參考實作是 37 層 + 3 層,比例完全不同。
  2. 混合架構:40 層裡只有 10 層是全注意力,30 層是 SSM 層。直接後果是 執行緒數的結論相反 —— 這裡 12 執行緒比 8 快約 6.5%,參考實作則是 8 之後開始變慢。SSM 層不受序列維度限制,能吃滿更多執行緒。

另外模型 metadata 裡多了一個 BOOL 陣列(recurrent_layers), 參考實作的 GGUF 標頭解析器會在那裡錯位。


其他結果

執行緒數

threads 4 8 10 12 16
平均 tok/s 4.65 5.96 6.16 6.35 6.27

4 → 8 是真實且顯著的提升(+28%)。8 之後的差異都在單輪抖動之內。 預設 8,建議 12(理由見上一節)。

預取(負結果)

跨層預取 5.95 ± 0.37、僅 end_token 5.78 ± 0.33、關閉 6.03 ± 0.44 —— 差異遠小於單輪抖動,統計上無法分辨,而且多數輪次 SSD 讀取量上升。 預設關閉。

儲存裝置會改變結論

本機是本地 NVMe。慢速儲存上 I/O 排程是瓶頸,預取與並行讀取才有意義; 快速 NVMe 上命中率(arena 大小)才是瓶頸。本 repo 的 tok/s 不能直接套用到 慢速儲存的機器上。 請先跑 tools/io_probe.py 再決定要最佳化哪一邊。


這個量測沒有證明的事

  1. 不是 cgroup 驗證。 這個環境的 cgroup2 掛載唯讀、沒有 CAP_SYS_ADMIN。 目前的保證是「kernel RLIMIT_DATA + 取樣殺行程」與「mlock 匿名頁」兩條腿。
  2. 主機 swap 是開啟的。 no-swap 由 mlock 讓它不可能發生,不是把主機的 swap 關掉。
  3. brk() 主堆疊未被鎖(glibc 內部直接下 syscall)。大型配置都走 mmap。
  4. 共享主機,單輪抖動可達 25%。
  5. 只驗到 4K context;模型原生支援 262 K。
  6. 只做文字推論,vision projector 是另一個檔案。
  7. 儲存裝置比歷史量測的機器快一到兩個數量級。

完整方法與證據見 docs/STRICT_8GIB.md。


Repository layout

路徑 內容
cpp/ C++ expert pager(sdq_pager.*、sdq_moe.cpp、sdq_buft.cpp、sdq_cli.cpp、sdq_mtp.cpp)
patches/ 把 pager 接進 llama.cpp 的 patch(針對固定 commit 9d7d5049)
sdq/ 設定、受管治的基準測量、實驗、驗證、HF sync
tools/ 8 GiB/no-swap 管治、GGUF 索引、決定性檢查、儲存 probe
bench/ 量測腳本與 bench/results/ 原始輸出(JSON)
docs/ 見下

文件:

文件 內容
docs/STRICT_8GIB.md 8 GiB / no-swap 的方法、證據與限制(先讀這份)
docs/DESIGN.md 記憶體帳目、SSD→RAM→CPU 設計、reserve 校正
docs/CORRECTNESS.md 三層驗證、決定性證據、Q8_K 量化邊界分析
docs/NEGATIVE_RESULTS.md 試過但沒留下的最佳化

重現

# 受管治的單次執行
python3 tools/strict8g.py --limit-mb 8192 --record /tmp/one.json -- \
  epq-chat -m model.gguf -p "..." -n 48 -c 4096 -t 8 -b 128 \
  --temp 0 --no-chat --no-interactive --ram-budget-mb 8192

# 標配基準測量(9 輪,配對交錯)
python3 -m sdq.strict_bench --model model.gguf --bins base=./build/bin/epq-chat \
  --rounds 9 --strict-mb 8192 --label headline

# 正確性與決定性
python3 -m sdq.verify --model model.gguf --bin ./build/bin/epq-chat --fast
python3 tools/determinism_check.py --model model.gguf --bin ./build/bin/epq-chat --runs 5 -n 48

# 曲線與負結果
python3 bench/run_budget_curve.py --model model.gguf --bin ./build/bin/epq-chat --rounds 5
python3 bench/run_ramsweep.py     --model model.gguf --bin ./build/bin/epq-chat --rounds 3
python3 bench/run_n9.py           --model model.gguf --bin ./build/bin/epq-chat --rounds 9
python3 bench/run_negative.py --which prefetch --model model.gguf --bin ./build/bin/epq-chat
python3 bench/run_negative.py --which threads  --model model.gguf --bin ./build/bin/epq-chat

# 儲存裝置特性、路由重疊、GGUF 結構
python3 tools/io_probe.py --model model.gguf
python3 tools/route_overlap.py --model model.gguf
python3 tools/gguf_index.py model.gguf --json bench/expert_index.json

License

MiniCPM-V base model 由 upstream 發布;請以 upstream model card 的目前條款為準。 本 repository 包含推論軟體與量測結果,不包含模型權重。 llama.cpp 與其相依套件保留各自授權條款。

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for HelloSun/minicpm47woek

Finetuned
(5)
this model