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 放的兩樣東西:

  1. sdq-qwen38/ — 改寫後的推論平台(SDQ expert pager 適配 Qwen3.8-Flash-Next)
  2. 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_embd 26.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 原專案的授權。

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/Qwen3.8-Flash-Next-GGUF

Finetuned
(2)
this model