--- license: other library_name: gguf base_model: unsloth/DeepSeek-V4-Flash-0731-GGUF tags: - gguf - deepseek - deepseek4 - moe - sd-paging - ssd-to-ram --- # DeepSeek-V4-Flash-0731 (UD-IQ4_NL) — SSD→RAM MoE 專家分頁實驗 這個 repo **不含權重**。權重在 [`unsloth/DeepSeek-V4-Flash-0731-GGUF`](https://huggingface.co/unsloth/DeepSeek-V4-Flash-0731-GGUF) 的 `UD-IQ4_NL/` 目錄(4 個 shard,136,662,446,656 bytes ≈ 127.27 GiB)。 這個 repo 放的東西: 1. **`sdq-dsv4/`** — 改寫後的推論平台(SDQ expert pager 適配 `deepseek4`) 2. **`results/`** — 多組 RAM 預算實驗的完整結果與驗證證據 3. **`HARDWARE.md`** — 硬體與執行環境(每個數字附量測指令;哪些是硬限制、哪些只是預算) 4. **`results/upstream_hardware_audit.md`** — 對上游 [`HelloSun/sddqwen35a3b_v01`](https://huggingface.co/HelloSun/sddqwen35a3b_v01) 硬體/環境/容量敘述的審查與勘誤(含 9 條硬性錯誤、6 條內部矛盾、6 條不清晰敘述) 上游是 [`HelloSun/Qwen3.8-Flash-Next-GGUF`](https://huggingface.co/HelloSun/Qwen3.8-Flash-Next-GGUF) 的 `sdq-qwen38/`(同一套 SDQ 架構,目標模型是 `qwen4exp`)。本 repo 是把同一套架構 搬到 `deepseek4` 之上,並且**真的把它改到能跑、能出正確答案**。 --- ## 1. 為什麼需要這個 `DeepSeek-V4-Flash-0731` UD-IQ4_NL 有 **127.27 GiB**,其中: | 類別 | 大小 | 佔比 | 說明 | |---|---:|---:|---| | MoE routed expert 權重 | **120.00 GiB** | 94.3% | 43 層 × 256 expert,每 token 只用 6 個 | | 非 expert 權重 | 7.27 GiB | 5.7% | attention 5.0 GiB、shared expert 1.07 GiB、token_embd 0.52 GiB、router 0.09 GiB、其餘 norm/hyper-connection | | **合計** | **127.27 GiB** | | | 「每層 256 個 expert、每 token 只用 6 個」代表 **97.7% 的 expert 權重在任何給定 時刻都不會被碰到**。把 120 GiB 的 expert 權重留在 SSD、RAM 只放有限大小的 expert arena,是把這個模型塞進小機器的正確做法。這就是 SDQ(SSD→RAM→CPU expert pager)的原始設計。 **本機實測**:RAM 預算 4 GiB(arena **1.79 GiB** = 164 個槽位)時,整個 127.27 GiB 的 模型跑在 **peak RSS 2,233 MB ≈ 2.18 GiB** 裡面(詳見第 3 節的矩陣與 `results/memory_levels_dsv4-4-8-16-32g.json`),代價是 0.69 tok/s。 實際生成內容(逐字,`--temp 0`): ``` 'm happy to help! The capital of France is **Paris**.<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf(Class)} ... ``` 語意上是對的(`Paris`),但**不是乾淨的一行答案**:模型在答完之後吐了 `<|end▁of▁sentence|>`,後面還接著 Java 樣板碼的亂補。這是第 6.4 節那個 decode 期不確定性的一部分,不同 RAM 預算/無關設定會換掉尾巴(32 GiB 那一次甚至 整段退化、第一個 token 就是 `<|end▁of▁sentence|>`,完全沒有 `Paris`)。 要重現請照第 3 節的設定跑,不要引用「乾淨回答」這種說法。 --- ## 2. 模型規格(從 GGUF metadata 實讀) | 項目 | 值 | |---|---| | 架構 | `deepseek4` | | 層數 | 43(另有 3 層 hash-routing) | | expert 數 / 每 token 用 / shared | 256 / **6** / 1 | | hidden size | 4096 | | expert FFN size | 2048 | | 單一 expert | **11.125 MiB**(42 層)/ **12.75 MiB**(layer 26) | | 原生 context | 1,048,576 | | attention | head 64 / kv head 1 / key·value len 512,另有 indexer(top-k 512)、compress_ratios {0, 4, 128}、hyper-connection 4 路 | | MoE 門控 | `expert_gating_func = 4`(sqrt-softplus)、`expert_weights_scale = 1.5`、`expert_weights_norm = 1` | | **gate/up 量化** | **IQ3_S**(42 層)/ **MXFP4**(layer 26) | | **down 量化** | **MXFP4**(全部 43 層) | | swiglu clamp | routed 與 shared 皆是 10.0(每層一個值) | > ⚠️ **三個和 qwen4exp 版本不一樣、而且每一個都會讓推論直接算錯的地方**: > IQ3_S / MXFP4 這兩個量化型別、`ggml_swiglu_clamp` 的夾法、以及 shared expert > 的存在。詳見第 5 節。 --- ## 3. 實驗結果 完整規格、每個數字的量測指令、以及「哪些是硬限制、哪些只是預算」的說明, 全部在 **[`HARDWARE.md`](HARDWARE.md)**。摘要: | | | |---|---| | CPU | Intel Xeon Platinum 8559C,2 socket × 48 core × 2 thread = **192 logical CPU 可見** | | **實際算力配額** | cgroup v2 `cpu.max = 1600000 100000` → **16.0 CPU**(實驗用 `-t 16`) | | 實體 RAM | **2,000.0 GiB = 1.953 TiB**(`MemTotal` 2,097,117,272 kB) | | cgroup v2 `memory.max` | 104,000,000,000 B = **96.86 GiB** | | **實際綁住本專案的上限** | `/root/memkill.py 80 2 64` → **單一子行程 64 GiB**、全部子行程合計 **80 GiB**、每 2 秒取樣、`SIGKILL` | | 儲存 | `/` = **overlayfs**,底層 `nvme0n1` = **Amazon Elastic Block Store**(**網路區塊裝置,不是本地 SSD**)7.8 TB | | 磁碟單流 `dd oflag=direct` | 寫 1.1 GB/s、讀 **2.0 GB/s** | | cgroup 檔案系統 | **唯讀** → 無法為每一級建立獨立限額 | 設定:ctx=2048、ubatch=128、`-t 16`、temp=0、每級 2 輪、`SDQ_RESERVE_MB=2048`。 > ⚠️ **這張矩陣實際量測時用的不是下面第 4.2 節那組可攜編譯選項**,而是 > `-march=native -mno-amx-int8 -mno-amx-tile -mno-amx-bf16`(見 JSON 的 > `build.note`)。AMX 同樣是關閉的,所以兩者在數學上等價;但 tok/s 是在**前者** > 量到的,直接引用時請照實說明,不要當成第 4.2 組選項的實測值。 > 這個矩陣另外用了 `SDQ_RESERVE_MB=2048`,而 pager 的**程式預設值是 900 MB** > (`sdq_pager.cpp`)。這個值直接決定 arena 大小(`arena = 預算 − RSS − 預留`), > 不帶這個環境變數重跑會得到比表中更大的 arena 與不同的槽位數。 > ✅ **這個保留意見可以撤掉了(2026-10-12 複驗)**:用第 4.2 節那組**可攜 ISA > 選項**(`-DGGML_NATIVE=OFF -DGGML_AVX512=ON ...`)重跑整個 4/8/16/32 GiB > 矩陣,8 輪全部無錯,**每一欄都在雜訊內重現**(發布值→複驗值): > > | RAM 預算 | arena (GiB) | slots | decode tok/s | hot % | SSD MiB/tok | peak RSS (MB) | > |---:|---|---|---|---|---|---| > | 4 GiB | 1.79→1.79 | 164→164 | 0.69→0.65 | 0.0→0.0 | 2944.4→2944.4 | 2233→2232 | > | 8 GiB | 5.79→5.79 | 531→531 | 1.40→1.41 | 42.3→42.4 | 1451.6→1444.7 | 6325→6325 | > | 16 GiB | 13.79→13.79 | 1264→1264 | 1.63→1.62 | 54.5→54.9 | 1021.8→1007.5 | 14508→14507 | > | 32 GiB | 29.79→29.79 | 2732→2732 | 1.71→1.72 | 63.9→64.0 | 690.7→688.0 | 30894→30894 | > > 決定性的欄位(arena、slots、peak RSS、SSD 總量)幾乎是位元級相同;只有 tok/s > 落在 ±6% 內,這與 4.2 節自己承認的「同一組設定重跑三次就差 1.66×」是同一個 > 量級的抖動。**所以這張表對第 4.2 節的編譯選項同樣有效。** > > 順帶釐清一處 JSON 內部矛盾:`build.cmake` 欄位寫的是第 4.2 節那組可攜選項, > `build.note` 卻說是用 `-march=native -mno-amx-*` 量的。複驗顯示兩者都對得上這張 > 表,矛盾不影響數字,但 `build.note` 那句「tok/s 要照實說明」已無必要。 > 三個容易搞混的地方,這裡先講清楚: > 1. `nproc` 在這台機器上回 16,是因為環境變數 `OMP_NUM_THREADS=16`, > **不是**因為配額或 affinity(`sched_getaffinity` 回 192)。 > 2. 指標名稱沿用 SDQ 原版的 `SSD MiB/tok`,但這裡的「SSD」是 overlayfs 底下的 > EBS 網路裝置,量到的是**這個容器讀檔案的速率**(單流上限 2.0 GB/s)。 > 3. `--ram-budget-mb` 是 **pager budget,不是 OS 限額**;真正會殺行程的是 > `memkill.py` 的 64 GiB 單行程上限。這也是本 repo 沒做「原生路徑 vs 分頁路徑」 > 端到端 A/B 的原因(原生路徑要 127.27 GiB 進 RSS,一定被殺)。 > 4. 表中每級的 tok/s 是**兩輪不同 prompt 的平均**(第 1 輪 `The capital of France is`、 > 第 2 輪中文 SSD 題),不是同一段文字的兩次重跑。原始逐輪數字在 JSON 裡。 | RAM 預算 | arena (GiB) | slots | decode tok/s | prefill tok/s | hot % | cold % | SSD MiB/tok | 全程 SSD 讀取 | peak RSS (MB) | swap | 每層命中率 min..max | |---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | 4 GiB | 1.79 | 164 | 0.69 | 1.65 | 0.0 | 100.0 | 2944.4 | 161.8 GB | 2233 | 0 MB | 0.0..0.0 | | 8 GiB | 5.79 | 531 | 1.40 | 1.65 | 42.3 | 57.7 | 1451.6 | 93.3 GB | 6325 | 0 MB | 3.5..58.0 | | 16 GiB | 13.79 | 1264 | 1.63 | 1.75 | 54.5 | 45.5 | 1021.8 | 73.6 GB | 14508 | 0 MB | 15.1..65.9 | | 32 GiB | 29.79 | 2732 | 1.71 | 1.80 | 63.9 | 36.1 | 690.7 | 58.4 GB | 30894 | 0 MB | 32.4..73.2 | * `hot %` = expert 已在 RAM arena、不需要碰 SSD;`cold %` = 必須回 SSD `pread`。 * **重點:127.27 GiB 的模型在 2.2 GiB peak RSS 裡跑得動**(0.69 tok/s), 給到 30.9 GiB 得到 1.71 tok/s(2.48×)、SSD 讀取量降到 23.5%。 * **32 GiB 那一級的 repeat 1 生成結果是退化的**:輸出從 `<|end▁of▁sentence|>` 就開始,後面接 Java 樣板碼亂補,整段沒有 `Paris`。 同級的 repeat 2(中文 SSD 題)則正常回答。 這兩輪的**熱命中率與 tok/s 相當接近**(65.2% vs 62.6%、1.86 vs 1.56 tok/s), 但**它們用的是不同 prompt,所以這不是受控對照**,不能用來證明退化與分頁無關; 只能說:在這個 RAM 預算下,回應品質並不穩定。不要把 32 GiB 那一級 說成「能穩定答對」,相關的不確定性見第 6.4 節。 * 4 GiB 那一級 `hot = 0%` 不是 bug:一個 token 在 43 層各用 6 個 expert, 工作集下限就是 43 × 6 = 258 個 expert ≈ 2.8 GiB,而這一級的 arena 只有 1.79 GiB → 任何一次請求都必然是 miss。 * **RAM 預算是 pager budget,不是 OS 硬性限額**:本機 `/sys/fs/cgroup` 唯讀 (`echo 0 > memory.high` 直接 Read-only),`cgroup.subtree_control` 為空, 沒辦法為每一級建立獨立的 cgroup 限額,所以 4/8/16/32 GiB 是「把預算交給 pager 去算 arena 大小」,不是四個各自受限的容器。各級的實際 peak RSS 都如實列出, 全部遠低於 `memory.max` 的 96.86 GiB 與 memkill 的 64 GiB 單行程上限。 * swap 全程 0 MB:SDQ 不用 swap,權重資料只存在 arena(實體 RAM)與檔案(SSD)兩處。 * tok/s 與 RAM/SSD/CPUs 綁定,不可跨機器移植。 原始逐輪資料:[`results/memory_levels_dsv4-4-8-16-32g.json`](results/memory_levels_dsv4-4-8-16-32g.json)、 說明:[`results/memory_levels_dsv4-4-8-16-32g.md`](results/memory_levels_dsv4-4-8-16-32g.md)。 --- ## 4. 怎麼跑 ### 4.1 取得權重(127.27 GiB) ```bash hf download unsloth/DeepSeek-V4-Flash-0731-GGUF --include "UD-IQ4_NL/*" \ --local-dir ./models ``` > ⚠️ **`hf` CLI 在某些環境會直接掛掉,要改用 Python API**(2026-10-12 實測)。 > 症狀是在開始下載之前就丟出: > > ``` > File ".../typer/core.py", line 725, in parse_args > ctx._opt_prefixes.update(parser._opt_prefixes) > AttributeError: 'StyledContext' object has no attribute '_opt_prefixes' > ``` > > 這是 `typer` 與 `click` 的版本不相容(typer ≥ 0.27 需要 click ≥ 8.2 的 > `Context._opt_prefixes`,但環境裡是 click 8.0.4),**跟 HF token、 > 權重、網路都無關**,改 token 沒用。權重是 public repo,其實根本不需要 token。 > 兩種解法: > > ```bash > # (a) 直接用 Python API(不經過 typer,最省事) > python3 -c " > from huggingface_hub import snapshot_download > print(snapshot_download('unsloth/DeepSeek-V4-Flash-0731-GGUF', > allow_patterns=['UD-IQ4_NL/*'], local_dir='./models'))" > > # (b) 升級 click > pip install -U 'click>=8.2' > ``` > > 下載完記得核對 4 個 shard 的 bytes 應為 > `5257664 + 49431060672 + 49605340544 + 37620787776 = 136662446656`, > 對不上就是下載沒完(本 repo 實測下載速率約 1.5 GB/s,約 92 秒完成)。 ### 4.2 建引擎 ```bash git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp git checkout 836d57176dc699a726c55418e4f96b8ca628e1bf git apply ../sdq-dsv4/0001-sdq-llama-glue.patch # llama.cpp 側的掛鉤 cp ../sdq-dsv4/sdq_pager.h ../sdq-dsv4/sdq_pager.cpp \ ../sdq-dsv4/sdq_moe.cpp ../sdq-dsv4/sdq_buft.cpp \ ../sdq-dsv4/sdq_cli.cpp src/ # SDQ 主體(直接覆蓋) git submodule update --init --recursive # x86:明確指定 ISA,不要用 -march=native(原因見第 5 節 (e)) cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF \ -DGGML_NATIVE=OFF \ -DGGML_AVX=ON -DGGML_AVX2=ON -DGGML_F16C=ON -DGGML_FMA=ON -DGGML_BMI2=ON \ -DGGML_AVX512=ON -DGGML_AVX512_VNNI=ON -DGGML_AVX512_BF16=ON cmake --build build -j --target sdq-chat ``` **為什麼不要 `-march=native`**:Sapphire Rapids 以後的 Xeon,`-march=native` 會直接 定義 `__AMX_INT8__`/`__AVX512VNNI__`,於是 llama.cpp 註冊 AMX extra buffer type, 然後在 deepseek4 上撞 `ggml_backend_sched_split_graph` 的 assert(第 5 節 (e))。 明確列 ISA 就不會踩到 —— AMX 的三個 CMake 選項本來就預設 OFF,而 SDQ 本來就 不走 repack,關掉 AMX 沒有任何損失。 三個 x86 基線都實測過(`-t 16`,16 GiB 預算,32 個生成 token,各 3 輪): | ISA 設定 | decode tok/s(3 輪) | 結果 | |---|---|---| | `-DGGML_AVX2=ON`(Haswell+) | 1.16 | 正確輸出,無 assert | | AVX512 + VNNI + BF16(Skylake-X / Ice Lake+) | 1.48 / 1.10 / 1.18 | 正確輸出,無 assert | | `-march=native -mno-amx-*`(同一台機器) | 1.71 / 1.09 / 1.03 | 正確輸出,但需要那個 `-mno-` hack | **這組數字不能支持「兩種編譯選項等速」的結論。** 每一列自己內部的落差 (`-march=native` 是 1.71 / 1.09 / 1.03,**同一組設定重跑三次**就差了 1.66×) 已經大於列與列之間的差距,所以這個 A/B 沒有統計力,只能說: > 三種設定在這台機器上都**能正確產出、都不會踩到 assert**; > 至於哪個比較快,現有資料**無法區分**,需要更多輪次、且固定同一段 > prompt 與輸出長度重測(decode tok/s 與觸發到多少個不同 expert 強相關, > 而 decode 期輸出本身對行程狀態敏感,見第 6.4 節)。 換句話說:**AMX 必須關閉這個結論是確定的**(會撞 assert,重現步驟見 (e)), 但「用明確的 AVX512 設定不會比 `-march=native` 慢」目前是**未證實**的。 另外第 3 節那張 RAM 矩陣用的是 `-march=native -mno-amx-*`,若要讓它對應到 第 4.2 節這組可攜選項,同樣需要先補一次有統計力的重測。 ARM / 其他架構請用 llama.cpp 自己的預設值(不要帶任何 `GGML_NATIVE` 或 AVX 選項)。 ### 4.3 跑(RAM 預算單位 MiB) ```bash ./build/bin/sdq-chat \ -m ./models/UD-IQ4_NL/DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf \ -p "The capital of France is" -n 48 -c 2048 -t 16 -b 128 --temp 0 \ --ram-budget-mb 8192 --no-interactive --stats-file /tmp/stats.jsonl ``` `--ram-budget-mb` 是**總 RAM 預算**(不是「還有多少可用」):pager 會扣掉當下的 RSS 與一份預留量(`SDQ_RESERVE_MB`,**程式預設 900 MB**)之後,剩下的才是 arena。 第 3 節的矩陣是用 `SDQ_RESERVE_MB=2048` 量出來的,重跑要對齊這個值: ```bash SDQ_RESERVE_MB=2048 python3 sdq-dsv4/memory_levels_dsv4.py \ --model ./models/UD-IQ4_NL/DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf \ --bin ./llama.cpp/build/bin/sdq-chat \ --levels-gib 4 8 16 32 --repeat 2 --n-predict 48 ``` --- ## 5. 改寫了什麼(以及踩到什麼坑) 改寫基於 `HelloSun/Qwen3.8-Flash-Next-GGUF` 的 `sdq-qwen38/`,共 5 處必要修正。 每一處都是「不改就跑不起來或跑錯」,不是風格調整。 ### (a) IQ3_S / MXFP4 的量化點積 SDQ 的 `dot_for()` 只認 Q2_K…Q8_0 / Q4_0 / Q5_0 / Q5_1 / IQ4_NL / IQ4_XS。 這個模型的 routed expert 是 **IQ3_S**(gate/up)與 **MXFP4**(down)→ **每一層**都會印 `不支援的專家量化型別` 然後什麼都不算。 **修正**:加上 | 權重型別 | vec_dot | activation 型別 | |---|---|---| | `GGML_TYPE_IQ3_S` | `ggml_vec_dot_iq3_s_q8_K` | `GGML_TYPE_Q8_K` | | `GGML_TYPE_MXFP4` | `ggml_vec_dot_mxfp4_q8_0` | `GGML_TYPE_Q8_0` | 配對不能自己猜,必須照 ggml 的 `type_traits` (`ggml/src/ggml-cpu/ggml-cpu.c`):`IQ3_S.vec_dot_type = Q8_K`、 `MXFP4.vec_dot_type = Q8_0`。特別注意 IQ3_S 落到 `default:` 會被配成 Q8_0, 而它的 6-bit scale 組出來的值範圍大於 q8_0 的 ±127 —— 不改就是整層數值錯掉。 ### (b) `ggml_swiglu_clamp` 的夾法 deepseek4(以及 glm5_next / maple / dflash / hy_v4)走的是 ggml 的融合算子 `ggml_swiglu_clamp`(`ggml/src/ggml-cpu/ops.cpp`): ```c const float gate = std::min(src0_p[k], limit); // 只夾上界 const float up = std::clamp(src1_p[k], -limit, limit); // 對稱 dst_p[k] = gate / (1.f + expf(-gate)) * up; // 夾在 silu 之前 ``` SDQ 原版夾的是 **silu 之後** 的 gate、而且是對稱的: ```c gc = clamp(silu(gate), -limit, limit); // 夾在 silu 之後、對稱 out = gc * clamp(up, -limit, limit); ``` 兩者不是同一個函式(夾的位置不同、gate 的邊界條件不同)。 **修正**:把夾法做成參數 `sdq_build_moe(..., swiglu_limit, swiglu_mode)`, 由 `build_moe_ffn()` 依 arch 決定(`0001-sdq-llama-glue.patch` 裡那段 switch 就是照著 `LLM_FFN_SILU` 的分支寫的)。`deepseek4` 走 `SDQ_SWIGLU_GGML_FUSED_CLAMP`。 > 誠實補充:這個模型的 clamp 值是 **10.0**,而 `silu(x) ≤ x`, > 所以在 `g ≥ limit` 的區域兩種寫法只差 `silu(10) = 9.9999` 對 `10` > (約 0.001%),`g ≤ -limit` 的區域兩者都趨近 0。也就是說**這一項在這個模型上 > 是 1e-4 等級的差異,不是災難**。仍然照上游寫是對的(正確的公式不該是 > 「差不多對的公式」),但不要把它講成修了一個大 bug。 ### (c) shared expert 不進 arena(而且確認沒有被吃掉) `deepseek4` 每層有 **1 個 shared expert**(`expert_shared_count = 1`, `ffn_{gate,up,down}_shexp`)。SDQ 的 `build_moe_ffn()` 掛鉤是**提早 return**的 ——對 qwen4exp(沒有 shared expert)沒問題,對 deepseek4 就會把 shared expert 整段跳過。 檢查結果(`llama.cpp` `src/models/deepseek4.cpp`):shared expert 是在 `build_moe_ffn()` **之外**算的獨立在線 ```cpp ggml_tensor * moe_out = build_moe_ffn(...); // ← SDQ 在這裡提早 return ggml_tensor * ffn_shexp = build_ffn(cur, layer.ffn_up_shexp, ..., il); cur = ggml_add(ctx0, moe_out, ffn_shexp); // ← 兩邊都保留 ``` `build_ffn()` 只用一般的 `ggml_mul_mat`(不是 `mul_mat_id`),所以提早 return 不會影響它。SDQ 的 buffer override 正則 `blk\.[0-9]+\.ffn_(gate|up|down|gate_up)_exps\.weight` 也**不會**誤配到 `_shexp`(那裡要求後面接 `_exps.weight`)。 這一項是「確認不用改」,但確認的過程比改動本身更花時間,所以把它寫下來。 ### (d) hash-routing 的前 3 層 `hash_layer_count = 3`:前 3 層的 expert 選擇不是 `argsort_top_k` 算出來的, 而是 `get_rows(ffn_gate_tid2eid, tokens)` 從一張 `[6, 129280]` 的查表得到 (`src/models/deepseek4.cpp`)。這兩條路徑產生的 `ids` **形狀與型別完全相同** (`I32 [n_expert_used, n_tokens]`),SDQ 的 op 是照 `src[1]` 的 `nb[0]/nb[1]` 讀的,所以不需要改。差異只在「有沒有 routing bias」,而 bias 是在 `ids` 之前 由 ggml 圖算完的。 ### (e) AMX 必須關掉(否則 deepseek4 在這版 llama.cpp 上起不來) 這不是 SDQ 的問題,是 llama.cpp `836d5717` 加上 AMX repack 時的問題: ``` ggml_backend_sched_split_graph: GGML_ASSERT(*cur_backend_id != -1) failed unassigned node: op=MUL_MAT ne=[1024,128,8,1] name='attn_wo_a-0' src[0] q8_0 buft=AMX src[1] f32 name='attn_derope-0 (reshaped) (permuted)' ← 非 contiguous ``` `ggml_backend_cpu_device_supports_op()` 在看到 src 落在 extra buffer type (AMX / REPACK)時會**直接**把決定權交給該 buffer type 的 `supports_op()`, 而 AMX 的實作要求 `ggml_is_contiguous(src1)`。deepseek4 的 `attn_output_a`(低秩輸出投影)拿到的是 de-rope 之後的 permute view, 不 contiguous → AMX 回 false → 整個 op 沒有任何 backend 能接 → assert。 本機 CPU 有 `amx_int8`/`amx_bf16`/`amx_tile`,而 **`-march=native` 會直接定義 `__AMX_INT8__`/`__AVX512VNNI__`**,於是 AMX extra buffer type 被註冊 (`ggml/src/ggml-cpu/amx/amx.cpp` 用 `#if defined(__AMX_INT8__) && defined(__AVX512VNNI__)` 註冊)。 **修正有兩種,擇一**: * **推薦(4.2 採用)**:不要用 `-march=native`,改成明確列 ISA (`-DGGML_NATIVE=OFF -DGGML_AVX512=ON ...`)。AMX 的三個 CMake 選項本來就 預設 OFF,所以 AMX extra buffer type 不會被註冊。 > ⚠️ **勘誤(2026-10-12 複驗)**:本節原本寫「`libggml-cpu.so` 裡連 `"AMX"` > 這個字串都不會出現」——**那句是錯的**,該字串確實會出現。 > `libggml-cpu.so` 裡有一個 `AMX_INT8` 字串,來自 ggml 的 CPUID 機型偵測 > (`ggml/src/ggml-cpu/arch/x86/cpu-feats.cpp` 的 `bool AMX_INT8(void)`), > 那段程式碼本來就**一定**會編譯進來,與 AMX buffer type 有沒有註冊無關。 > **正確的判準是看有沒有註冊的符號**,本機實測: > > ```bash > $ nm -D --defined-only build/bin/libggml-cpu.so | grep -i amx > 000000000001d400 T ggml_cpu_has_amx_int8 ← 只有這個 CPUID 探測 > $ strings build/bin/libggml-cpu.so | grep -c 'amx_extra_buffer_type' > 0 ← AMX buffer type 未註冊 > ``` > > 結論(AMX 必須關)不變,錯的只是拿來佐證的那句話。 * 如果你一定要 `-march=native`:加 `-DCMAKE_C_FLAGS="-mno-amx-int8 -mno-amx-tile -mno-amx-bf16"`(CXX 同樣)。 兩種都實測過,輸出與 tok/s 都一樣(第 4.2 節的表)。SDQ 本來就不走 repack (expert 權重根本不進 RAM),關掉 AMX 沒有任何損失。 > 想自己重現這個 assert、又不想改編譯參數的話, > `sdq-dsv4/optional-sched-debug.patch` 可以在 assert 前印出真正沒被接走的節點。 ### (f) 從 qwen4exp 版本繼承、但這個模型也會踩到的兩個坑 `sdq-qwen38/` 已經修好的兩件事,`deepseek4` 的 GGUF **一樣會踩到**,所以直接繼承: 1. **GGUF kv 解析器的型別寬度**。DeepSeek 的 metadata 有 3 個 `U16` (`split.no` / `split.tensors.count` / `split.count`),舊版 `sdq_pager.cpp` 把 `U8/I8/U16/I16` 都用 `u32()` 讀、且陣列元素遇到 `F64` 完全不消費位元組, 於是 header 整個錯位,症狀是 `[sdq] pager 初始化失敗:basic_string::_M_create`。 2. **sharded GGUF**。這個模型是 4 個 shard,而且 **shard 1 是 metadata-only** (`n_tensors = 0`,整個檔案就是 5.0 MiB 的 tokenizer),權重分散在 shard 2/3/4,每個 shard 有自己的 `data_start`。只讀 shard 1 → page map 全空; 把 4 個 shard 串起來卻共用第一個 shard 的 `data_start` → 每個 expert 讀到 錯開約 18 KB 的位置,**輸出是看似合理的亂碼**。 --- ## 6. 正確性驗證 ### 6.1 dequant 逐位元組對照(獨立實作) `verify_moe_dsv4.py` 裡的 IQ3_S / MXFP4 還原是照 ggml 原始碼手寫的 NumPy。 如果那份手寫有錯,後面所有「C++ kernel 對得上 NumPy」的結論都只是在 一份錯誤的 reference 上自我一致。所以用 `ggml_deq`(直接呼叫 ggml 的 C 函式) 當**第二份獨立實作**逐位元組比對: ```bash c++ -O2 -o /tmp/ggml_deq sdq-dsv4/ggml_deq.cpp \ -I/ggml/include -I/ggml/src \ -L/build/bin -lggml-base -Wl,-rpath,/build/bin python3 sdq-dsv4/verify_dequant_dsv4.py ``` | 張量 | 型別 | max|NumPy − ggml| | |---|---|---:| | `blk.0.ffn_gate_exps` | IQ3_S | **0.0** | | `blk.0.ffn_up_exps` | IQ3_S | **0.0** | | `blk.0.ffn_down_exps` | MXFP4 | **0.0** | | `blk.26.ffn_gate_exps` | MXFP4 | **0.0** | | `blk.26.ffn_down_exps` | MXFP4 | **0.0** | 15/15 列 **bit-exact**(差異精確為 0,不是「很小」)。 > 這裡是 5 個張量 × 3 個 row = **15 列**。原始輸出檔 > `results/verify_dequant.txt` 早先印成 21 列,但其中 `blk.0.ffn_down_exps` 與 > `blk.26.ffn_down_exps` 各被重複印了一次(每個 3 列,共 6 列重複), > 所以「21/21」是重複計數造成的,不是真的做了 21 次獨立比對。 > > ✅ **2026-10-12:根因已修掉(先前只改了結果檔,腳本沒動)**。 > 根因是 `verify_dequant_dsv4.py` 把 `TARGETS` 寫成一張「張量後綴」清單, > 然後外層 `for il in (0, 26)` 把整張清單套用到**兩層**;而 layer 0 與 layer 26 > 的 `ffn_down_exps` 都是 MXFP4(39),所以那 3 列被比對了兩次 → 印 21 列、實際 15 列。 > 已改成 `(層, 後綴, 型別, 元素數)` 逐項寫死,並加上兩道防呆: > 張量不存在或型別不符會直接 `SystemExit`(原本是 `continue` 靜靜跳過), > 以及比對結束後檢查「列數 == 唯一張量數」,重複就直接報錯。 > 修完重跑輸出 **15/15 bit-exact**,與本節表格與 `results/verify_dequant.txt` 一致。 ### 6.2 整個 expert 前向對照 `sdq-chat --selftest IL:IE:SEED:OUT.bin` 產出 C++ kernel 的結果,再用純 NumPy 從 GGUF 的**同一段位元組**重算 SiLU-gated MoE 前向: ```bash for spec in 0:7 1:7 2:123 3:255 12:7 25:7 26:7 27:7 41:7 42:7; do il=${spec%%:*}; ie=${spec##*:} ./build/bin/sdq-chat -m $MODEL --selftest "$il:$ie:1234:/tmp/st_l${il}e${ie}.bin:1" \ --selftest-limit 10 --selftest-mode 1 --ram-budget-mb 4096 --no-interactive done DSV4_LIMIT=10 DSV4_MODE=1 python3 sdq-dsv4/verify_moe_dsv4.py ``` | 層 | expert | gate/up | down | max 相對誤差 | 相關係數 | 結果 | |---:|---:|---|---|---:|---:|---| | 0 | 7 | iq3_s | mxfp4 | 0.0099 | 0.999945 | PASS | | 1 | 7 | iq3_s | mxfp4 | 0.0111 | 0.999949 | PASS | | 2 | 123 | iq3_s | mxfp4 | 0.0100 | 0.999950 | PASS | | 3 | 255 | iq3_s | mxfp4 | 0.0077 | 0.999948 | PASS | | 12 | 7 | iq3_s | mxfp4 | 0.0082 | 0.999950 | PASS | | 25 | 7 | iq3_s | mxfp4 | 0.0106 | 0.999941 | PASS | | **26** | 7 | **mxfp4** | mxfp4 | 0.0101 | 0.999947 | PASS | | 27 | 7 | iq3_s | mxfp4 | 0.0082 | 0.999949 | PASS | | 41 | 7 | iq3_s | mxfp4 | 0.0103 | 0.999942 | PASS | | 42 | 7 | iq3_s | mxfp4 | 0.0115 | 0.999948 | PASS | 殘差是 **activation 量化誤差**(sdq 走 q8_K / q8_0 量化點積,參考實作走 fp32), 量級 0.8–1.2%,不是結構性錯誤。layer 26 是唯一 gate/up 換成 MXFP4 的層, 單獨列出來是因為它是 (a) 那個修正唯一的執行路徑。 ### 6.3 分頁結果與 arena 大小無關(bit-exact) `SDQ_MOE_DUMP` 會在 MoE op 真的算完的那一刻,把該次呼叫的**輸入與輸出** (`cur` / `ids` / `wts` / `out`)寫成一個二進位檔。同一個 prompt、同一組設定, 只改 `--ram-budget-mb`: | 範圍 | 檔案數 | bit-identical | |---|---:|---:| | 4 GiB vs 32 GiB,**全部 43 層 × 9 次 op call**(1 次 prefill `n_tok=25` + 8 次 decode `n_tok=1`),含輸入與輸出 | 387 | **387 / 387** | hot/cold 比例、SSD 讀取量、槽位數(164 vs 2732)、淘汰次數全部不同, **但每一層每一個 token 的 MoE 輸入與輸出位元組完全一樣**。這是對「分頁有沒有偷偷 改變數學」最直接的證據。逐檔 md5 見 [`results/moe_dump_bitexact.md`](results/moe_dump_bitexact.md)。 > ⚠️ **重要限定:387/387 是「這個 prompt」才有的結果,不是通則**(2026-10-12 複驗)。 > 把 §A 的設定整個照抄、只換 prompt 成 `The capital of France is`, > 得到的**不是** 387/387,而是: > > | prompt | prefill bit-identical | decode bit-identical | 合計 | > |---|---:|---:|---:| > | `Explain mixture-of-experts routing in one sentence.`(§A 原文) | 43 / 43 | **344 / 344** | **387 / 387** | > | `The capital of France is` | 43 / 43 | **0 / 344** | 43 / 387 | > > 兩邊都是 `--ram-budget-mb 4096` vs `32768`、其餘設定完全相同。 > 第一列**精確重現**了 §A 的 387/387,所以 §A 本身沒有問題。 > 第二列說明:換成 top-1/top-2 本來就幾乎並列的 prompt,decode 期就會分岔。 > 分岔點同樣在**第一個 decode call** 就出現(`d.0.43`,也就是 layer 0 的第 1 次 > decode),此時 `cur` 的 `max|Δ|` 相對值已經是 2.1(不是微小浮點雜訊), > `ids` 也跟著換成別組 expert。 > > 合理的解釋是:prefill 結束後的 logits 有極小差異 → `--temp 0` 的 argmax 換成 > 另一個同義 token → 後續 decode 的輸入 `cur` 整條岔開。這與 6.4 節的結論一致 > (分歧來自 llama.cpp 的 decode 期 dsv4 CSA/HCA 路徑,不是 SDQ 的分頁)。 > > 所以這節標題「分頁結果與 arena 大小無關」要這樣讀: > **對這個 prompt 成立;對 top-1/top-2 接近的 prompt 不成立。** > 引用 387/387 這個數字時必須連 prompt 一起引用,否則會被誤讀成端到端通則。 另外同一設定重跑 6 次、兩個 RAM 預算各 6 次,greedy(`--temp 0`)輸出 **12/12 逐字相同**(`results/determinism.txt`)。 ### 6.4 誠實聲明:沒做的事,以及一個還沒解掉的問題 **沒有做端到端 A/B**(SDQ 分頁路徑 vs 原生 `mul_mat_id` 逐 token 比對文字)。 原因是這個模型在這台機器上沒辦法跑原生路徑 —— 原生路徑要把 127.27 GiB 權重 全部載入 RSS,遠超單一子行程 64 GiB 的上限。6.1–6.3 是目前能取得的最強證據。 **還沒解掉的問題:decode 期的 greedy 輸出對行程狀態敏感。** 固定設定下重跑 6 次輸出 100% 逐字相同;但只要改動**與計算無關**的東西 (`--stats-file`、`SDQ_STATS_FILE`、`SDQ_MOE_DUMP`、`SDQ_FADV_DONTNEED=0`、 `SDQ_IO_SPLIT=2`、`-c`、`-b`),第一個 token 就會換。`-t 1`(完全單執行緒) 也一樣會換,所以不是執行緒競爭。 > ⚠️ **`--ram-budget-mb` 也要加進這份清單(2026-10-12 複驗新增)**。 > 本來這份清單裡沒有 RAM 預算,本 repo 的 `results/determinism.txt` 也顯示 > 8192 MB 與 32768 MB 兩個預算**輸出相同**(同為 `0e637837ac89`)。 > 但在本機用 4.2 的可攜編譯選項重跑,每個預算**各自**仍是 6/6 逐字相同, > **兩個預算彼此卻不同**: > > | build | 8192 MB | 32768 MB | 是否相同 | > |---|---|---|---| > | 可攜 ISA(第 4.2 節) | `08c061788823` | `175b890dddeb` | **不同** | > | `-march=native -mno-amx-*` | `d5cf2396b424` | `54a418d53304` | **不同** | > > 兩種 build 都一樣,所以**不是編譯選項造成的**(原本懷疑是,已排除)。 > 實際內容:8192 MB 給出 `capital of France is **Paris**.`, > 32768 MB 給出 `'m sorry, I can't answer that question.`。 > 配合 6.3 節新加的限定表,這是同一件事的兩種表現: > **arena 大小會改變 decode 期的 I/O 時序,而這個 prompt 的 top-1/top-2 > 幾乎並列,於是 argmax 換掉、輸出整段岔開。** > > 換句話說,`determinism.txt` 那份「兩個預算 md5 相同」的結果, > 在本機**無法重現**;能重現的只有「同一預算內 6/6 相同」這個部分 > (本機兩個 build、兩個預算都成立,合計 24/24)。 > 要引用時請照這個範圍引用,不要說「RAM 預算不影響輸出」。 **而且分歧點已經定位到**:把兩種設定的 MoE dump 逐一比對(43 層 × 17 次 op call = 731 個檔案),**43 個 prefill 呼叫全部 bit-identical、688 個 decode 呼叫全部不同** (`results/moe_dump_bitexact.md` §B)。分歧已經出現在 MoE op 的**輸入** `cur` 裡, 也就是說: * **不是 SDQ 的分頁或 kernel**(§6.3 已經證明分頁對數學是 bit-exact 的); * 不是每個 expert 的請求數對不上 —— `expert_requests == expert_hits + expert_misses` 永遠成立,沒有任何 expert 被跳過(`acquire()` 拿不到槽位時是 `abort()`, 不是靜靜跳過); * 是 llama.cpp `deepseek4` 的 **decode 期 dsv4 CSA/HCA 壓縮 KV state 路徑**, 在這兩種設定下算出了不同的 `cur`。SDQ 的 op 只是忠實地把它算下去。 而且所有觀察到的變體**在語意上都是對的**(`capital of France is **Paris**.` / ` Paris.` / `'m happy to help! The capital of France is **Paris**.`), 這個 prompt 的 top-1/top-2 本來就非常接近。 要真的定位到 llama.cpp 那一側,需要用 sanitizer build 重跑(prefill 一致、 decode 不一致這個現象很適合用 MSan/valgrind 抓),本 repo 沒做到。 這件事會影響任何想用這個 port 做 token 級 A/B 的人,所以寫在這裡。 --- ## 7. 檔案 ``` sdq-dsv4/ sdq_pager.h / sdq_pager.cpp expert pager(sharded GGUF、修正後的 GGUF parser、大小兩種槽位) sdq_moe.cpp 自訂 MoE kernel(新增 IQ3_S/MXFP4、ggml_swiglu_clamp 語意) sdq_buft.cpp 讓 expert 權重不進 RAM 的假 buffer type sdq_cli.cpp sdq-chat 前端(新增 --selftest-limit/-mode/-xscale) ggml_deq.cpp 抽 ggml dequant 結果的小工具(驗證用) ggml_tables.py 從 ggml 原始碼抽出的 IQ3_S/MXFP4 常數表(由 gen_tables.py 產生) gen_tables.py 產生上面那個表的小工具 verify_dequant_dsv4.py NumPy dequant vs ggml dequant,逐位元組 verify_moe_dsv4.py expert 前向 vs 純 NumPy memory_levels_dsv4.py RAM 預算矩陣 runner determinism_probe.sh 決定性探針(同一設定重跑 N 次比對輸出) 0001-sdq-llama-glue.patch llama.cpp 側的掛鉤(build_moe_ffn / init_mappings / CMake) optional-sched-debug.patch 選用:sched assert 前印出沒被接走的節點 HARDWARE.md 硬體與執行環境(每個數字附量測指令) results/ upstream_hardware_audit.md 對上游 sddqwen35a3b_v01 的硬體/環境敘述審查與勘誤 memory_levels_dsv4-4-8-16-32g.json / .md 記憶體實驗(原始逐輪資料 + 表格) verify_dequant.txt NumPy dequant vs ggml dequant 的逐筆輸出(15/15 bit-exact) verify_moe_forward.txt expert 前向 vs 純 NumPy 的逐筆輸出(10/10 PASS) moe_dump_bitexact.md MoE op 的 bit-exact A/B(387/387 與分歧點定位) determinism.txt 決定性探針(同一設定重跑 6 次 × 2 個預算) --- 以下為 2026-10-12 獨立複驗新增(原始結果未被覆蓋) --- memory_levels_dsv4-4-8-16-32g-recheck.json / .md 用 4.2 可攜 ISA 選項重跑整個 4/8/16/32 GiB 矩陣 determinism_recheck.txt 決定性探針 × 2 種 build(證明編譯選項非成因) moe_dump_recheck_prompt_dependence.md 6.3 的 387/387 為何是 prompt 專屬(含對照表) verify_dequant_recheck.txt 修正後 verify_dequant_dsv4.py 的 15/15 輸出 ``` ### 重跑驗證 ```bash export DSV4_MODEL_DIR=./models/UD-IQ4_NL # verify_moe_dsv4.py 的權重位置 # (1) dequant bit-exact(需要先 build ggml_deq,見 6.1) python3 sdq-dsv4/verify_dequant_dsv4.py # (2) expert 前向(先跑 sdq-chat --selftest 產生 /tmp/st_l*e*.bin,見 6.2) DSV4_LIMIT=10 DSV4_MODE=1 python3 sdq-dsv4/verify_moe_dsv4.py # (3) 決定性探針(同一設定重跑 N 次比對輸出) bash sdq-dsv4/determinism_probe.sh 8192 32768 # (4) MoE op bit-exact A/B:同一設定、只改 --ram-budget-mb,各 dump 一次再逐檔 cmp # 見 6.3 / results/moe_dump_bitexact.md for mb in 4096 32768; do rm -rf /tmp/moe_$mb && mkdir -p /tmp/moe_$mb SDQ_MOE_DUMP=/tmp/moe_$mb/d SDQ_RESERVE_MB=2048 \ ./build/bin/sdq-chat -m $MODEL \ -p "Explain mixture-of-experts routing in one sentence." -n 8 -c 2048 -t 16 -b 128 \ --temp 0 --ram-budget-mb $mb --no-interactive >/dev/null 2>&1 done diff -r --brief /tmp/moe_4096 /tmp/moe_32768 && echo "387/387 bit-identical" ``` > ⚠️ **(4) 的結論強烈依賴 prompt**,請見 6.3 節的限定表:換成 > `The capital of France is` 只會得到 prefill 43/43、decode 0/344。 --- ## 8. 授權 權重沿用 `unsloth/DeepSeek-V4-Flash-0731-GGUF` 的授權(`license: mit`,見上游)。 本 repo 的程式碼沿用 SDQ 原專案 ([`HelloSun/sddqwen35a3b_v01`](https://huggingface.co/HelloSun/sddqwen35a3b_v01)) 與 [`HelloSun/Qwen3.8-Flash-Next-GGUF`](https://huggingface.co/HelloSun/Qwen3.8-Flash-Next-GGUF) 的授權。