Download docs/DESIGN.md from HelloSun/minicpm47woek_EPQ: direct link, hf CLI and curl.
- Browser
- Download file 6.12 kB
-
https://huggingface.co/HelloSun/minicpm47woek_EPQ/resolve/main/docs/DESIGN.md
- Command line
-
hf download hf://HelloSun/minicpm47woek_EPQ/docs/DESIGN.md
-
curl -L -o DESIGN.md https://huggingface.co/HelloSun/minicpm47woek_EPQ/resolve/main/docs/DESIGN.md
設計:記憶體帳目、SSD→RAM→CPU 資料流
這個模型在 8 GiB 裡能跑,不是因為它小,而是因為19.71 GiB 的權重裡有 18.16 GiB 從來不需要同時在 RAM 裡。
1. 權重帳目
MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf,21,166,255,008 B = 19.71 GiB,
架構 qwen35moe,40 個 block。
| 項目 | 大小 |
|---|---|
| 路由專家權重(40 層 × 256 個 × 3 個投影) | 18.16 GiB |
| 非專家常駐權重(embedding / lm_head / 注意力 / SSM / shared expert) | 1.54 GiB |
| 單一 token 的專家工作集(40 層 × top-8) | 14.53 MiB 平均 / 15.56 MiB 最大 |
單一 token 在解碼時需要 320 個 layer-expert,但因為槽位分兩種尺寸,正確的說法是 160 個大槽位 + 160 個小槽位。8 GiB 的 arena 塞得下 3714 個槽位,其中約四成是 大槽位(20/40 的層用大槽位),所以命中率可以到 70% 以上;2 GiB 的 arena 只有 332 個槽位湊不出這個組合,命中率崩到 22.7%(見 README 的 budget 曲線)。
兩種 expert 大小
這個模型的 ffn_down_exps 量化型別逐層不同:
| 層 | ffn_down_exps |
每個 expert 位元組 |
|---|---|---|
| 20 層 | Q6_K | 2,039,808 B |
| 20 層 | Q4_K | 1,769,472 B |
ffn_gate_exps 與 ffn_up_exps 全部是 Q4_K,每個 589,824 B。
平均下來每個 layer-expert 是 1.95 MiB,因此 arena 的槽位分成兩種尺寸: 小 1.69 MiB × 2420 + 大 1.95 MiB × 806(8 GiB 預算下)。
這一點是這個模型相對於參考實作最重要的結構差異:參考的 Qwen3.6-35B-A3B 是 37 層 1,900,544 B + 3 層 2,039,808 B,槽位比例完全不同。槽位尺寸必須從 模型 metadata 推導,不能寫死。
混合架構
qwen35moe.block_count = 40,但只有 10 層是全注意力
(full_attention_interval = 4),另外 30 層是 SSM / Mamba 層
(ssm.* 張量,recurrent_layers 40 個元素全為 1)。
這影響兩件事:
- 正確性:模型 metadata 裡因此多了一個 BOOL 陣列,參考實作的 GGUF 標頭
解析器會在這裡錯位(見
CORRECTNESS.md§3.2)。 - 效能:SSM 層的計算可以平行,不像注意力那樣受限於序列維度,所以
執行緒數的結論與參考實作相反(見
NEGATIVE_RESULTS.md)。
2. 資料流
GGUF 檔(19.71 GiB)
│
│ 非專家權重:mmap,乾淨的 file-backed 頁
│ 路由專家權重:自訂 buffer type,只回報 4 KB,
│ 真正的內容留在檔案裡(pager 用 pread 管理)
▼
SSD → RAM:pager 在需求觸發時把選中的 expert 讀進 arena 槽位
│
▼
RAM → CPU:ggml 自己的量化點積(ggml_vec_dot_q4_K_q8_K / q6_K_q8_K)
路由本身(logits / softmax / top-k / 權重正規化)仍然由 ggml 算完,
build_moe_ffn 只在「把被選到的 8 個 expert 算出來並加權求和」這一段
交給 sdq_build_moe。
3. 實作的關鍵決定
三段並行讀取
一個 expert 的 gate / up / down 在檔案上互不相鄰。循序讀是 3 × 延遲的鏈, 改成三段同時讀之後,一層 8 個全 miss 的 expert 明顯變快。
需求觸發的 LRU 淘汰 + pin
acquire() 是「先讀完再 publish」。reserve_slot() 會在資料還沒讀進來之前
就把 (layer, expert) 記進 page 表,一旦 pread 失敗,中斷槽就會留在表裡卻是
舊資料,之後的 probe() 會把它當成命中 → 靜默算錯。成本相同,但不能這麼做。
arena sizing 扣的是「匿名 RSS」
statm 的 resident 把 mmap 進來的模型權重頁也算成已用,但那些是乾淨的
file-backed 頁,記憶體吃緊時核心會直接回收,既不佔用寶貴的實體 RAM 也不會產生
swap I/O。把它算進「已用」會讓 arena 被無謂地縮水。
arena = 預算 − 匿名 RSS − SDQ_RESERVE_MB
RLIMIT_DATA 只當上界,不當預算來源:它算的是匿名虛擬記憶體,而
llama.cpp 會配置大量「映射了但永遠不會 touch」的圖與 compute buffer。
8 GiB 下的 reserve 校正
SDQ_RESERVE_MB 必須逐模型、逐 ctx/ubatch 校正。本模型在
ctx=4096 / ubatch=128 下量到的:
| reserve | arena | 槽位 | 峰值匿名 RSS | 8 GiB 內? |
|---|---|---|---|---|
| 128 MiB | — | — | 4042.6 MiB | 是(餘 53 MiB,太緊) |
| 256 MiB | — | — | 3918.1 MiB | 是(餘 178 MiB) |
| 384 MiB | — | — | 3795.7 MiB | 是(餘 300 MiB) |
| 512 MiB | 6.35 GiB | 3714 | 7799 MiB | 是(餘 446 MiB) |
| 640 MiB | 6.23 GiB | 3644 | 7623.0 MiB | 是(餘 292 MiB) |
| 768 MiB | 6.11 GiB | 3573 | 7498.5 MiB | 是 |
| 1024 MiB | 5.87 GiB | 3432 | 7251.6 MiB | 是 |
峰值匿名 RSS 幾乎與 reserve 成 1:1 反比(每減 128 MiB,RSS 多約 123 MiB), 因為省下來的預留全部變成 arena。512 MiB 是這個模型在 8 GiB 下的選擇。
換 RAM 預算時 reserve 也要跟著換。本 repo 的量測腳本用一條比例規則:
reserve = clamp(RAM 預算 / 16, 128 MiB, 1024 MiB)
在 8 GiB 重現 512 MiB,在 4 GiB 給出 256 MiB(餘 178 MiB)。 用固定的 640 MiB 跑 4 GiB 會直接超出上限。
4. 輸出不直接寫 dst
同一個 token 的 8 個 expert 會撞在一起,所以每個執行緒一份累加緩衝,
最後一道 barrier 之後歸約。同時每個 expert 算完立刻 release() 它的槽位 ——
不放的話 prefill 一層會把整層用到的 expert 全部釘住(最多 256 個),
大槽位不夠時會中止。
5. 這個設計沒有做的事
- 不碰路由。 路由必須完整算完才知道要哪些 expert,而路由本身很小。
- 不用 swap。 swap 的延遲不可預測,而且本專案的 no-swap 是核心保證。
- 不做壓縮。 權重已經是 Q4_K_M,再壓就是品質換速度,不在本專案範圍。
- 不處理視覺。 這個 GGUF 的 vision projector 在另一個檔案
(
mmproj-model-f16.gguf),本路徑只做文字推論。