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 上的版本)的移植與延伸,但不是簡單換模型:

  1. xing4_0 架構在 llama.cpp 上游不存在。本 repo 以第三方 port 為基底 (kingjones30/ROCmFPX 分支 xing4-port,commit cbc2cd5),並補上該 port 自己在註解裡列為 OPEN 的 split-MLA absorbed 路徑。
  2. 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

三件事同時成立:

  1. miss 次數凍結在 ~2,421。 routed expert 總共 2,496 個,也就是這個提示詞只碰過 2,421 個不同的 expert,剩下 75 個(3.0 %)從頭到尾沒被用到。再生成下去 miss 也不會增加 —— arena 已經裝得下全部。
  2. SSD 總量收斂到 14,299 MiB ≈ 14.396 GiB,就是整份 expert 權重的大小。 14,299 / 2,421 = 5.906 MiB 正好等於單一 routed expert 的大小。
  3. 逐出次數三輪都是 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):

  1. dot_rows()(nrc=16 的批次點積)寫了但從沒被呼叫 —— expert_forward() 的三個迴圈全部用 dot_row() 逐列呼叫(nrc=1)。原始實作的註解自己寫了 nrc=1 比 nrc=16 慢一個數量級。
  2. 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)。

證據

致謝

基底架構 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。

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/xing4_0-29b-IQ4_NL_EPQ

Finetuned
(1)
this model