File size: 6,044 Bytes
12eb3a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# 正確性驗證

分頁路徑的每一層都要能被獨立證明是對的。這份文件記錄三層驗證、決定性證據,
以及一個在這個模型上才會出現的數值現象(§4)。

三層的分工:

| 層 | 證明什麼 | 對照組 |
| --- | --- | --- |
| 1 kernel | 單一 expert 的前向(Q8_K 量化 → 三次量化點積 → SiLU → 加權) | NumPy 純重算,照抄 ggml 的 dequantize 與 quantize 定義 |
| 2 整層 | 多 token、真實路由、權重縮放、槽位配置、預取路徑 | NumPy 用模型自己產生的 `cur / selected_experts / weights` 重算 |
| 3 端到端 | 圖層面接線、跨層一致性 | llama.cpp 原生 `mul_mat_id`(**在 8 GiB 上不可能執行**,見 §3.3) |

執行方式:

```bash
python3 -m sdq.verify --model <gguf> --bin <epq-chat> --fast
python3 tools/determinism_check.py --model <gguf> --bin <epq-chat> --runs 5 -n 48
```

---

## 1. kernel 層級(單一 expert)

四組 (layer, expert),用 `--selftest` 讓 pager 從 arena 算出結果,Python 從
**同一段 GGUF 位元組**獨立重算:

| 層 | Expert | 結果 | max abs diff |
| --- | ---: | --- | ---: |
| 0 | 3 | PASS | 3.7e-09 |
| 17 | 200 | PASS | — |
| 33 | 7 | PASS | — |
| 39 | 100 | PASS | — |

**為什麼是這四組**:這個模型有**兩種 expert 大小**。

* 20 層的 `ffn_down_exps` 是 Q6_K → 每個 expert 2,039,808 B
* 20 層的 `ffn_down_exps` 是 Q4_K → 每個 expert 1,769,472 B

L39 屬於前者,是唯一會走到「大槽位」配置路徑的測試;少了它,槽位配置器把
小槽位發給大 expert 的錯誤不會被發現。L17 / L33 屬於後者。

`tools/moe_ref.py` 對 Q4_K / Q5_K / Q6_K 三種 block 格式各自實作反量化,
其中 Q6_K 的實作另外與 ggml 的 `dequantize_row_q6_K` **逐位元組比對過**
(4096 個 block,最大差 0)。

## 2. 整層層級(真實路由輸入)

用 `SDQ_MOE_DUMP` 導出某一層的真實輸入(21 個 token,中文提示詞)與輸出:

| 層 | max abs diff | max rel | 1-LSB 翻轉上界 | 判定 |
| --- | ---: | ---: | ---: | --- |
| 0 | 1.12e-08 | 5.1e-07 | 1.73e-03 | PASS |
| 39 | 8.82e-05 | 7.7e-03 | 3.22e-02 | PASS(量化邊界,見 §4) |

預取壓力測試(預取開著 + 極小 arena 強制回收,兩種槽位配置各跑一次)4 組全 PASS。

## 3. 端到端與決定性

### 3.3 為什麼沒有端到端 A/B

原生對照組需要把整個 21.17 GB 模型常駐 RAM。在 8 GiB 上這**在定義上不可能**,
而這正是本專案存在的理由。實測未修改的 llama.cpp 在 8 GiB 上連模型都載不進去:

```
ggml_backend_buft_alloc_buffer_n_default: failed to allocate CPU_REPACK buffer of size 15099494400
llama_model_load: error loading model: unable to allocate CPU_REPACK buffer
```

所以等價性改由 kernel / 整層 / 預取壓力三層,加上下面的決定性證據支撐。

### 3.1 決定性(決定性 = 同條件重複執行,逐位元組一致)

`tools/determinism_check.py`,5 次執行 × 48 token × 248,144 vocab:

```
run0: 14.1s   run1: 14.1s   run2: 14.4s   run3: 14.3s   run4: 15.6s
✅ run1..run4 與 run0 全部逐位元組完全相同
```

分頁路徑在同一台機器、同一組條件下是**決定性**的。快取命中與否都包含在這個
證據裡:5 次執行的 arena 內容不同(命中模式隨存取順序變動),輸出仍然逐位元組相同。

### 3.2 過程中修掉的問題

1. **GGUF 標頭解析在 BOOL 陣列上錯位。** 這個模型有
   `qwen35moe.attention.recurrent_layers`(40 個元素的 **BOOL 陣列**)。參考實作
   把陣列元素型別 7 讀成 4 bytes,標頭解析從那裡起全部錯位,最後以
   `basic_string::_M_create`(讀到垃圾長度)失敗。修正:BOOL 陣列元素讀 1 byte,
   並且讓未知的元素型別**丟出例外**而不是靜默跳過 —— 靜默跳過會讓 `r.o` 停在
   值的中間,之後每一個 key 都是錯的。
   這個 key 是混合架構(SSM 層)才有的,參考的 Qwen3.6-35B-A3B 沒有。

## 4. 一個 Q8_K 量化邊界造成的 7.9e-4 差異

L39 / 某個 token 上,整層比對差 8.82e-05,超過原本 3e-3 的相對容差門檻
(該元素 |out| 只有 0.0186,所以相對誤差 7.7e-3)。逐層追查的結果:

**這不是 pager 的錯誤,是驗證工具的浮點參考不夠準。** 完整證據鏈:

1. **Q8_K 輸入量化逐位元組一致。** 取出該 token 的 activation,用 ggml 自己的
   `quantize_row_q8_K_ref` 量化,與 Python 端的 481 個 block 總和
   (`bsums`)**全部一致**。
2. **權重反量化逐位元組一致。** Q6_K 的 `dequantize_row_q6_K` 在 4096 個 block 上
   與 NumPy 實作最大差 0。
3. **差異只來自 Q8_K 的 1 個 LSB。** 把 C++ 端算出的 SiLU 中間值 `h` 讀回來餵給
   NumPy 參考:481 個非零 Q8_K code 中**剛好 1 個差 1**,輸出立刻從 7.86e-04
   掉到 **6.7e-08**。
4. **上游 `go` 的差異來自 ggml 的 float32 累加。** ggml 的 `vec_dot` 在 float32
   裡累加,本專案的 NumPy 參考用 float64。C++ 端 `gate` / `up` 與 NumPy 最多差
   9.5e-07 —— 這是 ggml 側的捨入,不是 NumPy 的。
5. **把 `go` 人為擾動 1e-7(相對)就能重現整個差異**,而且只有那一個 token 受影響,
   同一層其他 5 個 token 仍在 1e-07。

也就是說:`h` 上 1e-6 的差 → Q8_K 四捨五入跨界 1 格 → 輸出跳 7.9e-4。
**單一 LSB 翻轉的理論上界是 3.22e-02,實測 8.82e-05,差 365 倍。**

因此 `tools/moe_layer_check.py` 的判定改成兩段式:先照舊用固定容差比對,
若不通過則檢查實測差異是否落在「單一 Q8_K LSB 翻轉」的理論上界之內,
並在輸出裡明確標成 `PASS (quantization-boundary)` —— 而不是把容差默默放寬。

**這個現象是模型性質,不是這個 repo 的性質。** 參考的 Qwen3.6-35B-A3B 在同樣的
檢查下是 8.11e-06(PASS),因為它沒有碰到這種條件數的輸入。