|
Download README.md from HelloSun/minicpm47woek_EPQ: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/HelloSun/minicpm47woek_EPQ/resolve/main/README.md
- Command line
-
hf download hf://HelloSun/minicpm47woek_EPQ/README.md
-
curl -L -o README.md https://huggingface.co/HelloSun/minicpm47woek_EPQ/resolve/main/README.md
16.8 kB
| license: apache-2.0 | |
| library_name: gguf | |
| base_model: openbmb/MiniCPM-V-4.7-35B-A3B | |
| pipeline_tag: text-generation | |
| tags: | |
| - minicpm | |
| - qwen35moe | |
| - gguf | |
| - moe | |
| - cpu | |
| - low-memory | |
| - expert-offloading | |
| - no-swap | |
| # 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`。 | |
| ```bash | |
| python3 demo.py --once | |
| ``` | |
| 從頭開始時,腳本會取得本 repo 原始碼、從 `openbmb/MiniCPM-V-4.7-35B-A3B-gguf` | |
| 下載 GGUF、套用 patch、編譯,然後跑一句示範。模型已下載的話: | |
| ```bash | |
| 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 | |
| ``` | |
| 互動式聊天: | |
| ```bash | |
| 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`](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`](docs/DESIGN.md) §3。 | |
| --- | |
| ## 正確性 | |
| ```bash | |
| 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`](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`](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`](docs/STRICT_8GIB.md) | **8 GiB / no-swap 的方法、證據與限制**(先讀這份) | | |
| | [`docs/DESIGN.md`](docs/DESIGN.md) | 記憶體帳目、SSD→RAM→CPU 設計、reserve 校正 | | |
| | [`docs/CORRECTNESS.md`](docs/CORRECTNESS.md) | 三層驗證、決定性證據、Q8_K 量化邊界分析 | | |
| | [`docs/NEGATIVE_RESULTS.md`](docs/NEGATIVE_RESULTS.md) | 試過但沒留下的最佳化 | | |
| --- | |
| ## 重現 | |
| ```bash | |
| # 受管治的單次執行 | |
| 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 與其相依套件保留各自授權條款。 |