Laguna-S-2.1 Q4_K_M:EPQ MoE 專家分頁實驗
把 EPQ(Expert Paging Quantization) 的
「MoE 專家常駐 RAM 分頁」推理架構,移植到 poolside/Laguna-S-2.1-GGUF 的
Q4_K_M 上。
Laguna-S-2.1-Q4_K_M 的 GGUF 有 96.0 GB,其中 85.4 GiB 是 48 層 × 256 個路由 expert 的權重。EPQ 不把這些 expert 全部放進 RAM,而是:
- 只保留 attention / dense / shared-expert / router / embedding 等非 expert 權重常駐;
- 把每個 expert 的
ffn_gate_exps/ffn_up_exps/ffn_down_exps放在 固定大小的 RAM arena 內當「槽位」; - 依 token 的路由結果,用 O_DIRECT 從 SSD 把選中的 expert 讀進槽位,算完即被淘汰;
- MoE 的加權累加用一個自訂 ggml op 完成,全程不是「先整批載入再一次算」。
結果:一顆 96 GB 的模型可以在 4 GiB RAM 上限內跑起來,代價是速度與 SSD 讀取量。
結論
| RAM 上限 | 結果 | arena | 槽位(小/大) | expert 覆蓋率 | 匿名峰值 | RSS 峰值 | decode | SSD |
|---|---|---|---|---|---|---|---|---|
| 2 GiB | FAIL(arena 太小) | - | - | - | 280 MB | 286 MB | - | - |
| 4 GiB | PASS | 1.94 GiB | 240 (180/60) | 2.3% | 2980 MB | 6858 MB | 0.56 tok/s | 3648 MB/tok |
| 8 GiB | PASS | 5.69 GiB | 804 (668/136) | 6.7% | 6818 MB | 10697 MB | 0.83 tok/s | 2456 MB/tok |
| 16 GiB | PASS | 13.7 GiB | 1932 (1604/328) | 16.0% | 15013 MB | 18891 MB | 1.11 tok/s | 1792 MB/tok |
| 32 GiB | PASS | 29.7 GiB | 4187 (3475/712) | 34.8% | 31397 MB | 35275 MB | 2.00 tok/s | 1020 MB/tok |
| 64 GiB | PASS | 57.7 GiB | 8134 (6750/1384) | 67.6% | 60073 MB | 63950 MB | 1.92 tok/s | 538 MB/tok |
- 量測方式:
prlimit --data=<上限>硬限制 + 20 ms 取樣的匿名 RSS 峰值追蹤 (strict_bench.py),EPQ_RESERVE_MB=2048,-c 2048 -b 128 -t 16,-n 16, greedy(--temp 0)。64 GiB 這列用EPQ_RESERVE_MB=6144(見「已知陷阱 1」)。 - 「expert 覆蓋率」= arena 大小 ÷ 路由 expert 總位元組(85.4 GiB)。
- 2 GiB 是真下限:扣除 non-expert 常駐與 I/O 緩衝後 arena 剩不到一個 大 expert(18 MiB)可用,pager 直接拒絕啟動(回報「expert arena 太小」), 不會給你一個靜默錯誤的答案。
快速開始
# 抓基底(poolsideai/llama.cpp 的 laguna 分支)
git clone -b laguna https://github.com/poolsideai/llama.cpp.git llama-laguna
cd llama-laguna && git checkout 06f8ceb
# 放 sdq_* + 套 patch
cp /path/to/laguna-epq/sdq_*.h /path/to/laguna-epq/sdq_*.cpp src/
git apply /path/to/laguna-epq/0001-epq-laguna.patch
cmake -B build_epq -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF \
-DGGML_NATIVE=OFF -DGGML_AVX512=ON -DGGML_AVX512_VBMI=ON \
-DGGML_AVX512_VNNI=ON -DGGML_AVX512_BF16=ON -DGGML_AVX2=ON \
-DGGML_FMA=ON -DGGML_F16C=ON
cmake --build build_epq -j16 --target epq-chat llama-server llama-perplexity
# 跑(4 GiB 上限)
EPQ_REQUIRE_PAGER=1 EPQ_RAM_BUDGET_MB=4096 EPQ_RESERVE_MB=2048 EPQ_PREFETCH=0 \
./build_epq/bin/epq-chat -m laguna-s-2.1-Q4_K_M.gguf \
-p "Explain MoE routing." -n 24 -c 2048 -b 128 -t 16 \
--temp 0 --no-interactive --no-chat --ram-budget-mb 4096
或直接 ./build.sh <llama.cpp-checkout> [build-dir]。
模型與路由
poolside/Laguna-S-2.1-GGUF Q4_K_M,general.architecture = laguna:
| 參數 | 值 |
|---|---|
block_count |
48 |
leading_dense_block_count |
1(第 0 層是 dense) |
expert_count |
256 |
expert_used_count |
10 |
embedding_length |
3072 |
expert_feed_forward_length |
1024 |
expert_shared_feed_forward_length |
1024(第 0 層) |
context_length |
1048576 |
routed_scaling_factor |
2.5 |
權重組成(路由 expert 部分)
| 層 | 張量型別 | 每個 expert 3 張矩陣 | 每 expert 位元組 |
|---|---|---|---|
| 1–39(39 層) | Q4_K(type 12) |
ffn_{gate,up,down}_exps,各 1,769,472 B |
5,308,416 B(5.06 MiB) |
| 40–47(8 層) | BF16(type 30) |
同上,各 6,291,456 B | 18,874,368 B(18 MiB) |
路由 expert 合計 91,653,931,008 B = 85.36 GiB。因為有兩種尺寸,pager 開兩種槽位
(小 5.06 MiB / 大 18 MiB),大 expert 只進大槽,小 expert 可進任一槽。
嚴格 RAM 上限實測
詳細輸出見 results/strict_memory_levels.md 與 results/strict_memory_levels_final.json。
掃描重點:
- 4 GiB:arena 1.94 GiB,命中率 0(工作集比 arena 大,幾乎每個 expert 每次都被淘汰 重讀),0.56 tok/s、3.6 GB/tok。
- 8 GiB:arena 5.69 GiB,命中率開始起來(20.7%),0.83 tok/s。
- 16 GiB:arena 13.7 GiB,命中率 31.8%,1.11 tok/s、1.8 GB/tok。
- 32 GiB:arena 29.7 GiB,命中率 44.7%,2.00 tok/s、1.0 GB/tok。
- 64 GiB:arena 57.7 GiB,命中率 51.8%,1.92 tok/s、538 MB/tok;arena 已達
routed expert 總量的 67.6%,工作集幾乎全部命中,
evictions為 0。
讀表時請注意
- 匿名峰值是硬上限量測的主角:
prlimit --data限制的是匿名映射的 size, 我們用 20 ms 取樣的AnonymousRSS 峰值確認它沒有超過上限(anon_within_limit)。 - RSS 峰值 > RAM 上限是正常的:96 GB 的模型檔仍然被
mmap,檔案對映 RSS不算在RLIMIT_DATA內;真正的預算限制是匿名記憶體。 - arena 不是全部的匿名用量:執行緒堆疊、ggml compute buffer、I/O 對齊緩衝
另外要算,所以
EPQ_RESERVE_MB必須蓋得住(見下)。
Top-k 消融:原始 10 vs 4
Laguna-S-2.1 的原始路由是 expert_used_count = 10。epq-chat --expert-used N
會同時覆寫 llama graph 與 pager 看到的 top-k(EPQ_EXPERT_USED),
在不改動 GGUF、不改 arena 設定的情況下做 A/B:
| RAM 上限 | top-k | decode | SSD | 命中率 | evictions | expert requests | PPL |
|---|---|---|---|---|---|---|---|
| 64 GiB | 10(原始) | 2.42 tok/s | 305 MB/tok | 78.4% | 64 | 35176 | 1.9462 |
| 64 GiB | 4 | 3.52 tok/s | 242 MB/tok | 69.0% | 0 | 14598 | 8.0007 |
| 16 GiB | 10(原始) | 1.30 tok/s | 1561 MB/tok | 49.0% | 15998 | 35176 | — |
| 16 GiB | 4 | 3.42 tok/s | 481 MB/tok | 56.2% | 4462 | 14598 | — |
- 量測:greedy、
-n 64、-c 2048 -b 128 -t 16、EPQ_PREFETCH=0; 64 GiB 用EPQ_RESERVE_MB=6144,16 GiB 用 2048(同上面的掃描)。 - 速度:64 GiB 下 top-4 快 1.45 倍,16 GiB 下快 2.63 倍;SSD 讀取量分別少 20.5% 與 69.2%。top-4 每 token 只碰 4 個 expert,所以請求數少 58.5%, 低 RAM 時工作集縮小、eviction 從 15998 降到 4462,加速最明顯。
- 品質:用
--ppl-file對同一段 799-token 英文散文(Alice in Wonderland 開頭,results/ppl_en.txt)做 teacher-forced NLL,top-10 的 perplexity 是 1.9462, top-4 是 8.0007(4.11 倍)。同一 prompt 的 greedy 輸出也很快分岔, top-4 的樣本有重複詞與離題。 - 結論:top-4 是「用模型品質換速度/記憶體」的旋鈕,不是等價加速;
要保留原始模型行為就維持 top-10。詳細原始資料見
results/topk_ablation*.json與results/topk_levels/。
Least-Stale 淘汰策略:LRU vs Least-Stale
SpecMD(arXiv 2602.03921)提出 Least-Stale:
把快取頁分成 stale(上一個 forward pass 用過、這一輪還沒用)與 current
(這一輪用過或已被預取),先淘汰 stale,並依 layer 位置保護這一輪還沒輪到的層。
我們在 sdq_pager 加入兩種實作,用 EPQ_EVICT 切換(預設 lru,原行為不變):
EPQ_EVICT |
策略 |
|---|---|
lru |
原本的 LRU(預設) |
ls |
論文式兩佇列:stale 先於 current;佇列內 layer 由小到大(FIFO) |
ls_pos |
位置感知:stale-left → current-left → stale-right → current-right;左右兩側都先挑 layer 較大者(離下一次使用最遠) |
量測條件同 top-k:greedy、原始 top-10、-n 64、-c 2048 -b 128 -t 16、
EPQ_PREFETCH=0。8 GiB 用 EPQ_RESERVE_MB=2000,16/64 GiB 用 6144:
| RAM 上限 | 策略 | 命中率 | evictions | SSD | decode |
|---|---|---|---|---|---|
| 8 GiB | LRU | 31.94% | 23131 | 2286 MB/tok | 0.88 tok/s |
| 8 GiB | LS | 19.41% | 27536 | 2693 MB/tok | 0.66 tok/s |
| 8 GiB | LS + 位置 | 27.90% | 24550 | 2541 MB/tok | 0.80 tok/s |
| 16 GiB | LRU | 42.21% | 18961 | 1840 MB/tok | 1.10 tok/s |
| 16 GiB | LS | 24.56% | 25170 | 2500 MB/tok | 0.81 tok/s |
| 16 GiB | LS + 位置 | 36.20% | 21073 | 2013 MB/tok | 1.00 tok/s |
| 64 GiB | LRU | 78.454% | 62 | 304.2 MB/tok | 1.87 tok/s |
| 64 GiB | LS | 78.451% | 63 | 304.4 MB/tok | 2.30 tok/s |
| 64 GiB | LS + 位置 | 78.468% | 57 | 302.7 MB/tok | 2.57 tok/s |
- 64 GiB 幾乎沒有壓力:arena 57.7 GiB、8134 槽位,整輪只有 57–63 次淘汰, 三種策略的命中率差 < 0.02 個百分點,SSD 差 < 0.5%,decode 速度差在雜訊內。 LS + 位置少 5 次淘汰、SSD 少 1.5 MB/tok,但不足以宣稱加速。
- 8/16 GiB 由 LRU 勝出:arena 分別只放得下 811 與 1368 個槽位,而 top-10 每輪工作集約 480 個 expert,等於快取可容納 1.7–2.9 輪。Least-Stale 會把 上一輪的 stale-left expert 淘汰掉,但這些 expert 下一個 token 很可能再用; LRU 淘汰的是「好幾輪沒被用到」的頁,對這個工作負載更準。
- 位置感知明顯優於論文式兩佇列:
ls在 8/16 GiB 會大量淘汰 stale-right (23164/21115 次),也就是這一輪還沒輪到、馬上要用的層,命中率因此掉到 19–25%。ls_pos把 stale-right 保護住(只剩 942/450 次),命中率回到 27.9%/36.2%,證明「不要淘汰這一輪還沒用到的層」這個部分是對的;但還是 敵不過 LRU 對跨 token 重複使用的掌握。 - 結論:預設維持 LRU;
EPQ_EVICT=ls/ls_pos保留為可重現的實驗旋鈕。 SpecMD 的加速前提是「快取小於一輪工作集 + 預取把 current 佇列補熱」;本實驗 關掉預取、且快取可放 1.7 輪以上,Least-Stale 的 stale 佇列反而會每輪洗掉 工作集。這是這個 pager/工作負載下的負面結果,不是正確性問題(淘汰只改變 哪些權重留在 RAM,不改變 routing 或數學)。
正確性
移植只改「權重從哪裡來」,不改數學。因此驗證有兩層:
一、逐層對純 NumPy(含 routing)
EPQ_MOE_DUMP 會把某一層 build_moe_ffn 的輸入(cur、selected_experts、weights)
與輸出 dump 出來,verify_moe_layer_laguna.py 用 NumPy 重算整層並比對:
| 層 | 型別 | rel error | corr | 結果 |
|---|---|---|---|---|
| 1 | Q4_K |
0.0096 | 0.99989 | PASS |
| 40 | BF16 |
0.0025 | 0.999997 | PASS |
(routing 的 selected_experts 與 weights 逐元素完全一致;殘差來自量化點積
的浮點順序差,量級符合量化誤差。)
二、跨 RAM 預算的 greedy 輸出一致性
同一 prompt、--temp 0、-n 16,分別在 4 GiB 與 32 GiB 上限下生成,
逐字比對生成文字:
IDENTICAL greedy output (4 GiB vs 32 GiB)
分頁層完全沒有改變結果——命中與否、槽位多寡只影響速度與 SSD 讀取量。
環境變數
| 變數 | 說明 |
|---|---|
EPQ_PAGER |
設 0 關閉 pager(走原生 mmap 路徑) |
EPQ_REQUIRE_PAGER |
設 1 時 pager 初始化失敗就讓載入失敗(不要靜默回退) |
EPQ_RAM_BUDGET_MB |
RAM 總預算(MB),arena 會扣掉已用與預留 |
EPQ_EXPERT_USED |
覆寫路由 top-k(同時作用於 llama graph 與 pager;預設用 GGUF 的 10) |
EPQ_RESERVE_MB |
保留給 arena 以外匿名用量的 MB(本機建議 2048,見下) |
EPQ_PREFETCH |
設 0 關閉背景預取 |
EPQ_EVICT |
淘汰策略:lru(預設)、ls(Least-Stale 兩佇列)、ls_pos(Least-Stale + layer 位置) |
EPQ_STATS_FILE |
pager 指標 JSONL 輸出(掃描腳本在用) |
EPQ_BENCH_NO_EOG |
忽略 EOG,固定生成 N token(量測用) |
EPQ_MOE_DUMP |
il=層 格式,dump 該層 MoE 輸入/輸出供驗證 |
EPQ_BIG_THRESHOLD_BYTES |
大小槽位的分界(預設依 tensor 型別自動判斷) |
已知陷阱
1. EPQ_RESERVE_MB 決定成敗(本機要用 2048)
arena 之外還有匿名用量:16 條 OpenMP 執行緒堆疊、glibc per-thread arena、
ggml compute buffer(約 56 MiB)等。用 EPQ_RESERVE_MB=1400 時,32 GiB 上限
會在 decode 途中 std::bad_alloc(在 worker 執行緒內未被捕捉 → abort)。
本機實測 EPQ_RESERVE_MB=2048 才穩定;掃描表中的數字都用 2048,
唯一例外是 64 GiB:本機記憶體守衛會在單一行程 RSS 超過 64 GiB 時直接 SIGKILL,
而 64 GiB 上限的 RSS 峰值約 66 GiB(arena + mmap 的非 expert 權重),
因此該列改用 EPQ_RESERVE_MB=6144,讓 RSS 峰值降到 63.95 GB(62.5 GiB)再跑;
arena 因此是 57.7 GiB,而不是 2 GiB reserve 下的 ~62.7 GiB。prlimit --data
與 --ram-budget-mb 仍然是 65536 MB,RAM 上限沒有放寬。
2. arena 下限
要跑得起來,arena 至少要能放一個大 expert(18 MiB)+ I/O 緩衝。
本機 context 2048、-t 16 下,2 GiB 預算扣完剩不到,pager 直接拒絕啟動。
3. BF16 expert 需要另外補 kernel 分派
Laguna 的第 40–47 層是 BF16。參考版 sdq_moe.cpp 的 dot_for / act_type_for /
quantize_act 沒有 BF16 case,若不補會落在未支援型別而 abort。已補。
4. 這個 fork 沒有 LLM_ARCH_BAILINGMOE3
從 xing4 帶過來的 swiglu clamp guard 引用了 LLM_ARCH_BAILINGMOE3,本 fork 沒有
這個列舉,編譯會失敗。改成只檢查 DEEPSEEK4 / DFLASH / STEP35。
5. hf CLI 在此環境壞掉
hf / 舊 huggingface-cli upload 會因 typer/click 版本不相容丟
AttributeError: 'StyledContext' object has no attribute '_opt_prefixes'。
用 huggingface_hub Python API(HfApi.upload_file / create_repo)上傳。
限制與注意事項
- 這是一台 Intel Xeon Platinum 8559C(雙插槽)、16 vCPU cgroup 配額、
1999 GiB 實體 RAM 的機器;build 用
GGML_NATIVE=OFF+ 明確 AVX512 旗標重現。 - 本機有一支記憶體守衛,所有子行程合計 RSS > 80 GiB、或單一行程 RSS > 64 GiB
就會被 SIGKILL,所以無法用原生路徑整份載入 96 GB 做端到端對照;
正確性改以逐層 NumPy + 跨預算一致性證明。64 GiB 那列因此提高了
EPQ_RESERVE_MB(見「已知陷阱 1」)。 - 速度是 CPU、單機、
-t 16的數字,沒有 GPU offload(EPQ 路徑強制n_gpu_layers=0)。 Q4_K_M的 attention 等非 expert 權重仍隨模型檔 mmap;EPQ 只分頁路由 expert。
證據
| 檔案 | 內容 |
|---|---|
results/strict_memory_levels.md |
嚴格 RAM 上限掃描表 |
results/strict_memory_levels_final.json |
掃描原始資料(含 pager stats 併入) |
results/strict_low.json、results/strict_high.json、results/strict_64g.json |
掃描的三個批次(64 GiB 為 EPQ_RESERVE_MB=6144) |
results/strict_levels/level_*_r0.jsonl |
各預算的 pager 指標(命中率、淘汰、I/O) |
results/topk_ablation.json、results/topk_ablation_16g.json |
top-10 vs top-4 A/B(64 GiB / 16 GiB,含 greedy 輸出) |
results/topk_ppl.json |
top-10 vs top-4 的 teacher-forced PPL |
results/ppl_en.txt |
PPL 用的 799-token 文字(Project Gutenberg #11 開頭,public domain) |
results/topk_levels/ |
top-k A/B 與 PPL 的 pager stats |
results/least_stale.json |
LRU vs Least-Stale vs Least-Stale+位置(8/16/64 GiB)彙總 |
results/topk_evict_levels/ |
上述淘汰策略實驗的 pager stats(含 stale/current 淘汰群組計數) |
results/verify_layer1_q4k.txt |
第 1 層(Q4_K)對 NumPy |
results/verify_layer40_bf16.txt |
第 40 層(BF16)對 NumPy |
results/identity_4096mb.gen、results/identity_32768mb.gen |
跨預算 greedy 一致性 |
results/epq_l1.1、results/epq_l40.40 |
上述兩層的 EPQ_MOE_DUMP 原始資料 |
致謝
- 基底推理框架:
poolsideai/llama.cpp分支laguna。 - 模型:
poolside/Laguna-S-2.1-GGUF。 - EPQ 架構與參考實作:
HelloSun/xing4_0-29b-IQ4_NL_EPQ。