Qwen3.8-Flash-Next (UD-Q4_K_XL) — SSD→RAM MoE 專家分頁實驗
這個 repo 不含權重。權重在 unsloth/Qwen3.8-Flash-Next-GGUF 的
UD-Q4_K_XL/ 目錄(4 個 shard,111,314,812,928 bytes ≈ 103.7 GiB)。
這個 repo 放的兩樣東西:
sdq-qwen38/— 改寫後的推論平台(SDQ expert pager 適配 Qwen3.8-Flash-Next)results/— 8 / 16 / 32 / 63 GiB 記憶體實驗的完整結果與驗證證據
1. 為什麼需要這個
Qwen3.8-Flash-Next 的 UD-Q4_K_XL 有 103.7 GiB,其中:
| 類別 | 大小 | 說明 |
|---|---|---|
| MoE expert 權重 | 71.73 GiB | 48 層 × 512 expert,每 token 只用 10 個 → 工作集遠小於總量 |
| 非 expert 權重 | 31.95 GiB | 其中 per_layer_token_embd 單一張量就 26.82 GiB |
| 合計 | 103.68 GiB |
「每層 512 個 expert、每 token 只用 10 個」代表 93.5% 的 expert 權重在任何給定
時刻都不會被碰到。把 71.7 GiB 的 expert 權重留在 SSD、RAM 只放有限大小的
expert arena,是把這個模型塞進小機器的正確做法。這就是 SDQ(SSD→RAM→CPU
expert pager)的原始設計,出處見
HelloSun/sddqwen35a3b_v01
(原本的目標模型是 22.1 GiB 的 Qwen3.6-35B-A3B)。
2. 模型規格(從 GGUF metadata 實讀)
| 項目 | 值 |
|---|---|
| 架構 | qwen4exp |
| 層數 | 48 |
| expert 數 / 每 token 用 | 512 / 10 |
| hidden size | 2560 |
| expert FFN size | 640 |
| 原生 context | 262,144 |
| attention 型態 | 混合:每 4 層 1 層 full attention(full_attention_interval = 4),其餘為 SSM/linear attention |
| 額外結構 | hyper-connection(4 路)、attention indexer(top-k 2048)、PLE n-gram embedding |
| gate/up 量化 | q4_K(其中 2 層是 q5_K) |
| down 量化 | q5_1(43 層)/ q8_0(5 層) |
| 單一 expert | 2.93 MiB(小)/ 3.81 MiB(大) |
⚠️
down_exps用 q5_1 是這個模型與原版 SDQ 目標模型最大的差異, 也是最容易造成「看起來能跑但數值全錯」的地方。詳見第 5 節。
3. 實驗結果
3.1 測試機
| 項目 | 值 |
|---|---|
| CPU | Intel Xeon Platinum 8559C(16 vCPU 配額、amx_bf16/amx_int8/avx512_vnni) |
| 磁碟 | overlay filesystem,單流 dd oflag=direct ≈ 320 MB/s |
| RAM | 實體 2 TiB,但實驗以 pager RAM 預算限制(見 3.4 的限制說明) |
| 引擎 | llama.cpp 836d5717 + SDQ patches 0001–0005 + 本 repo 的 qwen4exp 改寫 |
| 設定 | ctx=2048, ubatch=128, threads=16, temp=0 |
3.2 記憶體預算矩陣(48 生成 token,各 2 輪,中位數)
| RAM 預算 | arena (GiB) | slots | decode tok/s | prefill tok/s | hot % | cold % | SSD MiB/tok | peak RSS (MB) |
|---|---|---|---|---|---|---|---|---|
| 8 GiB | 5.91 | 2,002 | 2.84 | 4.7 | 47.2 | 52.8 | 617.5 | 15,255 |
| 16 GiB | 13.91 | 4,713 | 3.41 | 4.8 | 64.5 | 35.5 | 302.9 | 23,464 |
| 32 GiB | 29.91 | 10,136 | 4.09 | 4.8 | 71.2 | 28.8 | 180.6 | 38,720 |
| 63 GiB | 60.91 | 20,643 | 3.33 | 4.2 | 71.2 | 28.8 | 180.6 | 70,176 |
hot % = expert 已在 RAM arena、不需碰 SSD;cold % = 必須回 SSD pread。
3.3 長跑(128 生成 token)— 這組才看得出 arena 的邊際效益
短跑(48 token)會低估 arena 的價值,因為工作集還沒被「餵飽」。128 token:
| RAM 預算 | arena (GiB) | slots | decode tok/s(中位數) | 逐輪 tok/s | hot % | cold % | 淘汰次數 | SSD MiB/tok | 每層命中率 | peak RSS (MB) |
|---|---|---|---|---|---|---|---|---|---|---|
| 8 GiB | 5.91 | 2,002 | 2.63 | 2.63 | 50.3 | 49.7 | 30,987 | 345.9 | 21–71% | 15,390 |
| 16 GiB | 13.90 | 4,713 | 2.77 | 2.77 | 69.4 | 30.6 | 15,584 | 212.9 | 52–83% | 23,591 |
| 32 GiB | 29.90 | 10,136 | 3.76 | 3.35 / 4.79 / 3.76 | 85.0 | 15.0 | 5 | 103.9 | 81–90% | 39,978 |
| 56 GiB | 53.91 | 18,271 | 4.91 | 4.91 | 85.0 | 15.0 | 0 | 103.9 | 81–90% | 64,557 |
3.4 結論與限制(請先讀這段再看上面的數字)
- 甜蜜點是 32 GiB。 32 GiB 與 56 GiB 的 SSD 讀取量(29.03 GiB)、命中率(85.0%)、 淘汰次數(5 vs 0)幾乎相同 — 工作集已經完全裝得下。tok/s 的差距 (3.76 vs 4.91)落在重複量測的抖动範圍內:32 GiB 跑三輪得到 3.35 / 4.79 / 3.76, I/O 讀取量則三次完全一致(15,174.8 MB)。也就是說 32 GiB 以上再加算力/RAM, 不再改善 I/O 效率。
- 8 → 16 GiB 是最大的斜坡:SSD 讀取量減半(96.6 → 59.5 GiB), 每層命中率從 21–71% 拉開到 52–83%。
- 每層命中率在 8 GiB 下落差高達 50 個百分點(21% vs 71%):arena 是全層共用的
單一槽位池、沒有 per-layer 配額,前段層要的 expert 量又最大,於是被其他層擠掉。
這是原版 SDQ
docs/10記錄的同一個問題,在 512 experts/層的規模下更嚴重。 - 63 GiB 那一列在本機不可重現。 測試機的
/root/memkill.py對單一子行程設 64 GiB 上限;63 GiB 預算會讓sdq-chat的 RSS 達 68–70 GiB。48-token 矩陣那兩輪 因為守衛每 2 秒取樣、剛好沒取到尖峰而存活,但同樣設定的 128-token 長跑三次都被砍。 因此最高可達級以 56 GiB 代替。(這一條同時也是該守衛有效運作的證據。) - RAM 預算是 pager budget,不是 OS 硬性限額。 本機
/sys/fs/cgroup是唯讀, 無法為每一級建立獨立的 cgroup 限額,所以 8/16/32/56 GiB 是「把預算交給 pager 去算 arena 大小」,不是四個各自受限的容器。各級的實際 peak RSS 都如實列出。 - 不保證 8 GiB 可用。 非 expert 權重有 31.95 GiB(
per_layer_token_embd26.82 GiB 只經由get_rows取少量列,其餘以 mmap 檔案對映計入 RSS), 8 GiB 那一級的實際 peak RSS 是 15.2 GiB,不是 8 GiB。 - tok/s 與 RAM/SSD/CPUs 綁定,不可跨機器移植;hot/cold 比例與 arena 大小的關係則是 模型與工作負載的性質。
4. 怎麼跑
# 1) 取權重(4 個 shard)
hf download unsloth/Qwen3.8-Flash-Next-GGUF --include "UD-Q4_K_XL/*" \
--local-dir ./models
# 2) 建引擎
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
git checkout 836d57176dc699a726c55418e4f96b8ca628e1bf
# 套用 SDQ patches 0001..0005,再套用本 repo sdq-qwen38/ 的 qwen4exp 改寫
# (或直接取 sdq-qwen38/sdq_pager.{h,cpp} / sdq_moe.cpp / sdq_buft.cpp 覆蓋 src/)
git submodule update --init --recursive
cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF
cmake --build build -j --target sdq-chat
# 3) 跑(RAM 預算單位 MiB)
./build/bin/sdq-chat \
-m ./models/UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf \
-p "The capital of France is" -n 128 -c 2048 -t 16 -b 128 --temp 0 \
--ram-budget-mb 32768 --no-interactive --stats-file /tmp/stats.jsonl
重跑記憶體實驗:
python3 sdq-qwen38/memory_levels_qwen38.py \
--model ./models/UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf \
--bin ./llama.cpp/build/bin/sdq-chat \
--levels-gib 8 16 32 63 --repeat 2 --n-predict 48
5. 改寫了什麼(以及踩到什麼坑)
改寫基於 HelloSun/sddqwen35a3b_v01 的 SDQ patches,共 4 處必要修正。
每一處都是「不改就跑不起來或跑錯」,不是風格調整。
(a) GGUF kv 解析器的型別寬度 bug
原版 sdq_pager.cpp 的 GGUF metadata 解析把 U8/I8/U16/I16 都用 u32() 讀
(應為 1 / 1 / 2 / 2 bytes),而且陣列元素遇到 F64 時完全不消費位元組。
Qwen3.8-Flash-Next 的 metadata 含 2 個 U16(split.no、split.count),
於是解析到檔尾時整個 cursor 錯位 4 bytes,接著把亂數當成字串長度:
[sdq] pager 初始化失敗:basic_string::_M_create
(std::length_error,來自 std::string 建構時的長度爆掉。)
修正:gguf_reader 補上 u16()/i16()/f64(),kv 與陣列元素兩處的 switch
都改成正確寬度,陣列元素遇到未知型別改成丟錯而不是默默 break。
(b) sharded GGUF 支援
原版 pager 只 open() 一個檔案,並且假設「metadata 檔 == 權重檔」,
所有張量的絕對偏移都用第一個檔的 data_start 加上。
103.7 GiB 的模型一定會被切:這份 GGUF 的 shard 1 是 metadata-only
(n_tensors = 0,整個檔案就是 10.4 MiB 的 tokenizer),真正的權重分散在
shard 2/3/4,每個 shard 有**自己的 data_start**。
症狀有兩個,方向相反,都很難從表面前看出來:
- 只讀 shard 1 → page map 全空,pager 起來了但沒有任何 expert 可分頁;
- 若只是把 4 個 shard 串起來卻仍共用第一個 shard 的
data_start→ 每個 expert 都會讀到錯開約 18 KB 的位置,輸出是看似合理的亂碼。
修正:seg_desc 加上 shard 欄位;load_gguf_header() 依 split.count
列舉全部 shard、逐個解析 header 並記錄各自的 data_start;pread 路徑
(read_range / read_range_direct / read_range_staged)與 I/O 執行緒池的工作
項目都多帶一個 shard index,每個 shard 各開一組 buffered / O_DIRECT fd
(O_DIRECT 是 fd 層級屬性,不能共用)。shard 清單用 llama.cpp 自己的
llama_split_prefix / llama_split_path 產生 ——
自己刻字串會差一位數字(實測變成 ...XL--00001-of-...)。
(c) q5_1 / q8_0 量化點積
原版 dot_for() 只認 Q2_K/Q3_K/Q4_K/Q5_K/Q6_K/Q8_0/Q4_0/Q5_0/IQ4_NL/IQ4_XS。
這個模型的 down_exps 是 q5_1(43 層)與 q8_0(5 層) → 每一層的 MoE op
都直接印 不支援的專家量化型別 然後什麼都不算。
修正:加上 GGML_TYPE_Q5_1 → ggml_vec_dot_q5_1_q8_1。關鍵細節:
q5_1 的 activation 要配 q8_1 而不是 q8_0(q5_1 的 block 帶一個 fp16 的
m,語意與 q8_0 不同),所以 act_type_for() 與 quantize_act()
都要新增 GGML_TYPE_Q8_1 分支(quantize_row_q8_1)。
(d) per_layer_token_embd 26.82 GiB 的 RAM 帳目
build_buft override 的正則只擋 blk.N.ffn_(gate|up|down|gate_up)_exps.weight,
所以 26.82 GiB 的 per_layer_token_embd 走一般 CPU buffer,實測佔掉約
4.77 GiB 的 AMX repack buffer + 數 GiB 的檔案對映 RSS。
這是本模型與 22 GiB 舊模型最大的結構差異,也是「8 GiB 預算實際用掉 15 GiB」
的直接原因。本 repo 沒有把它做成第二層分頁 — 它只經由 get_rows 取每
token 24 列,理論上非常適合做 row-level 分頁,但那是另一個尺度的工作量。
列在此處作為後續工作的起點,而不是已經解決的問題。
6. 正確性驗證
分頁 + 自訂 MoE kernel 的數學正確性,用兩層獨立證據確認:
(1) dequant 逐位元組對照。 用 ggml 自己的 dequantize_row_* 抽一列權重,
與驗證腳本裡的 NumPy 實作比對:
| 張量 | 型別 | max|NumPy − ggml| |
|---|---|---|
blk.0.ffn_gate_exps |
q4_K | 0.0 |
blk.2.ffn_gate_exps |
q5_K | 0.0 |
blk.0.ffn_down_exps |
q5_1 | 0.0 |
blk.4.ffn_down_exps |
q8_0 | 0.0 |
(2) 整個 expert 前向對照。 sdq-chat --selftest IL:IE:SEED:OUT 產出
C++ kernel 的結果,再用純 NumPy 從 GGUF 的同一段位元組重算 SiLU-gated
MoE 前向:
| 層 | expert | gate/up | down | max 相對誤差 | 相關係數 | 結果 |
|---|---|---|---|---|---|---|
| 0 | 123 | q4_K | q5_1 | 0.0103 | 0.999952 | PASS |
| 1 | 123 | q4_K | q5_1 | 0.0081 | 0.999954 | PASS |
| 2 | 123 | q5_K | q8_0 | 0.0090 | 0.999958 | PASS |
| 4 | 123 | q4_K | q8_0 | 0.0102 | 0.999957 | PASS |
| 29 | 123 | q4_K | q5_1 | 0.0071 | 0.999956 | PASS |
| 46 | 123 | q4_K | q8_0 | 0.0105 | 0.999939 | PASS |
| 47 | 123 | q4_K | q8_0 | 0.0129 | 0.999956 | PASS |
殘差是 activation 量化誤差(sdq 走 q8_1/q8_K 量化點積,參考實作走 fp32), 量級 0.7–1.3%,不是結構性錯誤。
誠實聲明:尚未做端到端 A/B(pager vs 原生
mul_mat_id生成文字逐 token 比對)。 原因是這個模型在這台機器上沒有辦法跑原生路徑 —— 原生路徑要把 103.7 GiB 權重全部載入 RSS,遠超 64 GiB 的單行程上限。kernel 層與 expert 層的數值證據 是目前能取得的最強證據。
7. 檔案
sdq-qwen38/
sdq_pager.h / sdq_pager.cpp expert pager(含 sharded GGUF + 修正後的 GGUF parser)
sdq_moe.cpp 自訂 MoE kernel(新增 q5_1/q8_1)
sdq_buft.cpp 讓 expert 權重不進 RAM 的假 buffer type
sdq_cli.cpp sdq-chat 前端(未修改)
memory_levels_qwen38.py 記憶體預算矩陣 runner
verify_moe.py NumPy 對照驗證
ggml_deq.cpp 抽 ggml dequant 結果的小工具
results/
final_results.json / .md 整合結果
memory_levels_qwen38-8-16-32-63g.json / .md 逐輪原始資料
8. 授權
權重沿用 unsloth/Qwen3.8-Flash-Next-GGUF 的授權(license: other,見上游)。
本 repo 的程式碼沿用 SDQ 原專案的授權。
Model tree for HelloSun/Qwen3.8-Flash-Next-GGUF
Base model
Qwen/Qwen3.8-Flash-Next