Xing4.0-29B-A4B IQ4_NL:EPQ MoE 專家分頁實驗
這是 XingChen-AGI/Xing4.0-29B-A4B-GGUF
的 xing4_0-29b-IQ4_NL.gguf(20,104,012,544 B = 18.723 GiB)的 MoE expert paging
實驗,採用 EPQ(MoE Expert Paging + Quantization)命名。模型權重不包含在本 repo。
核心機制:把未啟用的 experts 留在 SSD,將目前路由選中的 experts 讀入有限 RAM arena,
再由 CPU 執行量化 MoE 計算。這不是 Sparse Decomposed Quantization;epq-xing4/sdq_*.cpp
是早期移植沿用的 implementation 名稱,對外名稱使用 EPQ。
本實驗是 HelloSun/ling30flash-moe-expert-paging-q4
(EPQ 在 Ling-3.0-flash Q4_K_M 上的版本)的移植與延伸,但不是簡單換模型:
xing4_0架構在 llama.cpp 上游不存在。本 repo 以第三方 port 為基底 (kingjones30/ROCmFPX分支xing4-port,commitcbc2cd5),並補上該 port 自己在註解裡列為 OPEN 的 split-MLA absorbed 路徑。- 39 層的 expert 大小完全相同(IQ4_NL),這讓原實作裡一個被 Ling 的「兩種槽位 大小」掩蓋的 arena 缺陷現形(見已知陷阱)。
結論
1. 可行。 18.723 GiB 的模型在 2 GiB 的硬性匿名記憶體上限下跑完
(prlimit --data、swap = 0),代價是 2.31 tok/s 與 918.8 MiB/tok 的 SSD 讀取。
往上到 32 GiB 全部 PASS,輸出與原生路徑在 perplexity 上只差 0.016 %
(8.5301 vs 8.5287,32 × 512 chunk)。
2. 成本全部來自 routed expert 權重。 14.396 GiB,占張量資料 **76.9 %**。 非 expert 權重只有 3.818 GiB,KV cache 只有 46.1 KiB/token(262K 上下文共 11.8 GiB)。 這個架構的瓶頸從來不是 attention,也不是 context 長度 —— 就是那 14.4 GiB。
3. 飽和點在 16 GiB。 arena 14.38 GiB = 2,493 槽位,裝得下全部 2,496 個 expert, 逐出歸零、穩態分頁約 1.06 MiB/token(≈0.18 個 expert/token,可忽略)。 對照 Ling-3.0-flash:expert 權重 69.46 GiB,32 GiB 預算仍只有 68.9 % hot。 這個型號的 expert 權重小到分頁很快就變成負擔。
4. 但「不再分頁」不等於「沒有代價」。 ≥16 GiB 時自訂 kernel 仍比原生
mul_mat_id 慢 1.6–1.8 倍(兩輪重跑:EPQ 6.90 / 6.91 tok/s,全 RAM 11.19 / 12.27;
逐出都是 0)。瓶頸在計算 kernel,有兩處已定位但尚未回收的浪費(dot_rows()
寫了沒被呼叫、quantize_act() 在 token 迴圈內被重複呼叫)。
見分頁成本的分解。
5. EPQ 在這個型號上的適用範圍。 它的價值是「把 14.4 GiB 變成可調 arena, 讓 2–8 GiB 的機器跑 29B 模型」;在 RAM 已經夠放全部權重時它輸給原生路徑。 這是誠實的適用邊界,不是修掉就能消失的問題。
6. AMX 在小 RAM 預算下不划算。 repack buffer 多佔 369 MiB,8 GiB 預算時 arena 少 0.44 GiB、SSD 多讀 11 %。16 GiB 以上才有明顯幫助(6.57 vs 5.48 tok/s)。
7. 量分頁量不要用 /proc/self/io。 它的 read_bytes 會把 mmap 頁被核心逐出後
重新 fault 也算進去(實測佔 3,498 MiB / 3,770 MiB)。請用 pager 自己的
ssb_demand_bytes。
快速開始
git clone https://huggingface.co/HelloSun/xing4_0-29b-IQ4_NL_EPQ
cd xing4_0-29b-IQ4_NL_EPQ
./epq-xing4/build.sh ~/llama.cpp # 會自動 clone ROCmFPX xing4-port @ cbc2cd5
下載權重(需要 HF token,XingChen-AGI/Xing4.0-29B-A4B-GGUF 是公開 repo):
huggingface-cli download XingChen-AGI/Xing4.0-29B-A4B-GGUF xing4_0-29b-IQ4_NL.gguf \
--local-dir ~/models
跑起來(EPQ_REQUIRE_PAGER=1 讓 pager 起不來時直接報錯,不要安靜地退回全量載入 RAM):
EPQ_REQUIRE_PAGER=1 ~/llama.cpp/build_epq/bin/epq-chat \
-m ~/models/xing4_0-29b-IQ4_NL.gguf \
-p "What is the capital of France?" -n 48 \
-c 2048 -b 128 -t 8 --temp 0 \
--no-interactive --no-chat --ram-budget-mb 8192
llama-server 也支援(chat template 由 llama.cpp 的 Jinja 引擎處理):
EPQ_REQUIRE_PAGER=1 ~/llama.cpp/build_epq/bin/llama-server \
-m ~/models/xing4_0-29b-IQ4_NL.gguf -c 2048 -b 128 -t 16 --port 22222
curl -s localhost:22222/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"What is the capital of France?"}],
"max_tokens":600,"temperature":0}'
⚠️ Xing4.0 是 thinking 模型,thinking 預設開啟。兩句話的答案可能需要 500+ 個
completion token,max_tokens: 200 會拿到空的 content(reasoning_content 才有東西)。
這是模型行為,不是 EPQ 的問題。
也可以指定 pager 預算:
EPQ_RAM_BUDGET_MB=4096 ./epq-chat ... --ram-budget-mb 4096
EPQ_RESERVE_MB 預設依 build 自動選(無 AMX 1,400 MB / AMX 2,048 MB),
需要時可手動覆寫,理由見已知陷阱。
模型與路由
下表已對 xing4_0-29b-IQ4_NL.gguf 的 GGUF metadata 逐項核對過。
| 項目 | 數值 | 來源 |
|---|---|---|
| 架構 | xing4_0 |
GGUF general.architecture |
| 模型檔 | 20,104,012,544 B = 18.723 GiB | 檔案大小 |
| 量化 | IQ4_NL(4.5 bpw) | general.file_type = 25 / 逐 tensor 檢查 |
| 張量數 | 977 | GGUF.tensor_count |
| blocks | 41 | xing4_0.block_count |
| leading dense | 2 | xing4_0.leading_dense_block_count |
| MoE 層 | 39(含 block 40 = MTP) | 逐層檢查 ffn_gate_exps |
| 每層 routed experts | 64 | xing4_0.expert_count |
| 每 token 啟用 | 4 | xing4_0.expert_used_count |
| expert groups | 1 / 用 1 | expert_group_count / expert_group_used_count |
| shared experts | 1 | xing4_0.expert_shared_count |
| 路由 | sigmoid + ffn_exp_probs_b(noaux_tc) |
expert_gating_func = 2 |
| 權重正規化 / 縮放 | 是 / 2.0 | expert_weights_norm / expert_weights_scale |
| hidden / dense FFN / expert FFN | 3584 / 9216 / 1024 | embedding_length 等 |
| attention | MLA(K=192、V=128、kv_lora=512、q_lora=768、32 heads / 1 kv group) | attention.* |
| hyper-connection | count=4、Sinkhorn 20 次、eps=1e-6 | hyper_connection.* |
| RoPE | YaRN,factor 64,orig 4096 | rope.scaling.* |
| MTP | 1 層(block 40,無 hc 張量) | nextn_predict_layers |
| context metadata | 262,144 | xing4_0.context_length |
權重組成
| 區塊 | 大小 | 占張量資料 |
|---|---|---|
routed experts(ffn_{gate,up,down}_exps,39 × 3) |
14.396 GiB | 76.9 % |
| attention(wq_a/b、wkv_a_mqa、k_b、v_b、wo、hc_*) | 2.222 GiB | 11.9 % |
token_embd |
0.875 GiB | 4.7 % |
| MTP head(block 40 的 nextn.*) | 0.506 GiB | 2.7 % |
output |
0.359 GiB | 1.9 % |
| shared experts(39 × 3) | 0.225 GiB | 1.2 % |
| dense FFN(2 層 × 3) | 0.104 GiB | 0.6 % |
router(ffn_gate_inp + ffn_exp_probs_b) |
0.033 GiB | 0.2 % |
| 合計 | 18.720 GiB |
- 單一 routed expert = 6,193,152 B = 5.906 MiB(gate/up/down 各 2,064,384 B)
- 每 token 工作集 = 38 層 × 4 experts = 152 experts = 0.877 GiB
- KV cache 是 MLA:41 層 × 576 float × 2 B(F16)= 46.1 KiB/token (實測 ctx 2048 → 8192 增加 6144 token,匿名記憶體多 266 MiB → 45.4 KiB/token)
- 上下文長度不是限制:262,144 tokens 的 KV 只要 11.8 GiB。瓶頸在 14.4 GiB 的 expert 權重。
嚴格 RAM 上限實測
每一輪都在核心層硬上限下執行,不是只把數字傳給 pager:
prlimit --data=<level> --nofile=65536 epq-chat ... --ram-budget-mb <level>
Linux 上 RLIMIT_DATA 涵蓋 brk() 與所有 mmap 的匿名記憶體,超限會直接 OOM。
swap 則在每輪抽樣 /proc/<pid>/status 的 VmSwap,確認為 0。
測試條件:Intel Xeon Platinum 8559C(192 threads)、ctx=2048、ubatch=128、threads=8、
temp=0、每輪生成 48 token、每級重跑 2 次;每輪前以 posix_fadvise(DONTNEED)
把 18.723 GiB 的 GGUF 逐頁逐出 page cache。
無 AMX build(GGML_NATIVE=OFF,EPQ_RESERVE_MB=1600)
掃描用 1,600 MB(保守取整)。本 repo 的預設值是 1,400 MB;兩者在本實驗條件下 量到的 anon 峰值相同(見下表),所以預設值不影響任何結論。
| RAM 上限 | 結果 | arena | slots | anon 峰值 | 餘量 | RSS 峰值 | decode tok/s | SSD MiB/tok | hot % | 逐出 | swap |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 GiB | FAIL(低於 arena 下限) | — | — | exit=1 | — | — | — | — | — | — | — |
| 2 GiB | PASS | 0.38 GiB | 66 | 1,327 MB | 721 MB | 4,576 MB | 2.31 | 918.8 | 0.0 | 9,014 | 0 |
| 4 GiB | PASS | 2.38 GiB | 413 | 3,376 MB | 720 MB | 6,627 MB | 3.46 | 620.5 | 26.1 | 6,298 | 0 |
| 8 GiB | PASS | 6.38 GiB | 1,106 | 7,469 MB | 723 MB | 10,720 MB | 3.83 | 356.4 | 49.2 | 3,508 | 0 |
| 16 GiB | PASS | 14.38 GiB | 2,493 | 15,661 MB | 723 MB | 18,911 MB | 5.48 | 59.2 | 75.2 | 0 | 0 |
| 32 GiB | PASS | 30.38 GiB | 5,267 | 32,045 MB | 723 MB | 35,295 MB | 5.57 | 59.2 | 75.2 | 0 | 0 |
AMX build(對照,EPQ_RESERVE_MB=2048)
| RAM 上限 | 結果 | arena | slots | anon 峰值 | 餘量 | RSS 峰值 | decode tok/s | SSD MiB/tok | hot % | 逐出 | swap |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 4 GiB | PASS | 1.94 GiB | 336 | 3,291 MB | 805 MB | 6,172 MB | 3.12 | 692.8 | 19.8 | 6,950 | 0 |
| 8 GiB | PASS | 5.94 GiB | 1,030 | 7,391 MB | 801 MB | 9,950 MB | 4.05 | 409.5 | 44.5 | 4,006 | 0 |
| 16 GiB | PASS | 13.94 GiB | 2,417 | 15,583 MB | 801 MB | 18,284 MB | 6.57 | 50.7 | 75.9 | 0 | 0 |
讀表時請注意三個欄位的差別
- anon 峰值是唯一受上限管制的量(
prlimit --data→RLIMIT_DATA)。所有 PASS 代表這一項確實落在上限之內。 - RSS 峰值較高是預期的:非 expert 權重(3.818 GiB,主圖會碰到的部分)以 mmap
檔案對映頁載入,屬檔案對映頁,
RLIMIT_DATA不管制。這部分可被核心回收,所以 不算在 arena 預算裡。 - tok/s 在本機跨設定不可比:同一設定重複量測跨度可達 ±20 %,主機還有其他租戶。 判斷 arena 價值的依據應是 hot % 與 SSD MiB/tok,這兩項是單調的。
- SSD MiB/tok 欄位高估了 pager 的分頁量:它來自
/proc/self/io的read_bytes,會把 mmap 頁被核心逐出後重新 fault 也算進去。真正的穩態分頁量請看 分頁成本的分解。 - 本機核心 6.18 的
smaps_rollup不匯出AnonHWM,上表的 anon 峰值是 20 ms 間隔取樣Anonymous:的最大值 —— 同一個量,取樣較粗。
與 Ling-3.0-flash 的關鍵差異:16 GiB 就飽和了
上表最值得注意的是 16 GiB 一列的「逐出 = 0」。arena 是 14.38 GiB = 2,493 個槽位, 而 routed expert 總共只有 39 × 64 = 2,496 個 —— 整份 expert 權重放得進去。 所以:
- 16 GiB 之後逐出次數為 0、hot % 固定在 75.2 %,再加 RAM 對分頁沒有任何意義。
- 那個 24.8 % 的 miss 不是 持續性的缺頁,而是冷啟動時把 arena 填滿的一次性成本。 逐出次數為 0 證實了這點;分頁成本的分解用 48/128/256 三種生成長度 量出 miss 會凍結在 2,421。
- ⚠️ 但「不再分頁」不等於「不再有代價」:≥16 GiB 時自訂 kernel 仍比原生
mul_mat_id慢 1.6–1.8 倍。見分頁成本的分解。 - 對照組:Ling-3.0-flash 的 expert 權重是 69.46 GiB,32 GiB 預算只能覆蓋不到一半, hot % 卡在 68.9 %,SSD 讀取要 28.8 MiB/tok。
換句話說,EPQ 對 Xing4.0 的價值不在「讓 29B 模型塞進小機器」,而在於把 14.4 GiB 的權重變成可調的 arena:
| RAM | arena | 覆蓋到的 expert 權重 | 每 token SSD 讀取 | 換來 |
|---|---|---|---|---|
| 2 GiB | 0.38 GiB | 2.6 % | 918.8 MiB | 用 2 GiB 跑完 18.7 GiB 的模型(2.31 tok/s) |
| 4 GiB | 2.38 GiB | 16.5 % | 620.5 MiB | 可用(3.46 tok/s) |
| 8 GiB | 6.38 GiB | 44.3 % | 356.4 MiB | 流暢(3.83 tok/s) |
| 16 GiB | 14.38 GiB | 99.9 % | 59.2 MiB | 不再分頁(5.48 tok/s) |
| 32 GiB | 30.38 GiB | 100 % | 59.2 MiB | 沒有任何改善 |
(2 GiB 一列的 hot % 是 0.0:66 個槽位連一個 token 的工作集 152 個 expert 都裝不下, 每一層都必須重讀。)
分頁成本的分解
讀 SSD 的量要分三層看:冷填充(一次)、穩態分頁(持續)、以及 mmap 重複 fault (與分頁無關)。下面把它們拆開。
一、矩陣的 SSD 欄位量到的是冷填充,不是穩態分頁
矩陣的 SSD 欄位是整輪總量 ÷ token 數,而 benchmark 只跑了 126 個 token (30 token prefill + 48 個生成)。14.4 GiB 的權重根本來不及攤平。把生成長度拉長, 其他條件完全不變(32 GiB 預算 → arena 30.38 GiB → 5,267 槽位):
| 生成 token | 總 token | expert 請求 | miss | 逐出 | pager 自報 SSD | MiB/tok |
|---|---|---|---|---|---|---|
| 48 | 126 | 9,080 | 2,253 | 0 | 13,307 MiB | 105.6 |
| 128 | 286 | 21,240 | 2,375 | 0 | 14,027 MiB | 49.0 |
| 256 | 542 | 40,696 | 2,421 | 0 | 14,299 MiB | 26.4 |
三件事同時成立:
- miss 次數凍結在 ~2,421。 routed expert 總共 2,496 個,也就是這個提示詞只碰過 2,421 個不同的 expert,剩下 75 個(3.0 %)從頭到尾沒被用到。再生成下去 miss 也不會增加 —— arena 已經裝得下全部。
- SSD 總量收斂到 14,299 MiB ≈ 14.396 GiB,就是整份 expert 權重的大小。
14,299 / 2,421 = 5.906 MiB正好等於單一 routed expert 的大小。 - 逐出次數三輪都是 0,arena 裡沒有任何一頁被換掉。
所以 16 GiB 以上 SSD 讀取量的衰減是 1/N(N = 已生成 token 數),不是
「每 token 都要讀」。
二、穩態分頁成本約 1.06 MiB/token
/proc/self/io 的 read_bytes 把 mmap 檔案頁被核心逐出後重新 fault 也算進去了,
在這台有其他租戶的共用主機上這個量不小。用 pager 自己的計數器
ssb_demand_bytes 取 n=128 → 256 的增量:
| 量 | 值 |
|---|---|
| 多生成的 token | 256 |
| pager 多讀的 SSD | 271.7 MiB = 1.06 MiB/token |
| 多出的 miss | 46 個(0.18 miss/token) |
| 逐出 | 0 |
同一輪 CLI 報的是 3,769.7 MB (14.8 MB/tok):pager 只貢獻 272 MiB,
另外 3,498 MiB 是 mmap 的非 expert 權重被重新 fault。
以 1.75 GiB/s 的裝置讀取速率算,1.06 MiB/token 在每 token 145 ms 裡只佔約
0.6 ms —— 完全可以忽略。要看分頁量請用 pager 的 ssb_demand_bytes,
不要用 /proc/self/io。
三、為什麼不乾脆預載
因為這個實作沒有預載路徑,而且預載會更差:
- arena 是空的,不是滿的。
alloc_arena()在模型載入時做的是posix_memalign()+memset(p, 0, alloc)—— 一塊全新的匿名零頁,used_ = 0。 槽位空著 ≠ 那一頁已經載入;槽位只有走過acquire()→pread之後才算命中。 - 沒有任何預載程式碼。 全檔 grep 不到
preload/warm_start/populate。EPQ_PROFILE_IN+EPQ_HOT_PIN只設定hot_這個 bitmap,改變的是 淘汰順序,一個位元組都沒讀。 - 哪些 expert 會被用到是 router 決定的,跑之前不知道。 而且上表那 75 個 從未被選到的 expert,再好的預測器也列舉不出來。
- 預載會讀得更多,不是更少。 預載全部 2,496 個 = 14.396 GiB;實際的 demand fill 只讀了 2,421 個 = 14.30 GiB。預載多讀 0.7 %。
- 預載會擋住第一個 token,而且沒辦法跟計算重疊。 裝置讀取速率中位數
1.75 GiB/s,預載要 約 8.2 s 純 I/O 才能開始算。demand fill 把這 14.3 GiB
攤在「取→算交錯、無中途 barrier」的 pipeline 上(
moe_custom_op的 mode C 就是為此存在的),使用者馬上就有輸出。
換句話說,冷填充不是缺陷,是這個設計在「權重比 RAM 大」時唯一可行的做法。
四、≥16 GiB 的瓶頸是計算 kernel,不是 I/O
同一個前端(llama-completion)、同一提示詞、254 個 decode token,兩輪獨立重跑:
| 輪次 | 全量載入 RAM(EPQ_PAGER=0) |
EPQ,32 GiB 預算 | 比值 |
|---|---|---|---|
| 1 | 11.19 tok/s | 6.90 tok/s | 0.617× |
| 2 | 12.27 tok/s | 6.91 tok/s | 0.563× |
| 逐出 | — | 0 | 穩態 ~1.06 MiB/tok |
EPQ 側非常穩定(6.90 / 6.91),全 RAM 側在這台有其他租戶的主機上變動 ±20 %。
1.6–1.8 倍的差距不在 I/O,在計算。 自訂 kernel 有兩處已定位但尚未回收的浪費
(epq-xing4/sdq_moe.cpp):
dot_rows()(nrc=16的批次點積)寫了但從沒被呼叫 ——expert_forward()的三個迴圈全部用dot_row()逐列呼叫(nrc=1)。原始實作的註解自己寫了nrc=1比nrc=16慢一個數量級。quantize_act()寫在 token 迴圈內,也就是「每個 (token, expert) 量化一次」。 decode 階段一個 ubatch 只有 1 個 token,同一份 3584 維 activation 會被重複 量化 152 次(38 層 × 4 experts)。一個 3584 × 4 B 的 cache 就解決了。
EPQ 在小 RAM 的目標場景(2–8 GiB)拿到的 I/O 收益是真的,但在 RAM 已經夠放 全部權重時,這個實作還贏不了原生路徑。不要把「16 GiB 之後 SSD 讀取歸零」 誤讀成「16 GiB 之後分頁就沒有代價了」。
AMX 的取捨
-march=native 會定義 __AMX_INT8__,llama.cpp 因此在 arena 之外多配一塊 AMX
repack buffer。實測(anon_peak − arena,2/8/16/32 GiB 四點都一樣):
| build | AMX repack 代價 | 最低可行 RAM |
|---|---|---|
GGML_NATIVE=OFF |
— | 2 GiB |
GGML_NATIVE=ON |
+369 MiB | 4 GiB |
比 Ling-3.0-flash 的 1,876 MiB 小得多,原因很直觀:這個模型只有 39 層的 shared expert 加上 2 層 dense FFN 值得 repack(routed expert 走 pager,根本不進 AMX buffer)。 所以在 4 GiB / 8 GiB 這一級,AMX build 的 arena 比無 AMX build 少約 0.44 GiB, SSD 讀取多 11 %(692.8 vs 620.5 MiB/tok)—— 這個型號上 AMX 在小 RAM 預算下不划算。 到 16 GiB(分頁停止)之後 AMX 才有明顯幫助(6.57 vs 5.48 tok/s)。
已知陷阱
1. EPQ_RESERVE_MB 決定成敗
pager::alloc_arena() 在 llama.cpp 載入權重之前就算 arena,此時 rss_now 只有
約 0.06 GiB,因此之後長出來的所有匿名配置都必須由 reserve 手動扣掉。這些包含
AMX model/repack buffer、CPU compute buffer、ggml graph/sched context、KV state
與執行緒堆疊。
本機(ctx=2048、ubatch=128、threads=8)實測:
| build | 實測 anon − arena |
可通過的最小 EPQ_RESERVE_MB |
預設值 |
|---|---|---|---|
| 無 AMX | 820 MiB | 1,400 MB | 1,400 MB |
| AMX | 1,187 MiB | 2,048 MB | 2,048 MB |
用 1,024 MB 當預留會在 4 GiB 預算下直接炸在 ggml_init()(GGML_ASSERT(ctx->mem_buffer != NULL)),
而且訊息完全不會提到 RAM 不夠。上個實驗對 Ling-3.0-flash 用的 3,600 MB 在這個模型上
會白白吃掉 2 GiB arena(8 GiB 預算時 arena 只剩 4.4 GiB 而不是 6.6 GiB)。
預設值已依 build 自動選:pager 在執行期向 CPU backend 要 extra buffer types,
看到名稱開頭是 AMX 的就用 2,048 MB,否則 1,400 MB。EPQ_RESERVE_MB 仍可手動覆寫。
calibrate_reserve.py 是用來重新量這個值的工具。
順帶一個坑:不能用
__AMX_INT8__判定。它只定義在ggml-cpu的編譯單元裡,src/*.cpp看不到。GGML_AMX_INT8也不可靠 ——ggml/CMakeLists.txt的option(... OFF)會在 CMake cache 留一份 OFF,而ggml-cpu/CMakeLists.txt只在 自己的目錄範圍把它設成 ON,add_subdirectory不會把變數往回傳。GGML_NATIVE=ON自動開啟 AMX 的情況只有執行期查得到。
2. arena 下限大約 1.2 GiB
alloc_arena() 要求至少 16 個槽位,每槽 5.906 MiB = 94.5 MiB,再加上 1,400 MB 的預留,
所以 1 GiB 的 RAM 上限連 arena 都配不出來。實測 1 GiB 這一列會乾淨地失敗
(expert arena 太小 → exit 1)。
這裡要配 EPQ_REQUIRE_PAGER=1:pager 起不來時直接報錯,不要安靜地退回
「整個模型載進 RAM」。在嚴格 RLIMIT_DATA 下那條路徑不是變慢,而是配置失敗後卡死
(實測:印完錯誤訊息後行程停在 allocator 重試迴圈,沒有任何輸出)。
3. 單尺寸槽位的 arena 會全部指向同一個位址(本次移植修掉)
slot_ptr() 在「大槽位區」的分支是
arena_ + small_bytes_*n_small_ + (s - n_small_)*big_bytes_
alloc_arena() 的「單一尺寸」路徑把 n_small_ 設成 0、n_big_ 設成全部,
卻沒有把 big_bytes_ 設成槽位大小(原本是 0)。結果每一個槽位都算成
arena_ + s*0 —— 所有 expert 互相覆蓋,輸出是垃圾。
為什麼原實驗沒事:Ling-3.0-flash 的 37 層是 1,900,544 B、3 層是 2,039,808 B,
有兩種 expert 大小,所以 big_bytes_ != 0,這個分支從來沒被執行過。
Xing4.0 的 39 層全部是 IQ4_NL、大小完全相同,於是每次都踩到。
特別陰險的地方是單一 expert 的自我測試會完全通過(只有一個槽位被用到), 只有整層(4 個 expert 以上)才會出錯。
修法見 epq-xing4/sdq_pager.cpp 的 alloc_arena()。
4. 假 buffer 的對齊(本次移植修掉)
自訂 buffer type 每個張量只回報 4 KB 的假位址。這塊記憶體必須自己對齊到
get_alignment():ggml 的 ggml_tallocr_new() 起始 offset 是
aligned_offset(base, 0, align),base 若沒對齊就會被往上推。117 個張量時足以讓
最後一個剛好配置失敗:
not enough space in the buffer to allocate blk.40.ffn_up_exps.weight (needed 4096, available 880)
修法:posix_memalign(4096) + 多配一個 slot。
5. 除錯 dump 節點不能取代計算路徑(本次移植修掉)
EPQ_MOE_OUT_DUMP 會把 MoE 輸出包一層 ggml_custom_4d。兩個坑:
- 節點沒有
ggml_build_forward_expand()進圖 → 排程器把它的輸出緩衝判成可重用, layer ≥ 1 的結果被後面的張量覆蓋。 - 即使註冊進圖,若把
moe_out換成 dump 節點的輸出,原路徑的非連續 view (cur_experts[0])會被當成連續讀取,模型直接算錯。
兩個症狀都是 PPL 從 8.5 變成 2.5e5。修法:dump 節點只當旁路 leaf 註冊,
函式回傳原本的 t。
6. pager 初始化失敗會在離開時卡死(本次移植修掉)
shutdown() 在 ready_ == false 時直接 return,但 init() 是在
io_pool_start() 之後才可能丟出例外。24 條 I/O 執行緒就這樣停在 io_cv_ 上,
而 static 析構同時把 mutex/cond 拆掉。修法:shutdown() 在未 ready 的情況下也要
停掉 I/O 池。
I/O 稽核
mincore(2) 掃過整個 GGUF,搭配 buffered 對照組:
- A:預設 —— pager 以
O_DIRECT讀模型檔,資料完全不進 kernel page cache - B:對照 ——
EPQ_NO_DIRECT_IO=1+--no-fadvise,改成 buffered 讀取並保留 cache
| 指標 | A:預設(O_DIRECT) | B:對照(buffered + 保留 cache) |
|---|---|---|
| 模型檔留在 page cache 的峰值(mincore) | 3.214 GiB | 15.836 GiB |
真正從 block device 讀入(/proc/pid/io read_bytes) |
28.50 GiB | 15.83 GiB |
| 系統 Cached 增量(峰值) | +3.49 GiB | +16.39 GiB |
| 行程 RssAnon 峰值 | 5.453 GiB | 5.453 GiB |
| 行程 RssFile 峰值 | 3.173 GiB | 3.174 GiB |
| pager 自報 SSD 讀取量 | 15,313 MB | 2,259 MB |
| pager 自報 SSD 讀取量 / token | 494.0 MiB/tok | 72.9 MiB/tok |
- routed expert 權重合計 14.396 GiB;主圖會碰到的非 expert 權重 3.818 GiB。
- A 的 page cache 峰值 3.214 GiB ≈ 非 expert 權重;B 衝到 15.836 GiB,幾乎是整個檔案。 差額 12.62 GiB 正是被分頁的 expert 權重量級。
- A 的
read_bytes一路爬到 28.5 GiB 而 resident 維持平線 —— 這就是 O_DIRECT 的特徵; B 的read_bytes在 15.8 GiB 附近停住,rchar繼續上升(重複讀由 cache 服務)。 - 這個量測有鑑別力:兩輪的 pager 自報 SSD 讀取量差 6.8 倍。
正確性
端到端:perplexity
固定 32 個 512-token chunk、ubatch=128、只評分後半段(llama-perplexity 的慣例)。
| 路徑 | PPL(8 chunks,可逐 chunk 對照) | PPL(32 chunks) |
|---|---|---|
原生 mul_mat_id(EPQ_PAGER=0) |
9.9573 ± 0.60609 | 8.5287 ± 0.25571 |
| EPQ 分頁 | 9.9353 ± 0.60407 | 8.5301 ± 0.25574 |
32 chunks 的差距是 **0.016 %**。逐 chunk 對照(同一批確定性輸入):
| chunk | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 原生 | 5.2868 | 8.9871 | 8.2689 | 8.5280 | 8.7734 | 9.2970 | 9.8500 | 9.9573 |
| EPQ | 5.3168 | 8.9757 | 8.2667 | 8.5213 | 8.7439 | 9.2702 | 9.8251 | 9.9353 |
每一個 chunk 都在 0.03–0.32 % 內。差異來自 Q8_0 activation 量化,不是系統性錯誤。
架構移植本身的健全性:ROCmFPX 的 port 在 BF16 下量到 PPL 8.3608、HF transformers
bf16 參考值 8.4252(同一個 4×512 chunk、同一個後半段評分窗口)。本 repo 在
IQ4_NL 下量到 8.5287 —— 對一個 4.5 bpw 的量化來說合理(該 port 的 Q4_0 + imatrix
是 9.1506)。
逐層 A/B:分頁路徑 vs 原生路徑
同一批確定性 prefill ubatch,兩邊各自 dump 38 層的 ffn_moe_out(458,752 個 float/層):
layers=38 worst NRMSE=1.054e-01 min corr=0.994441 max rel_max=2.634e-01
第一個 MoE 層(layer 2)NRMSE 只有 2.9e-05、corr = 1.000000;誤差隨深度累積 (兩條路徑的加權總和順序不同),到 layer 26 最差 NRMSE 10.5 %、corr 0.9944。 這是兩種同樣合法的浮點加總順序,不是錯誤 —— per-chunk PPL 幾乎不變就是佐證。
對純 NumPy:單一 expert
epq-chat --selftest IL:IE:SEED:OUT.bin:NTOK 會用 pager 的資料算一次單一 expert 的
輸出;verify_xing4.py 直接從 GGUF 依 ggml 的定義 dequant(IQ4_NL 的 16 項非線性
查表 + 單一 fp16 scale),在純 NumPy/fp32 重算:
| 測項 | max_rel_err | corr | 結果 |
|---|---|---|---|
| L2 E5 | 0.0113 | 0.999944 | PASS |
| L3 E5 | 0.0112 | 0.999945 | PASS |
| L20 E5 | 0.0126 | 0.999938 | PASS |
| L39 E5 | 0.0195 | 0.999938 | PASS |
殘差就是 Q8_0 activation 量化的誤差。
對純 NumPy:整層(含 routing)
verify_moe_layer_xing4.py 連 router 一起重算(sigmoid + ffn_exp_probs_b
- top-4 + sum 正規化 +
routed_scaling_factor=2.0),再對 4 個 expert 加權求和:
| 層 | max|diff| | rel | corr | 結果 |
|---|---|---|---|---|
| 2 | 0.258 | 0.0080 | 0.999963 | PASS |
| 20 | 0.021 | 0.0119 | 0.999935 | PASS |
| 39 | 0.037 | 0.0122 | 0.999917 | PASS |
layer 2 的第 4 名 expert 是近似平手(NumPy 選 60、ggml 選 9,權重 0.0693 vs
0.0668)—— argsort_top_k 的平手處理與 NumPy 的穩定 argsort 不同。這不是錯誤:
該 expert 權重小,整層輸出仍然對到 0.8 %。但這是本模型在低層確實存在的現象,
greedy 解碼時可能造成措辭差異。
環境變數
| 變數 | 預設 | 說明 |
|---|---|---|
EPQ_PAGER |
1 |
設 0 關閉分頁、回到上游行為(A/B 對照用) |
EPQ_RAM_BUDGET_MB |
實體 RAM 的 70%,上限 4 GiB | pager 的 RAM 總預算;arena 會配合 EPQ_RESERVE_MB 縮減 |
EPQ_ARENA_MB |
由預算推導 | 直接指定 arena 大小 |
EPQ_RESERVE_MB |
無 AMX 1,400 / AMX 2,048 | 預留给 arena 算完之後才長出來的匿名記憶體 |
EPQ_REQUIRE_PAGER |
未設 | 設 1:pager 起不來就直接失敗,不要退回全量載入 RAM |
EPQ_IO_THREADS |
3 × min(nproc, 8) | I/O 讀取執行緒數 |
EPQ_IO_SPLIT |
1 |
每一段再切成幾塊並行讀 |
EPQ_IO_PARALLEL |
1 |
gate/up/down 三段同時讀;設 0 回到序列讀 |
EPQ_NO_DIRECT_IO |
未設 | 設 1 改用 buffered 讀取(會進 page cache) |
EPQ_FADV_DONTNEED |
1 |
讀完對該範圍 posix_fadvise(DONTNEED) |
EPQ_PREFETCH |
0 |
背景預取。上個實驗量到開啟反而慢 8 % |
EPQ_PREFETCH_LAYER_AHEAD |
0 |
跨層預取(保留來重現那個負結果) |
EPQ_STATS_FILE |
未設 | 每個階段的指標 JSONL |
EPQ_HOT_PIN |
0 |
剖面熱門 expert 釘選數量 |
EPQ_PROFILE_IN / EPQ_PROFILE_OUT |
未設 | expert 剖面的讀取/輸出 |
EPQ_MOE_OUT_DUMP / _LAYER |
未設 | 逐層 dump ffn_moe_out(A/B 對照) |
EPQ_MOE_DUMP / _LAYER |
未設 | dump 整層的 cur / ids / wts / out(對 NumPy 用) |
EPQ_BENCH_NO_EOG |
未設 | 設 1 固定生成 n 個 token,不因 EOG 提前停 |
EPQ_PROFILE_TIME |
未設 | 設 1 印出自訂 MoE op 的分段計時 |
限制與注意事項
- RAM budget 與硬性上限不同。
--ram-budget-mb/EPQ_RAM_BUDGET_MB只影響 arena sizing;要真正設限必須靠 OS(memory.max、RLIMIT_DATA)。上表已用後者驗證。 - 本 repo 的量測環境沒有可用的 GPU:容器裡雖然有
/dev/nvidia0–/dev/nvidia7, 但nvidia-smi無法初始化 NVML,因此全部為 CPU 執行,build 也沒有編譯任何 GPU backend。EPQ 的分層本來就是 SSD → RAM → CPU,GPU 不在路徑上。 /sys/fs/cgroup在此容器可讀但memory.max = 96.8 GiB是容器總量,不是本實驗的 變數;用prlimit逐行程施加上限。- 本機核心 6.18 的
smaps_rollup**不匯出AnonHWM**。上表的 anon 峰值是 20 ms 間隔取樣Anonymous:的最大值 —— 同一個量,取樣較粗。舊核心上這兩者會有差異。 - 背景預取(
EPQ_PREFETCH)預設關閉。原實驗量到開啟反而慢 8 %(預測不夠準, 多讀的全是白讀,而且會和本層的 demand 讀搶頻寬)。這次實驗沒有重跑該對照。 - 已知缺陷(繼承自基底 port):任何大於約 128 token 的單一 ubatch 數值會損壞
(wikitext PPL 在
-ub 128為 81、-ub 512為 130345,CPU 與 GPU 相同)。本實驗 固定-b 128。這個缺陷不在 EPQ 的路徑上(原生路徑也會中招),但它限制了 prefill 的 ubatch 大小。 - IQ4_NL 在 greedy 的近似平手 prompt 上可能有量化輸出差異(見上面 layer 2 的說明)。
- 模型權重授權與使用條件請以
XingChen-AGI/Xing4.0-29B-A4B的 model card 為準(Apache-2.0)。
證據
results/strict_memory_levels.md:本機硬上限 RAM 矩陣(1–32 GiB,swap=0)results/strict_memory_levels_final.json:上述資料 + 每輪原始紀錄results/host_capability_audit.txt:量測主機/容器硬體能力results/tensor_sizes.txt:逐區塊權重大小(由 GGUF 張量偏移直接算出)results/compute_gap_32gib.txt:分頁結束後 仍比原生慢 1.6–1.8 倍的對照(證明瓶頸在計算 kernel)results/warmup_fill.txt:32 GiB 預算下把生成長度 48/128/256 拉長,證明 SSD 讀取是一次性冷填充且 miss 會凍結(原始 JSONL 在同目錄warmup_n*.jsonl)results/io_audit.md/io_audit_raw.log:O_DIRECT 與 page cache 稽核results/ab_moe_layers_ch8_ub128.txt:38 層分頁 vs 原生 A/Bresults/ppl_*.txt:perplexity(原生 / 分頁,8 與 32 chunk)results/verify_selftest_rerun.txt:單一 expert 對 NumPyresults/verify_moe_layer_rerun.txt:整層(含 routing)對 NumPyepq-xing4/:pager source、llama.cpp patch、xing4.cpp、實驗腳本epq-xing4/0001-epq-xing4_0.patch:五個 hook 點 + CMake + AMX guard
致謝
基底架構 port 來自 kingjones30/ROCmFPX
分支 xing4-port(commit cbc2cd5,作者 kingjones30)。EPQ pager 的原始實作來自
[HelloSun/ling30flash-moe-expert-paging-q4](results 與 sdq-ling/)。
Base model © China Telecom Artificial Intelligence Technology Co., Ltd.
(XingChen-AGI/Xing4.0-29B-A4B),
Apache-2.0。
Model tree for HelloSun/xing4_0-29b-IQ4_NL_EPQ
Base model
XingChen-AGI/Xing4.0-29B-A4B-GGUF