File size: 37,260 Bytes
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae9bb4c
78fa284
 
 
ae9bb4c
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446c618
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae9bb4c
 
 
 
 
 
 
 
 
 
 
 
 
 
446c618
 
 
 
 
 
 
 
 
f20da6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78fa284
ae9bb4c
 
 
 
 
 
 
 
446c618
 
ae9bb4c
78fa284
 
 
 
 
 
 
 
 
 
446c618
 
 
 
 
 
 
78fa284
 
 
ae9bb4c
 
78fa284
ae9bb4c
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f20da6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
39d0d4d
 
 
 
 
78fa284
 
 
39d0d4d
 
 
 
 
 
 
 
 
 
 
 
 
 
446c618
 
 
 
 
 
 
 
 
 
 
 
 
 
39d0d4d
78fa284
 
 
 
 
 
 
 
 
 
446c618
 
78fa284
 
446c618
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39d0d4d
 
 
 
 
 
 
 
f20da6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39d0d4d
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446c618
 
 
 
 
f20da6e
 
 
 
 
 
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f20da6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f20da6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae9bb4c
78fa284
ae9bb4c
78fa284
3143c45
78fa284
 
 
f20da6e
 
 
 
 
 
 
78fa284
 
 
 
 
 
 
 
 
 
 
 
 
f20da6e
78fa284
f20da6e
 
 
 
 
 
 
 
 
 
 
78fa284
 
f20da6e
 
 
78fa284
 
 
 
 
 
 
 
 
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
---
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)
的授權。