HelloSun's picture
Independent re-verification (2026-10-12) + fixes
f20da6e verified
|
Raw History Blame Contribute Delete
37.3 kB
---
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<llama.cpp>/ggml/include -I<llama.cpp>/ggml/src \
-L<llama.cpp>/build/bin -lggml-base -Wl,-rpath,<llama.cpp>/build/bin
python3 sdq-dsv4/verify_dequant_dsv4.py
```
| 張量 | 型別 | max&#124;NumPy − ggml&#124; |
|---|---|---:|
| `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)
的授權。