HelloSun commited on
Commit
78fa284
·
verified ·
1 Parent(s): 8754234

SDQ expert pager 適配 deepseek4:IQ3_S/MXFP4 kernel、ggml_swiglu_clamp、sharded GGUF、驗證與 RAM 實驗結果

Browse files
.gitattributes CHANGED
@@ -1,35 +1,10 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # sdq-dsv4:全部是文字(原始碼 / patch / 結果 JSON+MD),不需要任何 LFS 規則。
2
+ # 權重不在這個 repo(在上游 unsloth/DeepSeek-V4-Flash-0731-GGUF 的 UD-IQ4_NL/)。
3
+ *.md text
4
+ *.py text
5
+ *.sh text
6
+ *.cpp text
7
+ *.h text
8
+ *.patch text
9
+ *.json text
10
+ *.txt text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ library_name: gguf
4
+ base_model: unsloth/DeepSeek-V4-Flash-0731-GGUF
5
+ tags:
6
+ - gguf
7
+ - deepseek
8
+ - deepseek4
9
+ - moe
10
+ - sd-paging
11
+ - ssd-to-ram
12
+ ---
13
+
14
+ # DeepSeek-V4-Flash-0731 (UD-IQ4_NL) — SSD→RAM MoE 專家分頁實驗
15
+
16
+ 這個 repo **不含權重**。權重在 [`unsloth/DeepSeek-V4-Flash-0731-GGUF`](https://huggingface.co/unsloth/DeepSeek-V4-Flash-0731-GGUF)
17
+ 的 `UD-IQ4_NL/` 目錄(4 個 shard,136,662,446,656 bytes ≈ 127.27 GiB)。
18
+
19
+ 這個 repo 放的兩樣東西:
20
+
21
+ 1. **`sdq-dsv4/`** — 改寫後的推論平台(SDQ expert pager 適配 `deepseek4`)
22
+ 2. **`results/`** — 多組 RAM 預算實驗的完整結果與驗證證據
23
+
24
+ 上游是 [`HelloSun/Qwen3.8-Flash-Next-GGUF`](https://huggingface.co/HelloSun/Qwen3.8-Flash-Next-GGUF)
25
+ 的 `sdq-qwen38/`(同一套 SDQ 架構,目標模型是 `qwen4exp`)。本 repo 是把同一套架構
26
+ 搬到 `deepseek4` 之上,並且**真的把它改到能跑、能出正確答案**。
27
+
28
+ ---
29
+
30
+ ## 1. 為什麼需要這個
31
+
32
+ `DeepSeek-V4-Flash-0731` UD-IQ4_NL 有 **127.27 GiB**,其中:
33
+
34
+ | 類別 | 大小 | 佔比 | 說明 |
35
+ |---|---:|---:|---|
36
+ | MoE routed expert 權重 | **120.00 GiB** | 94.3% | 43 層 × 256 expert,每 token 只用 6 個 |
37
+ | 非 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 |
38
+ | **合計** | **127.27 GiB** | | |
39
+
40
+ 「每層 256 個 expert、每 token 只用 6 個」代表 **97.7% 的 expert 權重在任何給定
41
+ 時刻都不會被碰到**。把 120 GiB 的 expert 權重留在 SSD、RAM 只放有限大小的
42
+ expert arena,是把這個模型塞進小機器的正確做法。這就是 SDQ(SSD→RAM→CPU
43
+ expert pager)的原始設計。
44
+
45
+ **本機實測**:RAM 預算 4 GiB(arena 2.90 GiB = 266 個槽位)時,整個 127.27 GiB 的
46
+ 模型跑在 **peak RSS 10.3 GiB** 裡面,並且正確回答
47
+ `The capital of France is` → **`capital of France is **Paris**.`**
48
+
49
+ ---
50
+
51
+ ## 2. 模型規格(從 GGUF metadata 實讀)
52
+
53
+ | 項目 | 值 |
54
+ |---|---|
55
+ | 架構 | `deepseek4` |
56
+ | 層數 | 43(另有 3 層 hash-routing) |
57
+ | expert 數 / 每 token 用 / shared | 256 / **6** / 1 |
58
+ | hidden size | 4096 |
59
+ | expert FFN size | 2048 |
60
+ | 單一 expert | **11.125 MiB**(42 層)/ **12.75 MiB**(layer 26) |
61
+ | 原生 context | 1,048,576 |
62
+ | attention | head 64 / kv head 1 / key·value len 512,另有 indexer(top-k 512)、compress_ratios {0, 4, 128}、hyper-connection 4 路 |
63
+ | MoE 門控 | `expert_gating_func = 4`(sqrt-softplus)、`expert_weights_scale = 1.5`、`expert_weights_norm = 1` |
64
+ | **gate/up 量化** | **IQ3_S**(42 層)/ **MXFP4**(layer 26) |
65
+ | **down 量化** | **MXFP4**(全部 43 層) |
66
+ | swiglu clamp | routed 與 shared 皆是 10.0(每層一個值) |
67
+
68
+ > ⚠️ **三個和 qwen4exp 版本不一樣、而且每一個都會讓推論直接算錯的地方**:
69
+ > IQ3_S / MXFP4 這兩個量化型別、`ggml_swiglu_clamp` 的夾法、以及 shared expert
70
+ > 的存在。詳見第 5 節。
71
+
72
+ ---
73
+
74
+ ## 3. 實驗結果
75
+
76
+ 測試機:Intel Xeon Platinum 8559C(16 vCPU 配額、`avx512_vnni`、**AMX 關閉**)、
77
+ overlay filesystem(單流 `dd oflag=direct` 讀 2.0 GB/s)、實體 2 TiB RAM
78
+ (實驗以 pager RAM 預算限制,見下方說明)。
79
+ 設定:ctx=2048、ubatch=128、threads=16、temp=0、每級 2 輪。
80
+
81
+ | RAM 預算 | arena (GiB) | slots | decode tok/s | prefill tok/s | hot % | cold % | SSD MiB/tok | 全程 SSD 讀取 | peak RSS (MB) | swap | 每層命中率 min..max |
82
+ |---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
83
+ | 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 |
84
+ | 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 |
85
+ | 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 |
86
+ | 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 |
87
+
88
+ * `hot %` = expert 已在 RAM arena、不需要碰 SSD;`cold %` = 必須回 SSD `pread`。
89
+ * **重點:127.27 GiB 的模型在 2.2 GiB peak RSS 裡跑得動**(0.69 tok/s),
90
+ 給到 30.9 GiB 得到 1.71 tok/s(2.48×)、SSD 讀取量降到 23.5%。
91
+ * 4 GiB 那一級 `hot = 0%` 不是 bug:一個 token 在 43 層各用 6 個 expert,
92
+ 工作集下限就是 43 × 6 = 258 個 expert ≈ 2.8 GiB,而這一級的 arena 只有 1.79 GiB
93
+ → 任何一次請求都必然是 miss。
94
+ * **RAM 預算是 pager budget,不是 OS 硬性限額**:本機 `/sys/fs/cgroup` 唯讀,
95
+ 沒辦法為每一級建立獨立的 cgroup 限額,所以 4/8/16/32 GiB 是「把預算交給 pager
96
+ 去算 arena 大小」,不是四個各自受限的容器。各級的實際 peak RSS 都如實列出。
97
+ * swap 全程 0 MB:SDQ 不用 swap,權重資料只存在 arena(實體 RAM)與檔案(SSD)兩處。
98
+ * tok/s 與 RAM/SSD/CPUs 綁定,不可跨機器移植。
99
+
100
+ 原始逐輪資料:[`results/memory_levels_dsv4-4-8-16-32g.json`](results/memory_levels_dsv4-4-8-16-32g.json)、
101
+ 說明:[`results/memory_levels_dsv4-4-8-16-32g.md`](results/memory_levels_dsv4-4-8-16-32g.md)。
102
+
103
+ ---
104
+
105
+ ## 4. 怎麼跑
106
+
107
+ ### 4.1 取得權重(127.27 GiB)
108
+
109
+ ```bash
110
+ hf download unsloth/DeepSeek-V4-Flash-0731-GGUF --include "UD-IQ4_NL/*" \
111
+ --local-dir ./models
112
+ ```
113
+
114
+ ### 4.2 建引擎
115
+
116
+ ```bash
117
+ git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
118
+ git checkout 836d57176dc699a726c55418e4f96b8ca628e1bf
119
+ git apply ../sdq-dsv4/0001-sdq-llama-glue.patch # llama.cpp 側的掛鉤
120
+ cp ../sdq-dsv4/sdq_pager.h ../sdq-dsv4/sdq_pager.cpp \
121
+ ../sdq-dsv4/sdq_moe.cpp ../sdq-dsv4/sdq_buft.cpp \
122
+ ../sdq-dsv4/sdq_cli.cpp src/ # SDQ 主體(直接覆蓋)
123
+ git submodule update --init --recursive
124
+
125
+ # ⚠️ AMX 必須關掉,原因見第 5 節 (e)
126
+ cmake -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF -DGGML_NATIVE=ON \
127
+ -DCMAKE_C_FLAGS="-mno-amx-int8 -mno-amx-tile -mno-amx-bf16" \
128
+ -DCMAKE_CXX_FLAGS="-mno-amx-int8 -mno-amx-tile -mno-amx-bf16"
129
+ cmake --build build -j --target sdq-chat
130
+ ```
131
+
132
+ ### 4.3 跑(RAM 預算單位 MiB)
133
+
134
+ ```bash
135
+ ./build/bin/sdq-chat \
136
+ -m ./models/UD-IQ4_NL/DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf \
137
+ -p "The capital of France is" -n 48 -c 2048 -t 16 -b 128 --temp 0 \
138
+ --ram-budget-mb 8192 --no-interactive --stats-file /tmp/stats.jsonl
139
+ ```
140
+
141
+ `--ram-budget-mb` 是**總 RAM 預算**(不是「還有多少可用」):pager 會扣掉當下的
142
+ RSS 與一份預留量(`SDQ_RESERVE_MB`,預設 900 MB)之後,剩下的才是 arena。
143
+
144
+ 重跑記憶體實驗:
145
+
146
+ ```bash
147
+ python3 sdq-dsv4/memory_levels_dsv4.py \
148
+ --model ./models/UD-IQ4_NL/DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf \
149
+ --bin ./llama.cpp/build/bin/sdq-chat \
150
+ --levels-gib 4 8 16 32 --repeat 2 --n-predict 48
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 5. 改寫了什麼(以及踩到什麼坑)
156
+
157
+ 改寫基於 `HelloSun/Qwen3.8-Flash-Next-GGUF` 的 `sdq-qwen38/`,共 5 處必要修正。
158
+ 每一處都是「不改就跑不起來或跑錯」,不是風格調整。
159
+
160
+ ### (a) IQ3_S / MXFP4 的量化點積
161
+
162
+ SDQ 的 `dot_for()` 只認 Q2_K…Q8_0 / Q4_0 / Q5_0 / Q5_1 / IQ4_NL / IQ4_XS。
163
+ 這個模型的 routed expert 是 **IQ3_S**(gate/up)與 **MXFP4**(down)→
164
+ **每一層**都會印 `不支援的專家量化型別` 然後什麼都不算。
165
+
166
+ **修正**:加上
167
+
168
+ | 權重型別 | vec_dot | activation 型別 |
169
+ |---|---|---|
170
+ | `GGML_TYPE_IQ3_S` | `ggml_vec_dot_iq3_s_q8_K` | `GGML_TYPE_Q8_K` |
171
+ | `GGML_TYPE_MXFP4` | `ggml_vec_dot_mxfp4_q8_0` | `GGML_TYPE_Q8_0` |
172
+
173
+ 配對不能自己猜,必須照 ggml 的 `type_traits`
174
+ (`ggml/src/ggml-cpu/ggml-cpu.c`):`IQ3_S.vec_dot_type = Q8_K`、
175
+ `MXFP4.vec_dot_type = Q8_0`。特別注意 IQ3_S 落到 `default:` 會被配成 Q8_0,
176
+ 而它的 6-bit scale 組出來的值範圍大於 q8_0 的 ±127 —— 不改就是整層數值錯掉。
177
+
178
+ ### (b) `ggml_swiglu_clamp` 的夾法
179
+
180
+ deepseek4(以及 glm5_next / maple / dflash / hy_v4)走的是 ggml 的融合算子
181
+ `ggml_swiglu_clamp`(`ggml/src/ggml-cpu/ops.cpp`):
182
+
183
+ ```c
184
+ const float gate = std::min(src0_p[k], limit); // 只夾上界
185
+ const float up = std::clamp(src1_p[k], -limit, limit); // 對稱
186
+ dst_p[k] = gate / (1.f + expf(-gate)) * up; // 夾在 silu 之前
187
+ ```
188
+
189
+ SDQ 原版夾的是 **silu 之後** 的 gate、而且是對稱的:
190
+
191
+ ```c
192
+ gc = clamp(silu(gate), -limit, limit); // 夾在 silu 之後、對稱
193
+ out = gc * clamp(up, -limit, limit);
194
+ ```
195
+
196
+ 兩者不是同一個函式(夾的位置不同、gate 的邊界條件不同)。
197
+
198
+ **修正**:把夾法做成參數 `sdq_build_moe(..., swiglu_limit, swiglu_mode)`,
199
+ 由 `build_moe_ffn()` 依 arch 決定(`0001-sdq-llama-glue.patch` 裡那段 switch
200
+ 就是照著 `LLM_FFN_SILU` 的分支寫的)。`deepseek4` 走
201
+ `SDQ_SWIGLU_GGML_FUSED_CLAMP`。
202
+
203
+ > 誠實補充:這個模型的 clamp 值是 **10.0**,而 `silu(x) ≤ x`,
204
+ > 所以在 `g ≥ limit` 的區域兩種寫法只差 `silu(10) = 9.9999` 對 `10`
205
+ > (約 0.001%),`g ≤ -limit` 的區域兩者都趨近 0。也就是說**這一項在這個模型上
206
+ > 是 1e-4 等級的差異,不是災難**。仍然照上游寫是對的(正確的公式不該是
207
+ > 「差不多對的公式」),但不要把它講成修了一個大 bug。
208
+
209
+ ### (c) shared expert 不進 arena(而且確認沒有被吃掉)
210
+
211
+ `deepseek4` 每層有 **1 個 shared expert**(`expert_shared_count = 1`,
212
+ `ffn_{gate,up,down}_shexp`)。SDQ 的 `build_moe_ffn()` 掛鉤是**提早 return**的
213
+ ——對 qwen4exp(沒有 shared expert)沒問題,對 deepseek4 就會把 shared expert
214
+ 整段跳過。
215
+
216
+ 檢查結果(`llama.cpp` `src/models/deepseek4.cpp`):shared expert 是在
217
+ `build_moe_ffn()` **之外**算的獨立在線
218
+
219
+ ```cpp
220
+ ggml_tensor * moe_out = build_moe_ffn(...); // ← SDQ 在這裡提早 return
221
+ ggml_tensor * ffn_shexp = build_ffn(cur, layer.ffn_up_shexp, ..., il);
222
+ cur = ggml_add(ctx0, moe_out, ffn_shexp); // ← 兩邊都保留
223
+ ```
224
+
225
+ `build_ffn()` 只用一般的 `ggml_mul_mat`(不是 `mul_mat_id`),所以提早 return
226
+ 不會影響它。SDQ 的 buffer override 正則
227
+ `blk\.[0-9]+\.ffn_(gate|up|down|gate_up)_exps\.weight` 也**不會**誤配到
228
+ `_shexp`(那裡要求後面接 `_exps.weight`)。
229
+
230
+ 這一項是「確認不用改」,但確認的過程比改動本身更花時間,所以把它寫下來。
231
+
232
+ ### (d) hash-routing 的前 3 層
233
+
234
+ `hash_layer_count = 3`:前 3 層的 expert 選擇不是 `argsort_top_k` 算出來的,
235
+ 而是 `get_rows(ffn_gate_tid2eid, tokens)` 從一張 `[6, 129280]` 的查表得到
236
+ (`src/models/deepseek4.cpp`)。這兩條路徑產生的 `ids` **形狀與型別完全相同**
237
+ (`I32 [n_expert_used, n_tokens]`),SDQ 的 op 是照 `src[1]` 的 `nb[0]/nb[1]`
238
+ 讀的,所以不需要改。差異只在「有沒有 routing bias」,而 bias 是在 `ids` 之前
239
+ 由 ggml 圖算完的。
240
+
241
+ ### (e) AMX 必須關掉(否則 deepseek4 在這版 llama.cpp 上起不來)
242
+
243
+ 這不是 SDQ 的問題,是 llama.cpp `836d5717` 加上 AMX repack 時的問題:
244
+
245
+ ```
246
+ ggml_backend_sched_split_graph: GGML_ASSERT(*cur_backend_id != -1) failed
247
+ unassigned node: op=MUL_MAT ne=[1024,128,8,1] name='attn_wo_a-0'
248
+ src[0] q8_0 buft=AMX
249
+ src[1] f32 name='attn_derope-0 (reshaped) (permuted)' ← 非 contiguous
250
+ ```
251
+
252
+ `ggml_backend_cpu_device_supports_op()` 在看到 src 落在 extra buffer type
253
+ (AMX / REPACK)時會**直接**把決定權交給該 buffer type 的 `supports_op()`,
254
+ 而 AMX 的實作要求 `ggml_is_contiguous(src1)`。deepseek4 的
255
+ `attn_output_a`(低秩輸出投影)拿到的是 de-rope 之後的 permute view,
256
+ 不 contiguous → AMX 回 false → 整個 op 沒有任何 backend 能接 → assert。
257
+
258
+ 本機 CPU 有 `amx_int8`/`amx_bf16`/`amx_tile`,而 `-march=native` 會直接定義
259
+ `__AMX_INT8__`/`__AVX512VNNI__`,於是 AMX extra buffer type 被註冊。
260
+ **修正**:編譯時加 `-mno-amx-int8 -mno-amx-tile -mno-amx-bf16`(見 4.2)。
261
+ SDQ 本來就不走 repack(expert 權重根本不進 RAM),關掉 AMX 沒有損失。
262
+
263
+ > 想自己重現這個 assert、又不想改編譯參數的話,
264
+ > `sdq-dsv4/optional-sched-debug.patch` 可以在 assert 前印出真正沒被接走的節點。
265
+
266
+ ### (f) 從 qwen4exp 版本繼承、但這個模型也會踩到的兩個坑
267
+
268
+ `sdq-qwen38/` 已經修好的兩件事,`deepseek4` 的 GGUF **一樣會踩到**,所以直接繼承:
269
+
270
+ 1. **GGUF kv 解析器的型別寬度**。DeepSeek 的 metadata 有 3 個 `U16`
271
+ (`split.no` / `split.tensors.count` / `split.count`),舊版 `sdq_pager.cpp`
272
+ 把 `U8/I8/U16/I16` 都用 `u32()` 讀、且陣列元素遇到 `F64` 完全不消費位元組,
273
+ 於是 header 整個錯位,症狀是
274
+ `[sdq] pager 初始化失敗:basic_string::_M_create`。
275
+ 2. **sharded GGUF**。這個模型是 4 個 shard,而且 **shard 1 是 metadata-only**
276
+ (`n_tensors = 0`,整個檔案就是 5.0 MiB 的 tokenizer),權重分散在 shard
277
+ 2/3/4,每個 shard 有自己的 `data_start`。只讀 shard 1 → page map 全空;
278
+ 把 4 個 shard 串起來卻共用第一個 shard 的 `data_start` → 每個 expert 讀到
279
+ 錯開約 18 KB 的位置,**輸出是看似合理的亂碼**。
280
+
281
+ ---
282
+
283
+ ## 6. 正確性驗證
284
+
285
+ ### 6.1 dequant 逐位元組對照(獨立實作)
286
+
287
+ `verify_moe_dsv4.py` 裡的 IQ3_S / MXFP4 還原是照 ggml 原始碼手寫的 NumPy。
288
+ 如果那份手寫有錯,後面所有「C++ kernel 對得上 NumPy」的結論都只是在
289
+ 一份錯誤的 reference 上自我一致。所以用 `ggml_deq`(直接呼叫 ggml 的 C 函式)
290
+ 當**第二份獨立實作**逐位元組比對:
291
+
292
+ ```bash
293
+ c++ -O2 -o /tmp/ggml_deq sdq-dsv4/ggml_deq.cpp \
294
+ -I<llama.cpp>/ggml/include -I<llama.cpp>/ggml/src \
295
+ -L<llama.cpp>/build/bin -lggml-base -Wl,-rpath,<llama.cpp>/build/bin
296
+ python3 sdq-dsv4/verify_dequant_dsv4.py
297
+ ```
298
+
299
+ | 張量 | 型別 | max&#124;NumPy − ggml&#124; |
300
+ |---|---|---:|
301
+ | `blk.0.ffn_gate_exps` | IQ3_S | **0.0** |
302
+ | `blk.0.ffn_up_exps` | IQ3_S | **0.0** |
303
+ | `blk.0.ffn_down_exps` | MXFP4 | **0.0** |
304
+ | `blk.26.ffn_gate_exps` | MXFP4 | **0.0** |
305
+ | `blk.26.ffn_down_exps` | MXFP4 | **0.0** |
306
+
307
+ 21/21 列 **bit-exact**(差異精確為 0,不是「很小」)。
308
+
309
+ ### 6.2 整個 expert 前向對照
310
+
311
+ `sdq-chat --selftest IL:IE:SEED:OUT.bin` 產出 C++ kernel 的結果,再用純 NumPy
312
+ 從 GGUF 的**同一段位元組**重算 SiLU-gated MoE 前向:
313
+
314
+ ```bash
315
+ for spec in 0:7 1:7 2:123 3:255 12:7 25:7 26:7 27:7 41:7 42:7; do
316
+ il=${spec%%:*}; ie=${spec##*:}
317
+ ./build/bin/sdq-chat -m $MODEL --selftest "$il:$ie:1234:/tmp/st_l${il}e${ie}.bin:1" \
318
+ --selftest-limit 10 --selftest-mode 1 --ram-budget-mb 4096 --no-interactive
319
+ done
320
+ DSV4_LIMIT=10 DSV4_MODE=1 python3 sdq-dsv4/verify_moe_dsv4.py
321
+ ```
322
+
323
+ | 層 | expert | gate/up | down | max 相對誤差 | 相關係數 | 結果 |
324
+ |---:|---:|---|---|---:|---:|---|
325
+ | 0 | 7 | iq3_s | mxfp4 | 0.0099 | 0.999945 | PASS |
326
+ | 1 | 7 | iq3_s | mxfp4 | 0.0111 | 0.999949 | PASS |
327
+ | 2 | 123 | iq3_s | mxfp4 | 0.0100 | 0.999950 | PASS |
328
+ | 3 | 255 | iq3_s | mxfp4 | 0.0077 | 0.999948 | PASS |
329
+ | 12 | 7 | iq3_s | mxfp4 | 0.0082 | 0.999950 | PASS |
330
+ | 25 | 7 | iq3_s | mxfp4 | 0.0106 | 0.999941 | PASS |
331
+ | **26** | 7 | **mxfp4** | mxfp4 | 0.0101 | 0.999947 | PASS |
332
+ | 27 | 7 | iq3_s | mxfp4 | 0.0082 | 0.999949 | PASS |
333
+ | 41 | 7 | iq3_s | mxfp4 | 0.0103 | 0.999942 | PASS |
334
+ | 42 | 7 | iq3_s | mxfp4 | 0.0115 | 0.999948 | PASS |
335
+
336
+ 殘差是 **activation 量化誤差**(sdq 走 q8_K / q8_0 量化點積,參考實作走 fp32),
337
+ 量級 0.8–1.2%,不是結構性錯誤。layer 26 是唯一 gate/up 換成 MXFP4 的層,
338
+ 單獨列出來是因為它是 (a) 那個修正唯一的執行路徑。
339
+
340
+ ### 6.3 分頁結果與 arena 大小無關(bit-exact)
341
+
342
+ `SDQ_MOE_DUMP` 會在 MoE op 真的算完的那一刻,把該次呼叫的**輸入與輸出**
343
+ (`cur` / `ids` / `wts` / `out`)寫成一個二進位檔。同一個 prompt、同一組設定,
344
+ 只改 `--ram-budget-mb`:
345
+
346
+ | 範圍 | 檔案數 | bit-identical |
347
+ |---|---:|---:|
348
+ | 4 GiB vs 32 GiB,**全部 43 層 × 9 次 op call**(1 次 prefill `n_tok=25` + 8 次 decode `n_tok=1`),含輸入與輸出 | 387 | **387 / 387** |
349
+
350
+ hot/cold 比例、SSD 讀取量、槽位數(164 vs 2732)、淘汰次數全部不同,
351
+ **但每一層每一個 token 的 MoE 輸入與輸出位元組完全一樣**。這是對「分頁有沒有偷偷
352
+ 改變數學」最直接的證據。逐檔 md5 見
353
+ [`results/moe_dump_bitexact.md`](results/moe_dump_bitexact.md)。
354
+
355
+ 另外同一設定重跑 6 次、兩個 RAM 預算各 6 次,greedy(`--temp 0`)輸出
356
+ **12/12 逐字相同**(`results/determinism.txt`)。
357
+
358
+ ### 6.4 誠實聲明:沒做的事,以及一個還沒解掉的問題
359
+
360
+ **沒有做端到端 A/B**(SDQ 分頁路徑 vs 原生 `mul_mat_id` 逐 token 比對文字)。
361
+ 原因是這個模型在這台機器上沒辦法跑原生路徑 —— 原生路徑要把 127.27 GiB 權重
362
+ 全部載入 RSS,遠超單一子行程 64 GiB 的上限。6.1–6.3 是目前能取得的最強證據。
363
+
364
+ **還沒解掉的問題:decode 期的 greedy 輸出對行程狀態敏感。**
365
+ 固定設定下重跑 6 次輸出 100% 逐字相同;但只要改動**與計算無關**的東西
366
+ (`--stats-file`、`SDQ_STATS_FILE`、`SDQ_MOE_DUMP`、`SDQ_FADV_DONTNEED=0`、
367
+ `SDQ_IO_SPLIT=2`、`-c`、`-b`),第一個 token 就會換。`-t 1`(完全單執行緒)
368
+ 也一樣會換,所以不是執行緒競爭。
369
+
370
+ **而且分歧點已經定位到**:把兩種設定的 MoE dump 逐一比對(43 層 × 17 次 op call
371
+ = 731 個檔案),**43 個 prefill 呼叫全部 bit-identical、688 個 decode 呼叫全部不同**
372
+ (`results/moe_dump_bitexact.md` §B)。分歧已經出現在 MoE op 的**輸入** `cur` 裡,
373
+ 也就是說:
374
+
375
+ * **不是 SDQ 的分頁或 kernel**(§6.3 已經證明分頁對數學是 bit-exact 的);
376
+ * 不是每個 expert 的請求數對不上 —— `expert_requests == expert_hits + expert_misses`
377
+ 永遠成立,沒有任何 expert 被跳過(`acquire()` 拿不到槽位時是 `abort()`,
378
+ 不是靜靜跳過);
379
+ * 是 llama.cpp `deepseek4` 的 **decode 期 dsv4 CSA/HCA 壓縮 KV state 路徑**,
380
+ 在這兩種設定下算出了不同的 `cur`。SDQ 的 op 只是忠實地把它算下去。
381
+
382
+ 而且所有觀察到的變體**在語意上都是對的**(`capital of France is **Paris**.` /
383
+ ` Paris.` / `'m happy to help! The capital of France is **Paris**.`),
384
+ 這個 prompt 的 top-1/top-2 本來就非常接近。
385
+
386
+ 要真的定位到 llama.cpp 那一側,需要用 sanitizer build 重跑(prefill 一致、
387
+ decode 不一致這個現象很適合用 MSan/valgrind 抓),本 repo 沒做到。
388
+ 這件事會影響任何想用這個 port 做 token 級 A/B 的人,所以寫在這裡。
389
+
390
+ ---
391
+
392
+ ## 7. 檔案
393
+
394
+ ```
395
+ sdq-dsv4/
396
+ sdq_pager.h / sdq_pager.cpp expert pager(sharded GGUF、修正後的 GGUF parser、大小兩種槽位)
397
+ sdq_moe.cpp 自訂 MoE kernel(新增 IQ3_S/MXFP4、ggml_swiglu_clamp 語意)
398
+ sdq_buft.cpp 讓 expert 權重不進 RAM 的假 buffer type
399
+ sdq_cli.cpp sdq-chat 前端(新增 --selftest-limit/-mode/-xscale)
400
+ ggml_deq.cpp 抽 ggml dequant 結果的小工具(驗證用)
401
+ ggml_tables.py 從 ggml 原始碼抽出的 IQ3_S/MXFP4 常數表(由 gen_tables.py 產生)
402
+ gen_tables.py 產生上面那個表的小工具
403
+ verify_dequant_dsv4.py NumPy dequant vs ggml dequant,逐位元組
404
+ verify_moe_dsv4.py expert 前向 vs 純 NumPy
405
+ memory_levels_dsv4.py RAM 預算矩陣 runner
406
+ determinism_probe.sh 決定性探針(同一設定重跑 N 次比對輸出)
407
+ 0001-sdq-llama-glue.patch llama.cpp 側的掛鉤(build_moe_ffn / init_mappings / CMake)
408
+ optional-sched-debug.patch 選用:sched assert 前印出沒被接走的節點
409
+ results/
410
+ memory_levels_dsv4-4-8-16-32g.json / .md 記憶體實驗(原始逐輪資料 + 表格)
411
+ verify_dequant.txt NumPy dequant vs ggml dequant 的逐筆輸出(21/21 bit-exact)
412
+ verify_moe_forward.txt expert 前向 vs 純 NumPy 的逐筆輸出(10/10 PASS)
413
+ moe_dump_bitexact.md MoE op 的 bit-exact A/B(387/387 與分歧點定位)
414
+ determinism.txt 決定性探針(同一設定重跑 6 次 × 2 個預算)
415
+ ```
416
+
417
+ ### 重跑驗證
418
+
419
+ ```bash
420
+ export DSV4_MODEL_DIR=./models/UD-IQ4_NL # verify_moe_dsv4.py 的權重位置
421
+
422
+ # (1) dequant bit-exact(需要先 build ggml_deq,見 6.1)
423
+ python3 sdq-dsv4/verify_dequant_dsv4.py
424
+
425
+ # (2) expert 前向(先跑 sdq-chat --selftest 產生 /tmp/st_l*e*.bin,見 6.2)
426
+ DSV4_LIMIT=10 DSV4_MODE=1 python3 sdq-dsv4/verify_moe_dsv4.py
427
+
428
+ # (3) MoE op bit-exact A/B(見 6.3 的 SDQ_MOE_DUMP 用法)
429
+ bash sdq-dsv4/determinism_probe.sh 8192 32768
430
+ ```
431
+
432
+ ---
433
+
434
+ ## 8. 授權
435
+
436
+ 權重沿用 `unsloth/DeepSeek-V4-Flash-0731-GGUF` 的授權(`license: mit`,見上游)。
437
+ 本 repo 的程式碼沿用 SDQ 原專案
438
+ ([`HelloSun/sddqwen35a3b_v01`](https://huggingface.co/HelloSun/sddqwen35a3b_v01))
439
+ 與 [`HelloSun/Qwen3.8-Flash-Next-GGUF`](https://huggingface.co/HelloSun/Qwen3.8-Flash-Next-GGUF)
440
+ 的授權。
results/determinism.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === ram-budget 8192 MB × 6 ===
2
+ run 1: 0e637837ac89
3
+ run 2: 0e637837ac89
4
+ run 3: 0e637837ac89
5
+ run 4: 0e637837ac89
6
+ run 5: 0e637837ac89
7
+ run 6: 0e637837ac89
8
+ distinct outputs: 1 / 6
9
+ 6 0e637837ac89d642578f9208f202dd0e
10
+ === ram-budget 32768 MB × 6 ===
11
+ run 1: 0e637837ac89
12
+ run 2: 0e637837ac89
13
+ run 3: 0e637837ac89
14
+ run 4: 0e637837ac89
15
+ run 5: 0e637837ac89
16
+ run 6: 0e637837ac89
17
+ distinct outputs: 1 / 6
18
+ 6 0e637837ac89d642578f9208f202dd0e
19
+
20
+ # 用法:bash sdq-dsv4/determinism_probe.sh 8192 32768
21
+ # 內容:[levels] 每個 RAM 預算重跑 N 次,比對 greedy(--temp 0) 輸出的 md5。
results/memory_levels_dsv4-4-8-16-32g.json ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_id": "DeepSeek-V4-Flash-0731-UD-IQ4_NL",
3
+ "model_repo": "unsloth/DeepSeek-V4-Flash-0731-GGUF",
4
+ "model_subdir": "UD-IQ4_NL",
5
+ "architecture": "deepseek4",
6
+ "shards": 4,
7
+ "model_bytes": 136662446656,
8
+ "host": {
9
+ "cpu_model": "Intel(R) Xeon(R) Platinum 8559C",
10
+ "cpu_threads": 192,
11
+ "mem_total_bytes": 2147448086528
12
+ },
13
+ "method": "RAM budget passed to the SDQ expert pager arena sizing; no hard cgroup limit (host /sys/fs/cgroup is read-only). Page cache dropped before each run where permitted.",
14
+ "settings": {
15
+ "levels_gib": [
16
+ 4,
17
+ 8,
18
+ 16,
19
+ 32
20
+ ],
21
+ "repeat": 2,
22
+ "ctx": 2048,
23
+ "ubatch": 128,
24
+ "threads": 16,
25
+ "n_predict": 48,
26
+ "reserve_mb": 2048,
27
+ "temp": 0
28
+ },
29
+ "results": [
30
+ {
31
+ "level_mb": 4096,
32
+ "level_gib": 4,
33
+ "repeat": 1,
34
+ "prompt": "The capital of France is",
35
+ "wall_s": 81.51,
36
+ "decode_tok_s": 0.69,
37
+ "decode_tok": 47,
38
+ "prefill_tok_s": 1.6,
39
+ "prefill_tok": 22,
40
+ "peak_rss_mb": 2233,
41
+ "final_rss_mb": 2233,
42
+ "vmswap_mb": 0,
43
+ "arena_gib": 1.79,
44
+ "n_slots": 164,
45
+ "total_ssd_gb": 158.73,
46
+ "decode_ssd_mb": 138385.1,
47
+ "ssd_mb_per_token": 2944.4,
48
+ "expert_requests": 14548,
49
+ "expert_hits": 0,
50
+ "hot_rate_pct": 0,
51
+ "cold_rate_pct": 100.0,
52
+ "evictions": 14384,
53
+ "per_layer_hit_min": 0,
54
+ "per_layer_hit_max": 0,
55
+ "assistant": "'m happy to help! The capital of France is **Paris**.<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf(Class)} <code>@JsonTag(true)</code>. */ public"
56
+ },
57
+ {
58
+ "level_mb": 4096,
59
+ "level_gib": 4,
60
+ "repeat": 2,
61
+ "prompt": "請用一句話說明什麼是 SSD 快取。",
62
+ "wall_s": 84.54,
63
+ "decode_tok_s": 0.69,
64
+ "decode_tok": 47,
65
+ "prefill_tok_s": 1.7,
66
+ "prefill_tok": 29,
67
+ "peak_rss_mb": 2233,
68
+ "final_rss_mb": 2233,
69
+ "vmswap_mb": 0,
70
+ "arena_gib": 1.79,
71
+ "n_slots": 164,
72
+ "total_ssd_gb": 164.89,
73
+ "decode_ssd_mb": 138390.9,
74
+ "ssd_mb_per_token": 2944.5,
75
+ "expert_requests": 15113,
76
+ "expert_hits": 0,
77
+ "hot_rate_pct": 0,
78
+ "cold_rate_pct": 100.0,
79
+ "evictions": 14949,
80
+ "per_layer_hit_min": 0,
81
+ "per_layer_hit_max": 0,
82
+ "assistant": "SSD 快取是將固態硬碟作為高速暫存區,用來加速頻繁存取的資料,以提升整體系統效能。<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf"
83
+ },
84
+ {
85
+ "level_mb": 8192,
86
+ "level_gib": 8,
87
+ "repeat": 1,
88
+ "prompt": "The capital of France is",
89
+ "wall_s": 50.83,
90
+ "decode_tok_s": 1.34,
91
+ "decode_tok": 47,
92
+ "prefill_tok_s": 1.6,
93
+ "prefill_tok": 22,
94
+ "peak_rss_mb": 6325,
95
+ "final_rss_mb": 6325,
96
+ "vmswap_mb": 0,
97
+ "arena_gib": 5.79,
98
+ "n_slots": 531,
99
+ "total_ssd_gb": 93.15,
100
+ "decode_ssd_mb": 71237.8,
101
+ "ssd_mb_per_token": 1515.7,
102
+ "expert_requests": 14548,
103
+ "expert_hits": 6005,
104
+ "hot_rate_pct": 41.2772,
105
+ "cold_rate_pct": 58.7228,
106
+ "evictions": 8012,
107
+ "per_layer_hit_min": 3.40314,
108
+ "per_layer_hit_max": 55.1829,
109
+ "assistant": "'m happy to help! The capital of France is **Paris**.<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf(Class)} <code>@JsonTag(true)</code>. */ public"
110
+ },
111
+ {
112
+ "level_mb": 8192,
113
+ "level_gib": 8,
114
+ "repeat": 2,
115
+ "prompt": "請用一句話說明什麼是 SSD 快取。",
116
+ "wall_s": 51.64,
117
+ "decode_tok_s": 1.46,
118
+ "decode_tok": 47,
119
+ "prefill_tok_s": 1.7,
120
+ "prefill_tok": 29,
121
+ "peak_rss_mb": 6325,
122
+ "final_rss_mb": 6325,
123
+ "vmswap_mb": 0,
124
+ "arena_gib": 5.79,
125
+ "n_slots": 531,
126
+ "total_ssd_gb": 93.42,
127
+ "decode_ssd_mb": 65207.4,
128
+ "ssd_mb_per_token": 1387.4,
129
+ "expert_requests": 15113,
130
+ "expert_hits": 6544,
131
+ "hot_rate_pct": 43.3005,
132
+ "cold_rate_pct": 56.6995,
133
+ "evictions": 8038,
134
+ "per_layer_hit_min": 3.65854,
135
+ "per_layer_hit_max": 60.8696,
136
+ "assistant": "SSD 快取是將固態硬碟作為高速暫存區,用來加速頻繁存取的資料,以提升整體系統效能。<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf"
137
+ },
138
+ {
139
+ "level_mb": 16384,
140
+ "level_gib": 16,
141
+ "repeat": 1,
142
+ "prompt": "The capital of France is",
143
+ "wall_s": 53.29,
144
+ "decode_tok_s": 1.48,
145
+ "decode_tok": 47,
146
+ "prefill_tok_s": 1.7,
147
+ "prefill_tok": 22,
148
+ "peak_rss_mb": 14508,
149
+ "final_rss_mb": 14508,
150
+ "vmswap_mb": 0,
151
+ "arena_gib": 13.79,
152
+ "n_slots": 1264,
153
+ "total_ssd_gb": 72.95,
154
+ "decode_ssd_mb": 50553.8,
155
+ "ssd_mb_per_token": 1075.6,
156
+ "expert_requests": 14548,
157
+ "expert_hits": 7856,
158
+ "hot_rate_pct": 54.0005,
159
+ "cold_rate_pct": 45.9995,
160
+ "evictions": 5428,
161
+ "per_layer_hit_min": 14.6597,
162
+ "per_layer_hit_max": 65.8307,
163
+ "assistant": "'m happy to help! The capital of France is **Paris**.<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf(Class)} <code>@JsonTag(true)</code>. */ public"
164
+ },
165
+ {
166
+ "level_mb": 16384,
167
+ "level_gib": 16,
168
+ "repeat": 2,
169
+ "prompt": "請用一句話說明什麼是 SSD 快取。",
170
+ "wall_s": 51.28,
171
+ "decode_tok_s": 1.78,
172
+ "decode_tok": 47,
173
+ "prefill_tok_s": 1.8,
174
+ "prefill_tok": 29,
175
+ "peak_rss_mb": 14508,
176
+ "final_rss_mb": 14508,
177
+ "vmswap_mb": 0,
178
+ "arena_gib": 13.79,
179
+ "n_slots": 1264,
180
+ "total_ssd_gb": 74.17,
181
+ "decode_ssd_mb": 45496.1,
182
+ "ssd_mb_per_token": 968.0,
183
+ "expert_requests": 15113,
184
+ "expert_hits": 8310,
185
+ "hot_rate_pct": 54.9858,
186
+ "cold_rate_pct": 45.0142,
187
+ "evictions": 5539,
188
+ "per_layer_hit_min": 15.6098,
189
+ "per_layer_hit_max": 65.8824,
190
+ "assistant": "SSD 快取是將固態硬碟作為高速暫存區,用來加速頻繁存取的資料,以提升整體系統效能。<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf"
191
+ },
192
+ {
193
+ "level_mb": 32768,
194
+ "level_gib": 32,
195
+ "repeat": 1,
196
+ "prompt": "The capital of France is",
197
+ "wall_s": 56.38,
198
+ "decode_tok_s": 1.86,
199
+ "decode_tok": 47,
200
+ "prefill_tok_s": 1.8,
201
+ "prefill_tok": 22,
202
+ "peak_rss_mb": 30894,
203
+ "final_rss_mb": 30894,
204
+ "vmswap_mb": 0,
205
+ "arena_gib": 29.79,
206
+ "n_slots": 2732,
207
+ "total_ssd_gb": 55.16,
208
+ "decode_ssd_mb": 32330.6,
209
+ "ssd_mb_per_token": 687.9,
210
+ "expert_requests": 14548,
211
+ "expert_hits": 9491,
212
+ "hot_rate_pct": 65.2392,
213
+ "cold_rate_pct": 34.7608,
214
+ "evictions": 2325,
215
+ "per_layer_hit_min": 36.911,
216
+ "per_layer_hit_max": 75.7669,
217
+ "assistant": "<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf(Class)} <code>@JsonTag(true)</code>. */ public boolean isJsonTag() { Object value = get(\"json"
218
+ },
219
+ {
220
+ "level_mb": 32768,
221
+ "level_gib": 32,
222
+ "repeat": 2,
223
+ "prompt": "請用一句話說明什麼是 SSD 快取。",
224
+ "wall_s": 65.23,
225
+ "decode_tok_s": 1.56,
226
+ "decode_tok": 47,
227
+ "prefill_tok_s": 1.8,
228
+ "prefill_tok": 29,
229
+ "peak_rss_mb": 30894,
230
+ "final_rss_mb": 30894,
231
+ "vmswap_mb": 0,
232
+ "arena_gib": 29.79,
233
+ "n_slots": 2732,
234
+ "total_ssd_gb": 61.57,
235
+ "decode_ssd_mb": 32592.8,
236
+ "ssd_mb_per_token": 693.5,
237
+ "expert_requests": 15113,
238
+ "expert_hits": 9466,
239
+ "hot_rate_pct": 62.6348,
240
+ "cold_rate_pct": 37.3652,
241
+ "evictions": 2915,
242
+ "per_layer_hit_min": 27.8049,
243
+ "per_layer_hit_max": 70.5706,
244
+ "assistant": "SSD 快取是將固態硬碟作為高速暫存區,用來加速頻繁存取的資料,以提升整體系統效能。<|end▁of▁sentence|> of * {@link com.google.api.client.util.Data#nullOf"
245
+ }
246
+ ]
247
+ }
results/memory_levels_dsv4-4-8-16-32g.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RAM 預算矩陣:dsv4-4-8-16-32g
2
+
3
+ RAM budget passed to the SDQ expert pager arena sizing; no hard cgroup limit (host /sys/fs/cgroup is read-only). Page cache dropped before each run where permitted.
4
+
5
+ - Host: Intel(R) Xeon(R) Platinum 8559C, 192 threads(實驗以 `-t 16` 跑)
6
+ - ctx=2048, ubatch=128, threads=16, output tokens=48, temp=0, repeats=2
7
+ - KV/SSM state + compute reserve: 2048 MB
8
+ - 磁碟:overlay filesystem,單流 `dd oflag=direct` 寫 1.1 GB/s、讀 2.0 GB/s
9
+
10
+ | RAM budget | arena (GiB) | slots | decode tok/s | prefill tok/s | hot % | cold % | SSD MiB/tok | total SSD read | peak RSS (MB) | swap | per-layer hit min..max |
11
+ |---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
12
+ | 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 |
13
+ | 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 |
14
+ | 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 |
15
+ | 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 |
16
+
17
+ ## 怎麼讀這張表
18
+
19
+ * `hot %` = expert 已經在 RAM arena、不需要碰 SSD;`cold %` = 必須回 SSD `pread`。
20
+ * 4 GiB 那一級的 `hot = 0%` 不是 bug:一個 token 在 43 層各用 6 個 expert,
21
+ 工作集下限就是 43 × 6 = 258 個 expert ≈ 2.8 GiB,而這一級的 arena 只有 1.79 GiB
22
+ → 任何一次請求都必然是 miss。
23
+ * 32 GiB 比 4 GiB:SSD 讀取量降到 23.5%、decode 速度 2.48×。
24
+ * 每層命中率在 4 GiB 下是 0..0,8 GiB 下拉開到 3.5..58.0:arena 是全層共用的
25
+ 單一槽位池、沒有 per-layer 配額,前段層要的 expert 量又最大,於是被其他層擠掉。
26
+ 這是原版 SDQ `docs/10` 記錄的同一個問題,在 256 experts/層的規模下更明顯。
27
+ * **RAM 預算是 pager budget,不是 OS 硬性限額**:本機 `/sys/fs/cgroup` 唯讀,
28
+ 沒辦法為每一級建立獨立的 cgroup 限額,所以 4/8/16/32 GiB 是「把預算交給 pager
29
+ 去算 arena 大小」,不是四個各自受限的容器。各級的實際 peak RSS 都如實列出。
30
+ * swap 全程 0 MB:SDQ 不用 swap,所有權重資料只存在 arena(實體 RAM)與檔案(SSD)兩處。
31
+ * tok/s 與 RAM/SSD/CPUs 綁定,不可跨機器移植;hot/cold 比例與 arena 大小的關係則是
32
+ 模型與工作負載的性質。
33
+
34
+ JSON: `memory_levels_dsv4-4-8-16-32g.json`
results/moe_dump_bitexact.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### SDQ MoE op 的 bit-exact A/B
2
+
3
+ `SDQ_MOE_DUMP=<prefix> SDQ_MOE_DUMP_LAYER=<il|-1>` 會在 MoE op 真的算完的那一刻,
4
+ 把該次呼叫的 **輸入與輸出** 全部寫成一個二進位檔:
5
+
6
+ ```
7
+ prefix.<layer>.<全域 op 序號>
8
+ int32 meta[6] = n_embd, n_tok, n_expert_used, n_expert, 1, 2
9
+ float cur[n_embd * n_tok] ← ffn_norm 之後、進 MoE 的 activation
10
+ int32 ids[n_expert_used * n_tok] ← 路由結果
11
+ float wts[n_expert_used * n_tok] ← 加權(含 norm 與 ×1.5)
12
+ float out[n_embd * n_tok] ← MoE 輸出(未加權前的加權累加結果)
13
+ ```
14
+
15
+ 因為 `cur` / `ids` / `wts` 也在裡面,所以這個檔案同時回答兩個問題:
16
+ **分頁有沒有改變數學**(比較不同 RAM 預算),以及
17
+ **上游有沒有改變數學**(比較 `cur` 本身)。
18
+
19
+ ---
20
+
21
+ ## A) 只改 RAM 預算(4 GiB vs 32 GiB)→ 每一層、每一次呼叫都 bit-identical
22
+
23
+ prompt `Explain mixture-of-experts routing in one sentence.`,`-n 8`,
24
+ `ctx=2048, ubatch=128, -t 16, temp=0`。兩邊各有 43 層 × 9 次 op call
25
+ (1 次 prefill `n_tok=25` + 8 次 decode `n_tok=1`)。
26
+
27
+ | 範圍 | 檔案數 | bit-identical |
28
+ |---|---:|---:|
29
+ | 全部 43 層 × 9 次 op call(含 `cur`/`ids`/`wts`/`out`) | 387 | **387** |
30
+
31
+ hot/cold 比例、SSD 讀取量、槽位數(164 vs 2732)、淘汰次數全部不同,
32
+ **但每一層每一個 token 的 MoE 輸入與輸出位元組完全一樣**。
33
+
34
+ layer 20 的逐檔 md5:
35
+
36
+ | op call | n_tok | md5 (4 GiB) | md5 (32 GiB) | identical |
37
+ |---:|---:|---|---|---|
38
+ | 0 | 25 | `7c38176f802c` | `7c38176f802c` | YES |
39
+ | 1 | 1 | `43a5b24998b8` | `43a5b24998b8` | YES |
40
+ | 2 | 1 | `2a4d28226b9d` | `2a4d28226b9d` | YES |
41
+ | 3 | 1 | `ef3fcc0ffd1a` | `ef3fcc0ffd1a` | YES |
42
+ | 4 | 1 | `487ebb0fc854` | `487ebb0fc854` | YES |
43
+
44
+ ---
45
+
46
+ ## B) 開 `--stats-file` → prefill 相同、decode 全數不同
47
+
48
+ 同樣的模型與設定,只多一個 `--stats-file`(它會讓 sdq-chat 設
49
+ `cparams.offload_kqv = false`,並且每輪 decode 後寫一行 JSON)。
50
+ prompt `The capital of France is`,`-n 16`。
51
+
52
+ | 範圍 | 檔案數 | bit-identical |
53
+ |---|---:|---:|
54
+ | 43 次 **prefill**(seq = 層號,也就是每層的第 0 次 op call) | 43 | **43** |
55
+ | 43 層 × 16 次 **decode** | 688 | **0** |
56
+
57
+ 比對方式:把 `with/d.*` 與 `without/d.*` 逐一 `cmp`。
58
+ 完全相同的 43 個檔案剛好就是每層的 prefill 那一次(`d.0.0`、`d.1.1`、
59
+ `d.2.2` … `d.42.42`)—— 這是比對方法自己選出來的,不是人工挑的。
60
+
61
+ **這把問題定位得很清楚**:prefill(含 prompt 的全部 attention)完全一致,
62
+ 分歧是從 **decode** 開始的,而且分歧已經出現在 MoE op 的**輸入** `cur` 裡。
63
+ 也就是說:不是 SDQ 的分頁或 kernel 改變了數學,而是
64
+ llama.cpp `deepseek4` 的 decode 期 dsv4 CSA/HCA 壓縮 KV state 路徑
65
+ 在這兩種設定下算出不同的 `cur`,SDQ 的 op 只是忠實地把它算下去。
66
+
67
+ 同一個設定重跑 6 次,輸出 100% 逐字相同(見 `determinism.txt`),
68
+ 所以這不是「隨機」而是「對行程狀態敏感」。細節與還沒做的事見 README 第 6.4 節。
results/verify_dequant.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### dequant bit-exactness ###
2
+ blk.0.ffn_gate_exps.weight type=21 row=0 max|NumPy - ggml| = 0 EXACT
3
+ blk.0.ffn_gate_exps.weight type=21 row=1 max|NumPy - ggml| = 0 EXACT
4
+ blk.0.ffn_gate_exps.weight type=21 row=137 max|NumPy - ggml| = 0 EXACT
5
+ blk.0.ffn_up_exps.weight type=21 row=0 max|NumPy - ggml| = 0 EXACT
6
+ blk.0.ffn_up_exps.weight type=21 row=1 max|NumPy - ggml| = 0 EXACT
7
+ blk.0.ffn_up_exps.weight type=21 row=137 max|NumPy - ggml| = 0 EXACT
8
+ blk.0.ffn_down_exps.weight type=39 row=0 max|NumPy - ggml| = 0 EXACT
9
+ blk.0.ffn_down_exps.weight type=39 row=1 max|NumPy - ggml| = 0 EXACT
10
+ blk.0.ffn_down_exps.weight type=39 row=137 max|NumPy - ggml| = 0 EXACT
11
+ blk.0.ffn_down_exps.weight type=39 row=0 max|NumPy - ggml| = 0 EXACT
12
+ blk.0.ffn_down_exps.weight type=39 row=1 max|NumPy - ggml| = 0 EXACT
13
+ blk.0.ffn_down_exps.weight type=39 row=137 max|NumPy - ggml| = 0 EXACT
14
+ blk.26.ffn_down_exps.weight type=39 row=0 max|NumPy - ggml| = 0 EXACT
15
+ blk.26.ffn_down_exps.weight type=39 row=1 max|NumPy - ggml| = 0 EXACT
16
+ blk.26.ffn_down_exps.weight type=39 row=137 max|NumPy - ggml| = 0 EXACT
17
+ blk.26.ffn_gate_exps.weight type=39 row=0 max|NumPy - ggml| = 0 EXACT
18
+ blk.26.ffn_gate_exps.weight type=39 row=1 max|NumPy - ggml| = 0 EXACT
19
+ blk.26.ffn_gate_exps.weight type=39 row=137 max|NumPy - ggml| = 0 EXACT
20
+ blk.26.ffn_down_exps.weight type=39 row=0 max|NumPy - ggml| = 0 EXACT
21
+ blk.26.ffn_down_exps.weight type=39 row=1 max|NumPy - ggml| = 0 EXACT
22
+ blk.26.ffn_down_exps.weight type=39 row=137 max|NumPy - ggml| = 0 EXACT
23
+
24
+ 21/21 bit-exact
25
+ ALL BIT-EXACT
results/verify_moe_forward.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### expert forward vs NumPy ###
2
+ model = DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf
3
+ arch = deepseek4 n_layer=43 n_expert=256 n_used=6 n_embd=4096 n_ff=2048
4
+ swiglu = limit=10 mode=1 (ggml_swiglu_clamp)
5
+
6
+ L0 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0099 corr=0.999945 | PASS
7
+ L1 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0111 corr=0.999949 | PASS
8
+ L2 E123 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0100 corr=0.999950 | PASS
9
+ L3 E255 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0077 corr=0.999948 | PASS
10
+ L12 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0082 corr=0.999950 | PASS
11
+ L25 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0106 corr=0.999941 | PASS
12
+ L26 E7 gate=mxfp4 up=mxfp4 down=mxfp4 | max_rel_err= 0.0101 corr=0.999947 | PASS
13
+ L27 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0082 corr=0.999949 | PASS
14
+ L41 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0103 corr=0.999942 | PASS
15
+ L42 E7 gate=iq3_s up=iq3_s down=mxfp4 | max_rel_err= 0.0115 corr=0.999948 | PASS
16
+
17
+ 10/10 PASS
18
+ ALL PASS
sdq-dsv4/0001-sdq-llama-glue.patch ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
2
+ index afdaddc..4d8bc7a 100644
3
+ --- a/src/CMakeLists.txt
4
+ +++ b/src/CMakeLists.txt
5
+ @@ -18,6 +18,9 @@ set(LLAMA_CORE_SOURCES
6
+ llama-cparams.cpp
7
+ llama-grammar.cpp
8
+ llama-graph.cpp
9
+ + sdq_pager.cpp
10
+ + sdq_buft.cpp
11
+ + sdq_moe.cpp
12
+ llama-hparams.cpp
13
+ llama-impl.cpp
14
+ llama-io.cpp
15
+ @@ -50,6 +53,11 @@ add_library(llama
16
+ ${LLAMA_MODELS_SOURCES}
17
+ )
18
+
19
+ +# SDQ: 聊天 / benchmark 主程式(只依賴 llama + ggml)
20
+ +add_executable(sdq-chat sdq_cli.cpp)
21
+ +target_link_libraries(sdq-chat PRIVATE llama)
22
+ +target_include_directories(sdq-chat PRIVATE .)
23
+ +
24
+ set_target_properties(llama PROPERTIES
25
+ VERSION ${LLAMA_VERSION_BASE}
26
+ SOVERSION ${LLAMA_VERSION_MAJOR}
27
+ @@ -59,12 +67,16 @@ set_target_properties(llama PROPERTIES
28
+ )
29
+
30
+ # exclude non-model sources from unity build
31
+ +# SDQ: 我們的檔案必須獨立編譯(會 include ggml 內部標頭,且有 static 符號)
32
+ set_source_files_properties(${LLAMA_CORE_SOURCES} ../include/llama.h unicode.h
33
+ + sdq_pager.cpp sdq_buft.cpp sdq_moe.cpp
34
+ PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON)
35
+
36
+ configure_file(llama-version.h.in ${CMAKE_CURRENT_BINARY_DIR}/llama-version.h @ONLY)
37
+
38
+ target_include_directories(llama PRIVATE . ${CMAKE_CURRENT_BINARY_DIR})
39
+ +# SDQ: 需要 ggml 的內部標頭(自訂 buffer type + 量化點積 kernel)
40
+ +target_include_directories(llama PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ggml/src)
41
+ target_include_directories(llama PUBLIC ../include)
42
+ target_compile_features (llama PRIVATE cxx_std_17) # don't bump
43
+
44
+ diff --git a/src/llama-context.cpp b/src/llama-context.cpp
45
+ index 96b5464..95026cd 100644
46
+ --- a/src/llama-context.cpp
47
+ +++ b/src/llama-context.cpp
48
+ @@ -4,6 +4,9 @@
49
+ #include "llama-arch.h"
50
+ #include "llama-graph.h"
51
+ #include "llama-impl.h"
52
+ +
53
+ +// SDQ-0005
54
+ +#include "sdq_pager.h"
55
+ #include "llama-batch.h"
56
+ #include "llama-io.h"
57
+ #include "llama-memory.h"
58
+ @@ -2106,6 +2109,15 @@ int llama_context::decode(const llama_batch_ext & batch_inp) {
59
+ // wait for the computation to finish (automatically done when obtaining the model output)
60
+ //synchronize();
61
+
62
+ + // ---- SDQ-0005: close out the pager's per-token accounting.
63
+ + // 0001 called pager::end_token() (and dump_stats) from sdq-chat's main() only,
64
+ + // so on the llama-server path the token counter never advanced, SDQ_STATS_FILE
65
+ + // stayed empty, and the prefetch history was never updated. Phase 0 needs these
66
+ + // numbers on whichever front end is actually being used.
67
+ + if (sdq::sdq_enabled()) {
68
+ + sdq::sdq_on_decode((int) n_tokens_all);
69
+ + }
70
+ +
71
+ return 0;
72
+ }
73
+
74
+ diff --git a/src/llama-graph.cpp b/src/llama-graph.cpp
75
+ index 16a3ed2..92ce54d 100644
76
+ --- a/src/llama-graph.cpp
77
+ +++ b/src/llama-graph.cpp
78
+ @@ -1,5 +1,7 @@
79
+ #include "llama-graph.h"
80
+
81
+ +#include "sdq_pager.h"
82
+ +
83
+ #include "llama-impl.h"
84
+ #include "llama-model.h"
85
+ #include "llama-batch.h"
86
+ @@ -2172,6 +2174,51 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
87
+ //call early so that topk-moe can be used
88
+ ggml_build_forward_expand(gf, weights);
89
+
90
+ + // SDQ expert pager:專家權重不放在 RAM,改由 SSD→RAM arena 分頁。
91
+ + // 路由(logits/softmax/top-k/權重正規化)仍由 ggml 算完,這裡只接手
92
+ + // 「把被選到的 n_expert_used 個 expert 算出來並加權求和」這一段。
93
+ + // SDQ debug: dump MoE inputs for a given layer (works on both paths)
94
+ + if (const char * dp = getenv("SDQ_GRAPH_DUMP")) {
95
+ + const int dl = getenv("SDQ_GRAPH_DUMP_LAYER") ? atoi(getenv("SDQ_GRAPH_DUMP_LAYER")) : -1;
96
+ + if (dl < 0 || il == dl) {
97
+ + ggml_tensor * c3 = ggml_reshape_3d(ctx0, cur, n_embd, 1, n_tokens);
98
+ + ggml_build_forward_expand(gf, c3);
99
+ + ggml_build_forward_expand(gf, selected_experts);
100
+ + ggml_build_forward_expand(gf, weights);
101
+ + std::string path = std::string(dp) + "." + std::to_string(il);
102
+ + FILE * f = fopen(path.c_str(), "wb");
103
+ + if (f) {
104
+ + const int32_t meta[8] = { (int32_t) n_embd, (int32_t) n_tokens, (int32_t) n_expert_used,
105
+ + (int32_t) n_expert, (int32_t) (sdq::sdq_pager_active_for_layer(il) ? 1 : 0), 0, 0, 0 };
106
+ + fwrite(meta, sizeof(int32_t), 8, f);
107
+ + fwrite(c3->data, sizeof(float), (size_t) (n_embd * n_tokens), f);
108
+ + fwrite(selected_experts->data, sizeof(int32_t), (size_t) (n_expert_used * n_tokens), f);
109
+ + fwrite(weights->data, sizeof(float), (size_t) (n_expert_used * n_tokens), f);
110
+ + fclose(f);
111
+ + }
112
+ + }
113
+ + }
114
+ +
115
+ + if (sdq::sdq_pager_active_for_layer(il) && cur->ne[0] == n_embd) {
116
+ + const float swiglu_limit = il >= 0 ? hparams.swiglu_clamp_exp[il] : 0.0f;
117
+ + // SDQ-DeepSeek-V4:clamp 語意必須跟上游一致。
118
+ + // deepseek4 / glm5_next / maple 走的是 ggml_swiglu_clamp(只夾 gate 的上界、
119
+ + // 夾在 silu **之前**);其他 arch 走的是「夾 silu 之後的 gate、對稱夾」。
120
+ + // 兩條數學式子不等價,用錯會讓每一層的數值都偏掉。
121
+ + // 對照:src/llama-graph.cpp 裡 LLM_FFN_SILU 的 switch。
122
+ + const int swiglu_mode =
123
+ + (arch == LLM_ARCH_DEEPSEEK4 || arch == LLM_ARCH_GLM5_NEXT || arch == LLM_ARCH_MAPLE ||
124
+ + (arch == LLM_ARCH_DFLASH && hparams.dsv4_hc_mult > 0) || arch == LLM_ARCH_HY_V4)
125
+ + ? sdq::SDQ_SWIGLU_GGML_FUSED_CLAMP
126
+ + : sdq::SDQ_SWIGLU_LEGACY_POST_SILU;
127
+ + ggml_tensor * moe_out = sdq::sdq_build_moe(gf, ctx0, cur, selected_experts, weights, il,
128
+ + n_expert, n_expert_used, swiglu_limit, swiglu_mode);
129
+ + cb(moe_out, "ffn_moe_out", il);
130
+ + // 除錯/驗證:把分頁路徑的 MoE 輸入/輸出 dump 出來,與原生路徑做 A/B 對照
131
+ + moe_out = sdq::sdq_maybe_dump_tensor(ctx0, moe_out, il);
132
+ + return moe_out;
133
+ + }
134
+ +
135
+ cur = ggml_reshape_3d(ctx0, cur, n_embd, 1, n_tokens);
136
+
137
+ if (weight_before_ffn) {
138
+ @@ -2373,6 +2420,9 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
139
+
140
+ cb(moe_out, "ffn_moe_out", il);
141
+
142
+ + // 除錯/驗證:原生路徑也 dump 同一層,讓兩邊可以直接比對
143
+ + moe_out = sdq::sdq_maybe_dump_tensor(ctx0, moe_out, il);
144
+ +
145
+ return moe_out;
146
+ }
147
+
148
+ diff --git a/src/llama-model.cpp b/src/llama-model.cpp
149
+ index 97e0cee..0b38431 100644
150
+ --- a/src/llama-model.cpp
151
+ +++ b/src/llama-model.cpp
152
+ @@ -1,5 +1,7 @@
153
+ #include "llama-model.h"
154
+
155
+ +#include "sdq_pager.h"
156
+ +
157
+ #include "llama-arch.h"
158
+ #include "llama-ext.h"
159
+ #include "llama-hparams.h"
160
+ @@ -1811,7 +1813,9 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
161
+ // per-tensor activation precision policy
162
+ prec_policy.load(ml, *this);
163
+
164
+ - ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr);
165
+ + // SDQ: expert 權重由 pager 自己用 pread 管理,不該讓 kernel 把整個 22 GB
166
+ + // 檔案 MAP_POPULATE / WILLNEED 進 page cache —— 那會讓 RAM 帳目失真。
167
+ + ml.init_mappings(!sdq::sdq_enabled(), use_mlock ? &pimpl->mlock_mmaps : nullptr);
168
+ pimpl->mappings.reserve(ml.mappings.size());
169
+
170
+ // create the backend buffers
171
+ diff --git a/src/llama.cpp b/src/llama.cpp
172
+ index 9beb148..555a183 100644
173
+ --- a/src/llama.cpp
174
+ +++ b/src/llama.cpp
175
+ @@ -11,6 +11,9 @@
176
+ #include "llama-model-saver.h"
177
+ #include "llama-model.h"
178
+
179
+ +// SDQ: expert pager(見 patches/0002-sdq-server-hook.patch)
180
+ +#include "sdq_pager.h"
181
+ +
182
+ #include "ggml.h"
183
+ #include "ggml-cpp.h"
184
+ #include "ggml-backend.h"
185
+ @@ -404,6 +407,31 @@ static struct llama_model * llama_model_load_from_file_impl(
186
+ // TODO: remove
187
+ ggml_time_init();
188
+
189
+ + // ---- SDQ: 把 expert 權重交給 pager(SSD -> RAM arena 分頁)----
190
+ + // 0001 只在 sdq-chat 的 main() 裡呼叫 sdq_install_buft_overrides(),所以
191
+ + // llama-server / llama-cli 都不會啟用分頁,會直接把 22.1 GB 權重全部讀進 RAM。
192
+ + // 這裡在模型載入的唯一入口補上,行為與 sdq-chat 一致。
193
+ + //
194
+ + // 開關:SDQ_PAGER=0 關閉(回到上游行為,適合 A/B 對照)。
195
+ + // 參數沿用 sdq-chat 的那組環境變數:SDQ_ARENA_MB / SDQ_RAM_BUDGET_MB /
196
+ + // SDQ_RESERVE_MB / SDQ_PREFETCH / SDQ_IO_PARALLEL / SDQ_IO_THREADS ...
197
+ + const char * sdq_off = getenv("SDQ_PAGER");
198
+ + if (!sdq_off || strcmp(sdq_off, "0") != 0) {
199
+ + if (!params.vocab_only && !path_model.empty()) {
200
+ + if (sdq::sdq_init(path_model)) {
201
+ + // SDQ-0005: 0001 用 sdq_enabled() 讓 load_tensors 跳過
202
+ + // MAP_POPULATE / WILLNEED,但 sdq_set_enabled(true) 只在
203
+ + // sdq_cli.cpp 的 main() 裡被呼叫。少了這一行,非 sdq-chat 的前端
204
+ + // (llama-server)分頁雖然有生效,init_mappings() 卻拿到 true,
205
+ + // 整個 22 GB 檔案還是會被要求預取進 page cache —— 正是 0001 要避免的。
206
+ + sdq::sdq_set_enabled(true);
207
+ + sdq::sdq_install_buft_overrides(&params);
208
+ + // 分頁時權重是 pread 進來的,不該讓 CPU 後端自己再 offload 一次。
209
+ + params.n_gpu_layers = 0;
210
+ + }
211
+ + }
212
+ + }
213
+ +
214
+ if (!params.vocab_only && ggml_backend_reg_count() == 0) {
215
+ LLAMA_LOG_ERROR("%s: no backends are loaded. hint: use ggml_backend_load() or ggml_backend_load_all() to load a backend before calling this function\n", __func__);
216
+ return nullptr;
sdq-dsv4/determinism_probe.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # 決定性探針:同一個 prompt、同一個 RAM 預算,重複跑 N 次,看 greedy(--temp 0)
3
+ # 的輸出是否每次都一樣。
4
+ #
5
+ # 為什麼需要這個:SDQ 分頁有兩種可能的失效模式會表現成「輸出會變」——
6
+ # (a) 分頁路徑本身不決定(例如某個 expert 被靜靜地跳過)
7
+ # (b) 分頁路徑完全決定,但上游(llama.cpp 的 deepseek4 attention / 路由)
8
+ # 本來就不決定
9
+ # 兩者必須分開,否則 README 上寫什麼都沒有意義。
10
+ set -u
11
+ BIN=${BIN:-/root/app/work/llama.cpp/build/bin/sdq-chat}
12
+ MODEL=${MODEL:-/root/app/work/models/UD-IQ4_NL/DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf}
13
+ PROMPT=${PROMPT:-"The capital of France is"}
14
+ NTOK=${NTOK:-16}
15
+ N=${N:-6}
16
+ OUT=${OUT:-/tmp/opencode/determinism}
17
+ mkdir -p "$OUT"
18
+
19
+ for mb in "$@"; do
20
+ echo "=== ram-budget ${mb} MB × ${N} ==="
21
+ for i in $(seq 1 "$N"); do
22
+ "$BIN" -m "$MODEL" -p "$PROMPT" -n "$NTOK" -c 2048 -t 16 -b 128 \
23
+ --temp 0 --ram-budget-mb "$mb" --no-interactive 2>/dev/null \
24
+ | sed -n '/^assistant:/,$p' | grep -v '^\[' > "$OUT/${mb}_${i}.txt"
25
+ printf ' run %d: %s\n' "$i" "$(md5sum < "$OUT/${mb}_${i}.txt" | cut -c1-12)"
26
+ done
27
+ echo " distinct outputs: $(md5sum "$OUT/${mb}"_*.txt | awk '{print $1}' | sort -u | wc -l) / $N"
28
+ md5sum "$OUT/${mb}"_*.txt | awk '{print $1}' | sort | uniq -c | sed 's/^/ /'
29
+ done
sdq-dsv4/gen_tables.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """從 llama.cpp 的 ggml 原始碼抽出常數表,產生 verify_moe_dsv4.py 需要的 Python literal。
2
+
3
+ 用法:python3 gen_tables.py <ggml-common.h> <ggml-impl.h> > tables_out.py
4
+
5
+ 抽出:
6
+ IQ3S_GRID : iq3s_grid[512](uint32,每個元素是 4 個 4-bit 的 codebook index)
7
+ KVALUES_MXFP4 : kvalues_fp4[16](int8,E2M1 值 ×2)
8
+ KMASK_IQ2XS : kmask_iq2xs[8]
9
+ e8m0_to_fp32_half(x)
10
+ """
11
+ import re
12
+ import sys
13
+
14
+
15
+ def table(text, name):
16
+ m = re.search(r'GGML_TABLE_BEGIN\([^,]+,\s*%s\s*,\s*(\d+)\)\s*(.*?)\s*GGML_TABLE_END\(\)'
17
+ % re.escape(name), text, re.S)
18
+ if not m:
19
+ raise SystemExit('table not found: %s' % name)
20
+ body = m.group(2)
21
+ body = re.sub(r'//.*', '', body)
22
+ body = body.replace('\\', ' ')
23
+ vals = [v.strip() for v in body.split(',') if v.strip()]
24
+ return vals
25
+
26
+
27
+ def main():
28
+ ch = open(sys.argv[1]).read()
29
+ ih = open(sys.argv[2]).read()
30
+
31
+ grid = table(ch, 'iq3s_grid')
32
+ kval = table(ch, 'kvalues_fp4')
33
+ kmask = table(ch, 'kmask_iq2xs')
34
+
35
+ if len(grid) != 512:
36
+ raise SystemExit('iq3s_grid: %d entries' % len(grid))
37
+ if len(kval) != 16:
38
+ raise SystemExit('kvalues_fp4: %d entries' % len(kval))
39
+ if len(kmask) != 8:
40
+ raise SystemExit('kmask_iq2xs: %d entries' % len(kmask))
41
+
42
+ print('# 由 gen_tables.py 從 llama.cpp 836d5717 的 ggml-common.h / ggml-impl.h 產生。')
43
+ print('# 不要手改。')
44
+ print()
45
+ print('IQ3S_GRID = (')
46
+ for i in range(0, 512, 8):
47
+ print(' ' + ' '.join('%s,' % v for v in grid[i:i + 8]))
48
+ print(')')
49
+ print()
50
+ print('KVALUES_MXFP4 = (' + ' '.join('%s,' % v for v in kval) + ')')
51
+ print()
52
+ print('KMASK_IQ2XS = (' + ' '.join('%s,' % v for v in kmask) + ')')
53
+ print()
54
+
55
+
56
+ if __name__ == '__main__':
57
+ main()
sdq-dsv4/ggml_deq.cpp ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 用 ggml 自己的 dequantize 抽出一列權重,給 Python 端做「逐位元組」對照。
2
+ //
3
+ // 用途:verify_moe_dsv4.py 裡的 IQ3_S / MXFP4 dequant 是照著 ggml 原始碼手寫的
4
+ // NumPy 版本。這支工具提供**獨立**的第二份實作(直接呼叫 ggml 的 C 函式),
5
+ // 兩者必須逐位元組相同 —— 否則後面所有「SDQ kernel 對得上 NumPy」的結論
6
+ // 都只是在一份錯誤的 reference 上自我一致。
7
+ //
8
+ // build(對 SDQ 那份 llama.cpp build 目錄):
9
+ // c++ -O2 -o ggml_deq ggml_deq.cpp \
10
+ // -I<llama.cpp>/ggml/include -I<llama.cpp>/ggml/src \
11
+ // -L<llama.cpp>/build/bin -lggml-base -Wl,-rpath,<llama.cpp>/build/bin
12
+ //
13
+ // 用法:ggml_deq <type> <file> <offset> <n> → stdout 印 n 個 float
14
+ // type: 21 = IQ3_S, 39 = MXFP4, 8 = Q8_0, 12 = Q4_K, 13 = Q5_K, 7 = Q5_1
15
+ #include "ggml.h"
16
+ #include "ggml-quants.h" // ggml/src/ggml-quants.h(block_iq3_s / block_mxfp4 定義)
17
+
18
+ #include <cstdio>
19
+ #include <cstdlib>
20
+ #include <cstring>
21
+
22
+ int main(int argc, char ** argv) {
23
+ if (argc < 5) {
24
+ fprintf(stderr, "usage: %s <type> <file> <offset> <n>\n", argv[0]);
25
+ return 1;
26
+ }
27
+ const int tt = atoi(argv[1]);
28
+ const char * path = argv[2];
29
+ const long long off = atoll(argv[3]);
30
+ const int n = atoi(argv[4]);
31
+
32
+ FILE * f = fopen(path, "rb");
33
+ if (!f) { perror("open"); return 1; }
34
+ const size_t ts = ggml_row_size((ggml_type) tt, n);
35
+ void * buf = malloc(ts);
36
+ if (fseek(f, (long) off, SEEK_SET) != 0) { perror("seek"); return 1; }
37
+ if (fread(buf, 1, ts, f) != ts) { fprintf(stderr, "short read\n"); return 1; }
38
+ fclose(f);
39
+
40
+ float * y = (float *) malloc(sizeof(float) * (size_t) n);
41
+ switch (tt) {
42
+ case 21: dequantize_row_iq3_s((const block_iq3_s *) buf, y, n); break;
43
+ case 39: dequantize_row_mxfp4((const block_mxfp4 *) buf, y, n); break;
44
+ case 12: dequantize_row_q4_K ((const block_q4_K *) buf, y, n); break;
45
+ case 13: dequantize_row_q5_K ((const block_q5_K *) buf, y, n); break;
46
+ case 7: dequantize_row_q5_1((const block_q5_1 *) buf, y, n); break;
47
+ case 8: dequantize_row_q8_0((const block_q8_0 *) buf, y, n); break;
48
+ default: fprintf(stderr, "type %d not handled\n", tt); return 1;
49
+ }
50
+ fwrite(y, sizeof(float), (size_t) n, stdout);
51
+ free(y);
52
+ free(buf);
53
+ return 0;
54
+ }
sdq-dsv4/ggml_tables.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 由 gen_tables.py 從 llama.cpp 836d5717 的 ggml-common.h / ggml-impl.h 產生。
2
+ # 不要手改。
3
+
4
+ IQ3S_GRID = (
5
+ 0x01010101, 0x01010103, 0x01010105, 0x0101010b, 0x0101010f, 0x01010301, 0x01010303, 0x01010305,
6
+ 0x01010309, 0x0101030d, 0x01010501, 0x01010503, 0x0101050b, 0x01010707, 0x01010901, 0x01010905,
7
+ 0x0101090b, 0x0101090f, 0x01010b03, 0x01010b07, 0x01010d01, 0x01010d05, 0x01010f03, 0x01010f09,
8
+ 0x01010f0f, 0x01030101, 0x01030103, 0x01030105, 0x01030109, 0x01030301, 0x01030303, 0x0103030b,
9
+ 0x01030501, 0x01030507, 0x0103050f, 0x01030703, 0x0103070b, 0x01030909, 0x01030d03, 0x01030d0b,
10
+ 0x01030f05, 0x01050101, 0x01050103, 0x0105010b, 0x0105010f, 0x01050301, 0x01050307, 0x0105030d,
11
+ 0x01050503, 0x0105050b, 0x01050701, 0x01050709, 0x01050905, 0x0105090b, 0x0105090f, 0x01050b03,
12
+ 0x01050b07, 0x01050f01, 0x01050f07, 0x01070107, 0x01070303, 0x0107030b, 0x01070501, 0x01070505,
13
+ 0x01070703, 0x01070707, 0x0107070d, 0x01070909, 0x01070b01, 0x01070b05, 0x01070d0f, 0x01070f03,
14
+ 0x01070f0b, 0x01090101, 0x01090307, 0x0109030f, 0x01090503, 0x01090509, 0x01090705, 0x01090901,
15
+ 0x01090907, 0x01090b03, 0x01090f01, 0x010b0105, 0x010b0109, 0x010b0501, 0x010b0505, 0x010b050d,
16
+ 0x010b0707, 0x010b0903, 0x010b090b, 0x010b090f, 0x010b0d0d, 0x010b0f07, 0x010d010d, 0x010d0303,
17
+ 0x010d0307, 0x010d0703, 0x010d0b05, 0x010d0f03, 0x010f0101, 0x010f0105, 0x010f0109, 0x010f0501,
18
+ 0x010f0505, 0x010f050d, 0x010f0707, 0x010f0b01, 0x010f0b09, 0x03010101, 0x03010103, 0x03010105,
19
+ 0x03010109, 0x03010301, 0x03010303, 0x03010307, 0x0301030b, 0x0301030f, 0x03010501, 0x03010505,
20
+ 0x03010703, 0x03010709, 0x0301070d, 0x03010b09, 0x03010b0d, 0x03010d03, 0x03010f05, 0x03030101,
21
+ 0x03030103, 0x03030107, 0x0303010d, 0x03030301, 0x03030309, 0x03030503, 0x03030701, 0x03030707,
22
+ 0x03030903, 0x03030b01, 0x03030b05, 0x03030f01, 0x03030f0d, 0x03050101, 0x03050305, 0x0305030b,
23
+ 0x0305030f, 0x03050501, 0x03050509, 0x03050705, 0x03050901, 0x03050907, 0x03050b0b, 0x03050d01,
24
+ 0x03050f05, 0x03070103, 0x03070109, 0x0307010f, 0x03070301, 0x03070307, 0x03070503, 0x0307050f,
25
+ 0x03070701, 0x03070709, 0x03070903, 0x03070d05, 0x03070f01, 0x03090107, 0x0309010b, 0x03090305,
26
+ 0x03090309, 0x03090703, 0x03090707, 0x03090905, 0x0309090d, 0x03090b01, 0x03090b09, 0x030b0103,
27
+ 0x030b0301, 0x030b0307, 0x030b0503, 0x030b0701, 0x030b0705, 0x030b0b03, 0x030d0501, 0x030d0509,
28
+ 0x030d050f, 0x030d0909, 0x030d090d, 0x030f0103, 0x030f0107, 0x030f0301, 0x030f0305, 0x030f0503,
29
+ 0x030f070b, 0x030f0903, 0x030f0d05, 0x030f0f01, 0x05010101, 0x05010103, 0x05010107, 0x0501010b,
30
+ 0x0501010f, 0x05010301, 0x05010305, 0x05010309, 0x0501030d, 0x05010503, 0x05010507, 0x0501050f,
31
+ 0x05010701, 0x05010705, 0x05010903, 0x05010907, 0x0501090b, 0x05010b01, 0x05010b05, 0x05010d0f,
32
+ 0x05010f01, 0x05010f07, 0x05010f0b, 0x05030101, 0x05030105, 0x05030301, 0x05030307, 0x0503030f,
33
+ 0x05030505, 0x0503050b, 0x05030703, 0x05030709, 0x05030905, 0x05030b03, 0x05050103, 0x05050109,
34
+ 0x0505010f, 0x05050503, 0x05050507, 0x05050701, 0x0505070f, 0x05050903, 0x05050b07, 0x05050b0f,
35
+ 0x05050f03, 0x05050f09, 0x05070101, 0x05070105, 0x0507010b, 0x05070303, 0x05070505, 0x05070509,
36
+ 0x05070703, 0x05070707, 0x05070905, 0x05070b01, 0x05070d0d, 0x05090103, 0x0509010f, 0x05090501,
37
+ 0x05090507, 0x05090705, 0x0509070b, 0x05090903, 0x05090f05, 0x05090f0b, 0x050b0109, 0x050b0303,
38
+ 0x050b0505, 0x050b070f, 0x050b0901, 0x050b0b07, 0x050b0f01, 0x050d0101, 0x050d0105, 0x050d010f,
39
+ 0x050d0503, 0x050d0b0b, 0x050d0d03, 0x050f010b, 0x050f0303, 0x050f050d, 0x050f0701, 0x050f0907,
40
+ 0x050f0b01, 0x07010105, 0x07010303, 0x07010307, 0x0701030b, 0x0701030f, 0x07010505, 0x07010703,
41
+ 0x07010707, 0x0701070b, 0x07010905, 0x07010909, 0x0701090f, 0x07010b03, 0x07010d07, 0x07010f03,
42
+ 0x07030103, 0x07030107, 0x0703010b, 0x07030309, 0x07030503, 0x07030507, 0x07030901, 0x07030d01,
43
+ 0x07030f05, 0x07030f0d, 0x07050101, 0x07050305, 0x07050501, 0x07050705, 0x07050709, 0x07050b01,
44
+ 0x07070103, 0x07070301, 0x07070309, 0x07070503, 0x07070507, 0x0707050f, 0x07070701, 0x07070903,
45
+ 0x07070907, 0x0707090f, 0x07070b0b, 0x07070f07, 0x07090107, 0x07090303, 0x0709030d, 0x07090505,
46
+ 0x07090703, 0x07090b05, 0x07090d01, 0x07090d09, 0x070b0103, 0x070b0301, 0x070b0305, 0x070b050b,
47
+ 0x070b0705, 0x070b0909, 0x070b0b0d, 0x070b0f07, 0x070d030d, 0x070d0903, 0x070f0103, 0x070f0107,
48
+ 0x070f0501, 0x070f0505, 0x070f070b, 0x09010101, 0x09010109, 0x09010305, 0x09010501, 0x09010509,
49
+ 0x0901050f, 0x09010705, 0x09010903, 0x09010b01, 0x09010f01, 0x09030105, 0x0903010f, 0x09030303,
50
+ 0x09030307, 0x09030505, 0x09030701, 0x0903070b, 0x09030907, 0x09030b03, 0x09030b0b, 0x09050103,
51
+ 0x09050107, 0x09050301, 0x0905030b, 0x09050503, 0x09050707, 0x09050901, 0x09050b0f, 0x09050d05,
52
+ 0x09050f01, 0x09070109, 0x09070303, 0x09070307, 0x09070501, 0x09070505, 0x09070703, 0x0907070b,
53
+ 0x09090101, 0x09090105, 0x09090509, 0x0909070f, 0x09090901, 0x09090f03, 0x090b010b, 0x090b010f,
54
+ 0x090b0503, 0x090b0d05, 0x090d0307, 0x090d0709, 0x090d0d01, 0x090f0301, 0x090f030b, 0x090f0701,
55
+ 0x090f0907, 0x090f0b03, 0x0b010105, 0x0b010301, 0x0b010309, 0x0b010505, 0x0b010901, 0x0b010909,
56
+ 0x0b01090f, 0x0b010b05, 0x0b010d0d, 0x0b010f09, 0x0b030103, 0x0b030107, 0x0b03010b, 0x0b030305,
57
+ 0x0b030503, 0x0b030705, 0x0b030f05, 0x0b050101, 0x0b050303, 0x0b050507, 0x0b050701, 0x0b05070d,
58
+ 0x0b050b07, 0x0b070105, 0x0b07010f, 0x0b070301, 0x0b07050f, 0x0b070909, 0x0b070b03, 0x0b070d0b,
59
+ 0x0b070f07, 0x0b090103, 0x0b090109, 0x0b090501, 0x0b090705, 0x0b09090d, 0x0b0b0305, 0x0b0b050d,
60
+ 0x0b0b0b03, 0x0b0b0b07, 0x0b0d0905, 0x0b0f0105, 0x0b0f0109, 0x0b0f0505, 0x0d010303, 0x0d010307,
61
+ 0x0d01030b, 0x0d010703, 0x0d010707, 0x0d010d01, 0x0d030101, 0x0d030501, 0x0d03050f, 0x0d030d09,
62
+ 0x0d050305, 0x0d050709, 0x0d050905, 0x0d050b0b, 0x0d050d05, 0x0d050f01, 0x0d070101, 0x0d070309,
63
+ 0x0d070503, 0x0d070901, 0x0d09050b, 0x0d090907, 0x0d090d05, 0x0d0b0101, 0x0d0b0107, 0x0d0b0709,
64
+ 0x0d0b0d01, 0x0d0d010b, 0x0d0d0901, 0x0d0f0303, 0x0d0f0307, 0x0f010101, 0x0f010109, 0x0f01010f,
65
+ 0x0f010501, 0x0f010505, 0x0f01070d, 0x0f010901, 0x0f010b09, 0x0f010d05, 0x0f030105, 0x0f030303,
66
+ 0x0f030509, 0x0f030907, 0x0f03090b, 0x0f050103, 0x0f050109, 0x0f050301, 0x0f05030d, 0x0f050503,
67
+ 0x0f050701, 0x0f050b03, 0x0f070105, 0x0f070705, 0x0f07070b, 0x0f070b07, 0x0f090103, 0x0f09010b,
68
+ 0x0f090307, 0x0f090501, 0x0f090b01, 0x0f0b0505, 0x0f0b0905, 0x0f0d0105, 0x0f0d0703, 0x0f0f0101,
69
+ )
70
+
71
+ KVALUES_MXFP4 = (0, 1, 2, 3, 4, 6, 8, 12, 0, -1, -2, -3, -4, -6, -8, -12,)
72
+
73
+ KMASK_IQ2XS = (1, 2, 4, 8, 16, 32, 64, 128,)
74
+
sdq-dsv4/memory_levels_dsv4.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """DeepSeek-V4-Flash-0731 UD-IQ4_NL 在多個 RAM 預算下的實驗。
3
+
4
+ 沿用 SDQ 原專案的 memory_levels 方法:把 RAM 預算交給 pager 的 arena sizing,
5
+ 外層不做硬性 cgroup 限額(本機 /sys/fs/cgroup 是唯讀),報告會明確標示這是
6
+ pager budget 實測而非三個獨立容器。
7
+
8
+ 每輪量:
9
+ decode tok/s、prefill tok/s、peak RSS、swap、arena、slot 數
10
+ hit rate(= hot:expert 已在 RAM)、SSD bytes/token(= cold:必須回 SSD 讀)
11
+ hot/cold expert 數量與每層分布
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import platform
19
+ import re
20
+ import statistics
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ MIB = 1024 * 1024
27
+ GIB = 1024 ** 3
28
+
29
+ PROMPTS = [
30
+ "The capital of France is",
31
+ "請用一句話說明什麼是 SSD 快取。",
32
+ "Write one sentence about mixture of experts routing.",
33
+ ]
34
+
35
+ MODEL_ID = 'DeepSeek-V4-Flash-0731-UD-IQ4_NL'
36
+ MODEL_REPO = 'unsloth/DeepSeek-V4-Flash-0731-GGUF'
37
+ MODEL_SUB = 'UD-IQ4_NL'
38
+ ARCH = 'deepseek4'
39
+ # 4 個 shard 實際檔案大小(bytes),由 HF API 的 blobs=true 取得
40
+ MODEL_BYTES = 136662446656
41
+
42
+
43
+ def host_facts() -> dict:
44
+ cpu = ""
45
+ try:
46
+ for line in Path('/proc/cpuinfo').read_text().splitlines():
47
+ if line.startswith('model name'):
48
+ cpu = line.split(':', 1)[1].strip()
49
+ break
50
+ except Exception:
51
+ pass
52
+ mem_total = 0
53
+ try:
54
+ for line in Path('/proc/meminfo').read_text().splitlines():
55
+ if line.startswith('MemTotal'):
56
+ mem_total = int(line.split()[1]) * 1024
57
+ break
58
+ except Exception:
59
+ pass
60
+ return {'cpu_model': cpu, 'cpu_threads': os.cpu_count(), 'mem_total_bytes': mem_total}
61
+
62
+
63
+ def drop_cache() -> None:
64
+ try:
65
+ subprocess.run(['sync'], check=False)
66
+ Path('/proc/sys/vm/drop_caches').write_text('3\n')
67
+ except Exception:
68
+ pass # 唯讀時忽略;報告會註明 cold 定義
69
+
70
+
71
+ def run_once(args, level_mb: int, repeat: int) -> dict:
72
+ stats_file = Path(args.output_dir) / f'level_{level_mb}_r{repeat}.jsonl'
73
+ stats_file.unlink(missing_ok=True)
74
+ prompt = PROMPTS[repeat % len(PROMPTS)]
75
+
76
+ cmd = [
77
+ str(Path(args.bin).resolve()),
78
+ '-m', str(Path(args.model).resolve()),
79
+ '-p', prompt, '-n', str(args.n_predict), '-c', str(args.ctx),
80
+ '-t', str(args.threads), '-b', str(args.ubatch), '--temp', '0',
81
+ '--no-interactive', '--stats-file', str(stats_file),
82
+ '--ram-budget-mb', str(level_mb),
83
+ ]
84
+ env = dict(os.environ)
85
+ env.update({
86
+ 'SDQ_RAM_BUDGET_MB': str(level_mb),
87
+ 'SDQ_RESERVE_MB': str(args.reserve_mb),
88
+ 'SDQ_PREFETCH': '0',
89
+ 'SDQ_FADV_DONTNEED': '1',
90
+ })
91
+
92
+ drop_cache()
93
+ t0 = time.perf_counter()
94
+ try:
95
+ # 不用 text=True:這個模型的 tokenizer(joyai-llm BPE)會吐出半個
96
+ # UTF-8 多位元組序列,嚴格解碼會直接 UnicodeDecodeError 讓整個
97
+ # runner 掛掉 —— 而且是在第一輪結束之後才炸,等於白跑。
98
+ proc = subprocess.run(cmd, capture_output=True, env=env,
99
+ timeout=args.timeout, check=False)
100
+ except subprocess.TimeoutExpired:
101
+ return {'level_mb': level_mb, 'repeat': repeat + 1,
102
+ 'error': f'timeout after {args.timeout}s'}
103
+ wall = time.perf_counter() - t0
104
+ out = proc.stdout.decode('utf-8', errors='replace')
105
+ err = proc.stderr.decode('utf-8', errors='replace')
106
+ # sdq-chat 的進度與摘要都寫在 stderr(decode/prefill/記憶體摘要),
107
+ # assistant 文字寫在 stdout。兩邊都要掃。
108
+ both = out + '\n' + err
109
+
110
+ if proc.returncode:
111
+ tail = '\n'.join([l for l in err.splitlines()
112
+ if 'sdq' in l or 'error' in l.lower()][-6:])
113
+ return {'level_mb': level_mb, 'repeat': repeat + 1,
114
+ 'error': f'exit {proc.returncode}', 'wall_s': round(wall, 2),
115
+ 'failure_reason': tail}
116
+
117
+ def grab(pat, cast=float, default=None, text=None):
118
+ m = re.search(pat, text if text is not None else out, re.M)
119
+ return cast(m.group(1)) if m else default
120
+
121
+ decode_line = re.search(r'decode:\s*(\d+)\s+tok\s*/\s*([\d.]+)s\s*=\s*([\d.]+)\s+tok/s', both)
122
+ prefill_line = re.search(r'prefill:\s*(\d+)\s+tok\s*/\s*([\d.]+)s\s*=\s*([\d.]+)\s+tok/s', both)
123
+ mem_line = re.search(r'總 RSS\s+(\d+)\s+MB\s*/\s*峰值\s+(\d+)\s+MB\s*/\s*swap\s+(\d+)\s+MB', both)
124
+ arena_line = re.search(r'arena\s+([\d.]+)\s+GiB', err)
125
+ slots_line = re.search(r'slots=(\d+)', err)
126
+ ssd_total = re.search(r'全程 SSD 實際讀取:\s*([\d.]+)\s*GB', both)
127
+ assistant = ''
128
+ started = False
129
+ for line in out.splitlines():
130
+ if line.startswith('assistant:'):
131
+ started = True
132
+ rest = line[len('assistant:'):].strip()
133
+ if rest:
134
+ assistant = rest
135
+ continue
136
+ if started and line.strip() and not line.startswith('['):
137
+ assistant += ' ' + line.strip()
138
+ assistant = assistant.strip()
139
+
140
+ m = re.search(r'SSD 讀\s*([\d.]+)\s*MB\s*\(([\d.]+)\s*MB/tok\)', both)
141
+
142
+ # stats jsonl 的最後一筆 decode 記錄
143
+ rec = {}
144
+ if stats_file.exists():
145
+ for line in stats_file.read_text().splitlines():
146
+ r = json.loads(line)
147
+ if r.get('phase') == 'decode':
148
+ rec = r
149
+ stats_file.unlink(missing_ok=True)
150
+ stats_file.unlink(missing_ok=True)
151
+
152
+ per_layer = rec.get('per_layer') or []
153
+ hit_rate = rec.get('hit_rate')
154
+ per_layer_rates = [p['hit_rate'] for p in per_layer if p.get('req', 0) > 0]
155
+
156
+ return {
157
+ 'level_mb': level_mb,
158
+ 'level_gib': level_mb // 1024,
159
+ 'repeat': repeat + 1,
160
+ 'prompt': prompt,
161
+ 'wall_s': round(wall, 2),
162
+ 'decode_tok_s': float(decode_line.group(3)) if decode_line else None,
163
+ 'decode_tok': int(decode_line.group(1)) if decode_line else None,
164
+ 'prefill_tok_s': float(prefill_line.group(3)) if prefill_line else None,
165
+ 'prefill_tok': int(prefill_line.group(1)) if prefill_line else None,
166
+ 'peak_rss_mb': int(mem_line.group(2)) if mem_line else None,
167
+ 'final_rss_mb': int(mem_line.group(1)) if mem_line else None,
168
+ 'vmswap_mb': int(mem_line.group(3)) if mem_line else None,
169
+ 'arena_gib': float(arena_line.group(1)) if arena_line else None,
170
+ 'n_slots': int(slots_line.group(1)) if slots_line else None,
171
+ 'total_ssd_gb': float(ssd_total.group(1)) if ssd_total else None,
172
+ 'decode_ssd_mb': float(m.group(1)) if m else None,
173
+ 'ssd_mb_per_token': float(m.group(2)) if m else None,
174
+ 'expert_requests': rec.get('expert_requests'),
175
+ 'expert_hits': rec.get('expert_hits'),
176
+ 'hot_rate_pct': hit_rate,
177
+ 'cold_rate_pct': (100.0 - hit_rate) if hit_rate is not None else None,
178
+ 'evictions': rec.get('evictions'),
179
+ 'per_layer_hit_min': min(per_layer_rates) if per_layer_rates else None,
180
+ 'per_layer_hit_max': max(per_layer_rates) if per_layer_rates else None,
181
+ 'assistant': assistant[:200],
182
+ }
183
+
184
+
185
+ def main() -> None:
186
+ ap = argparse.ArgumentParser()
187
+ ap.add_argument('--model', required=True)
188
+ ap.add_argument('--bin', required=True)
189
+ ap.add_argument('--levels-gib', type=int, nargs='+', default=[4, 8, 16, 32])
190
+ ap.add_argument('--repeat', type=int, default=2)
191
+ ap.add_argument('--ctx', type=int, default=2048)
192
+ ap.add_argument('--ubatch', type=int, default=128)
193
+ ap.add_argument('--threads', type=int, default=16)
194
+ ap.add_argument('--n-predict', type=int, default=48)
195
+ ap.add_argument('--reserve-mb', type=int, default=2048)
196
+ ap.add_argument('--timeout', type=int, default=1800)
197
+ ap.add_argument('--output-dir', default='/tmp/opencode/results')
198
+ ap.add_argument('--label', default='dsv4-4-8-16-32g')
199
+ args = ap.parse_args()
200
+
201
+ Path(args.output_dir).mkdir(parents=True, exist_ok=True)
202
+ for req in (args.model, args.bin):
203
+ if not Path(req).is_file():
204
+ ap.error(f'file does not exist: {req}')
205
+
206
+ print(f'[levels] host: {host_facts()["cpu_model"]} / {host_facts()["cpu_threads"]} threads')
207
+ results = []
208
+ for gib in args.levels_gib:
209
+ mb = gib * 1024
210
+ for r in range(args.repeat):
211
+ print(f'[levels] {gib} GiB run {r + 1}/{args.repeat} ...', flush=True)
212
+ res = run_once(args, mb, r)
213
+ results.append(res)
214
+ print(f' decode={res.get("decode_tok_s")} tok/s '
215
+ f'peak_rss={res.get("peak_rss_mb")} MB '
216
+ f'hot={res.get("hot_rate_pct")}% '
217
+ f'ssd/tok={res.get("ssd_mb_per_token")} MB'
218
+ + (f' ERR {res["error"]}' if res.get('error') else ''), flush=True)
219
+
220
+ doc = {
221
+ 'model_id': MODEL_ID,
222
+ 'model_repo': MODEL_REPO,
223
+ 'model_subdir': MODEL_SUB,
224
+ 'architecture': ARCH,
225
+ 'shards': 4,
226
+ 'model_bytes': MODEL_BYTES,
227
+ 'host': host_facts(),
228
+ 'method': ('RAM budget passed to the SDQ expert pager arena sizing; '
229
+ 'no hard cgroup limit (host /sys/fs/cgroup is read-only). '
230
+ 'Page cache dropped before each run where permitted.'),
231
+ 'settings': {'levels_gib': args.levels_gib, 'repeat': args.repeat,
232
+ 'ctx': args.ctx, 'ubatch': args.ubatch,
233
+ 'threads': args.threads, 'n_predict': args.n_predict,
234
+ 'reserve_mb': args.reserve_mb, 'temp': 0},
235
+ 'results': results,
236
+ }
237
+ out = Path(args.output_dir)
238
+ (out / f'memory_levels_{args.label}.json').write_text(
239
+ json.dumps(doc, ensure_ascii=False, indent=2) + '\n')
240
+
241
+ def med(rows, key):
242
+ vals = [r[key] for r in rows if r.get(key) is not None]
243
+ return statistics.median(vals) if vals else None
244
+
245
+ lines = [
246
+ f'# RAM 預算矩陣:{args.label}', '',
247
+ doc['method'], '',
248
+ f"- Host: {host_facts()['cpu_model']}, {host_facts()['cpu_threads']} threads",
249
+ f"- ctx={args.ctx}, ubatch={args.ubatch}, threads={args.threads}, "
250
+ f"output tokens={args.n_predict}, temp=0, repeats={args.repeat}",
251
+ f"- KV/SSM state + compute reserve: {args.reserve_mb} MB", '',
252
+ '| RAM budget | arena (GiB) | slots | decode tok/s | prefill tok/s | hot % | cold % | SSD MiB/tok | total SSD read | peak RSS (MB) | swap | per-layer hit min..max |',
253
+ '|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|',
254
+ ]
255
+ # 注意:不要寫 `x or 'n/a'` —— swap = 0 是真的量到 0(0 是 falsy,會被印成 n/a)。
256
+ def i(v):
257
+ return 'n/a' if v is None else f'{int(round(v))}'
258
+ for gib in args.levels_gib:
259
+ mb = gib * 1024
260
+ rows = [r for r in results if r['level_mb'] == mb and not r.get('error')]
261
+ errs = [r for r in results if r['level_mb'] == mb and r.get('error')]
262
+ f = lambda v, n=2: 'n/a' if v is None else f'{v:.{n}f}'
263
+ status = 'FAIL: ' + str(errs[0].get('failure_reason', errs[0]['error']))[:120] if errs else f'{len(rows)}/{args.repeat}'
264
+ lo = med(rows, 'per_layer_hit_min'); hi = med(rows, 'per_layer_hit_max')
265
+ lines.append(
266
+ f"| {gib} GiB | {f(med(rows, 'arena_gib'))} | {i(med(rows, 'n_slots'))} "
267
+ f"| {f(med(rows, 'decode_tok_s'))} | {f(med(rows, 'prefill_tok_s'))} "
268
+ f"| {f(med(rows, 'hot_rate_pct'), 1)} | {f(med(rows, 'cold_rate_pct'), 1)} "
269
+ f"| {f(med(rows, 'ssd_mb_per_token'), 1)} | {f(med(rows, 'total_ssd_gb'), 1)} GB "
270
+ f"| {i(med(rows, 'peak_rss_mb'))} | {i(med(rows, 'vmswap_mb'))} MB "
271
+ f"| {f(lo, 1)}..{f(hi, 1)} |")
272
+ if errs:
273
+ lines.append(f'| | | | | | | | | | | | | {status} |')
274
+ lines += ['', f'JSON: `memory_levels_{args.label}.json`', '']
275
+ (out / f'memory_levels_{args.label}.md').write_text('\n'.join(lines))
276
+ print(f'[levels] wrote {out}/memory_levels_{args.label}.{{json,md}}')
277
+ return 1 if any(r.get('error') for r in results) else 0
278
+
279
+
280
+ if __name__ == '__main__':
281
+ sys.exit(main() or 0)
sdq-dsv4/optional-sched-debug.patch ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/ggml/src/ggml-backend.cpp b/ggml/src/ggml-backend.cpp
2
+ index 5bdade6..41d898e 100644
3
+ --- a/ggml/src/ggml-backend.cpp
4
+ +++ b/ggml/src/ggml-backend.cpp
5
+ @@ -1431,6 +1431,19 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra
6
+ for (int b = 0; b < sched->n_backends && *cur_backend_id == -1; b++) {
7
+ ggml_backend_sched_set_if_supported(sched, node, b, cur_backend_id);
8
+ }
9
+ + if (*cur_backend_id == -1 && getenv("SDQ_SCHED_DEBUG")) {
10
+ + fprintf(stderr, "[sched-dbg] unassigned node: op=%s ne=[%lld,%lld,%lld,%lld] type=%s name='%s' n_backends=%d\n",
11
+ + ggml_op_name(node->op),
12
+ + (long long) node->ne[0], (long long) node->ne[1], (long long) node->ne[2], (long long) node->ne[3],
13
+ + ggml_type_name(node->type), ggml_get_name(node), sched->n_backends);
14
+ + for (int j = 0; j < GGML_MAX_SRC && node->src[j]; j++) {
15
+ + fprintf(stderr, "[sched-dbg] src[%d] op=%s type=%s name='%s' buf=%p buft=%s\n",
16
+ + j, ggml_op_name(node->src[j]->op), ggml_type_name(node->src[j]->type),
17
+ + ggml_get_name(node->src[j]), (void *) node->src[j]->buffer,
18
+ + node->src[j]->buffer && node->src[j]->buffer->buft
19
+ + ? ggml_backend_buft_name(node->src[j]->buffer->buft) : "(none)");
20
+ + }
21
+ + }
22
+ GGML_ASSERT(*cur_backend_id != -1);
23
+ }
24
+
sdq-dsv4/sdq_buft.cpp ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SDQ expert buffer type
2
+ //
3
+ // 這是一個「假」buffer type:它宣稱接受所有張量,但實際上
4
+ // * 不配置任何 RAM(alloc_buffer 回傳 0 大小的 buffer)
5
+ // * set_tensor 忽略傳入的資料(資料來自 llama.cpp 的 mmap,我們不碰)
6
+ // * 真正的資料由 sdq::pager 從檔案 pread 到自己的 arena
7
+ //
8
+ // 用途:讓 llama.cpp 把 18.2 GiB 的 MoE 專家權重「掛在模型上」卻不放進 RAM,
9
+ // 改由分頁器管理 SSD → RAM → CPU 的三層資料流。
10
+
11
+ #include "sdq_pager.h"
12
+
13
+ #include "ggml-backend-impl.h"
14
+ #include "llama-mmap.h"
15
+
16
+ #include <cstdio>
17
+ #include <cstdlib>
18
+ #include <cstring>
19
+ #include <algorithm>
20
+ #include <mutex>
21
+ #include <unordered_map>
22
+ #include <vector>
23
+
24
+ namespace sdq {
25
+
26
+ // 給 tensor->data 的假位址。每個 expert 權重張量分到一小塊(預設 4 KB),
27
+ // 指標必須有效(ggml 會做指標運算),但內容永遠不會被讀。
28
+ struct expert_buffer {
29
+ std::vector<uint8_t> scratch;
30
+ };
31
+
32
+ static bool g_enabled = false;
33
+
34
+ void sdq_set_enabled(bool on) { g_enabled = on; }
35
+ bool sdq_enabled() { return g_enabled; }
36
+
37
+ static config g_cfg;
38
+ void sdq_set_config(const config & c) { g_cfg = c; }
39
+ const config & sdq_get_config() { return g_cfg; }
40
+
41
+ // ---------------------------------------------------------------- buffer iface
42
+
43
+ // ggml_backend_buffer_i 的欄位依賴下面的實作,先給宣告
44
+ struct ggml_backend_buffer_i sdq_buffer_iface();
45
+
46
+ static const char * sdq_buft_get_name(ggml_backend_buffer_type_t buft) {
47
+ (void) buft;
48
+ return "SDQ_ExpertSSD";
49
+ }
50
+
51
+ static size_t sdq_buft_get_alignment(ggml_backend_buffer_type_t buft) {
52
+ (void) buft;
53
+ return 4096;
54
+ }
55
+
56
+ static size_t sdq_buft_get_max_size(ggml_backend_buffer_type_t buft) {
57
+ (void) buft;
58
+ return SIZE_MAX;
59
+ }
60
+
61
+ // 每個 expert 權重張量只回報 4 KB:18.2 GiB 的權重「不會」被載入 RAM 的關鍵。
62
+ static size_t sdq_buft_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) {
63
+ (void) buft;
64
+ (void) tensor;
65
+ return 4096;
66
+ }
67
+
68
+ static bool sdq_buft_is_host(ggml_backend_buffer_type_t buft) {
69
+ (void) buft;
70
+ return false;
71
+ }
72
+
73
+ static ggml_backend_buffer_t sdq_buft_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) {
74
+ // llama.cpp 會用 get_alloc_size() 計算總大小後呼叫本函式;
75
+ // 我們只保留真正需要的假位址空間(每個張量 4 KB)。
76
+ const size_t bytes = size < 4096 ? 4096 : size;
77
+ auto * buf = new expert_buffer();
78
+ buf->scratch.assign(bytes, 0);
79
+ auto * b = new ggml_backend_buffer{
80
+ /* .iface = */ sdq_buffer_iface(),
81
+ /* .buft = */ buft,
82
+ /* .context = */ buf,
83
+ /* .size = */ bytes,
84
+ /* .usage = */ GGML_BACKEND_BUFFER_USAGE_WEIGHTS,
85
+ };
86
+ return b;
87
+ }
88
+
89
+ // 一次配置整個 context 的張量:每個張量 4 KB 假位址,實際權重 0 RAM。
90
+ static ggml_backend_buffer_t sdq_buft_alloc_buffer_n(ggml_backend_buffer_type_t buft,
91
+ ggml_tensor ** tensors, int n_tensors) {
92
+ const size_t slot = 4096;
93
+ const size_t bytes = slot * (size_t) std::max(n_tensors, 1);
94
+ ggml_backend_buffer_t buffer = sdq_buft_alloc_buffer(buft, bytes);
95
+ if (!buffer) {
96
+ return nullptr;
97
+ }
98
+ uint8_t * base = (uint8_t *) buffer->iface.get_base(buffer);
99
+ size_t off = 0;
100
+ for (int i = 0; i < n_tensors; i++) {
101
+ ggml_tensor * t = tensors[i];
102
+ t->data = base + off;
103
+ t->buffer = buffer;
104
+ off += slot;
105
+ buffer->iface.init_tensor(buffer, t);
106
+ }
107
+ return buffer;
108
+ }
109
+
110
+ static void sdq_buft_free_buffer(ggml_backend_buffer_t buffer) {
111
+ // 只釋放自己的資料;ggml_backend_buffer_free() 會負責 delete buffer 本體,
112
+ // 這裡再 delete 一次會造成 double free。
113
+ delete (expert_buffer *) buffer->context;
114
+ buffer->context = nullptr;
115
+ }
116
+
117
+ static void * sdq_buft_get_base(ggml_backend_buffer_t buffer) {
118
+ return ((expert_buffer *) buffer->context)->scratch.data();
119
+ }
120
+
121
+ static ggml_status sdq_buft_init_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor) {
122
+ (void) buffer;
123
+ (void) tensor;
124
+ return GGML_STATUS_SUCCESS;
125
+ }
126
+
127
+ static void sdq_buft_memset_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
128
+ uint8_t value, size_t offset, size_t size) {
129
+ (void) buffer; (void) tensor; (void) value; (void) offset; (void) size;
130
+ }
131
+
132
+ static void sdq_buft_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
133
+ const void * data, size_t offset, size_t size) {
134
+ // 關鍵:忽略。llama.cpp 會把 mmap 的資料交給我們,這裡一個位元組都不落地,
135
+ // 專家權利永遠不會佔用 RAM。
136
+ (void) buffer; (void) tensor; (void) data; (void) offset; (void) size;
137
+ }
138
+
139
+ static void sdq_buft_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * tensor,
140
+ void * data, size_t offset, size_t size) {
141
+ (void) buffer; (void) tensor; (void) offset;
142
+ memset(data, 0, size);
143
+ }
144
+
145
+ static bool sdq_buft_cpy_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * src, ggml_tensor * dst) {
146
+ (void) buffer; (void) src; (void) dst;
147
+ return true;
148
+ }
149
+
150
+ static void sdq_buft_clear(ggml_backend_buffer_t buffer, uint8_t value) {
151
+ (void) buffer; (void) value;
152
+ }
153
+
154
+ struct ggml_backend_buffer_i sdq_buffer_iface() {
155
+ static ggml_backend_buffer_i i = [] {
156
+ ggml_backend_buffer_i x {};
157
+ x.free_buffer = sdq_buft_free_buffer;
158
+ x.get_base = sdq_buft_get_base;
159
+ x.init_tensor = sdq_buft_init_tensor;
160
+ x.memset_tensor = sdq_buft_memset_tensor;
161
+ x.set_tensor = sdq_buft_set_tensor;
162
+ x.get_tensor = sdq_buft_get_tensor;
163
+ x.cpy_tensor = sdq_buft_cpy_tensor;
164
+ x.clear = sdq_buft_clear;
165
+ return x;
166
+ }();
167
+ return i;
168
+ }
169
+
170
+ ggml_backend_buffer_type_t sdq_expert_buft() {
171
+ static ggml_backend_buffer_type_t buft = [] {
172
+ return new ggml_backend_buffer_type{
173
+ /* .iface = */ {
174
+ /* .get_name = */ sdq_buft_get_name,
175
+ /* .alloc_buffer = */ sdq_buft_alloc_buffer,
176
+ /* .alloc_buffer_n = */ sdq_buft_alloc_buffer_n,
177
+ /* .get_alignment = */ sdq_buft_get_alignment,
178
+ /* .get_max_size = */ sdq_buft_get_max_size,
179
+ /* .get_alloc_size = */ sdq_buft_get_alloc_size,
180
+ /* .get_alloc_size_n = */ nullptr,
181
+ /* .is_host = */ sdq_buft_is_host,
182
+ },
183
+ /* .device = */ nullptr,
184
+ /* .context = */ nullptr,
185
+ };
186
+ }();
187
+ return buft;
188
+ }
189
+
190
+ // llama.cpp 端需要的介面宣告
191
+ void sdq_install_buft_overrides(llama_model_params * params);
192
+
193
+ static const llama_model_tensor_buft_override * sdq_overrides() {
194
+ static llama_model_tensor_buft_override ov[] = {
195
+ { "blk\\.[0-9]+\\.ffn_(gate|up|down|gate_up)_exps\\.weight", sdq_expert_buft() },
196
+ { nullptr, nullptr },
197
+ };
198
+ return ov;
199
+ }
200
+
201
+ void sdq_install_buft_overrides(llama_model_params * params) {
202
+ if (!params) {
203
+ return;
204
+ }
205
+ if (params->tensor_buft_overrides == nullptr) {
206
+ params->tensor_buft_overrides = sdq_overrides();
207
+ } else {
208
+ // 使用者已有 override:把手動指定的順序原樣保留,附加在尾端
209
+ size_t n = 0;
210
+ while (params->tensor_buft_overrides[n].pattern != nullptr) {
211
+ n++;
212
+ }
213
+ static std::vector<llama_model_tensor_buft_override> merged;
214
+ merged.clear();
215
+ for (size_t i = 0; i < n; i++) {
216
+ merged.push_back(params->tensor_buft_overrides[i]);
217
+ }
218
+ for (size_t i = 0; sdq_overrides()[i].pattern != nullptr; i++) {
219
+ merged.push_back(sdq_overrides()[i]);
220
+ }
221
+ merged.push_back({ nullptr, nullptr });
222
+ params->tensor_buft_overrides = merged.data();
223
+ }
224
+ }
225
+
226
+ } // namespace sdq
sdq-dsv4/sdq_cli.cpp ADDED
@@ -0,0 +1,825 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SDQ 聊天 / benchmark 主程式
2
+ //
3
+ // sdq-chat -m <model.gguf> [-c ctx] [-t threads] [-n n_predict] [-p prompt] ...
4
+ //
5
+ // 這個工具存在的理由:官方 llama-cli 不會安裝我們的 tensor buffer override,
6
+ // 而且需要我們在載入模型前後插入 pager 的初始化/收尾。用最少的相依度
7
+ // (只鏈 llama + ggml)自己寫一支,行為完全可控。
8
+
9
+ #include "llama.h"
10
+ #include "sdq_pager.h"
11
+
12
+ #include <algorithm>
13
+ #include <chrono>
14
+ #include <cinttypes>
15
+ #include <cmath>
16
+ #include <cstdio>
17
+ #include <cstdlib>
18
+ #include <cstring>
19
+ #include <functional>
20
+ #include <thread>
21
+ #include <iostream>
22
+ #include <string>
23
+ #include <vector>
24
+
25
+ using namespace sdq;
26
+
27
+ namespace {
28
+
29
+ struct options {
30
+ std::string model;
31
+ std::string prompt;
32
+ std::string system = "You are a helpful assistant. Answer in the user's language.";
33
+ std::string stats_file;
34
+ int32_t n_ctx = 4096; // 驗證範圍:4K(理論支援 1M)
35
+ int32_t n_predict = 256;
36
+ int32_t n_threads = 0;
37
+ int32_t n_ubatch = 0;
38
+ int32_t n_keep = 0;
39
+ float temp = 0.7f;
40
+ float top_p = 0.95f;
41
+ int32_t top_k = 20;
42
+ bool chat = true;
43
+ bool interactive = true;
44
+ bool bench = false;
45
+ bool no_prefetch = false;
46
+ bool dump_logits = false;
47
+ std::string logits_out;
48
+ std::string selftest;
49
+ float selftest_limit = 0.0f;
50
+ float selftest_xscale = 1.0f;
51
+ int selftest_mode = SDQ_SWIGLU_GGML_FUSED_CLAMP;
52
+ std::string span_test;
53
+ std::string dump_tensors; // ggml eval callback:把指定張量的資料存檔(A/B 比對用)
54
+ bool no_pager = false; // A/B 測試用:不啟用分頁,走上游 mul_mat_id(整個模型在 RAM)
55
+ int32_t arena_mb = 0;
56
+ int32_t ram_budget_mb = 0;
57
+ bool fadvise = true;
58
+ };
59
+
60
+ double now_s() {
61
+ static auto t0 = std::chrono::steady_clock::now();
62
+ return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
63
+ }
64
+
65
+ size_t rss_mb() { return process_rss_bytes() / (1024 * 1024); }
66
+
67
+ // 從 /proc/self/status 讀 VmHWM(峰值 RSS)/ VmSwap(這個行程真的用了多少 swap)
68
+ size_t proc_status_kb(const char * key) {
69
+ FILE * f = fopen("/proc/self/status", "r");
70
+ if (!f) return 0;
71
+ char line[256];
72
+ const size_t klen = strlen(key);
73
+ size_t val = 0;
74
+ while (fgets(line, sizeof(line), f)) {
75
+ if (strncmp(line, key, klen) == 0) {
76
+ val = (size_t) strtoull(line + klen, nullptr, 10);
77
+ break;
78
+ }
79
+ }
80
+ fclose(f);
81
+ return val;
82
+ }
83
+
84
+ size_t peak_rss_mb() { return proc_status_kb("VmHWM:") / 1024; }
85
+ size_t swap_mb() { return proc_status_kb("VmSwap:") / 1024; }
86
+
87
+ // /proc/self/smaps_rollup:把 RSS 拆成「檔案對映」與「匿名」兩塊。
88
+ // 這很重要:模型檔是 mmap 進來的,核心預讀會讓「檔案對映頁」也算進 RSS,
89
+ // 報告時必須分開看,否則會誤以為 RAM 用量比實際少(或多)。
90
+ size_t smaps_kb(const char * key) {
91
+ FILE * f = fopen("/proc/self/smaps_rollup", "r");
92
+ if (!f) return 0;
93
+ char line[256];
94
+ const size_t klen = strlen(key);
95
+ size_t val = 0;
96
+ while (fgets(line, sizeof(line), f)) {
97
+ if (strncmp(line, key, klen) == 0) {
98
+ val = (size_t) strtoull(line + klen, nullptr, 10);
99
+ break;
100
+ }
101
+ }
102
+ fclose(f);
103
+ return val;
104
+ }
105
+
106
+ void print_mem_report(const char * when) {
107
+ fprintf(stderr,
108
+ "[sdq] 記憶體(%s):匿名 RSS %zu MB / 檔案對映 RSS %zu MB / 總 RSS %zu MB / 峰值 %zu MB / swap %zu MB\n",
109
+ when, smaps_kb("Anonymous:") / 1024, smaps_kb("RssFile:") / 1024,
110
+ smaps_kb("Rss:") / 1024, peak_rss_mb(), swap_mb());
111
+ }
112
+
113
+ // 讀 /proc/self/io:從 block device 實際讀了多少(SSD 讀取)
114
+ uint64_t read_bytes_io() {
115
+ FILE * f = fopen("/proc/self/io", "r");
116
+ if (!f) return 0;
117
+ char line[256];
118
+ uint64_t v = 0;
119
+ while (fgets(line, sizeof(line), f)) {
120
+ if (strncmp(line, "read_bytes:", 11) == 0) {
121
+ v = strtoull(line + 11, nullptr, 10);
122
+ }
123
+ }
124
+ fclose(f);
125
+ return v;
126
+ }
127
+
128
+ struct chat_state {
129
+ std::vector<llama_chat_message> msgs;
130
+ };
131
+
132
+ std::string build_prompt(const options & opt, const chat_state & st) {
133
+ if (!opt.chat) {
134
+ return opt.prompt;
135
+ }
136
+ const char * tmpl = llama_model_chat_template(nullptr, nullptr);
137
+ // 沒有 model 指標時用內建模板;下面 main() 會用實際模型的模板重做
138
+ (void) tmpl;
139
+ size_t n_chars = 0;
140
+ for (const auto & m : st.msgs) {
141
+ n_chars += strlen(m.role) + strlen(m.content) + 32;
142
+ }
143
+ std::vector<char> buf(2 * n_chars + 1024);
144
+ const char * t = st.msgs.empty() ? nullptr : nullptr;
145
+ (void) t;
146
+ int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
147
+ buf.data(), (int32_t) buf.size());
148
+ if (len > (int32_t) buf.size()) {
149
+ buf.resize(len + 1);
150
+ llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
151
+ buf.data(), (int32_t) buf.size());
152
+ }
153
+ return std::string(buf.data());
154
+ }
155
+
156
+ } // namespace
157
+
158
+ #include <execinfo.h>
159
+ #include <signal.h>
160
+ #include <unistd.h>
161
+ static void sdq_segv(int sig) {
162
+ void * bt[64];
163
+ const int n = backtrace(bt, 64);
164
+ fprintf(stderr, "\n*** sdq: signal %d, backtrace:\n", sig);
165
+ backtrace_symbols_fd(bt, n, 2);
166
+ _exit(9);
167
+ }
168
+
169
+ // eval callback:把感興趣的張量(用 SDQ_DUMP_NAMES 指定)寫進檔案
170
+ struct dump_state {
171
+ FILE * fp = nullptr;
172
+ uint64_t step = 0;
173
+ std::vector<std::string> names;
174
+ };
175
+
176
+ static dump_state g_dump;
177
+
178
+ static bool sdq_eval_callback(ggml_tensor * t, bool ask, void * user_data) {
179
+ (void) user_data;
180
+ auto & ds = g_dump;
181
+ if (ask) {
182
+ // 除錯模式:每個節點都要資料 → 會強制逐節點同步(慢,但能逐節點比對)
183
+ return ds.fp != nullptr;
184
+ }
185
+ if (!ds.fp) {
186
+ return false;
187
+ }
188
+ const char * nm = ggml_get_name(t);
189
+ const size_t n_len = nm ? strlen(nm) : 0;
190
+ bool want = ds.names.empty();
191
+ for (const auto & n : ds.names) {
192
+ if (nm && strstr(nm, n.c_str())) {
193
+ want = true;
194
+ break;
195
+ }
196
+ }
197
+ const int32_t ne[4] = {(int32_t) t->ne[0], (int32_t) t->ne[1], (int32_t) t->ne[2], (int32_t) t->ne[3]};
198
+ const int32_t type = (int32_t) t->type;
199
+ fwrite(&n_len, sizeof(size_t), 1, ds.fp);
200
+ if (n_len) {
201
+ fwrite(nm, 1, n_len, ds.fp);
202
+ }
203
+ fwrite(&ds.step, sizeof(uint64_t), 1, ds.fp);
204
+ fwrite(ne, sizeof(int32_t), 4, ds.fp);
205
+ fwrite(&type, sizeof(int32_t), 1, ds.fp);
206
+
207
+ // 永遠寫「摘要」(前 8 個值 + 平方和 + 最大值),這樣即使沒有名字也能 A/B 找出第一個分歧點
208
+ float summary[10] = {0};
209
+ if (t->type == GGML_TYPE_F32 && t->data) {
210
+ const size_t n = (size_t) ne[0] * ne[1] * ne[2] * ne[3];
211
+ const float * p = (const float *) t->data;
212
+ double ss = 0;
213
+ float mx = 0;
214
+ for (size_t i = 0; i < n; i++) {
215
+ ss += (double) p[i] * p[i];
216
+ if (std::fabs(p[i]) > std::fabs(mx)) {
217
+ mx = p[i];
218
+ }
219
+ }
220
+ for (int i = 0; i < 8 && (size_t) i < n; i++) {
221
+ summary[i] = p[i];
222
+ }
223
+ summary[8] = (float) std::sqrt(ss);
224
+ summary[9] = mx;
225
+ fwrite(summary, sizeof(float), 10, ds.fp);
226
+ if (want) {
227
+ fwrite(p, sizeof(float), n, ds.fp);
228
+ }
229
+ } else {
230
+ fwrite(summary, sizeof(float), 10, ds.fp);
231
+ }
232
+ ds.step++;
233
+ return true; // 這個節點的資料我們已經讀過了,標記成已完成
234
+ }
235
+
236
+ int main(int argc, char ** argv) {
237
+ signal(SIGSEGV, sdq_segv);
238
+ signal(SIGBUS, sdq_segv);
239
+ signal(SIGABRT, sdq_segv);
240
+ options opt;
241
+ const char * env_model = getenv("SDQ_MODEL_PATH");
242
+ if (env_model) {
243
+ opt.model = env_model;
244
+ }
245
+
246
+ for (int i = 1; i < argc; i++) {
247
+ std::string a = argv[i];
248
+ auto next = [&](const char * what) -> std::string {
249
+ if (i + 1 >= argc) {
250
+ fprintf(stderr, "%s 需要參數\n", what);
251
+ exit(1);
252
+ }
253
+ return argv[++i];
254
+ };
255
+ if (a == "-m" || a == "--model") opt.model = next("-m");
256
+ else if (a == "-p" || a == "--prompt") opt.prompt = next("-p");
257
+ else if (a == "--sys") opt.system = next("--sys");
258
+ else if (a == "-c" || a == "--ctx") opt.n_ctx = atoi(next("-c").c_str());
259
+ else if (a == "-n") opt.n_predict = atoi(next("-n").c_str());
260
+ else if (a == "-t") opt.n_threads = atoi(next("-t").c_str());
261
+ else if (a == "-b" || a == "--ubatch") opt.n_ubatch = atoi(next("-b").c_str());
262
+ else if (a == "--temp") opt.temp = atof(next("--temp").c_str());
263
+ else if (a == "--top-p") opt.top_p = atof(next("--top-p").c_str());
264
+ else if (a == "--top-k") opt.top_k = atoi(next("--top-k").c_str());
265
+ else if (a == "--no-chat") opt.chat = false;
266
+ else if (a == "--no-interactive") opt.interactive = false;
267
+ else if (a == "--bench") opt.bench = true;
268
+ else if (a == "--no-prefetch") opt.no_prefetch = true;
269
+ else if (a == "--arena-mb") opt.arena_mb = atoi(next("--arena-mb").c_str());
270
+ else if (a == "--ram-budget-mb") opt.ram_budget_mb = atoi(next("--ram-budget-mb").c_str());
271
+ else if (a == "--no-fadvise") opt.fadvise = false;
272
+ else if (a == "--stats-file") opt.stats_file = next("--stats-file");
273
+ else if (a == "--dump-logits") opt.dump_logits = true;
274
+ else if (a == "--logits-out") opt.logits_out = next("--logits-out");
275
+ else if (a == "--selftest") opt.selftest = next("--selftest");
276
+ else if (a == "--selftest-limit") opt.selftest_limit = (float) atof(next("--selftest-limit").c_str());
277
+ else if (a == "--selftest-xscale") opt.selftest_xscale = (float) atof(next("--selftest-xscale").c_str());
278
+ else if (a == "--selftest-mode") opt.selftest_mode = atoi(next("--selftest-mode").c_str());
279
+ else if (a == "--dump-tensors") opt.dump_tensors = next("--dump-tensors");
280
+ else if (a == "--no-pager") opt.no_pager = true;
281
+ else if (a == "--span-test") opt.span_test = next("--span-test");
282
+ else if (a == "-h" || a == "--help") {
283
+ printf(
284
+ "用法:sdq-chat -m <model.gguf> [選項]\n"
285
+ " -c, --ctx N 上下文長度(預設 4096;模型原生 262144)\n"
286
+ " -n N 要生成的 token 數\n"
287
+ " -t N CPU 執行緒數\n"
288
+ " -b N ubatch 大小\n"
289
+ " -p, --prompt TEXT 提示詞\n"
290
+ " --sys TEXT system 訊息\n"
291
+ " --no-chat 不要套 chat template(純 completion)\n"
292
+ " --no-interactive 跑完提示詞就結束\n"
293
+ " --bench benchmark 模式(輸出 JSON 到 stats 檔)\n"
294
+ " --arena-mb N expert arena 大小(MB)\n"
295
+ " --ram-budget-mb N RAM 總預算(MB),arena 會配合縮減\n"
296
+ " --no-prefetch 關閉背景預取\n"
297
+ " --no-fadvise 讀完不丟 kernel page cache(會讓 RAM 帳目失真)\n"
298
+ " --stats-file PATH 指標 JSONL 輸出\n"
299
+ " --dump-logits 把每個 token 的 logits 存檔(驗證用)\n"
300
+ " --logits-out PATH logits 輸出路徑\n");
301
+ return 0;
302
+ } else {
303
+ fprintf(stderr, "未知參數:%s(用 --help 看用法)\n", a.c_str());
304
+ return 1;
305
+ }
306
+ }
307
+
308
+ if (opt.model.empty()) {
309
+ fprintf(stderr, "缺少模型檔(-m 或 SDQ_MODEL_PATH)\n");
310
+ return 1;
311
+ }
312
+
313
+ // ---- 環境變數(pager 讀)
314
+ setenv("SDQ_MODEL_PATH", opt.model.c_str(), 1);
315
+ // I/O 讀取執行緒數:預設 3 × 計算執行緒(上限 48)。
316
+ // 必須在這裡設定 —— pager 的 I/O 池是在「載入模型」時啟動的,那已經比
317
+ // context 建立早了;放到後面設定會讓池子只拿到下限 4 條執行緒。
318
+ // 量測(repeat=3,中位數):序列讀 3.79 → 12 條 5.84 → 24 條 6.68 tok/s。
319
+ if (!getenv("SDQ_IO_THREADS")) {
320
+ const int hw_io = (int) std::max(1u, std::thread::hardware_concurrency());
321
+ const int dflt_io = hw_io < 8 ? hw_io : 8;
322
+ const int n_io = opt.n_threads > 0 ? opt.n_threads : dflt_io;
323
+ setenv("SDQ_IO_THREADS", std::to_string(n_io * 3).c_str(), 1);
324
+ }
325
+ // 只有明確指定時才設定,否則保留呼叫者從環境變數傳入的值
326
+ // (舊版無條件 setenv 會讓 SDQ_PREFETCH=0 被悄悄蓋回 1,除錯時極易誤判)
327
+ if (opt.no_prefetch) {
328
+ setenv("SDQ_PREFETCH", "0", 1);
329
+ }
330
+ setenv("SDQ_FADV_DONTNEED", opt.fadvise ? "1" : "0", 1);
331
+ if (opt.arena_mb > 0) {
332
+ setenv("SDQ_ARENA_MB", std::to_string(opt.arena_mb).c_str(), 1);
333
+ }
334
+ if (opt.ram_budget_mb > 0) {
335
+ setenv("SDQ_RAM_BUDGET_MB", std::to_string(opt.ram_budget_mb).c_str(), 1);
336
+ }
337
+ if (!opt.stats_file.empty()) {
338
+ setenv("SDQ_STATS_FILE", opt.stats_file.c_str(), 1);
339
+ }
340
+
341
+ llama_backend_init();
342
+ llama_numa_init(GGML_NUMA_STRATEGY_DISABLED);
343
+
344
+ // ---- 模型載入:expert 權重交給 pager 的 buffer type(0 RAM)
345
+ llama_model_params mparams = llama_model_default_params();
346
+ mparams.n_gpu_layers = 0;
347
+ mparams.load_mode = LLAMA_LOAD_MODE_MMAP;
348
+ if (!opt.no_pager) {
349
+ sdq_set_enabled(true);
350
+ sdq_install_buft_overrides(&mparams);
351
+ } else {
352
+ printf("[sdq] --no-pager:對照模式,專家權重全部留在 RAM(需要 %s RAM)\n", "22GB");
353
+ }
354
+
355
+ printf("[sdq] 載入模型:%s\n", opt.model.c_str());
356
+ const double t_load0 = now_s();
357
+ llama_model * model = llama_model_load_from_file(opt.model.c_str(), mparams);
358
+ if (!model) {
359
+ fprintf(stderr, "[sdq] 模型載入失敗\n");
360
+ return 1;
361
+ }
362
+ const double t_load = now_s() - t_load0;
363
+
364
+ // ---- pager 初始化(讀 GGUF 檔頭、建頁面表、配置 arena)
365
+ auto & P = pager::instance();
366
+ if (!opt.no_pager && !sdq_init(opt.model)) {
367
+ fprintf(stderr, "[sdq] pager 初始化失敗\n");
368
+ return 1;
369
+ }
370
+
371
+ // ---- 張量 dump(A/B 比對用):在 op 真的算完的那一刻把資料寫出來
372
+ FILE * tensor_fp = nullptr;
373
+ dump_state dump_state_;
374
+ if (!opt.dump_tensors.empty()) {
375
+ tensor_fp = fopen(opt.dump_tensors.c_str(), "wb");
376
+ dump_state_.fp = tensor_fp;
377
+ if (const char * names = getenv("SDQ_DUMP_NAMES")) {
378
+ std::string s = names;
379
+ size_t pos = 0;
380
+ while (pos <= s.size()) {
381
+ size_t c = s.find(',', pos);
382
+ if (c == std::string::npos) c = s.size();
383
+ dump_state_.names.push_back(s.substr(pos, c - pos));
384
+ pos = c + 1;
385
+ }
386
+ }
387
+ g_dump = dump_state_;
388
+ }
389
+
390
+ // ---- context
391
+ llama_context_params cparams = llama_context_default_params();
392
+ cparams.n_ctx = (uint32_t) opt.n_ctx;
393
+ cparams.n_batch = (uint32_t) opt.n_ctx;
394
+ // ubatch 預設 128(不是 512)。理由是「8 GB 預算要真的裝得下」:
395
+ // llama.cpp 的 CPU compute buffer 是按 n_ubatch 線性成長的,實測
396
+ // ubatch=512 → 502 MiB、256 → 251 MiB、128 → 125.5 MiB
397
+ // 而 compute buffer 是在 pager 算完 arena 大小之後才配置的,所以它不算在
398
+ // arena 的扣減裡。實測在 RLIMIT_DATA 8 GB 下:
399
+ // ubatch=512 → 提示詞超過 ~256 token 直接 std::bad_alloc(崩潰)
400
+ // ubatch=256 → 同一個提示詞一樣崩潰
401
+ // ubatch=128 → 正常,681 token 的 prefill 跑到 28.8 tok/s
402
+ // 而且 ubatch=128 還更快(docs/04 實測 +7%),因為 prefill 分更多批、
403
+ // 每批的 expert 聯集比較小、命中率比較高。
404
+ cparams.n_ubatch = opt.n_ubatch > 0 ? (uint32_t) opt.n_ubatch : (uint32_t) std::min(opt.n_ctx, 128);
405
+ // 執行緒預設值:不要直接用 hardware_concurrency()。
406
+ // 實測(docs/04,repeat=3):mode C 下 t=8 最快,給更多執行緒反而變慢 ——
407
+ // t=8 4.02 / t=10 3.79 / t=12 3.55 / t=16 3.60 tok/s
408
+ // 原因很直接:每層每 token 只有 n_expert_used = 8 個 expert 可以平行,
409
+ // 執行緒超過 8 之後就只是多開執行緒搶同一批 CPU。
410
+ // 下限取實際核心數,避免在 4 核心的目標機器上開太多。
411
+ const int hw = (int) std::max(1u, std::thread::hardware_concurrency());
412
+ const int dflt_threads = hw < 8 ? hw : 8;
413
+ cparams.n_threads = opt.n_threads > 0 ? opt.n_threads : (int32_t) dflt_threads;
414
+ cparams.n_threads_batch = cparams.n_threads;
415
+ if (!opt.stats_file.empty()) {
416
+ cparams.offload_kqv = false;
417
+ }
418
+
419
+ if (tensor_fp) {
420
+ cparams.cb_eval = sdq_eval_callback;
421
+ cparams.cb_eval_user_data = &g_dump;
422
+ }
423
+
424
+ llama_context * ctx = llama_init_from_model(model, cparams);
425
+ if (!ctx) {
426
+ fprintf(stderr, "[sdq] context 建立失敗(ctx=%d 太大?)\n", opt.n_ctx);
427
+ return 1;
428
+ }
429
+
430
+ printf("[sdq] ctx=%d threads=%d ubatch=%u RAM(RSS)=%zu MB 模型載入 %.1fs\n",
431
+ opt.n_ctx, cparams.n_threads, cparams.n_ubatch, rss_mb(), t_load);
432
+ print_mem_report("載入 + pager 後");
433
+
434
+ // ---- sampler
435
+ auto sparams = llama_sampler_chain_default_params();
436
+ sparams.no_perf = true;
437
+ llama_sampler * smpl = llama_sampler_chain_init(sparams);
438
+ llama_sampler_chain_add(smpl, llama_sampler_init_top_k(opt.top_k));
439
+ llama_sampler_chain_add(smpl, llama_sampler_init_top_p(opt.top_p, 1));
440
+ llama_sampler_chain_add(smpl, llama_sampler_init_temp(opt.temp));
441
+ llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED));
442
+
443
+ chat_state st;
444
+ if (!opt.system.empty()) {
445
+ st.msgs.push_back({"system", opt.system.c_str()});
446
+ }
447
+
448
+ std::vector<llama_token> decoded;
449
+
450
+ const llama_vocab * vocab = llama_model_get_vocab(model);
451
+
452
+ auto tokenize = [&](const std::string & text) {
453
+ std::vector<llama_token> toks(text.size() + 16);
454
+ int32_t n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
455
+ toks.data(), (int32_t) toks.size(), true, true);
456
+ if (n < 0) {
457
+ toks.resize((size_t) -n);
458
+ n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
459
+ toks.data(), (int32_t) toks.size(), true, true);
460
+ }
461
+ toks.resize(n);
462
+ return toks;
463
+ };
464
+
465
+ // ---- 送進去一個 ubatch,回傳生成的 token(-1 = 需要繼續生成)
466
+ std::function<int(llama_token, bool)> submit;
467
+ uint64_t n_prefill_tok = 0;
468
+ double t_prefill = 0;
469
+ uint64_t n_decode_tok = 0;
470
+ double t_decode = 0;
471
+ uint64_t io_read0 = read_bytes_io();
472
+
473
+ std::vector<float> logits_store;
474
+ FILE * logits_fp = nullptr;
475
+ if (opt.dump_logits) {
476
+ logits_fp = fopen(opt.logits_out.empty() ? "sdq_logits.bin" : opt.logits_out.c_str(), "wb");
477
+ }
478
+
479
+ submit = [&](llama_token tok, bool last) -> int {
480
+ const uint32_t ctx_size = llama_n_ctx(ctx);
481
+ llama_memory_t mem = llama_get_memory(ctx);
482
+ if (tok == 1) { // BOS:重開記憶體
483
+ llama_memory_clear(mem, true);
484
+ decoded.clear();
485
+ }
486
+ if (decoded.size() >= opt.n_ctx - 4) {
487
+ // 環形丟掉最舊的 token(保留 opt.n_keep)
488
+ const size_t drop = decoded.size() - (size_t) opt.n_keep;
489
+ llama_memory_seq_rm(mem, 0, 0, (int32_t) drop);
490
+ decoded.erase(decoded.begin(), decoded.begin() + (long) drop);
491
+ }
492
+
493
+ llama_batch b = llama_batch_get_one((llama_token *) &tok, 1);
494
+
495
+ const bool is_prefill = decoded.empty();
496
+ const double t0 = now_s();
497
+ const int rc = llama_decode(ctx, b);
498
+ const double dt = now_s() - t0;
499
+ if (rc != 0) {
500
+ fprintf(stderr, "\n[sdq] llama_decode 失敗 rc=%d(記憶體不足?)\n", rc);
501
+ exit(3);
502
+ }
503
+ decoded.push_back(tok);
504
+
505
+ // 每個 ubatch 結束 → 更新路由歷史並觸發下一輪預取
506
+ P.end_token();
507
+
508
+ if (is_prefill) {
509
+ n_prefill_tok++;
510
+ t_prefill += dt;
511
+ } else {
512
+ n_decode_tok++;
513
+ t_decode += dt;
514
+ P.get_stats().tokens.fetch_add(1, std::memory_order_relaxed);
515
+ }
516
+
517
+ if (logits_fp) {
518
+ const int32_t n_logits = llama_vocab_n_tokens(vocab);
519
+ const float * lg = llama_get_logits_ith(ctx, -1);
520
+ if (lg) {
521
+ logits_store.assign(lg, lg + n_logits);
522
+ fwrite(&n_logits, sizeof(int32_t), 1, logits_fp);
523
+ fwrite(logits_store.data(), sizeof(float), n_logits, logits_fp);
524
+ fflush(logits_fp);
525
+ }
526
+ }
527
+
528
+ if (last) {
529
+ return -1;
530
+ }
531
+ const llama_token id = llama_sampler_sample(smpl, ctx, -1);
532
+ return (int) id;
533
+ };
534
+
535
+ // ---- 產生 prompt
536
+ auto run_prompt = [&](const std::string & user_text, bool add_to_history) {
537
+ if (opt.chat) {
538
+ st.msgs.push_back({"user", user_text.c_str()});
539
+ }
540
+ std::string text;
541
+ if (opt.chat) {
542
+ const char * tmpl = llama_model_chat_template(model, nullptr);
543
+ size_t n_chars = 0;
544
+ for (const auto & m : st.msgs) {
545
+ n_chars += strlen(m.role) + strlen(m.content) + 32;
546
+ }
547
+ std::vector<char> buf(2 * n_chars + 1024);
548
+ int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(),
549
+ true, buf.data(), (int32_t) buf.size());
550
+ if (len > (int32_t) buf.size()) {
551
+ buf.resize((size_t) len + 1);
552
+ llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
553
+ buf.data(), (int32_t) buf.size());
554
+ }
555
+ text.assign(buf.data());
556
+ } else {
557
+ text = user_text;
558
+ }
559
+
560
+ auto toks = tokenize(text);
561
+ const int n_prompt = (int) toks.size();
562
+ const uint64_t io0 = read_bytes_io();
563
+
564
+ // 分批送進去(每批 = 一個 ubatch,控制記憶體)
565
+ const int32_t ub = (int32_t) cparams.n_ubatch;
566
+ llama_sampler_reset(smpl);
567
+ int32_t pos = 0;
568
+ for (int32_t i = 0; i < n_prompt; i += ub) {
569
+ const int32_t n = std::min(ub, n_prompt - i);
570
+ llama_batch b = llama_batch_init((uint32_t) n, 0, 1);
571
+ for (int32_t j = 0; j < n; j++) {
572
+ b.token[j] = toks[i + j];
573
+ b.pos[j] = pos++;
574
+ b.n_seq_id[j] = 1;
575
+ b.seq_id[j][0] = 0;
576
+ }
577
+ b.n_tokens = (uint32_t) n;
578
+ const double t0 = now_s();
579
+ const int rc = llama_decode(ctx, b);
580
+ t_prefill += now_s() - t0;
581
+ n_prefill_tok += (uint64_t) n;
582
+ if (rc != 0) {
583
+ fprintf(stderr, "\n[sdq] prefill 失敗 rc=%d(%d tokens)\n", rc, n);
584
+ exit(3);
585
+ }
586
+ P.end_token();
587
+ llama_batch_free(b);
588
+ }
589
+ const double t_p = now_s();
590
+ (void) t_p;
591
+ (void) io0;
592
+
593
+ if (add_to_history && opt.chat) {
594
+ st.msgs.push_back({"assistant", ""});
595
+ }
596
+ return n_prompt;
597
+ };
598
+
599
+ // ---- 產生
600
+ auto generate = [&](int32_t n_max) {
601
+ const uint64_t io0 = read_bytes_io();
602
+ const uint64_t io_read_before = io0;
603
+ (void) io_read_before;
604
+ const double t0 = now_s();
605
+ int32_t n_gen = 0;
606
+ llama_token id = llama_sampler_sample(smpl, ctx, -1);
607
+ fputs("assistant: ", stdout);
608
+ fflush(stdout);
609
+ while (n_gen < n_max) {
610
+ const int rc = submit(id, /*last=*/n_gen + 1 >= n_max);
611
+ if (rc < 0) {
612
+ break;
613
+ }
614
+ id = (llama_token) rc;
615
+ char piece_buf[256];
616
+ const int n_piece = llama_token_to_piece(vocab, id, piece_buf, sizeof(piece_buf), 0, true);
617
+ const std::string piece(piece_buf, (size_t) std::max(0, n_piece));
618
+ fputs(piece.c_str(), stdout);
619
+ fflush(stdout);
620
+ n_gen++;
621
+ }
622
+ fputs("\n", stdout);
623
+ const double dt = now_s() - t0;
624
+ const uint64_t io1 = read_bytes_io();
625
+ fprintf(stderr, "[sdq] decode: %d tok / %.2fs = %.2f tok/s | SSD 讀 %.1f MB (%.1f MB/tok) | RSS %zu MB\n",
626
+ n_gen, dt, dt > 0 ? n_gen / dt : 0.0,
627
+ (io1 - io0) / 1048576.0, n_gen ? (io1 - io0) / 1048576.0 / n_gen : 0.0, rss_mb());
628
+ if (opt.chat) {
629
+ for (auto & m : st.msgs) {
630
+ if (strcmp(m.role, "assistant") == 0) {
631
+ m.content = "";
632
+ }
633
+ }
634
+ }
635
+ P.dump_stats("decode");
636
+ return n_gen;
637
+ };
638
+
639
+ // ---- 合併讀取自我測試:--span-test IL:E0:E1:...
640
+ if (!opt.span_test.empty()) {
641
+ std::vector<int> parts;
642
+ std::string cur_s;
643
+ for (char ch : opt.span_test + ":") {
644
+ if (ch == ':') { parts.push_back(atoi(cur_s.c_str())); cur_s.clear(); }
645
+ else cur_s += ch;
646
+ }
647
+ if (parts.size() < 3) {
648
+ fprintf(stderr, "用法:--span-test IL:E0:E1[:E2...](E 要連續)\n");
649
+ return 1;
650
+ }
651
+ const int il = parts[0];
652
+ std::vector<int32_t> ex;
653
+ for (size_t i = 1; i < parts.size(); i++) {
654
+ ex.push_back(parts[i]);
655
+ }
656
+ std::vector<int32_t> slots(ex.size(), -1);
657
+ for (size_t i = 0; i < ex.size(); i++) {
658
+ slots[i] = P.reserve_slot(il, ex[i]);
659
+ }
660
+ const uint64_t got = P.read_expert_span(il, ex.data(), (int) ex.size(), slots.data());
661
+ printf("[sdq-span-test] L%d experts=%zu 讀取 %.2f MB\n", il, ex.size(), got / 1048576.0);
662
+ int bad = 0;
663
+ for (size_t i = 0; i < ex.size(); i++) {
664
+ std::vector<uint8_t> one((size_t) P.layer(il)->experts[ex[i]].bytes, 0);
665
+ if (!P.read_expert(il, ex[i], one.data())) {
666
+ printf(" 單獨讀取失敗\n");
667
+ return 1;
668
+ }
669
+ const uint8_t * span_ptr = P.slot_ptr(slots[i]);
670
+ size_t diff_at = (size_t) -1;
671
+ for (size_t b = 0; b < one.size(); b++) {
672
+ if (one[b] != span_ptr[b]) { diff_at = b; break; }
673
+ }
674
+ printf(" expert %4d: %s", ex[i], diff_at == (size_t) -1 ? "一致 ✅" : "不一致 ❌");
675
+ if (diff_at != (size_t) -1) {
676
+ printf("(第一個不同的 byte 在 %zu / %zu,slot=%d)", diff_at, one.size(), slots[i]);
677
+ bad++;
678
+ }
679
+ printf("\n");
680
+ }
681
+ sdq_shutdown();
682
+ llama_free(ctx);
683
+ llama_model_free(model);
684
+ llama_backend_free();
685
+ return bad ? 1 : 0;
686
+ }
687
+
688
+ // ---- 自我測試模式:--selftest IL:IE:SEED:OUT.bin
689
+ if (!opt.selftest.empty()) {
690
+ int il = 0, ie = 0, seed = 1234, n_tok = 1;
691
+ std::string outpath;
692
+ {
693
+ std::vector<std::string> parts;
694
+ std::string cur_s;
695
+ for (char c : opt.selftest + ":") {
696
+ if (c == ':') { parts.push_back(cur_s); cur_s.clear(); }
697
+ else cur_s += c;
698
+ }
699
+ if (parts.size() >= 3) {
700
+ il = atoi(parts[0].c_str());
701
+ ie = atoi(parts[1].c_str());
702
+ seed = atoi(parts[2].c_str());
703
+ }
704
+ if (parts.size() >= 4) {
705
+ outpath = parts[3];
706
+ }
707
+ if (parts.size() >= 5) {
708
+ n_tok = atoi(parts[4].c_str());
709
+ }
710
+ }
711
+ const int64_t n_embd = P.info().n_embd;
712
+ const int64_t n_ff = P.info().n_ff_exp;
713
+ std::vector<float> x((size_t) n_embd * n_tok);
714
+ uint32_t rng = (uint32_t) seed * 2654435761u + 1u;
715
+ for (auto & v : x) {
716
+ rng = rng * 1664525u + 1013904223u;
717
+ v = ((float) (rng >> 8) / (float) (1 << 24)) * 2.0f - 1.0f;
718
+ }
719
+ // xscale:把 activation 放大到 swiglu clamp 真的會生效的量級。
720
+ // 用預設的 1.0 時,4096 維點積出來的 gate/up 幾乎永遠小於 limit,
721
+ // clamp 兩種語意(silu 前夾 vs silu 後夾)會算出同一個結果 ——
722
+ // 那就等於沒有測到 clamp。用 50.0 時 gate 會到幾十,兩種語意差很多。
723
+ if (opt.selftest_xscale != 1.0f) {
724
+ for (auto & v : x) {
725
+ v *= opt.selftest_xscale;
726
+ }
727
+ }
728
+ std::vector<float> io((size_t) n_embd * n_tok * 2);
729
+ const int rc = sdq_moe_selftest(il, ie, x.data(), io.data(), n_embd, n_ff, n_tok,
730
+ opt.selftest_limit, opt.selftest_mode);
731
+ if (rc == 0 && !outpath.empty()) {
732
+ FILE * f = fopen(outpath.c_str(), "wb");
733
+ fwrite(io.data(), sizeof(float), io.size(), f);
734
+ fclose(f);
735
+ printf("[sdq] selftest 結果已寫到 %s(layer=%d expert=%d seed=%d n_embd=%lld n_ff=%lld "
736
+ "limit=%g mode=%d)\n",
737
+ outpath.c_str(), il, ie, seed, (long long) n_embd, (long long) n_ff,
738
+ opt.selftest_limit, opt.selftest_mode);
739
+ }
740
+ sdq_shutdown();
741
+ llama_free(ctx);
742
+ llama_model_free(model);
743
+ llama_backend_free();
744
+ return rc;
745
+ }
746
+
747
+ // ---- 主流程
748
+ if (!opt.prompt.empty()) {
749
+ run_prompt(opt.prompt, true);
750
+ generate(opt.n_predict);
751
+ }
752
+
753
+ if (opt.bench) {
754
+ // benchmark:固定提示詞 ×3,量 prefill / decode / 分頁指標
755
+ const std::vector<std::string> prompts = {
756
+ "請用中文簡短介紹 SSD、RAM、CPU 之間的資料分層是什麼,以及為什麼快取很重要。",
757
+ "Write a short paragraph explaining how a mixture-of-experts model routes tokens to experts.",
758
+ "請列出十種日常會用到電腦的工作,並各用一句話說明。",
759
+ };
760
+ for (int round = 0; round < 3; round++) {
761
+ fprintf(stderr, "[bench] round %d\n", round);
762
+ llama_memory_clear(llama_get_memory(ctx), true);
763
+ decoded.clear();
764
+ P.get_stats().tokens.store(0);
765
+ run_prompt(prompts[round % prompts.size()], false);
766
+ generate(48);
767
+ P.dump_stats(round == 0 ? "bench_warm" : "bench_hot");
768
+ }
769
+ }
770
+
771
+ if (opt.interactive) {
772
+ printf("\n(輸入文字開始對話;/exit 離開、/reset 重設、/stats 顯示指標)\n");
773
+ std::string line;
774
+ while (true) {
775
+ printf("\nuser> ");
776
+ fflush(stdout);
777
+ if (!std::getline(std::cin, line)) {
778
+ break;
779
+ }
780
+ if (line == "/exit" || line == "/quit") {
781
+ break;
782
+ }
783
+ if (line == "/reset") {
784
+ llama_memory_clear(llama_get_memory(ctx), true);
785
+ decoded.clear();
786
+ st.msgs.clear();
787
+ if (!opt.system.empty()) {
788
+ st.msgs.push_back({"system", opt.system.c_str()});
789
+ }
790
+ printf("(已重設)\n");
791
+ continue;
792
+ }
793
+ if (line == "/stats") {
794
+ P.dump_stats("manual");
795
+ fprintf(stderr, "hit_rate=%.1f%% arena=%zu MB RSS=%zu MB\n",
796
+ P.hit_rate() * 100, P.used_bytes() / 1048576, rss_mb());
797
+ continue;
798
+ }
799
+ if (line.empty()) {
800
+ continue;
801
+ }
802
+ run_prompt(line, true);
803
+ generate(opt.n_predict);
804
+ }
805
+ }
806
+
807
+ P.get_stats().moe_calls.fetch_add(0);
808
+ sdq_shutdown();
809
+
810
+ fprintf(stderr, "\n[sdq] ===== 總結 =====\n");
811
+ fprintf(stderr, "[sdq] prefill: %" PRIu64 " tok / %.2fs = %.1f tok/s\n",
812
+ n_prefill_tok, t_prefill, t_prefill > 0 ? n_prefill_tok / t_prefill : 0.0);
813
+ const uint64_t io_end = read_bytes_io();
814
+ fprintf(stderr, "[sdq] 全程 SSD 實際讀取: %.2f GB\n", (io_end - io_read0) / 1073741824.0);
815
+ print_mem_report("結束時");
816
+
817
+ if (logits_fp) {
818
+ fclose(logits_fp);
819
+ }
820
+ llama_sampler_free(smpl);
821
+ llama_free(ctx);
822
+ llama_model_free(model);
823
+ llama_backend_free();
824
+ return 0;
825
+ }
sdq-dsv4/sdq_moe.cpp ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SDQ 被分頁的 MoE FFN(自訂 ggml op)
2
+ //
3
+ // 為什麼要自己算:上游 build_moe_ffn() 用 ggml_mul_mat_id 直接對
4
+ // blk.N.ffn_{gate,up,down}_exps.weight 這個「三維大張量」做矩陣乘,
5
+ // 權重必須整個在 RAM 裡。20.6 GiB 的專家權重在 8 GB 機器上不可能。
6
+ //
7
+ // 這裡改成:
8
+ // 1. 路由(gate_inp matmul + softmax + top-k + 權重正規化)仍由 ggml 圖算完,
9
+ // 我們只拿 selected_experts [n_expert_used, n_tokens] 與 weights。
10
+ // 2. 每個 (layer, token) 需要的 8 個 expert 由 pager 保證在 RAM:
11
+ // 缺頁 → 從 SSD pread 進 arena(背景執行緒的預取可先後到)。
12
+ // 3. 用 ggml 自己最佳化的量化點積(ggml_vec_dot_*,AVX2/AVX512)逐 expert 算
13
+ // gate/up → SiLU → down → 加權累加。
14
+ // 4. 多執行緒:ith/nth 由 ggml 分配;分頁階段各執行緒各自 pread,餵飽 SSD 佇列深度。
15
+
16
+ #include "sdq_pager.h"
17
+
18
+ #include "ggml-cpu/quants.h"
19
+
20
+ #include <algorithm>
21
+ #include <atomic>
22
+ #include <chrono>
23
+ #include <cmath>
24
+ #include <condition_variable>
25
+ #include <cstdio>
26
+ #include <cstring>
27
+ #include <mutex>
28
+ #include <utility>
29
+ #include <string>
30
+ #include <vector>
31
+
32
+ namespace sdq {
33
+
34
+ // ---------------------------------------------------------------- 效能計時探針
35
+ // 用途:找出「自訂 MoE op」裡面時間到底花在哪裡。開 SDQ_PROFILE_TIME=1 才啟用,
36
+ // 關閉時是零成本(幾個 if constexpr 級別的布林檢查)。
37
+ struct moe_prof {
38
+ std::atomic<uint64_t> op_ns{0}; // 整個 op
39
+ std::atomic<uint64_t> page_ns{0}; // probe / reserve_slot(含鎖)
40
+ std::atomic<uint64_t> setup_ns{0}; // op 前置:unpin_all + 建 tok_of_expert + 前兩道 barrier
41
+ std::atomic<uint64_t> bar_ns{0}; // 所有 barrier 的等待時間
42
+ std::atomic<uint64_t> read_ns{0}; // read_expert_span / acquire 的 I/O
43
+ std::atomic<uint64_t> fwd_ns{0}; // expert_forward 合計
44
+ std::atomic<uint64_t> qact_ns{0}; // 其中:量化 activation
45
+ std::atomic<uint64_t> dot_ns{0}; // 其中:gate/up/down 三次點積
46
+ std::atomic<uint64_t> act_ns{0}; // 其中:SiLU + 加權
47
+ std::atomic<uint64_t> calls{0};
48
+ std::atomic<uint64_t> experts{0};
49
+ };
50
+ static moe_prof g_prof;
51
+
52
+ static inline bool prof_on() {
53
+ static const bool on = [] { const char * e = getenv("SDQ_PROFILE_TIME"); return e && *e && *e != '0'; }();
54
+ return on;
55
+ }
56
+ static inline uint64_t now_ns() {
57
+ return (uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(
58
+ std::chrono::steady_clock::now().time_since_epoch()).count();
59
+ }
60
+ #define PROF_T0_x(v) const uint64_t v = prof_on() ? now_ns() : 0
61
+ #define PROF_ADD(field, tv) do { if (prof_on()) g_prof.field += now_ns() - (tv); } while (0)
62
+
63
+ // ---------------------------------------------------------------- 執行緒柵欄
64
+
65
+ class gen_barrier {
66
+ public:
67
+ void sync(int n) {
68
+ PROF_T0_x(_tbar);
69
+ std::unique_lock<std::mutex> lk(mu_);
70
+ const unsigned long g = gen_;
71
+ if (++count_ == n) {
72
+ count_ = 0;
73
+ gen_++;
74
+ cv_.notify_all();
75
+ PROF_ADD(bar_ns, _tbar);
76
+ return;
77
+ }
78
+ cv_.wait(lk, [this, g] { return gen_ != g; });
79
+ PROF_ADD(bar_ns, _tbar);
80
+ }
81
+
82
+ private:
83
+ int count_ = 0;
84
+ unsigned long gen_ = 0;
85
+ std::mutex mu_;
86
+ std::condition_variable cv_;
87
+ };
88
+
89
+ // ---------------------------------------------------------------- 量化點積派送
90
+
91
+ using vec_dot_fn = void (*)(int, float *, size_t, const void *, size_t, const void *, size_t, int);
92
+
93
+ static vec_dot_fn dot_for(ggml_type t) {
94
+ switch (t) {
95
+ case GGML_TYPE_Q4_K: return ggml_vec_dot_q4_K_q8_K;
96
+ case GGML_TYPE_Q5_K: return ggml_vec_dot_q5_K_q8_K;
97
+ case GGML_TYPE_Q6_K: return ggml_vec_dot_q6_K_q8_K;
98
+ case GGML_TYPE_Q3_K: return ggml_vec_dot_q3_K_q8_K;
99
+ case GGML_TYPE_Q2_K: return ggml_vec_dot_q2_K_q8_K;
100
+ case GGML_TYPE_Q8_0: return ggml_vec_dot_q8_0_q8_0;
101
+ case GGML_TYPE_Q4_0: return ggml_vec_dot_q4_0_q8_0;
102
+ case GGML_TYPE_Q5_0: return ggml_vec_dot_q5_0_q8_0;
103
+ // Qwen3.8-Flash-Next 的 down_exps 用 q5_1(unsloth UD-Q4_K_XL)。
104
+ // q5_1 在 ggml 裡配的是 q8_1 activation(見 type_traits:q5_1 的
105
+ // vec_dot_type 是 q8_1),不是 q8_0 —— 配錯會整層數值錯掉。
106
+ case GGML_TYPE_Q5_1: return ggml_vec_dot_q5_1_q8_1;
107
+ case GGML_TYPE_IQ4_NL: return ggml_vec_dot_iq4_nl_q8_0;
108
+ case GGML_TYPE_IQ4_XS: return ggml_vec_dot_iq4_xs_q8_K;
109
+ // ---- DeepSeek-V4-Flash-0731(deepseek4,unsloth UD-IQ4_NL)----
110
+ // gate/up_exps = IQ3_S(42 層;layer 26 是 MXFP4)、down_exps = MXFP4。
111
+ // 這兩個型別是 UD-IQ4_NL 量化才會出現的,SDQ 原版沒認得,
112
+ // 症狀是每一層都印「不支援的專家量化型別」然後什麼都不算。
113
+ // 配對��須照 ggml 的 type_traits:
114
+ // IQ3_S.vec_dot_type = Q8_K、MXFP4.vec_dot_type = Q8_0。
115
+ case GGML_TYPE_IQ3_S: return ggml_vec_dot_iq3_s_q8_K;
116
+ case GGML_TYPE_MXFP4: return ggml_vec_dot_mxfp4_q8_0;
117
+ default: return nullptr;
118
+ }
119
+ }
120
+
121
+ // K-quant 的權重配 Q8_K activation;其餘配 Q8_0(與 ggml 的 vec_dot_type 一致)
122
+ static ggml_type act_type_for(ggml_type w) {
123
+ switch (w) {
124
+ case GGML_TYPE_Q4_K:
125
+ case GGML_TYPE_Q5_K:
126
+ case GGML_TYPE_Q6_K:
127
+ case GGML_TYPE_Q3_K:
128
+ case GGML_TYPE_Q2_K:
129
+ case GGML_TYPE_IQ4_XS:
130
+ return GGML_TYPE_Q8_K;
131
+ // q5_1 配 q8_1(q5_1 的 min 值是 fp16,不是 q8_0 的 0)
132
+ case GGML_TYPE_Q5_1:
133
+ return GGML_TYPE_Q8_1;
134
+ // IQ3_S 配 Q8_K(iq3_s 的 6-bit scale 組出來的值範圍大於 q8_0 的 ±127)
135
+ case GGML_TYPE_IQ3_S:
136
+ return GGML_TYPE_Q8_K;
137
+ // MXFP4 配 Q8_0(E2M1 的值只有 0/±0.5/±1/±1.5/±2/±3/±4/±6,q8_0 綽綽有餘)
138
+ case GGML_TYPE_MXFP4:
139
+ return GGML_TYPE_Q8_0;
140
+ default:
141
+ return GGML_TYPE_Q8_0;
142
+ }
143
+ }
144
+
145
+ // 把 activation 量化成對應的 dot 型別
146
+ // 注意:ggml_quantize_chunk() 不支援 Q8_K(Q8_K 只在 mul_mat kernel 內部產生),
147
+ // 所以這裡直接呼叫 ggml-cpu 的 quantize_row_q8_K。
148
+ static void quantize_act(ggml_type t, const float * x, void * dst, int64_t n) {
149
+ switch (t) {
150
+ case GGML_TYPE_Q8_K: quantize_row_q8_K(x, dst, n); break;
151
+ case GGML_TYPE_Q8_1: quantize_row_q8_1(x, dst, n); break;
152
+ case GGML_TYPE_Q8_0: quantize_row_q8_0(x, dst, n); break;
153
+ default: ggml_quantize_chunk(t, x, dst, 0, 1, n, nullptr); break;
154
+ }
155
+ }
156
+
157
+ // ⚠️ 不要在這裡「批次化」點積。
158
+ // ggml 的 x86 版 K-quant kernel(ggml/src/ggml-cpu/arch/x86/quants.c)開頭就是
159
+ // assert(nrc == 1); UNUSED(nrc); UNUSED(bx); UNUSED(by); UNUSED(bs);
160
+ // 只有 ARM 的 SVE+MATMUL 版本才支援 nrc==2。而 Release 編譯(-DNDEBUG)會把 assert
161
+ // 編譯掉,於是「傳 nrc=16」不會報錯、只會安靜地算第 1 列,其餘 15 列留在記憶體裡
162
+ // 是舊資料 —— 產出看起來合理但數值全錯。
163
+ // (本專案真的踩過:微基準測量到「nrc=16 快 16 倍」,其實是「只做了 1/16 的工作」,
164
+ // 而且權重填 0x5A 讓 checksum 兩邊都是 0,完美掩蓋了錯誤。教訓見 docs/04。)
165
+ // 想要真的多列 SIMD 效能,只能自己寫 AVX512-VNNI 的 kernel,不是靠改參數。
166
+
167
+ static inline void dot_row(vec_dot_fn fn, const uint8_t * row, const void * xq,
168
+ size_t stride_row, int n, float & acc) {
169
+ float s = 0.0f;
170
+ fn(n, &s, 0, row, stride_row, xq, 0, 1);
171
+ acc += s;
172
+ }
173
+
174
+ // 一次算 SDQ_DOT_BATCH 個輸出列。
175
+ // 為什麼需要:ggml 的 K-quant 點積 kernel 是為「多列」設計的,nrc>1 會走完全不同的
176
+ // SIMD 路徑。實測(tools 裡的 microdot):nrc=1 只有 24~28 GMAC/s,nrc=16 有
177
+ // 347~438 GMAC/s(快 14~16 倍),而且 **checksum 完全相同**(逐位元組等價)。
178
+ // 舊程式逐列呼叫 nrc=1,等於把 14 倍的效能丟掉。
179
+ static const int SDQ_DOT_BATCH = 16;
180
+
181
+ static inline void dot_rows(vec_dot_fn fn, const uint8_t * rows, size_t row_stride,
182
+ const void * xq, int n, int64_t n_rows, float * out) {
183
+ int64_t r = 0;
184
+ for (; r + SDQ_DOT_BATCH <= n_rows; r += SDQ_DOT_BATCH) {
185
+ fn(n, out + r, 1, rows + (size_t) r * row_stride, row_stride, xq, 0, SDQ_DOT_BATCH);
186
+ }
187
+ for (; r < n_rows; r++) {
188
+ fn(n, out + r, 0, rows + (size_t) r * row_stride, row_stride, xq, 0, 1);
189
+ }
190
+ }
191
+
192
+ // ---------------------------------------------------------------- 路由紀錄(SDQ_ROUTE_LOG)
193
+ // 用途:量測「連續 k 個 token 的 expert 集合重疊多少」。
194
+ // 這決定多 token 批次(投機解碼 / 批次解碼)能不能省下 SSD 讀取:
195
+ // 每層的關鍵路徑是「有沒有任何一個 expert 沒命中」,所以 U(k) < 8k 就有賺。
196
+ // 格式(每行一次 op 呼叫,空白分欄):
197
+ // <layer> <n_tok> <t0:e0,e1,...> <t1:...> ...
198
+ static void sdq_route_log(int il, int64_t n_tok, const ggml_tensor * ids,
199
+ size_t ids_nb0, size_t ids_nb1, int n_used) {
200
+ static const char * path = getenv("SDQ_ROUTE_LOG");
201
+ if (!path || !*path) {
202
+ return;
203
+ }
204
+ static std::mutex mtx;
205
+ static FILE * f = nullptr;
206
+ {
207
+ std::lock_guard<std::mutex> lk(mtx);
208
+ if (!f) {
209
+ f = fopen(path, "w");
210
+ }
211
+ if (!f) {
212
+ return;
213
+ }
214
+ fprintf(f, "%d %lld", il, (long long) n_tok);
215
+ const uint8_t * base = (const uint8_t *) ids->data;
216
+ for (int64_t t = 0; t < n_tok; t++) {
217
+ fprintf(f, " %lld:", (long long) t);
218
+ for (int k = 0; k < n_used; k++) {
219
+ const int32_t e = *(const int32_t *) (base + (size_t) k * ids_nb0 + (size_t) t * ids_nb1);
220
+ fprintf(f, "%s%d", k ? "," : "", e);
221
+ }
222
+ }
223
+ fputc('\n', f);
224
+ }
225
+ }
226
+
227
+ // ---------------------------------------------------------------- SiLU-gated 激活
228
+ //
229
+ // ⚠️ 這裡的 clamp 語意**必須**逐字對齊上游,否則每一層的數值都是錯的(而且是
230
+ // 「看起來合理」的錯:文字大致通順、只是品質變差)。
231
+ //
232
+ // deepseek4 / glm5_next / maple 走的是 ggml 的融合算子 ggml_swiglu_clamp
233
+ // (ggml/src/ggml-cpu/ops.cpp):
234
+ // gate = min(gate, limit); // ← 只有上界,不夾下界
235
+ // up = clamp(up, -limit, limit); // 對稱
236
+ // out = silu(gate) * up;
237
+ //
238
+ // SDQ 原版(qwen4exp 用的是上游的另一條分支)夾的是 **silu 之後** 的 gate,而且是
239
+ // 對稱的:
240
+ // gc = clamp(silu(gate), -limit, limit);
241
+ // out = gc * clamp(up, -limit, limit);
242
+ // 兩者數學上不等價,而且 clamp 的位置不同(gate 前 vs gate 後)。
243
+ // 這個 repo 預設走 mode 1(deepseek4 實測用的那一條);mode 0 保留給舊模型。
244
+ // ---------------------------------------------------------------- 每層執行紀錄
245
+
246
+ // 重要:input 張量的指標**不能**在建圖時暫存起來 —— llama.cpp 每次 decode 都會
247
+ // 重新建圖,graph context 的配置器可能把張量搬到不同位址(形狀不同時尤其明顯)。
248
+ // 所以 op 執行時一律從 dst->src[] 取得,這才是唯一可靠的來源。
249
+ struct layer_rec {
250
+ int il = -1;
251
+ int64_t n_expert = 0;
252
+ int64_t n_expert_used = 0;
253
+ float swiglu_limit = 0.0f;
254
+ int swiglu_mode = SDQ_SWIGLU_LEGACY_POST_SILU;
255
+
256
+ // 每個 expert 需要處理的 (token, topk 位置) 配對
257
+ std::vector<std::vector<std::pair<int32_t, int32_t>>> tok_of_expert;
258
+ // 每個 token 用到的 (expert, topk 位置) —— token 切分時用(避免輸出競爭)
259
+ std::vector<std::vector<std::pair<int32_t, int32_t>>> exp_of_tok;
260
+ std::vector<int32_t> need; // 本層要用的 expert 編號
261
+ std::vector<int32_t> need_sorted; // 依編號排序(合併讀取用)
262
+ std::vector<int32_t> need_slot; // 對應的 arena 槽位
263
+ std::vector<std::vector<float>> partial; // mode A:每個執行緒一份累加緩衝
264
+ std::vector<int32_t> slot_of_expert; // [expert] -> arena 槽位(-1 = 沒分頁)
265
+ std::vector<int32_t> need_slot_of_expert; // 合併讀取時暫存槽位
266
+ std::vector<int32_t> miss_sorted; // 需要從 SSD 讀的 expert(已排序)
267
+ gen_barrier bar;
268
+ std::mutex mtx;
269
+ bool warned = false;
270
+ };
271
+
272
+ static std::vector<layer_rec *> g_recs;
273
+
274
+ static layer_rec * rec_for(int il) {
275
+ static std::mutex m;
276
+ std::lock_guard<std::mutex> lk(m);
277
+ if ((int) g_recs.size() <= il) {
278
+ g_recs.resize(il + 1, nullptr);
279
+ }
280
+ if (!g_recs[il]) {
281
+ g_recs[il] = new layer_rec();
282
+ g_recs[il]->il = il;
283
+ }
284
+ return g_recs[il];
285
+ }
286
+
287
+ // ---------------------------------------------------------------- 自訂 op 本體
288
+
289
+ // mode C(預設,partial 緩衝 <= 上限):依 expert 流水線,取→算交錯、無中途 barrier。
290
+ // mode B(partial 緩衝太大時的退路):先分頁完再一起算,會有 barrier 等待。
291
+ // 上限給一個很大的值=永遠走 mode C;給 0=強制 mode B(除錯用)。
292
+ static size_t env_partial_cap_mb() {
293
+ const char * e = getenv("SDQ_PARTIAL_CAP_MB");
294
+ return e ? (size_t) atoi(e) : 4096; // MB,預設 4096 MB → 一律 mode C
295
+ }
296
+
297
+ static inline float sdq_swiglu(float g, float u, float limit, int mode) {
298
+ if (limit <= 1e-6f) {
299
+ return g / (1.0f + std::exp(-g)) * u;
300
+ }
301
+ if (mode == SDQ_SWIGLU_GGML_FUSED_CLAMP) {
302
+ const float gc = std::min(g, limit);
303
+ const float uc = std::max(-limit, std::min(limit, u));
304
+ return gc / (1.0f + std::exp(-gc)) * uc;
305
+ }
306
+ const float uc = std::max(-limit, std::min(limit, u));
307
+ float gc = g / (1.0f + std::exp(-g));
308
+ gc = std::max(-limit, std::min(limit, gc));
309
+ return gc * uc;
310
+ }
311
+
312
+ // ---------------------------------------------------------------- 單一 expert 的數學
313
+ // 從 arena 的槽位記憶體算出一個 expert 對多個 token 的輸出(加權後直接累加到 out)
314
+ static void expert_forward(const expert_desc & ed, const uint8_t * base,
315
+ const float * cur, int64_t n_embd,
316
+ const std::vector<std::pair<int32_t, int32_t>> & toks,
317
+ const float * wts, size_t wts_nb0, size_t wts_nb1, int /*n_used*/,
318
+ float swiglu_limit, int swiglu_mode,
319
+ float * out, std::vector<uint8_t> * scratch) {
320
+ int64_t gate_rows = ed.ne1;
321
+ int64_t up_rows = ed.ne1;
322
+ size_t gate_row = ggml_row_size(ed.type[part::GATE], ed.ne0);
323
+ size_t up_row = gate_row;
324
+ if (ed.fused_gate_up) {
325
+ gate_rows = ed.ne1 / 2;
326
+ up_rows = gate_rows;
327
+ } else {
328
+ up_row = ggml_row_size(ed.type[part::UP], ed.ne0);
329
+ }
330
+
331
+ const vec_dot_fn f_gate = dot_for(ed.type[part::GATE]);
332
+ const vec_dot_fn f_up = ed.fused_gate_up ? f_gate : dot_for(ed.type[part::UP]);
333
+ const vec_dot_fn f_down = dot_for(ed.type[part::DOWN]);
334
+ if (!f_gate || !f_up || !f_down) {
335
+ fprintf(stderr, "[sdq] 不支援的專家量化型別:gate=%s up=%s down=%s\n",
336
+ ggml_type_name(ed.type[part::GATE]),
337
+ ggml_type_name(ed.type[part::UP]),
338
+ ggml_type_name(ed.type[part::DOWN]));
339
+ return;
340
+ }
341
+
342
+ const uint8_t * wg = base + ed.slot_off[part::GATE];
343
+ const uint8_t * wu = base + ed.slot_off[part::UP];
344
+ const uint8_t * wd = base + ed.slot_off[part::DOWN];
345
+ const size_t down_row = ggml_row_size(ed.type[part::DOWN], ed.down_ne0);
346
+
347
+ const ggml_type act_gate = act_type_for(ed.type[part::GATE]);
348
+ const ggml_type act_down = act_type_for(ed.type[part::DOWN]);
349
+ const size_t xq_sz = ggml_row_size(act_gate, ed.ne0);
350
+ const size_t hq_sz = ggml_row_size(act_down, ed.down_ne0);
351
+ if (scratch->size() < xq_sz + hq_sz) {
352
+ scratch->assign(xq_sz + hq_sz, 0);
353
+ }
354
+ uint8_t * xq = scratch->data();
355
+ uint8_t * hq = xq + xq_sz;
356
+
357
+ std::vector<float> gate((size_t) gate_rows);
358
+ std::vector<float> upv((size_t) up_rows);
359
+ std::vector<float> h((size_t) ed.down_ne0);
360
+
361
+ PROF_T0_x(_ta); // expert_forward 總計時
362
+ for (const auto & tk : toks) {
363
+ const int32_t t = tk.first;
364
+ const int32_t k = tk.second;
365
+
366
+ PROF_T0_x(_tb); // 量化 activation
367
+ quantize_act(act_gate, cur + (size_t) t * n_embd, xq, ed.ne0);
368
+ PROF_ADD(qact_ns, _tb);
369
+
370
+ PROF_T0_x(_tc); // gate / up / down 三次點積
371
+ for (int64_t r = 0; r < gate_rows; r++) {
372
+ float acc = 0.0f;
373
+ dot_row(f_gate, wg + r * gate_row, xq, gate_row, (int) ed.ne0, acc);
374
+ gate[(size_t) r] = acc;
375
+ }
376
+ for (int64_t r = 0; r < up_rows; r++) {
377
+ float acc = 0.0f;
378
+ const uint8_t * src = ed.fused_gate_up ? (wg + (gate_rows + r) * gate_row)
379
+ : (wu + r * up_row);
380
+ dot_row(f_up, src, xq, ed.fused_gate_up ? gate_row : up_row, (int) ed.ne0, acc);
381
+ upv[(size_t) r] = acc;
382
+ }
383
+
384
+ PROF_ADD(dot_ns, _tc);
385
+
386
+ PROF_T0_x(_td); // SiLU + 加權求和
387
+ for (int64_t r = 0; r < ed.down_ne0; r++) {
388
+ h[(size_t) r] = sdq_swiglu(gate[(size_t) r], upv[(size_t) r],
389
+ swiglu_limit, swiglu_mode);
390
+ }
391
+ quantize_act(act_down, h.data(), hq, ed.down_ne0);
392
+
393
+ PROF_ADD(act_ns, _td);
394
+
395
+ const float w = *(const float *) ((const uint8_t *) wts + k * wts_nb0 + (size_t) t * wts_nb1);
396
+ float * o = out + (size_t) t * n_embd;
397
+ for (int64_t r = 0; r < ed.down_ne1; r++) {
398
+ float acc = 0.0f;
399
+ dot_row(f_down, wd + r * down_row, hq, down_row, (int) ed.down_ne0, acc);
400
+ o[r] += acc * w;
401
+ }
402
+ }
403
+ PROF_ADD(fwd_ns, _ta);
404
+ }
405
+
406
+
407
+ static void moe_custom_op(ggml_tensor * dst, int ith, int nth, void * userdata) {
408
+ layer_rec * R = (layer_rec *) userdata;
409
+ pager & P = pager::instance();
410
+
411
+ // 從 op 的 src 取得輸入(保證是「這一次」真的被計算的張量)
412
+ const ggml_tensor * src_cur = dst->src[0];
413
+ const ggml_tensor * src_ids = dst->src[1];
414
+ const ggml_tensor * src_wts = dst->src[2];
415
+ if (!src_cur || !src_ids || !src_wts) {
416
+ if (ith == 0) {
417
+ fprintf(stderr, "[sdq] op 缺少 src(%d)\n", R->il);
418
+ }
419
+ return;
420
+ }
421
+
422
+ const layer_desc * L = P.layer(R->il);
423
+ PROF_T0_x(_tg); // op 總計時
424
+ g_prof.calls++;
425
+ const int64_t n_embd = src_cur->ne[0];
426
+ const int64_t n_tok = src_cur->ne[2];
427
+ const int64_t n_used = R->n_expert_used;
428
+ // ids 是 2D [n_expert_used, n_tokens] → 用 nb[0]/nb[1]
429
+ // wts 是 3D [1, n_expert_used, n_tokens] → 用 nb[1]/nb[2](nb[0] 會是 4 而不是 n_used*4!)
430
+ const size_t ids_nb0 = src_ids->nb[0], ids_nb1 = src_ids->nb[1];
431
+ const size_t wts_nb0 = src_wts->nb[src_wts->ne[0] == 1 ? 1 : 0];
432
+ const size_t wts_nb1 = src_wts->nb[src_wts->ne[0] == 1 ? 2 : 1];
433
+ const float * cur = (const float *) src_cur->data;
434
+ const float * wts = (const float *) src_wts->data;
435
+
436
+ if (!L || L->experts.empty() || dst->type != GGML_TYPE_F32) {
437
+ if (ith == 0 && !R->warned) {
438
+ R->warned = true;
439
+ fprintf(stderr, "[sdq] 層 %d 沒有可用的分頁描述\n", R->il);
440
+ }
441
+ return;
442
+ }
443
+
444
+ if (R->il == 0 && getenv("SDQ_DBG_OP")) {
445
+ fprintf(stderr, "[sdq-op] L%d n_tok=%lld nth=%d need=%d ids_nb1=%zu wts_nb1=%zu\n",
446
+ R->il, (long long) n_tok, nth, (int) R->need.size(), ids_nb1, wts_nb1);
447
+ }
448
+ // ---- 1) 解除上一層的 pin(mode B 直接累加輸出,需要先清空)
449
+ PROF_T0_x(_tsetup);
450
+ if (ith == 0) {
451
+ P.unpin_all();
452
+ }
453
+ if (nth > 1) { R->bar.sync(nth); }
454
+
455
+ // ---- 2) 依 expert 分組 token(只有執行緒 0 做,其餘在此等待)
456
+ if (ith == 0) {
457
+ std::lock_guard<std::mutex> lk(R->mtx);
458
+ R->tok_of_expert.assign((size_t) R->n_expert, {});
459
+ R->exp_of_tok.assign((size_t) n_tok, {});
460
+ const uint8_t * ids_base = (const uint8_t *) src_ids->data;
461
+ for (int64_t t = 0; t < n_tok; t++) {
462
+ for (int64_t k = 0; k < n_used; k++) {
463
+ const int32_t e = *(const int32_t *) (ids_base + k * ids_nb0 + (size_t) t * ids_nb1);
464
+ if (e >= 0 && e < R->n_expert) {
465
+ auto & v = R->tok_of_expert[(size_t) e];
466
+ bool seen = false;
467
+ for (const auto & p : v) {
468
+ if (p.first == (int32_t) t) { seen = true; break; }
469
+ }
470
+ if (!seen) {
471
+ v.emplace_back((int32_t) t, (int32_t) k);
472
+ }
473
+ R->exp_of_tok[(size_t) t].emplace_back(e, (int32_t) k);
474
+ }
475
+ }
476
+ }
477
+ R->need.clear();
478
+ for (int64_t e = 0; e < R->n_expert; e++) {
479
+ if (!R->tok_of_expert[(size_t) e].empty()) {
480
+ R->need.push_back((int32_t) e);
481
+ }
482
+ }
483
+ R->need_sorted = R->need;
484
+ std::sort(R->need_sorted.begin(), R->need_sorted.end());
485
+ R->need_slot.assign(R->need.size(), -1); // 只能由 ith==0 配置,其餘執行緒唯讀
486
+ R->slot_of_expert.assign((size_t) R->n_expert, -1);
487
+ R->need_slot_of_expert.assign((size_t) R->n_expert, -1);
488
+ R->miss_sorted.clear();
489
+ R->miss_sorted.reserve((size_t) R->n_expert);
490
+ if (R->partial.size() < 64) {
491
+ R->partial.resize(64);
492
+ }
493
+ sdq_route_log(R->il, n_tok, src_ids, ids_nb0, ids_nb1, (int) n_used);
494
+ }
495
+ if (nth > 1) { R->bar.sync(nth); }
496
+ PROF_ADD(setup_ns, _tsetup);
497
+
498
+ const int n_need = (int) R->need.size();
499
+
500
+ // ---- 3+4) 分頁與計算融合成一條 pipeline
501
+ //
502
+ // 關鍵效能點:不要「先把整層的 expert 都讀完再開始算」。做法是每個執行緒
503
+ // 自己依序處理它負責的 expert:讀一個 → 立刻算它 → 再讀下一個。
504
+ // 這樣 SSD 讀取和 CPU 計算自然重疊,而且 8 條執行緒各自發出 pread,
505
+ // SSD 佇列深度也夠(不需要再靠 queue depth)。
506
+ //
507
+ // 平行切分:mode A 依 expert 切(每個執行緒有自己的累加緩衝,最後相加);
508
+ // mode B 依 token 切(同一個 token 的所有 expert 由同一執行緒算,
509
+ // 但必須先把該 token 的 expert 都備齊,所以先分頁再計算)。
510
+ const int64_t out_elems = n_embd * n_tok;
511
+ const size_t partial_bytes = (size_t) out_elems * 4 * (size_t) nth;
512
+ const bool mode_a = partial_bytes <= (size_t) env_partial_cap_mb() << 20;
513
+ float * out = (float *) dst->data;
514
+
515
+ if (!mode_a) {
516
+ memset(dst->data, 0, (size_t) ggml_nbytes(dst));
517
+ }
518
+
519
+ if (mode_a) {
520
+ // ===================== mode C(預設):依 expert 流水線 =====================
521
+ // 三個設計重點,每個都來自實測:
522
+ //
523
+ // (1) **取→算交錯,中間沒有 barrier**。
524
+ // 舊的 mode B 是「先把整層 8 個 expert 全部讀完 → barrier → 才開始算」。
525
+ // 量測顯示 decode 有 66% 的時間卡在 barrier 上等最慢的執行緒
526
+ // (SDQ_PROFILE_TIME 的 barrier 等待 = op 時間的 70%)。改成交錯後
527
+ // 讀取與計算自然重疊,實測 2.79 → 3.49 tok/s。
528
+ //
529
+ // (2) **分頁走 acquire(),不用 reserve_slot + read_expert_span**。
530
+ // reserve_slot() 會在資料還沒讀進來之前就把 (layer,expert) publish 到
531
+ // page_ 表;一旦該層的 pread 失敗或被中斷,這個槽就會留在 page_ 裡
532
+ // 卻是舊資料,之後的 probe() 會把它當成命中 → 靜默算錯。
533
+ // acquire() 是「先讀完再 publish」,成本相同但不會有這個失效模式。
534
+ //
535
+ // (3) **i += nth 交錯分配**,不是切成連續區塊。
536
+ // 每個 expert 的 SSD 延遲差異很大,交錯分配讓快執行緒自然多幫忙。
537
+ //
538
+ // 輸出不直接寫 dst(同一個 token 的不同 expert 會撞在一起),
539
+ // 所以仍然用「每執行緒一份累加緩衝 + 最後歸約」。
540
+ std::vector<float> & mine = R->partial[(size_t) ith];
541
+ mine.assign((size_t) out_elems, 0.0f);
542
+ std::vector<uint8_t> scratch;
543
+
544
+ // 在取本層的 expert **之前**先替下一層排預取。
545
+ //
546
+ // 實測結論(S7):**預設關閉,而且量到開啟反而慢 8%。**
547
+ // 預測來自 prev_used_[il+1](上一個 token 在該層選的專家)。想用它躲在
548
+ // 「本層的讀取+計算」後面,但量到的結果是:
549
+ // 開(5.68 tok/s,預取使用率 0.48 %)vs 關(6.20 tok/s,0.28 %)
550
+ // 命中率幾乎一樣(70.5 % vs 70.7 %),但 SSD 位元組從 201.5 升到 216.0 MB/token
551
+ // → 預測不夠準,多讀的全是白讀,而且還要和本層的 demand 讀搶頻寬。
552
+ // 另外開啟時同一提示詞的輸出不一致 → 預取佔用的槽位(busy)會讓
553
+ // acquire() 偶爾拿不到槽位而**靜默跳過一個 expert**,這是正確性問題。
554
+ //
555
+ // 保留程式碼是為了讓後人不用重新發明;文件見 docs/06_slots.md §4。
556
+ // SDQ_PREFETCH_LAYER_AHEAD=1 可以打開來重現那個負結果。
557
+ static const bool layer_ahead = [] {
558
+ const char * e = getenv("SDQ_PREFETCH_LAYER_AHEAD");
559
+ return e ? atoi(e) != 0 : false;
560
+ }();
561
+ if (layer_ahead && ith == 0) {
562
+ P.prefetch_layer_ahead(R->il + 1);
563
+ }
564
+
565
+ for (int i = ith; i < n_need; i += nth) {
566
+ const int32_t e = R->need[(size_t) i];
567
+ const int32_t sl = P.acquire(R->il, e, true);
568
+ if (sl < 0) {
569
+ continue;
570
+ }
571
+ P.note_used(R->il, e);
572
+ expert_forward(L->experts[(size_t) e], P.slot_ptr(sl), cur, n_embd,
573
+ R->tok_of_expert[(size_t) e], wts, wts_nb0, wts_nb1,
574
+ (int) n_used, R->swiglu_limit, R->swiglu_mode,
575
+ mine.data(), &scratch);
576
+ // expert_forward 回傳後這個槽位就沒人讀了(最後的歸約只讀 mine),
577
+ // 所以立刻放掉 pin,不要讓它被釘到下一層開頭。
578
+ // 不放的話 prefill 一層會把整層用到的 expert 全部釘住(最多 256 個),
579
+ // 大槽位不夠時就會中止(見 pager::release 的說明)。
580
+ P.release(sl);
581
+ }
582
+
583
+ if (nth > 1) { R->bar.sync(nth); }
584
+ for (int64_t idx = ith; idx < out_elems; idx += nth) {
585
+ float acc = 0.0f;
586
+ for (int k = 0; k < nth; k++) {
587
+ acc += R->partial[(size_t) k][(size_t) idx];
588
+ }
589
+ out[idx] = acc;
590
+ }
591
+ } else {
592
+ // mode B:token 多到 partial 緩衝太大時,依 token 切分
593
+ // 先把所有需要的 expert 分頁(各執行緒平行 pread)
594
+ {
595
+ PROF_T0_x(_th); // 分頁 / SSD 讀取
596
+ for (int i = ith; i < n_need; i += nth) {
597
+ const int32_t e = R->need[(size_t) i];
598
+ const int s = P.acquire(R->il, e, true);
599
+ g_prof.experts++;
600
+ R->need_slot[(size_t) i] = s;
601
+ if (s >= 0) {
602
+ R->slot_of_expert[(size_t) e] = s;
603
+ P.note_used(R->il, e);
604
+ }
605
+ }
606
+ PROF_ADD(read_ns, _th);
607
+ }
608
+ if (nth > 1) { R->bar.sync(nth); }
609
+ const float * cur_p = (const float *) src_cur->data;
610
+ const float * wts_p = (const float *) src_wts->data;
611
+ for (int64_t t = ith; t < n_tok; t += nth) {
612
+ std::vector<uint8_t> scratch;
613
+ for (const auto & ek : R->exp_of_tok[(size_t) t]) {
614
+ const int slot = R->slot_of_expert[(size_t) ek.first];
615
+ if (slot < 0) {
616
+ continue;
617
+ }
618
+ std::vector<std::pair<int32_t, int32_t>> one = {{(int32_t) t, ek.second}};
619
+ expert_forward(L->experts[(size_t) ek.first], P.slot_ptr(slot), cur_p, n_embd,
620
+ one, wts_p, wts_nb0, wts_nb1, (int) n_used,
621
+ R->swiglu_limit, R->swiglu_mode, out, &scratch);
622
+ }
623
+ }
624
+ }
625
+
626
+ // 除錯:把某一層的實際輸入/輸出 dump 出來,用 NumPy 對照。
627
+ // 必須先 barrier:上面的歸約是由各執行緒分片寫 out 的,沒有這一道會讀到半成品。
628
+ if (nth > 1) { R->bar.sync(nth); }
629
+ if (const char * dump_path = getenv("SDQ_MOE_DUMP")) {
630
+ const int dump_layer = getenv("SDQ_MOE_DUMP_LAYER") ? atoi(getenv("SDQ_MOE_DUMP_LAYER")) : -1;
631
+ if ((dump_layer < 0 || R->il == dump_layer) && ith == 0) {
632
+ // 檔名帶上 op 呼叫序號:不然同一層的每次 op 都寫進同一個檔,只剩最後一次
633
+ // (通常是 decode 的 n_tok=1),prefill 那批 n_tok=128 的就看不到 ——
634
+ // 而 prefill 才是 arena 壓力最大、hot/cold 差最大的地方。
635
+ static std::atomic<uint64_t> dump_seq{0};
636
+ std::string path = std::string(dump_path) + "." + std::to_string(R->il) +
637
+ "." + std::to_string(dump_seq++);
638
+ FILE * f = fopen(path.c_str(), "wb");
639
+ if (f) {
640
+ const int32_t meta[6] = {(int32_t) n_embd, (int32_t) n_tok, (int32_t) n_used,
641
+ (int32_t) R->n_expert, 1, 2};
642
+ fwrite(meta, sizeof(int32_t), 6, f);
643
+ fwrite(cur, sizeof(float), (size_t) (n_embd * n_tok), f);
644
+ const uint8_t * ids_base = (const uint8_t *) src_ids->data;
645
+ for (int64_t t = 0; t < n_tok; t++) {
646
+ for (int64_t k = 0; k < n_used; k++) {
647
+ const int32_t e = *(const int32_t *) (ids_base + k * ids_nb0 + (size_t) t * ids_nb1);
648
+ fwrite(&e, sizeof(int32_t), 1, f);
649
+ }
650
+ }
651
+ fwrite(wts, sizeof(float), (size_t) (n_used * n_tok), f);
652
+ fwrite(out, sizeof(float), (size_t) (n_embd * n_tok), f);
653
+ fclose(f);
654
+ fprintf(stderr, "[sdq] dumped layer %d -> %s\n", R->il, path.c_str());
655
+ }
656
+ }
657
+ }
658
+
659
+ if (nth > 1) { R->bar.sync(nth); }
660
+ PROF_ADD(op_ns, _tg);
661
+ }
662
+
663
+ // 輸出計時結果(sdq_shutdown 呼叫)
664
+ void sdq_print_time_profile() {
665
+ if (!prof_on()) {
666
+ return;
667
+ }
668
+ const double ms = 1e6;
669
+ auto v = [ms](const std::atomic<uint64_t> & a) { return a.load() / ms; };
670
+ const double op = v(g_prof.op_ns);
671
+ const double fwd = v(g_prof.fwd_ns);
672
+ fprintf(stderr, "[sdq-prof] 自訂 MoE op(%llu 次呼叫、%llu 個 expert 分頁)\n",
673
+ (unsigned long long) g_prof.calls.load(), (unsigned long long) g_prof.experts.load());
674
+ fprintf(stderr, "[sdq-prof] op 合計 %9.1f ms\n", op);
675
+ fprintf(stderr, "[sdq-prof] expert_forward %9.1f ms (%5.1f%%) 其中:\n", fwd, op > 0 ? 100.0 * fwd / op : 0.0);
676
+ fprintf(stderr, "[sdq-prof] 量化 activation%9.1f ms (%5.1f%% of fwd)\n", v(g_prof.qact_ns), fwd > 0 ? 100.0 * v(g_prof.qact_ns) / fwd : 0.0);
677
+ fprintf(stderr, "[sdq-prof] 三次點積 %9.1f ms (%5.1f%% of fwd)\n", v(g_prof.dot_ns), fwd > 0 ? 100.0 * v(g_prof.dot_ns) / fwd : 0.0);
678
+ fprintf(stderr, "[sdq-prof] SiLU+加權 %9.1f ms (%5.1f%% of fwd)\n", v(g_prof.act_ns), fwd > 0 ? 100.0 * v(g_prof.act_ns) / fwd : 0.0);
679
+ fprintf(stderr, "[sdq-prof] SSD 讀取/分頁 %9.1f ms (%5.1f%%)\n", v(g_prof.read_ns), op > 0 ? 100.0 * v(g_prof.read_ns) / op : 0.0);
680
+ fprintf(stderr, "[sdq-prof] 前置 setup %9.1f ms (%5.1f%%)\n", v(g_prof.setup_ns), op > 0 ? 100.0 * v(g_prof.setup_ns) / op : 0.0);
681
+ fprintf(stderr, "[sdq-prof] barrier 等待 %9.1f ms (%5.1f%%)\n", v(g_prof.bar_ns), op > 0 ? 100.0 * v(g_prof.bar_ns) / op : 0.0);
682
+ }
683
+
684
+ // ---------------------------------------------------------------- graph 建構
685
+
686
+ ggml_tensor * sdq_build_moe(ggml_cgraph * gf, ggml_context * ctx, ggml_tensor * cur,
687
+ ggml_tensor * ids, ggml_tensor * weights, int il, int64_t n_expert,
688
+ int64_t n_expert_used, float swiglu_limit,
689
+ int swiglu_mode) {
690
+ layer_rec * R = rec_for(il);
691
+ R->n_expert = n_expert;
692
+ R->n_expert_used = n_expert_used;
693
+ R->swiglu_limit = swiglu_limit;
694
+ R->swiglu_mode = swiglu_mode;
695
+
696
+ // 呼叫端給的是 2D [n_embd, n_tokens],這裡跟上游一樣先 reshape 成 3D
697
+ // [n_embd, 1, n_tokens](上游 mul_mat_id 路徑也是這樣拿 token 數的)
698
+ ggml_tensor * cur3 = (cur->ne[1] == 1) ? cur : ggml_reshape_3d(ctx, cur, cur->ne[0], 1, cur->ne[1]);
699
+ const int64_t n_tok = cur3->ne[2];
700
+
701
+ ggml_tensor * args[3] = { cur3, ids, weights };
702
+ ggml_tensor * out = ggml_custom_4d(ctx, GGML_TYPE_F32, cur3->ne[0], n_tok, 1, 1,
703
+ args, 3, moe_custom_op, GGML_N_TASKS_MAX, R);
704
+ ggml_set_name(out, "sdq_moe_out");
705
+ // 關鍵:把這個節點註冊進圖,ggml 才會為它配置獨立的 compute buffer。
706
+ // 少了這一行,layer>=1 的輸出會被其他張量覆蓋(layer 0 僥倖正確)。
707
+ ggml_build_forward_expand(gf, out);
708
+ return out;
709
+ }
710
+
711
+ } // namespace sdq
712
+ namespace sdq {
713
+ // 自我測試:對指定 (layer, expert) 用 pager 的資料算一次 MoE 輸出,
714
+ // 讓 Python 用純 NumPy 從同一段 GGUF 位元組重算並比對。
715
+ // 輸出:out[0..n_embd) = 輸出,out[n_embd..2*n_embd) = 輸入 activation
716
+ int sdq_moe_selftest(int il, int ie, const float * x, float * out_io,
717
+ int64_t n_embd, int64_t n_ff, int n_tok,
718
+ float swiglu_limit, int swiglu_mode) {
719
+ pager & P = pager::instance();
720
+ const layer_desc * L = P.layer(il);
721
+ if (!L || ie < 0 || ie >= (int) L->experts.size()) {
722
+ fprintf(stderr, "[sdq-selftest] 層/expert 不存在:%d/%d\n", il, ie);
723
+ return 1;
724
+ }
725
+ const int slot = P.acquire(il, ie, true);
726
+ if (slot < 0) {
727
+ fprintf(stderr, "[sdq-selftest] 分頁失敗\n");
728
+ return 2;
729
+ }
730
+ const expert_desc & ed = L->experts[(size_t) ie];
731
+ if (ed.ne0 != n_embd || ed.ne1 != n_ff) {
732
+ fprintf(stderr, "[sdq-selftest] 維度不符:ne0=%lld ne1=%lld(預期 %lld/%lld)\n",
733
+ (long long) ed.ne0, (long long) ed.ne1, (long long) n_embd, (long long) n_ff);
734
+ return 3;
735
+ }
736
+ std::vector<std::pair<int32_t, int32_t>> toks;
737
+ std::vector<float> wts((size_t) n_tok, 1.0f);
738
+ for (int t = 0; t < n_tok; t++) {
739
+ toks.emplace_back(t, 0);
740
+ }
741
+ std::vector<float> out((size_t) n_embd * n_tok, 0.0f);
742
+ std::vector<uint8_t> scratch;
743
+ expert_forward(ed, P.slot_ptr(slot), x, n_embd, toks, wts.data(), 4, 4, 1,
744
+ swiglu_limit, swiglu_mode, out.data(), &scratch);
745
+ for (int64_t t = 0; t < n_tok; t++) {
746
+ for (int64_t i = 0; i < n_embd; i++) {
747
+ out_io[t * n_embd + i] = out[(size_t) t * n_embd + i];
748
+ out_io[n_tok * n_embd + t * n_embd + i] = x[(size_t) t * n_embd + i];
749
+ }
750
+ }
751
+ P.unpin_all();
752
+ fprintf(stderr, "[sdq-selftest] L%d E%d n_tok=%d 計算完成(gate %s / up %s / down %s,"
753
+ "swiglu_limit=%g mode=%d)\n",
754
+ il, ie, n_tok, ggml_type_name(ed.type[part::GATE]), ggml_type_name(ed.type[part::UP]),
755
+ ggml_type_name(ed.type[part::DOWN]), swiglu_limit, swiglu_mode);
756
+ return 0;
757
+ }
758
+ } // namespace sdq
sdq-dsv4/sdq_pager.cpp ADDED
@@ -0,0 +1,2282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SDQ expert pager 實作:見 sdq_pager.h 的設計說明
2
+ #include "sdq_pager.h"
3
+
4
+ #include "llama-mmap.h"
5
+
6
+ #include <algorithm>
7
+ #include <thread>
8
+ #include <cinttypes>
9
+ #include <cmath>
10
+ #include <cstdio>
11
+ #include <cstdlib>
12
+ #include <cstring>
13
+ #include <fcntl.h>
14
+ #include <ctime>
15
+ #include <fstream>
16
+ #include <map>
17
+ #include <mutex>
18
+ #include <sstream>
19
+ #include <stdexcept>
20
+ #include <sys/mman.h>
21
+ #include <sys/stat.h>
22
+ #include <sys/types.h>
23
+ #include <unistd.h>
24
+
25
+ namespace sdq {
26
+
27
+ // ============================================================ 小工具
28
+
29
+ uint64_t now_us() {
30
+ struct timespec ts;
31
+ clock_gettime(CLOCK_MONOTONIC, &ts);
32
+ return (uint64_t) ts.tv_sec * 1000000ull + (uint64_t) ts.tv_nsec / 1000ull;
33
+ }
34
+
35
+ size_t process_rss_bytes() {
36
+ std::ifstream f("/proc/self/statm");
37
+ size_t total = 0, resident = 0;
38
+ if (f >> total >> resident) {
39
+ return resident * (size_t) sysconf(_SC_PAGESIZE);
40
+ }
41
+ return 0;
42
+ }
43
+
44
+ size_t physical_ram_bytes() {
45
+ std::ifstream f("/proc/meminfo");
46
+ std::string key;
47
+ size_t value = 0;
48
+ std::string unit;
49
+ while (f >> key >> value >> unit) {
50
+ if (key == "MemTotal:") {
51
+ return value * 1024;
52
+ }
53
+ }
54
+ return 0;
55
+ }
56
+
57
+ static size_t env_size(const char * name, size_t dflt) {
58
+ const char * v = getenv(name);
59
+ if (!v || !*v) {
60
+ return dflt;
61
+ }
62
+ return (size_t) strtoull(v, nullptr, 10);
63
+ }
64
+
65
+ static bool env_flag(const char * name, bool dflt) {
66
+ const char * v = getenv(name);
67
+ if (!v || !*v) {
68
+ return dflt;
69
+ }
70
+ return !(v[0] == '0' && v[1] == '\0');
71
+ }
72
+
73
+ // ============================================================ GGUF 檔頭解析
74
+
75
+ namespace {
76
+
77
+ struct gguf_reader {
78
+ const uint8_t * p = nullptr;
79
+ size_t size = 0;
80
+ size_t o = 0;
81
+
82
+ uint8_t u8() { return p[o++]; }
83
+ uint32_t u32() { uint32_t v; memcpy(&v, p + o, 4); o += 4; return v; }
84
+ uint64_t u64() { uint64_t v; memcpy(&v, p + o, 8); o += 8; return v; }
85
+ int64_t i64() { int64_t v; memcpy(&v, p + o, 8); o += 8; return v; }
86
+ float f32() { float v; memcpy(&v, p + o, 4); o += 4; return v; }
87
+ std::string str() {
88
+ uint64_t n = u64();
89
+ std::string s((const char *) p + o, (size_t) n);
90
+ o += n;
91
+ return s;
92
+ }
93
+ void skip(uint64_t n) { o += (size_t) n; }
94
+ uint16_t u16() { uint16_t v; memcpy(&v, p + o, 2); o += 2; return v; }
95
+ int16_t i16() { int16_t v; memcpy(&v, p + o, 2); o += 2; return v; }
96
+ double f64() { double v; memcpy(&v, p + o, 8); o += 8; return v; }
97
+ };
98
+
99
+ struct kv_value {
100
+ enum kind { NONE, INT, REAL, STR, ARR } k = NONE;
101
+ int64_t i = 0;
102
+ double f = 0;
103
+ std::string s;
104
+ std::vector<int64_t> arr;
105
+ };
106
+
107
+ struct tensor_info {
108
+ std::string name;
109
+ std::vector<int64_t> dims;
110
+ ggml_type type = GGML_TYPE_COUNT;
111
+ uint64_t offset = 0; // 在所屬 shard 檔案內、相對 data_start 的偏移
112
+ int shard = 0; // 屬於哪一個 shard(sharded GGUF)
113
+ };
114
+
115
+ size_t ggml_type_size_of(ggml_type t) {
116
+ return ggml_row_size(t, 1);
117
+ }
118
+
119
+ } // namespace
120
+
121
+ // ============================================================ pager
122
+
123
+ pager & pager::instance() {
124
+ static pager p;
125
+ return p;
126
+ }
127
+
128
+ // 依 GGUF split 命名規則把第一個 shard 路徑展開成全部 shard。
129
+ // 規則是 "<prefix>-00001-of-00004.gguf";llama.cpp 用 llama_split_prefix/llama_split_path
130
+ // 做同一件事,這裡獨立實作以免 pager 依賴 loader 的內部符號。
131
+ std::vector<std::string> pager::list_splits(const std::string & first_path, int n_split) {
132
+ std::vector<std::string> out;
133
+ if (n_split <= 1) {
134
+ out.push_back(first_path);
135
+ return out;
136
+ }
137
+ // 用 llama.cpp 自己的命名規則(%s-%05d-of-%05d.gguf,且 split_no 由 1 起算)。
138
+ // 自己刻字串很容易差一位數字 —— 實測就是這樣變成 "...XL--00001-of-..."。
139
+ std::vector<char> pre(llama_path_max(), 0);
140
+ const int pre_len = llama_split_prefix(pre.data(), pre.size(), first_path.c_str(), 0, n_split);
141
+ if (pre_len <= 0) {
142
+ throw std::runtime_error("無法解析 split 檔名:" + first_path);
143
+ }
144
+ std::vector<char> buf(llama_path_max(), 0);
145
+ for (int i = 0; i < n_split; i++) {
146
+ const int len = llama_split_path(buf.data(), buf.size(), pre.data(), i, n_split);
147
+ if (len <= 0) {
148
+ throw std::runtime_error("無法產生 split 路徑:" + first_path);
149
+ }
150
+ out.emplace_back(buf.data(), (size_t) len);
151
+ }
152
+ return out;
153
+ }
154
+
155
+ void pager::load_gguf_header(const std::string & path) {
156
+ const int fd = open(path.c_str(), O_RDONLY);
157
+ if (fd < 0) {
158
+ throw std::runtime_error("無法開啟 GGUF:" + path);
159
+ }
160
+
161
+ struct stat st;
162
+ fstat(fd, &st);
163
+
164
+ // 只讀檔頭:metadata + tensor 資訊大約 11 MB(shard 1 還含整份 tokenizer,
165
+ // 這裡的 64 MiB 上限對它足夠)
166
+ const size_t want = (size_t) std::min<int64_t>(st.st_size, 64ull << 20);
167
+ std::vector<uint8_t> buf(want);
168
+ ssize_t got = 0;
169
+ while (got < (ssize_t) want) {
170
+ ssize_t n = pread(fd, buf.data() + got, want - got, got);
171
+ if (n <= 0) {
172
+ break;
173
+ }
174
+ got += n;
175
+ }
176
+ buf.resize((size_t) std::max<ssize_t>(got, 0));
177
+
178
+ gguf_reader r{buf.data(), buf.size(), 0};
179
+ if (memcmp(buf.data(), "GGUF", 4) != 0) {
180
+ close(fd);
181
+ throw std::runtime_error("不是 GGUF 檔:" + path);
182
+ }
183
+ r.o = 4;
184
+ const uint32_t version = r.u32();
185
+ if (version < 2 || version > 3) {
186
+ close(fd);
187
+ throw std::runtime_error("不支援的 GGUF 版本");
188
+ }
189
+ const uint64_t n_tensors = r.u64();
190
+ const uint64_t n_kv = r.u64();
191
+
192
+ std::map<std::string, kv_value> kv;
193
+ for (uint64_t i = 0; i < n_kv; i++) {
194
+ std::string key = r.str();
195
+ const uint32_t t = r.u32();
196
+ kv_value v;
197
+ switch (t) {
198
+ // GGUF 型別寬度:U8/I8/BOOL=1、U16/I16=2、U32/I32/F32=4、U64/I64/F64=8。
199
+ // 舊版把 case 0/1/2/3 都讀成 u32,遇到 tokenizer 或 U16 metadata 就整個
200
+ // header 錯位(症狀是 std::length_error: basic_string::_M_create)。
201
+ case 0: v.k = kv_value::INT; v.i = (int64_t) r.u8(); v.f = (double) v.i; break;
202
+ case 1: v.k = kv_value::INT; v.i = (int64_t) (int8_t) r.u8(); v.f = (double) v.i; break;
203
+ case 2: v.k = kv_value::INT; v.i = (int64_t) r.u16(); v.f = (double) v.i; break;
204
+ case 3: v.k = kv_value::INT; v.i = (int64_t) r.i16(); v.f = (double) v.i; break;
205
+ case 4: v.k = kv_value::INT; v.i = (int64_t) (uint32_t) r.u32(); v.f = (double) v.i; break;
206
+ case 5: v.k = kv_value::INT; v.i = (int64_t) (int32_t) r.u32(); v.f = (double) v.i; break;
207
+ case 6: v.k = kv_value::REAL; v.f = r.f32(); v.i = (int64_t) v.f; break;
208
+ case 7: v.k = kv_value::INT; v.i = (int64_t) r.u8(); v.f = (double) v.i; break; // BOOL = 1 byte
209
+ case 8: v.k = kv_value::STR; v.s = r.str(); break;
210
+ case 9: {
211
+ v.k = kv_value::ARR;
212
+ const uint32_t et = r.u32();
213
+ const uint64_t n = r.u64();
214
+ for (uint64_t j = 0; j < n; j++) {
215
+ switch (et) {
216
+ // 元素寬度必須跟 kv 一致(U8/I8/BOOL=1、U16/I16=2 …)。
217
+ // F64 一定要消費 8 bytes,舊版沒讀會讓 header 往後全部錯位。
218
+ case 0: v.arr.push_back(r.u8()); break;
219
+ case 1: v.arr.push_back((int8_t) r.u8()); break;
220
+ case 2: v.arr.push_back(r.u16()); break;
221
+ case 3: v.arr.push_back(r.i16()); break;
222
+ case 4: v.arr.push_back((uint32_t) r.u32()); break;
223
+ case 5: v.arr.push_back((int32_t) r.u32()); break;
224
+ case 6: v.arr.push_back((int64_t) r.f32()); break;
225
+ case 7: v.arr.push_back(r.u8()); break;
226
+ case 10: v.arr.push_back((int64_t) r.u64()); break;
227
+ case 11: v.arr.push_back(r.i64()); break;
228
+ case 12: r.f64(); v.arr.push_back(0); break;
229
+ case 8: r.skip(r.u64()); v.arr.push_back(0); break;
230
+ default:
231
+ close(fd);
232
+ throw std::runtime_error("GGUF 陣列元素型別不支援: " + std::to_string(et));
233
+ }
234
+ }
235
+ } break;
236
+ case 10: v.k = kv_value::INT; v.i = (int64_t) r.u64(); v.f = (double) v.i; break;
237
+ case 11: v.k = kv_value::INT; v.i = r.i64(); v.f = (double) v.i; break;
238
+ case 12: v.k = kv_value::REAL; { double d; memcpy(&d, buf.data() + r.o, 8); r.o += 8; v.f = d; v.i = (int64_t) d; } break;
239
+ default: close(fd); throw std::runtime_error("GGUF kv 型別不支援: " + std::to_string(t));
240
+ }
241
+ kv[key] = v;
242
+ }
243
+
244
+ std::vector<tensor_info> tensors;
245
+ tensors.reserve(n_tensors);
246
+ for (uint64_t i = 0; i < n_tensors; i++) {
247
+ tensor_info ti;
248
+ ti.name = r.str();
249
+ const uint32_t nd = r.u32();
250
+ for (uint32_t d = 0; d < nd; d++) {
251
+ ti.dims.push_back((int64_t) r.u64());
252
+ }
253
+ ti.type = (ggml_type) r.u32();
254
+ ti.offset = r.u64();
255
+ tensors.push_back(std::move(ti));
256
+ }
257
+ const uint64_t align = kv.count("general.alignment") ? (uint64_t) kv["general.alignment"].i : 32;
258
+ const uint64_t data_start = (r.o + align - 1) / align * align;
259
+
260
+ close(fd);
261
+
262
+ // ---- sharded GGUF:shard 1 只帶 metadata(n_tensors=0),真正的權重分散在
263
+ // 其他 shard,而且每個 shard 有自己的 data_start。舊版只讀第一個檔,
264
+ // 於是 page map 會是空的(症狀:pager 起來但沒有任何 expert 可分頁)。
265
+ // 這裡把每個 shard 的 header 都讀一遍,張量名稱為全���鍵,offset 記成 (shard, off)。
266
+ {
267
+ const int64_t n_split = kv.count("split.count") ? kv["split.count"].i : 1;
268
+ shard_paths_ = list_splits(path, (int) n_split);
269
+ shard_data_start_.assign(shard_paths_.size(), 0);
270
+ shard_data_start_[0] = data_start;
271
+
272
+ if (n_split > 1) {
273
+ std::vector<tensor_info> merged;
274
+ merged.reserve((size_t) n_tensors + 1024);
275
+ for (size_t si = 1; si < shard_paths_.size(); si++) {
276
+ const int sfd = open(shard_paths_[si].c_str(), O_RDONLY);
277
+ if (sfd < 0) {
278
+ throw std::runtime_error("無法開啟 GGUF shard:" + shard_paths_[si]);
279
+ }
280
+ struct stat sst;
281
+ fstat(sfd, &sst);
282
+ // 後續 shard 的 header 很小(沒有 tokenizer),讀 16 MiB 綽綽有餘
283
+ const size_t swant = (size_t) std::min<int64_t>(sst.st_size, 16ull << 20);
284
+ std::vector<uint8_t> sbuf(swant);
285
+ ssize_t sgot = 0;
286
+ while (sgot < (ssize_t) swant) {
287
+ const ssize_t n = pread(sfd, sbuf.data() + sgot, swant - sgot, sgot);
288
+ if (n <= 0) {
289
+ break;
290
+ }
291
+ sgot += n;
292
+ }
293
+ sbuf.resize((size_t) std::max<ssize_t>(sgot, 0));
294
+
295
+ gguf_reader sr{sbuf.data(), sbuf.size(), 0};
296
+ if (sbuf.size() < 16 || memcmp(sbuf.data(), "GGUF", 4) != 0) {
297
+ close(sfd);
298
+ throw std::runtime_error("shard 不是 GGUF:" + shard_paths_[si]);
299
+ }
300
+ sr.o = 4;
301
+ sr.u32(); // version
302
+ const uint64_t s_ntensors = sr.u64();
303
+ const uint64_t s_nkv = sr.u64();
304
+ for (uint64_t i = 0; i < s_nkv; i++) {
305
+ sr.str(); // key
306
+ const uint32_t t = sr.u32();
307
+ switch (t) {
308
+ case 0: case 1: case 7: sr.skip(1); break;
309
+ case 2: case 3: sr.skip(2); break;
310
+ case 4: case 5: case 6: sr.skip(4); break;
311
+ case 10: case 11: case 12: sr.skip(8); break;
312
+ case 8: sr.skip(sr.u64()); break;
313
+ case 9: {
314
+ const uint32_t et = sr.u32();
315
+ const uint64_t n = sr.u64();
316
+ if (et == 8) {
317
+ for (uint64_t j = 0; j < n; j++) { sr.skip(sr.u64()); }
318
+ } else {
319
+ size_t esz = 0;
320
+ switch (et) {
321
+ case 0: case 1: case 7: esz = 1; break;
322
+ case 2: case 3: esz = 2; break;
323
+ case 4: case 5: case 6: esz = 4; break;
324
+ case 10: case 11: case 12: esz = 8; break;
325
+ default: esz = 0; break;
326
+ }
327
+ sr.skip((uint64_t) n * esz);
328
+ }
329
+ } break;
330
+ default:
331
+ close(sfd);
332
+ throw std::runtime_error("shard kv 型別不支援: " + std::to_string(t));
333
+ }
334
+ }
335
+ for (uint64_t i = 0; i < s_ntensors; i++) {
336
+ tensor_info ti;
337
+ ti.name = sr.str();
338
+ const uint32_t nd = sr.u32();
339
+ for (uint32_t d = 0; d < nd; d++) {
340
+ ti.dims.push_back((int64_t) sr.u64());
341
+ }
342
+ ti.type = (ggml_type) sr.u32();
343
+ ti.offset = sr.u64();
344
+ ti.shard = (int) si;
345
+ merged.push_back(std::move(ti));
346
+ }
347
+ shard_data_start_[si] = (sr.o + align - 1) / align * align;
348
+ close(sfd);
349
+ }
350
+ for (auto & ti : tensors) {
351
+ ti.shard = 0;
352
+ merged.push_back(std::move(ti));
353
+ }
354
+ tensors.swap(merged);
355
+ fprintf(stderr, "[sdq] sharded GGUF:%d 個 shard,合計 %zu 個張量(shard1 為 metadata-only)\n",
356
+ (int) shard_paths_.size(), tensors.size());
357
+ }
358
+ }
359
+
360
+ // ---- hparams
361
+ auto gi = [&](const std::string & k, int64_t dflt) -> int64_t {
362
+ auto it = kv.find(k);
363
+ return it == kv.end() ? dflt : it->second.i;
364
+ };
365
+ info_.arch = kv.count("general.architecture") ? kv["general.architecture"].s : "";
366
+ info_.name = kv.count("general.name") ? kv["general.name"].s : "";
367
+ const std::string pfx = info_.arch + ".";
368
+ info_.n_layer = gi(pfx + "block_count", 0);
369
+ info_.n_expert = gi(pfx + "expert_count", 0);
370
+ info_.n_expert_used= gi(pfx + "expert_used_count", 0);
371
+ info_.n_embd = gi(pfx + "embedding_length", 0);
372
+ info_.n_ff_exp = gi(pfx + "expert_feed_forward_length", 0);
373
+ info_.n_ff_shexp = gi(pfx + "expert_shared_feed_forward_length", 0);
374
+ info_.n_expert_shared = gi(pfx + "expert_shared_count", 0);
375
+ info_.n_head = gi(pfx + "attention.head_count", 0);
376
+ info_.n_head_kv = gi(pfx + "attention.head_count_kv", 0);
377
+ info_.n_embd_head_k= gi(pfx + "attention.key_length", 0);
378
+ info_.n_embd_head_v= gi(pfx + "attention.value_length", info_.n_embd_head_k);
379
+ info_.context_length = gi(pfx + "context_length", 0);
380
+ info_.full_attention_interval = gi(pfx + "full_attention_interval", 4);
381
+
382
+ if (info_.n_expert <= 0 || info_.n_layer <= 0) {
383
+ throw std::runtime_error("這不是 MoE 模型(expert_count=" + std::to_string(info_.n_expert) + ")");
384
+ }
385
+ // deepseek4 的 shared expert(ffn_{gate,up,down}_shexp)是 2D 稠密權重、走一般的
386
+ // ggml_mul_mat,**不進 arena**,也不該被這個 pager 當成 routed expert。
387
+ // 它的 FFN 寬度是 n_ff_exp * n_expert_shared(見 llama.cpp src/models/deepseek4.cpp
388
+ // 的 create_tensor),所以沒有 expert_shared_feed_forward_length 這個 key 時,
389
+ // 用兩者相乘補起來(只拿來印訊息,不影響任何偏移計算)。
390
+ if (info_.n_ff_shexp == 0 && info_.n_expert_shared > 0) {
391
+ info_.n_ff_shexp = info_.n_ff_exp * info_.n_expert_shared;
392
+ }
393
+
394
+ fprintf(stderr, "[sdq] arch=%s layers=%lld experts=%lld used=%lld shared=%lld "
395
+ "n_embd=%lld n_ff_exp=%lld n_ff_shexp=%lld ctx=%lld\n",
396
+ info_.arch.c_str(), (long long) info_.n_layer, (long long) info_.n_expert,
397
+ (long long) info_.n_expert_used, (long long) info_.n_expert_shared,
398
+ (long long) info_.n_embd, (long long) info_.n_ff_exp,
399
+ (long long) info_.n_ff_shexp, (long long) info_.context_length);
400
+
401
+ // ---- 頁面表:每個 (layer, expert) 的三段
402
+ layers_.assign((size_t) info_.n_layer, {});
403
+ std::map<std::string, const tensor_info *> by_name;
404
+ for (const auto & ti : tensors) {
405
+ by_name[ti.name] = &ti;
406
+ }
407
+
408
+ size_t max_expert_bytes = 0;
409
+ for (int64_t il = 0; il < info_.n_layer; il++) {
410
+ const std::string b = "blk." + std::to_string(il) + ".";
411
+ layer_desc & L = layers_[(size_t) il];
412
+ L.experts.assign((size_t) info_.n_expert, {});
413
+
414
+ auto get = [&](const std::string & suffix) -> const tensor_info * {
415
+ auto it = by_name.find(b + suffix);
416
+ return it == by_name.end() ? nullptr : it->second;
417
+ };
418
+
419
+ const tensor_info * t_gate = get("ffn_gate_exps.weight");
420
+ const tensor_info * t_up = get("ffn_up_exps.weight");
421
+ const tensor_info * t_gu = get("ffn_gate_up_exps.weight");
422
+ const tensor_info * t_down = get("ffn_down_exps.weight");
423
+
424
+ if (!t_down || (t_gate && !t_up && !t_gu)) {
425
+ continue; // 這層不是 MoE(理論上不會發生)
426
+ }
427
+
428
+ // sharded GGUF:每段的絕對偏移要用「該張量自己 shard 的 data_start」,
429
+ // 不能用第一個 shard 的(兩者差了近 10 MB,直接用會讀到別的權重)。
430
+ auto abs_off = [&](const tensor_info * t, uint64_t rel) -> uint64_t {
431
+ const size_t s = (size_t) t->shard;
432
+ const uint64_t base = (s < shard_data_start_.size()) ? shard_data_start_[s] : 0;
433
+ return base + rel;
434
+ };
435
+
436
+ for (int64_t e = 0; e < info_.n_expert; e++) {
437
+ expert_desc & ed = L.experts[(size_t) e];
438
+ if (t_gate || t_gu) {
439
+ const tensor_info * t = t_gate ? t_gate : t_gu;
440
+ const uint64_t nb = (uint64_t) ggml_row_size(t->type, t->dims[0]) * (uint64_t) t->dims[1];
441
+ ed.seg[part::GATE].off = abs_off(t, t->offset + (uint64_t) e * nb);
442
+ ed.seg[part::GATE].size = nb;
443
+ ed.seg[part::GATE].shard = t->shard;
444
+ ed.ne0 = t->dims[0];
445
+ ed.ne1 = t->dims[1];
446
+ ed.type[part::GATE] = t->type;
447
+ }
448
+ if (t_up) {
449
+ const uint64_t nb = (uint64_t) ggml_row_size(t_up->type, t_up->dims[0]) * (uint64_t) t_up->dims[1];
450
+ ed.seg[part::UP].off = abs_off(t_up, t_up->offset + (uint64_t) e * nb);
451
+ ed.seg[part::UP].size = nb;
452
+ ed.seg[part::UP].shard = t_up->shard;
453
+ ed.type[part::UP] = t_up->type; // gate/up 都是 Q4_K,型別一定要各自填
454
+ if (!t_gate) {
455
+ ed.ne0 = t_up->dims[0];
456
+ ed.ne1 = t_up->dims[1];
457
+ ed.type[part::GATE] = t_up->type;
458
+ }
459
+ }
460
+ {
461
+ const uint64_t nb = (uint64_t) ggml_row_size(t_down->type, t_down->dims[0]) * (uint64_t) t_down->dims[1];
462
+ ed.seg[part::DOWN].off = abs_off(t_down, t_down->offset + (uint64_t) e * nb);
463
+ ed.seg[part::DOWN].size = nb;
464
+ ed.seg[part::DOWN].shard = t_down->shard;
465
+ ed.type[part::DOWN] = t_down->type;
466
+ ed.down_ne0 = t_down->dims[0];
467
+ ed.down_ne1 = t_down->dims[1];
468
+ }
469
+ ed.fused_gate_up = (t_gate == nullptr && t_gu != nullptr);
470
+ ed.bytes = 0;
471
+ int64_t acc = 0;
472
+ for (int p = 0; p < 3; p++) {
473
+ if (ed.seg[p].size == 0) {
474
+ continue;
475
+ }
476
+ acc = (acc + 63) & ~(int64_t) 63; // 每段 64B 對齊
477
+ ed.slot_off[p] = acc;
478
+ acc += (int64_t) ed.seg[p].size;
479
+ }
480
+ ed.bytes = acc;
481
+ max_expert_bytes = std::max(max_expert_bytes, (size_t) ed.bytes);
482
+ }
483
+ L.bytes_per_expert = L.experts.empty() ? 0 : L.experts[0].bytes;
484
+ }
485
+
486
+ // ---- RAM 帳目摘要 ---------------------------------------------------
487
+ // expert / 非 expert 的權重總量。這決定「分頁能省多少」——
488
+ // 若非 expert 權重本身就很大,arena 再大也塞不下整個模型。
489
+ {
490
+ int64_t exp_total = 0;
491
+ std::map<std::string, std::map<int, int>> types; // part -> ggml_type -> 層數
492
+ for (const auto & L : layers_) {
493
+ exp_total += L.bytes_per_expert * (int64_t) info_.n_expert;
494
+ const expert_desc & ed = L.experts.empty() ? expert_desc{} : L.experts[0];
495
+ for (int p = 0; p < 3; p++) {
496
+ static const char * pname[3] = { "gate", "up", "down" };
497
+ types[pname[p]][(int) ed.type[p]]++;
498
+ }
499
+ }
500
+ fprintf(stderr, "[sdq] expert 權重合計 %lld B(%.2f GiB)= %lld 層 × %lld expert\n",
501
+ (long long) exp_total, exp_total / (double) (1ll << 30),
502
+ (long long) info_.n_layer, (long long) info_.n_expert);
503
+ for (const auto & kv : types) {
504
+ fprintf(stderr, "[sdq] %-5s :", kv.first.c_str());
505
+ for (const auto & t : kv.second) {
506
+ fprintf(stderr, " %s ×%d 層", ggml_type_name((ggml_type) t.first), t.second);
507
+ }
508
+ fprintf(stderr, "\n");
509
+ }
510
+ }
511
+
512
+ slot_bytes_ = (max_expert_bytes + 4095) & ~(size_t) 4095;
513
+ if (slot_bytes_ < (256ull << 10)) {
514
+ slot_bytes_ = 256ull << 10;
515
+ }
516
+
517
+ // ---- 大小兩種槽位 ----------------------------------------------------
518
+ // DeepSeek-V4-Flash-0731 UD-IQ4_NL:42 層的 expert 是 11,665,408 B
519
+ // (IQ3_S gate 3,604,480 + IQ3_S up 3,604,480 + MXFP4 down 4,456,448),
520
+ // 但 layer 26 的 gate/up 換成 MXFP4 → 4,456,448*3 = 13,369,344 B。
521
+ // 若所有槽位都照最大的配,arena 有 21.8% 被浪費 —— 而 arena 是 RAM 預算裡
522
+ // 最寶貴的資源。
523
+ //
524
+ // 解法:槽位分成兩區。「小槽位」給 42 層用,「大槽位」只給 layer 26 用,
525
+ // 大槽位的數量按「那 1 層佔請求的比例」分配(1/43 ≈ 2.3%)。
526
+ // 選「大多數層」當小的:用眾數而不是最大值,這樣即使某個模型只有一種大小,
527
+ // 也不會誤判。
528
+ {
529
+ std::map<size_t, int> hist;
530
+ for (const auto & L : layers_) {
531
+ hist[(size_t) L.bytes_per_expert]++;
532
+ }
533
+ size_t small = 0;
534
+ int best_n = -1;
535
+ for (const auto & kv : hist) {
536
+ if (kv.second > best_n) {
537
+ best_n = kv.second;
538
+ small = kv.first;
539
+ }
540
+ }
541
+ const size_t mx = (size_t) (layers_.empty() ? 0 : layers_[0].bytes_per_expert);
542
+ size_t big = 0;
543
+ for (const auto & kv : hist) {
544
+ (void) kv;
545
+ }
546
+ big = 0;
547
+ for (const auto & L : layers_) {
548
+ if ((size_t) L.bytes_per_expert > small) {
549
+ big = std::max(big, (size_t) L.bytes_per_expert);
550
+ }
551
+ }
552
+ small_bytes_ = (small + 4095) & ~(size_t) 4095;
553
+ big_bytes_ = big ? ((big + 4095) & ~(size_t) 4095) : 0;
554
+ if (big_bytes_ && big_bytes_ <= small_bytes_) {
555
+ big_bytes_ = 0; // 其實一樣大,別分兩區
556
+ }
557
+ slot_bytes_ = big_bytes_ ? big_bytes_ : small_bytes_;
558
+ int n_big_layers = 0;
559
+ for (auto & L : layers_) {
560
+ L.needs_big_slot = big_bytes_ && (size_t) L.bytes_per_expert > small_bytes_;
561
+ if (L.needs_big_slot) {
562
+ n_big_layers++;
563
+ }
564
+ }
565
+ (void) mx;
566
+ fprintf(stderr,
567
+ "[sdq] 槽位大小:小的 %zu B(%zu 層)/大的 %zu B(%d 層)\n",
568
+ small_bytes_, (size_t) ((int) layers_.size() - n_big_layers), big_bytes_, n_big_layers);
569
+ }
570
+ }
571
+
572
+ bool pager::need_big_slot(int il, int ie) const {
573
+ if (!big_bytes_) {
574
+ return false;
575
+ }
576
+ const layer_desc * L = layer(il);
577
+ if (!L) {
578
+ return false;
579
+ }
580
+ if (L->needs_big_slot) {
581
+ return true;
582
+ }
583
+ return ie >= 0 && ie < (int) L->experts.size() &&
584
+ (size_t) L->experts[(size_t) ie].bytes > small_bytes_;
585
+ }
586
+
587
+ void pager::alloc_arena() {
588
+ const size_t slot = cfg_.slot_bytes ? cfg_.slot_bytes : slot_bytes_;
589
+ slot_bytes_ = slot;
590
+
591
+ // arena 大小決定:RAM 是「總預算」,不是「還有多少可用」
592
+ // 總預算 = 非專家權重(2.4 GB) + KV cache + ggml 計算緩衝 + 詞表 + arena
593
+ // 所以有指定 ram_budget 時,用「目前 RSS(不含 arena)」當作已用額度扣掉。
594
+ // 預留量要涵蓋 arena 之外、但「在 pager 算完之後才長出來」的記憶體:
595
+ // llama.cpp 的 CPU compute buffer(125 MiB @ubatch=128、502 MiB @512)
596
+ // + KV cache(ctx=4096 約 670 MiB)+ 圖與執行緒堆疊
597
+ // 這些在 sizing 的當下還沒配置,所以不會出現在 rss_now 裡,必須手動扣。
598
+ // 設太小 = 實際用量會超過 RAM 預算。實測(RLIMIT_DATA 8 GB、ctx 4096、681 token 提示詞):
599
+ // 預留 700 MB → 峰值 RSS 8231 MB(超出 8 GB 預算 39 MB)
600
+ // 預留 900 MB → 峰值 RSS 8035 MB(留 157 MB 餘量)
601
+ // 代價是槽位數:2039 → 1936 槽,decode 5.85 → 5.37 tok/s(約 -8%)。
602
+ // 寧可少 8% 速度,也不要在目標機器的 8 GB 上 OOM —— 這是本專案的硬約束。
603
+ const size_t reserve = (size_t) env_size("SDQ_RESERVE_MB", 900) << 20;
604
+ const size_t rss_now = process_rss_bytes();
605
+
606
+ size_t arena;
607
+ if (cfg_.ram_budget_bytes) {
608
+ const size_t used = rss_now; // 這時候 arena 還沒配置
609
+ size_t avail = cfg_.ram_budget_bytes > used + reserve
610
+ ? cfg_.ram_budget_bytes - used - reserve
611
+ : 0;
612
+ arena = avail;
613
+ if (cfg_.arena_bytes) {
614
+ arena = std::min(arena, cfg_.arena_bytes);
615
+ }
616
+ fprintf(stderr, "[sdq] RAM 預算 %.2f GiB:已用(RSS) %.2f GiB、預留 %.2f GiB → arena %.2f GiB\n",
617
+ cfg_.ram_budget_bytes / 1073741824.0, used / 1073741824.0,
618
+ reserve / 1073741824.0, arena / 1073741824.0);
619
+ } else {
620
+ const size_t phys = physical_ram_bytes();
621
+ const size_t budget = (size_t) (phys * 0.70);
622
+ arena = cfg_.arena_bytes ? cfg_.arena_bytes : std::min(budget, (size_t) (4ull << 30));
623
+ }
624
+ if (arena < slot_bytes_ * 16) {
625
+ throw std::runtime_error("expert arena 太小");
626
+ }
627
+ // 切成兩區:大槽位只分給「需要大槽位的層」,比例 = 那些層佔全部層的比例。
628
+ const bool force_uniform = env_flag("SDQ_FORCE_UNIFORM_SLOT", false);
629
+ if (big_bytes_ && small_bytes_ && !layers_.empty() && !force_uniform) {
630
+ int n_big_layers = 0;
631
+ for (const auto & L : layers_) {
632
+ if (L.needs_big_slot) {
633
+ n_big_layers++;
634
+ }
635
+ }
636
+ const double f = (double) n_big_layers / (double) layers_.size();
637
+ const double avg = f * (double) big_bytes_ + (1.0 - f) * (double) small_bytes_;
638
+ size_t n_total = (size_t)((double) arena / avg);
639
+ size_t nb = (size_t)(n_total * f);
640
+ // 大槽位的下限不是「有點就好」,而是**工作集的下限**:
641
+ // 一個 token 在每個大槽位層要 8 個大 expert,這個模型有 3 個大槽位層
642
+ // → 至少要 3 × 8 = 24 個大槽位,而且還要留給預取的空間。
643
+ // 只給 16 個時量到過「找不到能放 il=34 ie=114 的槽位」而中止
644
+ // (這是在 verify 新增的預取壓力測試 [2b] 抓到的)。
645
+ const size_t big_need = (size_t) (n_big_layers * info_.n_expert_used);
646
+ if (nb < big_need) {
647
+ nb = big_need;
648
+ }
649
+ if (nb > n_total / 4) {
650
+ nb = n_total / 4;
651
+ }
652
+ // 連工作集下限都放不進去時,寧可讓小槽位少一點(n_total 保持不變),
653
+ // 否則會算出一個「必定會中止」的配置。
654
+ n_big_ = nb;
655
+ n_small_ = n_total > nb ? n_total - nb : 0;
656
+ arena = n_small_ * small_bytes_ + n_big_ * big_bytes_;
657
+ } else {
658
+ // 單一尺寸:全部槽位都是「大的那一種」(slot_bytes_)。
659
+ // 這是 SDQ_FORCE_UNIFORM_SLOT=1 與「模型只有一種 expert 大小」時的路徑。
660
+ //
661
+ // 這裡必須記成 n_small_=0 / n_big_=全部,不能記成「n_small_=全部、單位用大的」:
662
+ // slot_ptr()/slot_size_of() 是用「索引 < n_small_ → 小槽位」來分支的,
663
+ // 把它記成全部都是小槽位的話,大 expert 會拿到小槽位的 stride
664
+ // (寫出槽位邊界、算錯),而且大 expert 永遠配不到槽位 → 推論卡死。
665
+ // (第一版就是這樣,實驗跑到一半 hang 才找出來。)
666
+ const size_t unit = slot_bytes_ ? slot_bytes_ : small_bytes_;
667
+ n_small_ = 0;
668
+ n_big_ = arena / unit;
669
+ arena = n_big_ * unit;
670
+ }
671
+ if (n_small_ == 0 && n_big_ == 0) {
672
+ throw std::runtime_error("expert arena 太小(算不出槽位)");
673
+ }
674
+ n_slots_ = n_small_ + n_big_;
675
+ if (n_slots_ < 16) {
676
+ throw std::runtime_error("expert arena 太小(算不出 16 個槽位)");
677
+ }
678
+
679
+ void * p = nullptr;
680
+ const size_t alloc = arena + (2ull << 20);
681
+ if (posix_memalign(&p, 2ull << 20, alloc) != 0 || !p) {
682
+ throw std::runtime_error("無法配置 expert arena(" + std::to_string(arena >> 20) + " MB)");
683
+ }
684
+ // 讓 arena 全部變成實體 RAM,並提示核心使用 huge page(減少 TLB 壓力)
685
+ memset(p, 0, alloc);
686
+ madvise(p, alloc, MADV_HUGEPAGE);
687
+ arena_ = (uint8_t *) p;
688
+ arena_bytes_ = arena;
689
+
690
+ // OPT-1:剖面計數表(n_layer × n_expert,40×256 = 10240 筆 ≈ 40 KB,可忽略)
691
+ prof_.assign((size_t) info_.n_layer * (size_t) info_.n_expert, 0);
692
+ ensure_layer_stats((int) info_.n_layer); // SDQ-0004
693
+ if (!cfg_.profile_in.empty()) {
694
+ load_profile(cfg_.profile_in);
695
+ }
696
+ profile_out_ = cfg_.profile_out;
697
+
698
+ slots_.assign(n_slots_, {});
699
+ free_slots_.reserve(n_small_);
700
+ for (size_t i = n_small_; i-- > 0;) {
701
+ free_slots_.push_back((int32_t) i);
702
+ }
703
+ free_big_slots_.reserve(n_big_);
704
+ for (size_t i = n_slots_; i-- > n_small_;) {
705
+ free_big_slots_.push_back((int32_t) i);
706
+ }
707
+ used_.store(0);
708
+
709
+ hist_.assign((size_t) info_.n_layer, {});
710
+ for (auto & per_layer : hist_) {
711
+ per_layer.assign((size_t) info_.n_expert, {});
712
+ }
713
+ prev_used_.assign((size_t) info_.n_layer, {});
714
+ cur_used_.assign((size_t) info_.n_layer, {});
715
+ }
716
+
717
+ void pager::init(const config & cfg, ggml_context * ctx) {
718
+ (void) ctx;
719
+ if (ready_) {
720
+ return;
721
+ }
722
+ cfg_ = cfg;
723
+ if (cfg_.model_path.empty()) {
724
+ const char * v = getenv("SDQ_MODEL_PATH");
725
+ cfg_.model_path = v ? v : "";
726
+ }
727
+ if (cfg_.model_path.empty()) {
728
+ throw std::runtime_error("SDQ 缺少模型路徑(設定 SDQ_MODEL_PATH)");
729
+ }
730
+
731
+ // 先讀 header(順便列舉 shard),再開 fd:shard 清單只有在解析
732
+ // split.count 之後才知道,而 open_model_fd() 需要它。
733
+ load_gguf_header(cfg_.model_path);
734
+ open_model_fd();
735
+ if (shard_fd_.empty() || shard_fd_[0] < 0) {
736
+ throw std::runtime_error("無法開啟模型檔:" + shard_path(0));
737
+ }
738
+ // 順序有意義:I/O 池的對齊緩衝必須在 alloc_arena() 之前配置,
739
+ // 否則 arena 會把它當成「不存在的 RAM」,實際上就偷用了 8 GB 預算。
740
+ io_pool_start();
741
+ alloc_arena();
742
+
743
+ if (cfg_.prefetch) {
744
+ pf_ = std::thread([this] { pf_worker(); });
745
+ }
746
+ ready_ = true;
747
+ print_summary();
748
+ }
749
+
750
+ const layer_desc * pager::layer(int il) const {
751
+ if (il < 0 || il >= (int) layers_.size()) {
752
+ return nullptr;
753
+ }
754
+ const layer_desc & L = layers_[(size_t) il];
755
+ if (L.experts.empty()) {
756
+ return nullptr;
757
+ }
758
+ return &L;
759
+ }
760
+
761
+ void pager::open_model_fd() {
762
+ if (shard_paths_.empty()) {
763
+ shard_paths_.push_back(cfg_.model_path);
764
+ }
765
+ shard_fd_.assign(shard_paths_.size(), -1);
766
+ shard_fd_direct_.assign(shard_paths_.size(), -1);
767
+ for (size_t i = 0; i < shard_paths_.size(); i++) {
768
+ shard_fd_[i] = open(shard_paths_[i].c_str(), O_RDONLY);
769
+ if (shard_fd_[i] < 0) {
770
+ fprintf(stderr, "[sdq] 無法開啟 shard %zu: %s\n", i, shard_paths_[i].c_str());
771
+ return;
772
+ }
773
+ }
774
+ // O_DIRECT:讀資料完全不進 kernel page cache。
775
+ // 這是「RAM 帳目誠實」的關鍵 —— 否則核心預讀會讓 mmap 檔案的頁算進 RSS
776
+ // (實測 4K prefill 時檔案對映 RSS 可以多到 6 GB 以上),而且量到的
777
+ // 「SSD 讀取量」會被上一輪的 page cache 灌水。
778
+ const char * no_direct = getenv("SDQ_NO_DIRECT_IO");
779
+ if (no_direct && *no_direct && *no_direct != '0') {
780
+ return;
781
+ }
782
+ #ifdef O_DIRECT
783
+ // 每個 shard 各自開一個 O_DIRECT fd:O_DIRECT 是 fd 層級的屬性,不能共用。
784
+ for (size_t i = 0; i < shard_paths_.size(); i++) {
785
+ shard_fd_direct_[i] = open(shard_paths_[i].c_str(), O_RDONLY | O_DIRECT);
786
+ if (shard_fd_direct_[i] < 0) {
787
+ break;
788
+ }
789
+ }
790
+ use_direct_ = shard_fd_direct_[0] >= 0;
791
+ if (use_direct_) {
792
+ fprintf(stderr, "[sdq] 使用 O_DIRECT 讀取模型檔(%zu 個 shard,不佔用 kernel page cache)\n",
793
+ shard_paths_.size());
794
+ } else {
795
+ fprintf(stderr, "[sdq] O_DIRECT 不可用,改用 buffered + fadvise(DONTNEED)\n");
796
+ }
797
+ #endif
798
+ }
799
+
800
+ // 有對齊的緩衝區(O_DIRECT 要求),thread_local 避免競爭
801
+ static thread_local uint8_t * g_direct_buf = nullptr;
802
+ static thread_local size_t g_direct_sz = 0;
803
+
804
+ // 與 read_range_direct 相同,但用呼叫端提供的緩衝區(已配置好、已算進 RSS)。
805
+ // I/O 執行緒池走這條:每條 worker 在啟動時就配置好對齊緩衝,於是 arena sizing 時
806
+ // 這筆記憶體已��在 RSS 裡,不會變成「額外偷用的 RAM」。
807
+ // sharded GGUF:把「哪個 shard」一路帶到 pread。
808
+ static inline int sdq_fd_for(const std::vector<int> & fds, int shard) {
809
+ if (shard < 0 || shard >= (int) fds.size()) {
810
+ return -1;
811
+ }
812
+ return fds[(size_t) shard];
813
+ }
814
+
815
+ bool pager::read_range_staged(int shard, uint64_t off, uint8_t * dst, size_t size,
816
+ uint8_t * stage, size_t stage_sz) {
817
+ if (!use_direct_) {
818
+ return read_range(shard, off, dst, size);
819
+ }
820
+ const int fd = sdq_fd_for(shard_fd_direct_, shard);
821
+ if (fd < 0) {
822
+ return false;
823
+ }
824
+ const size_t blk = 4096;
825
+ const uint64_t start = off & ~(uint64_t)(blk - 1);
826
+ const uint64_t end = (off + size + blk - 1) & ~(uint64_t)(blk - 1);
827
+ const size_t bytes = (size_t)(end - start);
828
+ if (stage_sz < bytes) {
829
+ // 預先配置不足(不該發生):退回 thread_local 的通用路徑
830
+ return read_range_direct(shard, off, dst, size);
831
+ }
832
+ size_t done = 0;
833
+ while (done < bytes) {
834
+ const ssize_t n = pread(fd, stage + done, bytes - done,
835
+ (off_t)(start + done));
836
+ if (n <= 0) {
837
+ return false;
838
+ }
839
+ done += (size_t) n;
840
+ }
841
+ memcpy(dst, stage + (off - start), size);
842
+ return true;
843
+ }
844
+
845
+ bool pager::read_range_direct(int shard, uint64_t off, uint8_t * dst, size_t size) {
846
+ if (!use_direct_) {
847
+ return read_range(shard, off, dst, size);
848
+ }
849
+ const int fd = sdq_fd_for(shard_fd_direct_, shard);
850
+ if (fd < 0) {
851
+ return false;
852
+ }
853
+ const size_t blk = 4096;
854
+ const uint64_t start = off & ~(uint64_t)(blk - 1);
855
+ const uint64_t end = (off + size + blk - 1) & ~(uint64_t)(blk - 1);
856
+ const size_t bytes = (size_t)(end - start);
857
+ if (g_direct_sz < bytes) {
858
+ if (g_direct_buf) {
859
+ free(g_direct_buf);
860
+ }
861
+ if (posix_memalign((void **) &g_direct_buf, blk, bytes) != 0) {
862
+ g_direct_buf = nullptr;
863
+ g_direct_sz = 0;
864
+ use_direct_ = false;
865
+ return read_range(shard, off, dst, size);
866
+ }
867
+ g_direct_sz = bytes;
868
+ }
869
+ size_t done = 0;
870
+ while (done < bytes) {
871
+ const ssize_t n = pread(fd, g_direct_buf + done, bytes - done,
872
+ (off_t)(start + done));
873
+ if (n <= 0) {
874
+ return false;
875
+ }
876
+ done += (size_t) n;
877
+ }
878
+ memcpy(dst, g_direct_buf + (off - start), size);
879
+ return true;
880
+ }
881
+
882
+ bool pager::read_range(int shard, uint64_t off, uint8_t * dst, size_t size) {
883
+ const int fd = sdq_fd_for(shard_fd_, shard);
884
+ if (fd < 0) {
885
+ return false;
886
+ }
887
+ size_t done = 0;
888
+ while (done < size) {
889
+ const ssize_t n = pread(fd, dst + done, size - done, (off_t) (off + done));
890
+ if (n <= 0) {
891
+ return false;
892
+ }
893
+ done += (size_t) n;
894
+ }
895
+ // 讓 RAM 帳目誠實:把 kernel 對這個檔案的 page cache 丟掉,避免「藏在核心裡的 RAM」
896
+ if (cfg_.fadvise_dontneed) {
897
+ posix_fadvise(fd, (off_t) off, (off_t) size, POSIX_FADV_DONTNEED);
898
+ }
899
+ return true;
900
+ }
901
+
902
+ bool pager::read_expert(int il, int ie, uint8_t * dst) {
903
+ const layer_desc * L = layer(il);
904
+ if (!L || ie < 0 || ie >= (int) L->experts.size()) {
905
+ return false;
906
+ }
907
+ const expert_desc & ed = L->experts[(size_t) ie];
908
+ for (int p = 0; p < 3; p++) {
909
+ if (ed.seg[p].size == 0) {
910
+ continue;
911
+ }
912
+ if (!read_range_direct(ed.seg[p].shard, ed.seg[p].off,
913
+ dst + ed.slot_off[p], (size_t) ed.seg[p].size)) {
914
+ return false;
915
+ }
916
+ }
917
+ return true;
918
+ }
919
+
920
+ // ---------------------------------------------------------------- I/O 執行緒池
921
+ //
922
+ // 問題:一個 expert = gate / up / down 三段,檔案上是三段不相鄰的資料。
923
+ // 循序讀就是 3 次 pread 排在同一條執行緒上,而單次 1.9 MB 的 pread 在本機量到
924
+ // ~7 ms(≈275 MB/s 的單流頻寬)。因此「一個 expert 要多久」= 3 × 單次延遲,
925
+ // 這條路徑的長度跟「有多少條執行緒」無關 —— mode C 再怎麼排程都救不了。
926
+ //
927
+ // 做法:把三段交給三條執行緒同時 pread,臨界路徑降到約「單段」的時間。
928
+ // 量測(8 個 expert 全部 miss 的一層,各 9 次取中位數):
929
+ // 8 執行緒 × 三段序列讀 : 12.60 ms
930
+ // 16 執行緒 × 三段平行讀 : 7.57 ms
931
+ // 24 執行緒 × 三段平行讀 : 7.58 ms ← 16 就飽和,所以預設 3 × n_threads
932
+ //
933
+ // 這個池子對 prefill 更有用:一層要 256 個 expert = 768 個讀取工作,
934
+ // 序列讀時 16 執行緒只能拿到 16 路的頻寬 utilization。
935
+ void pager::io_pool_start() {
936
+ if (!cfg_.io_parallel) {
937
+ fprintf(stderr, "[sdq] I/O 段平行:關(每個 expert 的 gate/up/down 循序讀)\n");
938
+ return;
939
+ }
940
+ int n = cfg_.io_threads > 0 ? cfg_.io_threads : cfg_.n_threads * 3;
941
+ if (n < 4) {
942
+ n = 4;
943
+ }
944
+ if (n > 48) {
945
+ n = 48;
946
+ }
947
+ // 先算「一段權重最大有多少位元組」,每條 worker 配一塊對齊緩衝。
948
+ // 這必须在 alloc_arena() 之前做完,否則這筆 RAM 會變成帳外支出。
949
+ size_t max_seg = 0;
950
+ for (const auto & L : layers_) {
951
+ for (const auto & ed : L.experts) {
952
+ for (int p = 0; p < 3; p++) {
953
+ max_seg = std::max(max_seg, (size_t) ed.seg[p].size);
954
+ }
955
+ }
956
+ }
957
+ const size_t stage_bytes = ((max_seg + 2 * 4096) + 4095) & ~(size_t) 4095;
958
+ io_stage_.assign((size_t) n, io_stage{});
959
+ for (int i = 0; i < n; i++) {
960
+ void * p = nullptr;
961
+ if (posix_memalign(&p, 4096, stage_bytes) == 0 && p) {
962
+ memset(p, 0, stage_bytes); // 實際碰頁,讓它出現在 RSS 裡
963
+ io_stage_[(size_t) i].buf = (uint8_t *) p;
964
+ io_stage_[(size_t) i].sz = stage_bytes;
965
+ }
966
+ }
967
+ io_split_ = cfg_.io_split > 0 ? cfg_.io_split : 1;
968
+ io_stop_ = false;
969
+ io_pool_.reserve((size_t) n);
970
+ for (int i = 0; i < n; i++) {
971
+ const size_t wid = (size_t) i;
972
+ try {
973
+ io_pool_.emplace_back([this, wid] { io_worker(wid); });
974
+ } catch (...) {
975
+ break;
976
+ }
977
+ }
978
+ io_threads_ = (int) io_pool_.size();
979
+ io_pool_on_ = io_threads_ > 0;
980
+ if (io_pool_on_) {
981
+ fprintf(stderr, "[sdq] I/O 段平行:%d 條讀取執行緒 × %zu KB 對齊緩衝"
982
+ "(gate/up/down 同時讀,每段再切 %d 塊)\n",
983
+ io_threads_, stage_bytes >> 10, io_split_);
984
+ }
985
+ }
986
+
987
+ void pager::io_pool_stop() {
988
+ if (io_pool_.empty()) {
989
+ for (auto & s : io_stage_) {
990
+ free(s.buf);
991
+ s.buf = nullptr;
992
+ }
993
+ io_stage_.clear();
994
+ return;
995
+ }
996
+ {
997
+ std::lock_guard<std::mutex> lk(io_mu_);
998
+ io_stop_ = true;
999
+ }
1000
+ io_cv_.notify_all();
1001
+ for (auto & t : io_pool_) {
1002
+ if (t.joinable()) {
1003
+ t.join();
1004
+ }
1005
+ }
1006
+ io_pool_.clear();
1007
+ io_pool_on_ = false;
1008
+ for (auto & s : io_stage_) {
1009
+ free(s.buf);
1010
+ s.buf = nullptr;
1011
+ s.sz = 0;
1012
+ }
1013
+ io_stage_.clear();
1014
+ }
1015
+
1016
+ void pager::io_worker(size_t wid) {
1017
+ for (;;) {
1018
+ io_job job{};
1019
+ {
1020
+ std::unique_lock<std::mutex> lk(io_mu_);
1021
+ io_cv_.wait(lk, [this] { return io_stop_ || !io_q_.empty(); });
1022
+ if (io_q_.empty()) {
1023
+ if (io_stop_) {
1024
+ return;
1025
+ }
1026
+ continue;
1027
+ }
1028
+ job = io_q_.front();
1029
+ io_q_.pop_front();
1030
+ }
1031
+ io_jobs_.fetch_add(1, std::memory_order_relaxed);
1032
+ // 讀取本身不持鎖:io_mu_ 只保護佇列與完成計數
1033
+ const bool ok = (wid < io_stage_.size() && io_stage_[wid].buf)
1034
+ ? read_range_staged(job.shard, job.off, job.dst, job.size,
1035
+ io_stage_[wid].buf, io_stage_[wid].sz)
1036
+ : read_range_direct(job.shard, job.off, job.dst, job.size);
1037
+ {
1038
+ std::lock_guard<std::mutex> lk(io_mu_);
1039
+ if (!ok) {
1040
+ job.ctx->ok = false;
1041
+ }
1042
+ if (job.ctx->remaining.fetch_sub(1) == 1) {
1043
+ io_done_cv_.notify_all();
1044
+ }
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ // 把 (il,ie) 的三段交給執行緒池並行讀,呼叫端等到整批完成才返回。
1050
+ // ctx 放在呼叫端 stack:worker 只會在 remaining 歸零之後才再碰它,
1051
+ // 而我們正是等到 remaining==0 才離開函式,所以沒有 use-after-return。
1052
+ bool pager::read_expert_par(int il, int ie, uint8_t * dst) {
1053
+ const layer_desc * L = layer(il);
1054
+ if (!L || ie < 0 || ie >= (int) L->experts.size()) {
1055
+ return false;
1056
+ }
1057
+ if (!io_pool_on_) {
1058
+ return read_expert(il, ie, dst);
1059
+ }
1060
+ const expert_desc & ed = L->experts[(size_t) ie];
1061
+ io_ctx ctx;
1062
+ int n = 0;
1063
+ // 把每一段再切成一樣大的好幾塊,交給不同的執行緒同時讀。
1064
+ //
1065
+ // 為什麼:單一 pread 的時間 ≈ 固定延遲 + 位元組數 / 單流頻寬。
1066
+ // 本機實測單流約 275 MB/s、4K 隨機讀延遲約 0.57 ms,所以
1067
+ // 590 KB 讀一次 ≈ 0.57 + 2.1 = 2.7 ms
1068
+ // 590 KB 切成 2 塊 ≈ 0.57 + 1.05 = 1.6 ms
1069
+ // 而「讀完一個 expert」的時間正是每層的關鍵路徑,切塊就是直接砍關鍵路徑。
1070
+ // 設定檔 SDQ_IO_SPLIT。**實測預設是 1(不切塊)**:切塊在 24 條讀取執行緒
1071
+ // 之下沒有可重現的改善(split=1..6 的中位數在 4.9~5.9 之間跳動,純噪音),
1072
+ // 因為 8 個 expert × 3 段 = 24 個並行請求已經把裝置的佇列灌飽,
1073
+ // 這時瓶頸是裝置總頻寬而不是單一請求的延遲。保留這個開關是為了其他裝置。
1074
+ const int split = io_split_;
1075
+ const size_t blk = 4096;
1076
+ {
1077
+ std::lock_guard<std::mutex> lk(io_mu_);
1078
+ for (int p = 0; p < 3; p++) {
1079
+ const size_t sz = (size_t) ed.seg[p].size;
1080
+ if (sz == 0) {
1081
+ continue;
1082
+ }
1083
+ uint8_t * base = dst + ed.slot_off[p];
1084
+ const int sh = ed.seg[p].shard;
1085
+ if (split <= 1 || sz <= 2 * blk) {
1086
+ io_q_.push_back(io_job{sh, ed.seg[p].off, base, sz, &ctx});
1087
+ n++;
1088
+ continue;
1089
+ }
1090
+ size_t chunk = ((sz + (size_t) split - 1) / (size_t) split + blk - 1) & ~(blk - 1);
1091
+ for (size_t off = 0; off < sz; off += chunk) {
1092
+ const size_t take = sz - off < chunk ? sz - off : chunk;
1093
+ io_q_.push_back(io_job{sh, ed.seg[p].off + off, base + off, take, &ctx});
1094
+ n++;
1095
+ }
1096
+ }
1097
+ ctx.remaining.store(n);
1098
+ }
1099
+ if (n == 0) {
1100
+ return true;
1101
+ }
1102
+ io_cv_.notify_all();
1103
+ const uint64_t t0 = now_us();
1104
+ {
1105
+ std::unique_lock<std::mutex> lk(io_mu_);
1106
+ io_done_cv_.wait(lk, [&ctx] { return ctx.remaining.load() == 0; });
1107
+ }
1108
+ io_wait_ns_.fetch_add((now_us() - t0) * 1000ull, std::memory_order_relaxed);
1109
+ return ctx.ok;
1110
+ }
1111
+
1112
+ uint64_t pager::read_expert_span(int il, const int32_t * experts, int n, const int32_t * slots) {
1113
+ const layer_desc * L = layer(il);
1114
+ if (!L || n <= 0) {
1115
+ return 0;
1116
+ }
1117
+ const uint64_t t_start = now_us();
1118
+ // 每個執行緒一份暫存區(多執行緒會同時呼叫,不能共用一個 buffer)
1119
+ static thread_local std::vector<uint8_t> scratch_buf;
1120
+ uint64_t total = 0;
1121
+
1122
+ // 把 experts 切成「檔案編號連續」的區段
1123
+ int i = 0;
1124
+ while (i < n) {
1125
+ int j = i + 1;
1126
+ while (j < n && experts[j] == experts[j - 1] + 1) {
1127
+ j++;
1128
+ }
1129
+ const int cnt = j - i;
1130
+ const expert_desc & first = L->experts[(size_t) experts[i]];
1131
+ const expert_desc & last = L->experts[(size_t) experts[j - 1]];
1132
+
1133
+ for (int p = 0; p < 3; p++) {
1134
+ if (first.seg[p].size == 0 || last.seg[p].size == 0) {
1135
+ continue;
1136
+ }
1137
+ // 連續區段必須在同一個 shard 內;跨 shard 就切斷這個區段。
1138
+ if (last.seg[p].shard != first.seg[p].shard) {
1139
+ continue;
1140
+ }
1141
+ const uint64_t off = first.seg[p].off;
1142
+ const uint64_t end = last.seg[p].off + last.seg[p].size;
1143
+ const size_t bytes = (size_t) (end - off);
1144
+ if (scratch_buf.size() < bytes) {
1145
+ scratch_buf.assign(bytes, 0);
1146
+ }
1147
+ if (!read_range_direct(first.seg[p].shard, off, scratch_buf.data(), bytes)) {
1148
+ return total;
1149
+ }
1150
+ total += bytes;
1151
+ // 散到各自的槽位:scratch 從 off 開始,所以偏移是「相對於 off」
1152
+ for (int k = i; k < j; k++) {
1153
+ const expert_desc & ed = L->experts[(size_t) experts[k]];
1154
+ const size_t seg_bytes = (size_t) ed.seg[p].size;
1155
+ const size_t delta = (size_t) (ed.seg[p].off - off);
1156
+ if (slots[k] < 0) {
1157
+ continue;
1158
+ }
1159
+ if ((size_t) slots[k] >= n_slots_ || (uint64_t) ed.slot_off[p] + seg_bytes > slot_bytes_
1160
+ || delta + seg_bytes > scratch_buf.size()) {
1161
+ fprintf(stderr, "[sdq-pager] span 越界:il=%d slot=%d/%zu slot_off=%lld seg=%zu delta=%zu scratch=%zu p=%d e=%d\n",
1162
+ il, slots[k], n_slots_, (long long) ed.slot_off[p], seg_bytes, delta, scratch_buf.size(), p, experts[k]);
1163
+ return total;
1164
+ }
1165
+ memcpy(slot_ptr(slots[k]) + ed.slot_off[p],
1166
+ scratch_buf.data() + delta, seg_bytes);
1167
+ }
1168
+ }
1169
+ i = j;
1170
+ }
1171
+ stats_.demand_wait_us.fetch_add(now_us() - t_start, std::memory_order_relaxed);
1172
+ return total;
1173
+ }
1174
+
1175
+ int pager::reserve_slot(int il, int ie) {
1176
+ if (!ready_) {
1177
+ return -1;
1178
+ }
1179
+ count_request(il, ie);
1180
+ std::lock_guard<std::mutex> lk(mu_);
1181
+ const int32_t exist = find_slot_locked(il, ie);
1182
+ if (exist >= 0) {
1183
+ slots_[(size_t) exist].last_use = ++clock_;
1184
+ slots_[(size_t) exist].pin++;
1185
+ touch_freq_locked(slots_[(size_t) exist]);
1186
+ return exist;
1187
+ }
1188
+ const int32_t s = victim_locked();
1189
+ if (s < 0) {
1190
+ return -1;
1191
+ }
1192
+ slot_state & st = slots_[(size_t) s];
1193
+ if (st.layer >= 0) {
1194
+ page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
1195
+ stats_.evictions.fetch_add(1, std::memory_order_relaxed);
1196
+ used_.fetch_sub(1, std::memory_order_relaxed);
1197
+ }
1198
+ st.layer = il;
1199
+ st.expert = ie;
1200
+ st.last_use = ++clock_;
1201
+ st.freq = 1;
1202
+ st.pin = 1;
1203
+ st.is_hot = hot_lookup(il, ie);
1204
+ page_[((int64_t) il << 20) | (int64_t) ie] = s;
1205
+ used_.fetch_add(1, std::memory_order_relaxed);
1206
+ stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
1207
+ return s;
1208
+ }
1209
+
1210
+ int pager::probe(int il, int ie) {
1211
+ if (!ready_) {
1212
+ return -1;
1213
+ }
1214
+ stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
1215
+ count_request(il, ie);
1216
+ std::lock_guard<std::mutex> lk(mu_);
1217
+ const int32_t s = find_slot_locked(il, ie);
1218
+ if (s < 0) {
1219
+ stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
1220
+ return -1;
1221
+ }
1222
+ slots_[(size_t) s].last_use = ++clock_;
1223
+ slots_[(size_t) s].pin++;
1224
+ touch_freq_locked(slots_[(size_t) s]);
1225
+ stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
1226
+ return s;
1227
+ }
1228
+
1229
+ void pager::note_span_read(uint64_t bytes) {
1230
+ stats_.ssb_read_bytes_demand.fetch_add(bytes, std::memory_order_relaxed);
1231
+ stats_.demand_wait_us.fetch_add(0, std::memory_order_relaxed);
1232
+ }
1233
+
1234
+ // 衰減式頻率:每 8192 次使用就把所有計數減半,讓「過去熱門、現在變冷」的 expert 能被淘汰
1235
+ void pager::touch_freq_locked(slot_state & st) {
1236
+ if (++freq_tick_ >= 8192) {
1237
+ freq_tick_ = 0;
1238
+ for (auto & s : slots_) {
1239
+ s.freq /= 2;
1240
+ }
1241
+ }
1242
+ if (st.freq < 0xFFFFFFFFu) {
1243
+ st.freq++;
1244
+ }
1245
+ }
1246
+
1247
+ // ============================== OPT-1:熱門 expert 剖面 ==============================
1248
+ // 問題實測(docs/03):arena 3.88 GiB 只放得下 2044 個 expert,總共卻有 10240 個,
1249
+ // prefill 的幾百個「只用一次」的專家會把熱區整個洗掉,命中率卡在 ~70%。
1250
+ // 解法借用 Strata 的做法(src/core/expert_cache.cpp):**離線剖面 + 靜態排行榜 +
1251
+ // 熱頁永不淘汰**。這裡不動檔案格式,用純文字存排行榜。
1252
+
1253
+ // ---- SDQ-0004
1254
+ void pager::ensure_layer_stats(int n_layer) {
1255
+ if (lstats_ || n_layer <= 0) {
1256
+ return;
1257
+ }
1258
+ lstats_.reset(new layer_stat[(size_t) n_layer]);
1259
+ }
1260
+
1261
+ void pager::count_request(int il, int ie) {
1262
+ if (prof_.empty() || il < 0 || ie < 0) {
1263
+ return;
1264
+ }
1265
+ const size_t idx = (size_t) il * (size_t) info_.n_expert + (size_t) ie;
1266
+ if (idx < prof_.size()) {
1267
+ // 單執行緒 ++ 即可:請求次數是統計用途,不參與任何正確性判斷
1268
+ if (prof_[idx] < 0xFFFFFFFFu) {
1269
+ prof_[idx]++;
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ bool pager::hot_lookup(int il, int ie) const {
1275
+ if (hot_.empty() || il < 0 || ie < 0) {
1276
+ return false;
1277
+ }
1278
+ const size_t idx = (size_t) il * (size_t) info_.n_expert + (size_t) ie;
1279
+ return idx < hot_.size() && hot_[idx] != 0;
1280
+ }
1281
+
1282
+ // 挑一個容得下 (il,ie) 的槽。與 victim_locked 的差別:**先只在冷頁裡找**,
1283
+ // 找不到才退讓去動熱頁。這就是「熱區不會被 prefill 洗掉」的機制。
1284
+ // 這個槽位放得下「需要 big 的 expert」嗎?
1285
+ // 小槽位:只能放小 expert
1286
+ // 大槽位:小expert 也放得下(只是浪費一點空間)
1287
+ static inline bool slot_accepts(bool need_big, bool slot_is_big) {
1288
+ return !need_big || slot_is_big;
1289
+ }
1290
+
1291
+ int pager::admit_slot_locked(int il, int ie) {
1292
+ // il/ie 都 < 0 代表「不指定大小」→ 兩種槽位都可以(舊的呼叫路徑)。
1293
+ const bool need_big = (il >= 0 && ie >= 0) ? need_big_slot(il, ie) : false;
1294
+ // 完全沒用過的空槽永遠優先(它不是熱頁,佔用等於浪費)
1295
+ // 大 expert 先看大槽位;小 expert 先看小槽位。
1296
+ std::vector<int32_t> & first = need_big ? free_big_slots_ : free_slots_;
1297
+ std::vector<int32_t> & second = need_big ? free_slots_ : free_big_slots_;
1298
+ while (!first.empty()) {
1299
+ const int32_t s = first.back();
1300
+ if (!slots_[(size_t) s].busy) {
1301
+ first.pop_back();
1302
+ return s;
1303
+ }
1304
+ }
1305
+ // 第一輪:只在「冷頁」裡挑最久未用的。
1306
+ // want_hot=true → 熱門 expert 進來時,把冷頁換掉(不動其他熱頁)
1307
+ // want_hot=false → 冷門 expert 進來時,優先吃冷頁
1308
+ // 兩種情況的規則相同,所以 hot_lookup 的結果不影響選擇順序,只影響標記。
1309
+ for (int pass = 0; pass < 2; pass++) {
1310
+ int64_t best = -1;
1311
+ uint64_t best_clock = UINT64_MAX;
1312
+ for (size_t s = 0; s < n_slots_; s++) {
1313
+ const slot_state & st = slots_[s];
1314
+ if (st.pin > 0 || st.busy) {
1315
+ continue;
1316
+ }
1317
+ if (pass == 0 && st.is_hot) {
1318
+ continue; // 第一輪不動熱頁
1319
+ }
1320
+ if (!slot_accepts(need_big, is_big_slot(s))) {
1321
+ continue;
1322
+ }
1323
+ if (st.last_use < best_clock) {
1324
+ best_clock = st.last_use;
1325
+ best = (int64_t) s;
1326
+ }
1327
+ }
1328
+ if (best >= 0) {
1329
+ return (int) best;
1330
+ }
1331
+ }
1332
+ // 退讓:另一種大小的空槽。**一定要檢查放不放得下** —— 小槽位放不下大 expert,
1333
+ // 硬塞會讓 read_expert 寫出槽位邊界(靜默把別的 expert 寫壞)。
1334
+ while (!second.empty()) {
1335
+ const int32_t s = second.back();
1336
+ if (!slots_[(size_t) s].busy &&
1337
+ slot_accepts(need_big, is_big_slot(s))) {
1338
+ second.pop_back();
1339
+ return s;
1340
+ }
1341
+ if (!slots_[(size_t) s].busy) {
1342
+ second.pop_back();
1343
+ } else {
1344
+ break;
1345
+ }
1346
+ }
1347
+ return -1;
1348
+ }
1349
+
1350
+ bool pager::load_profile(const std::string & path) {
1351
+ std::FILE * f = std::fopen(path.c_str(), "rb");
1352
+ if (!f) {
1353
+ fprintf(stderr, "[sdq] 找不到 expert 剖面 %s(將以動態 LRU/LFU 運作)\n", path.c_str());
1354
+ return false;
1355
+ }
1356
+ char magic[8] = {0};
1357
+ if (std::fread(magic, 1, 4, f) != 4 || std::memcmp(magic, "SDQP", 4) != 0) {
1358
+ std::fclose(f);
1359
+ fprintf(stderr, "[sdq] %s 不是 SDQ expert 剖面(缺少 SDQP 標頭)\n", path.c_str());
1360
+ return false;
1361
+ }
1362
+ uint32_t hdr[3] = {0, 0, 0};
1363
+ if (std::fread(hdr, sizeof(uint32_t), 3, f) != 3) {
1364
+ std::fclose(f);
1365
+ return false;
1366
+ }
1367
+ const uint32_t n = hdr[2];
1368
+ std::vector<std::pair<uint32_t, std::pair<int32_t, int32_t>>> ranked;
1369
+ ranked.reserve(n);
1370
+ for (uint32_t i = 0; i < n; i++) {
1371
+ int32_t l = 0, e = 0;
1372
+ uint32_t c = 0;
1373
+ if (std::fread(&l, sizeof(int32_t), 1, f) != 1) break;
1374
+ if (std::fread(&e, sizeof(int32_t), 1, f) != 1) break;
1375
+ if (std::fread(&c, sizeof(uint32_t), 1, f) != 1) break;
1376
+ ranked.push_back({c, {l, e}});
1377
+ }
1378
+ std::fclose(f);
1379
+ std::sort(ranked.begin(), ranked.end(),
1380
+ [](const auto & a, const auto & b) { return a.first > b.first; });
1381
+
1382
+ hot_.assign((size_t) info_.n_layer * (size_t) info_.n_expert, 0);
1383
+ const int want = cfg_.hot_pin > 0 ? cfg_.hot_pin : (int) ranked.size();
1384
+ n_hot_ = 0;
1385
+ for (const auto & r : ranked) {
1386
+ if (n_hot_ >= want) {
1387
+ break;
1388
+ }
1389
+ const int l = r.second.first, e = r.second.second;
1390
+ if (l < 0 || l >= info_.n_layer || e < 0 || e >= info_.n_expert) {
1391
+ continue;
1392
+ }
1393
+ const size_t idx = (size_t) l * (size_t) info_.n_expert + (size_t) e;
1394
+ if (!hot_[idx]) {
1395
+ hot_[idx] = 1;
1396
+ n_hot_++;
1397
+ }
1398
+ }
1399
+ fprintf(stderr, "[sdq] expert 剖面:%s → 釘選 %d / %zu 個熱門 expert(永不淘汰)\n",
1400
+ path.c_str(), n_hot_, ranked.size());
1401
+ return n_hot_ > 0;
1402
+ }
1403
+
1404
+ void pager::dump_profile(const std::string & path) {
1405
+ if (prof_.empty()) {
1406
+ return;
1407
+ }
1408
+ std::vector<std::pair<uint32_t, std::pair<int32_t, int32_t>>> ranked;
1409
+ ranked.reserve(prof_.size());
1410
+ uint64_t total = 0;
1411
+ for (int l = 0; l < info_.n_layer; l++) {
1412
+ for (int e = 0; e < info_.n_expert; e++) {
1413
+ const uint32_t c = prof_[(size_t) l * (size_t) info_.n_expert + (size_t) e];
1414
+ if (c > 0) {
1415
+ ranked.push_back({c, {l, e}});
1416
+ total += c;
1417
+ }
1418
+ }
1419
+ }
1420
+ std::sort(ranked.begin(), ranked.end(),
1421
+ [](const auto & a, const auto & b) { return a.first > b.first; });
1422
+ std::FILE * f = std::fopen(path.c_str(), "wb");
1423
+ if (!f) {
1424
+ fprintf(stderr, "[sdq] 無法寫出 expert 剖面 %s\n", path.c_str());
1425
+ return;
1426
+ }
1427
+ std::fwrite("SDQP", 1, 4, f);
1428
+ const uint32_t hdr[3] = {1, (uint32_t) info_.n_layer, (uint32_t) ranked.size()};
1429
+ std::fwrite(hdr, sizeof(uint32_t), 3, f);
1430
+ for (const auto & r : ranked) {
1431
+ const int32_t l = r.second.first, e = r.second.second;
1432
+ const uint32_t c = r.first;
1433
+ std::fwrite(&l, sizeof(int32_t), 1, f);
1434
+ std::fwrite(&e, sizeof(int32_t), 1, f);
1435
+ std::fwrite(&c, sizeof(uint32_t), 1, f);
1436
+ }
1437
+ std::fclose(f);
1438
+ // 覆盖率報告:前 K 名可以蓋掉多少請求 → 直接決定 hot_pin 該設多少
1439
+ uint64_t acc = 0;
1440
+ const size_t ks[6] = {512, 1024, 1536, 2048, 3072, 4096};
1441
+ fprintf(stderr, "[sdq] expert 剖面已寫到 %s(%zu 個有被用到,總請求 %llu)\n",
1442
+ path.c_str(), ranked.size(), (unsigned long long) total);
1443
+ size_t ki = 0;
1444
+ for (size_t i = 0; i < ranked.size(); i++) {
1445
+ acc += ranked[i].first;
1446
+ if (ki < 6 && i + 1 == ks[ki]) {
1447
+ fprintf(stderr, "[sdq] 前 %5zu 名涵蓋 %6.2f%% 的請求\n",
1448
+ ks[ki], 100.0 * (double) acc / (double) (total ? total : 1));
1449
+ ki++;
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ int pager::find_slot_locked(int il, int ie) {
1455
+ const int64_t key = ((int64_t) il << 20) | (int64_t) ie;
1456
+ auto it = page_.find(key);
1457
+ return it == page_.end() ? -1 : it->second;
1458
+ }
1459
+
1460
+ // 找一個「可以放 (il,ie) 而且 last_use <= stale_before」的槽位。
1461
+ //
1462
+ // 為什麼不能直接用 victim_stale_locked():大小兩種槽位之後,那個函式會掃過**所有**
1463
+ // 槽位(包含小槽位),於是一個需要大槽位的 expert 可能拿到小槽位,
1464
+ // 預取時 read_expert 會寫出槽位邊界 → 靜默把鄰居槽位的資料寫壞。
1465
+ // 這是引進大小兩種槽位時真正會咬人的地方。
1466
+ //
1467
+ // 順序:先看對應尺寸的���槽(從沒用過,最該拿來放預取),
1468
+ // 再掃全部槽位挑最久未用的(放得下的那種尺寸)。
1469
+ int pager::victim_stale_for(int il, int ie, uint64_t stale_before) {
1470
+ const bool need_big = need_big_slot(il, ie);
1471
+ std::vector<int32_t> & first = need_big ? free_big_slots_ : free_slots_;
1472
+ std::vector<int32_t> & second = need_big ? free_slots_ : free_big_slots_;
1473
+ for (std::vector<int32_t> * lst : { &first, &second }) {
1474
+ while (!lst->empty()) {
1475
+ const int32_t s = lst->back();
1476
+ if (slots_[(size_t) s].busy) {
1477
+ break;
1478
+ }
1479
+ if (slots_[(size_t) s].last_use <= stale_before &&
1480
+ slot_accepts(need_big, is_big_slot(s))) {
1481
+ lst->pop_back();
1482
+ return s;
1483
+ }
1484
+ lst->pop_back();
1485
+ }
1486
+ }
1487
+ int64_t best = -1;
1488
+ uint64_t best_clock = UINT64_MAX;
1489
+ for (size_t i = 0; i < n_slots_; i++) {
1490
+ const slot_state & st = slots_[i];
1491
+ if (st.pin > 0 || st.busy || st.last_use > stale_before) {
1492
+ continue;
1493
+ }
1494
+ if (!slot_accepts(need_big, is_big_slot(i))) {
1495
+ continue;
1496
+ }
1497
+ if (st.last_use < best_clock) {
1498
+ best_clock = st.last_use;
1499
+ best = (int64_t) i;
1500
+ }
1501
+ }
1502
+ return (int) best;
1503
+ }
1504
+
1505
+ int pager::victim_stale_locked(uint64_t stale_before) {
1506
+ // 只挑「last_use 很舊」而且沒被 pin 的槽:保護最近的工作集
1507
+ while (!free_slots_.empty()) {
1508
+ const int32_t s = free_slots_.back();
1509
+ if (!slots_[(size_t) s].busy && slots_[(size_t) s].last_use <= stale_before) {
1510
+ free_slots_.pop_back();
1511
+ return s;
1512
+ }
1513
+ if (free_slots_.size() * 4 < n_slots_) {
1514
+ break;
1515
+ }
1516
+ free_slots_.pop_back();
1517
+ }
1518
+ int64_t best = -1;
1519
+ uint64_t best_clock = UINT64_MAX;
1520
+ for (size_t i = 0; i < n_slots_; i++) {
1521
+ const slot_state & st = slots_[i];
1522
+ if (st.pin > 0 || st.busy || st.last_use > stale_before) {
1523
+ continue;
1524
+ }
1525
+ if (st.last_use < best_clock) {
1526
+ best_clock = st.last_use;
1527
+ best = (int64_t) i;
1528
+ }
1529
+ }
1530
+ return (int) best;
1531
+ }
1532
+
1533
+ int pager::acquire(int il, int ie, bool blocking) {
1534
+ if (!ready_) {
1535
+ return -1;
1536
+ }
1537
+ std::unique_lock<std::mutex> lk(mu_);
1538
+ int32_t s = find_slot_locked(il, ie);
1539
+ if (s >= 0 && slots_[s].busy) {
1540
+ // 這個槽位**正在被填入**:另一條執行緒剛 admit 它、資料還沒讀完。
1541
+ //
1542
+ // 這裡一定要等,不能當成命中直接用。否則讀到的是「寫到一半的權重」,
1543
+ // 而且那個錯誤是**非決定性**的 —— 每次執行結果可能不同(實測在
1544
+ // prefill 時會讓同一個提示詞跑出不同的文字)。
1545
+ // busy 的槽位在 admit/victim 那邊本來就會被跳過,所以這裡是唯一
1546
+ // 還能拿到它的路徑,也因此必須在這裡等。
1547
+ const uint64_t t0 = now_us();
1548
+ while (slots_[s].busy) {
1549
+ lk.unlock();
1550
+ std::this_thread::sleep_for(std::chrono::microseconds(200));
1551
+ lk.lock();
1552
+ if (now_us() - t0 > (uint64_t) env_size("SDQ_BUSY_WAIT_MS", 30000) * 1000) {
1553
+ fprintf(stderr,
1554
+ "[sdq] 致命:等待槽位 il=%d ie=%d 讀入超過 %u ms(busy 一直沒解除)\n",
1555
+ il, ie, (unsigned) env_size("SDQ_BUSY_WAIT_MS", 30000));
1556
+ std::fflush(stderr);
1557
+ abort();
1558
+ }
1559
+ }
1560
+ // 讀取可能失敗(那時槽位已被清空)→ 重新走一次正常的 miss 路徑
1561
+ if (find_slot_locked(il, ie) != s) {
1562
+ s = -1;
1563
+ }
1564
+ }
1565
+ if (s >= 0) {
1566
+ slots_[s].last_use = ++clock_;
1567
+ slots_[s].pin++;
1568
+ touch_freq_locked(slots_[(size_t) s]);
1569
+ stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
1570
+ stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
1571
+ if (lstats_ && il >= 0 && il < (int) info_.n_layer) {
1572
+ lstats_[il].req.fetch_add(1, std::memory_order_relaxed);
1573
+ lstats_[il].hit.fetch_add(1, std::memory_order_relaxed);
1574
+ }
1575
+ // A prefetch that already finished counts as "used" here. Without this
1576
+ // the finished-before-request case was invisible and prefetch precision
1577
+ // was understated (docs/10 §f).
1578
+ if (slots_[s].from_prefetch) {
1579
+ slots_[s].from_prefetch = false;
1580
+ stats_.prefetch_used.fetch_add(1, std::memory_order_relaxed);
1581
+ stats_.prefetch_bytes_used.fetch_add(slot_size_of(s), std::memory_order_relaxed);
1582
+ }
1583
+ return s;
1584
+ }
1585
+
1586
+ stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
1587
+ count_request(il, ie);
1588
+ stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
1589
+ if (lstats_ && il >= 0 && il < (int) info_.n_layer) {
1590
+ lstats_[il].req.fetch_add(1, std::memory_order_relaxed);
1591
+ lstats_[il].miss.fetch_add(1, std::memory_order_relaxed);
1592
+ }
1593
+
1594
+ // 先找一個已經在背景讀入這頁的槽
1595
+ for (size_t i = 0; i < n_slots_; i++) {
1596
+ slot_state & st = slots_[i];
1597
+ if (st.busy && st.layer == il && st.expert == ie) {
1598
+ // 等背景執行緒讀完
1599
+ lk.unlock();
1600
+ const uint64_t t0 = now_us();
1601
+ while (true) {
1602
+ lk.lock();
1603
+ if (!slots_[i].busy) {
1604
+ break;
1605
+ }
1606
+ lk.unlock();
1607
+ std::this_thread::sleep_for(std::chrono::microseconds(200));
1608
+ }
1609
+ s = (int32_t) i;
1610
+ lk.unlock();
1611
+ stats_.prefetch_wait_us.fetch_add(now_us() - t0, std::memory_order_relaxed);
1612
+ std::unique_lock<std::mutex> lk2(mu_);
1613
+ slots_[s].last_use = ++clock_;
1614
+ slots_[s].pin++;
1615
+ stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
1616
+ stats_.prefetch_used.fetch_add(1, std::memory_order_relaxed);
1617
+ return s;
1618
+ }
1619
+ }
1620
+
1621
+ if (!blocking) {
1622
+ return -1;
1623
+ }
1624
+
1625
+ // 選槽位時必須知道「這個 expert 需要哪一種大小」,否則大 expert 可能被塞進小槽位
1626
+ // (寫出邊界 → 靜默算錯),或者小 expert 佔掉大槽位(浪費 6.8% 的 arena)。
1627
+ s = admit_slot_locked(il, ie);
1628
+ if (s < 0) {
1629
+ // 拿不到槽位:短暫等待別的執行緒解除 pin(每層開頭會 unpin_all)。
1630
+ // 這裡「重試」而不是直接回 -1,因為回 -1 會讓呼叫端**跳過這個 expert**,
1631
+ // 結果是靜默少算一條專家分支 —— 那是最嚴重的錯誤。
1632
+ // 2000 次 × 250 µs = 最多等 0.5 秒;真的撐不到那麼久,代表 arena 被
1633
+ // 預取的 busy 槽位佔住了(實測:跨層預取開啟時會發生)。
1634
+ for (int attempt = 0; attempt < 2000 && s < 0; attempt++) {
1635
+ lk.unlock();
1636
+ std::this_thread::sleep_for(std::chrono::microseconds(250));
1637
+ lk.lock();
1638
+ s = admit_slot_locked(il, ie);
1639
+ }
1640
+ if (s < 0) {
1641
+ fprintf(stderr,
1642
+ "[sdq] 嚴重:找不到能放 il=%d ie=%d(need_big=%d)的槽位,"
1643
+ "大槽 %zu/小槽 %zu\n",
1644
+ il, ie, (int) need_big_slot(il, ie), n_big_, n_small_);
1645
+ // 這裡「回 -1」會讓呼叫端**跳過這個 expert**,結果是靜默少算一條專家分支
1646
+ // —— 那是整個引擎最嚴重的錯誤型態(模型會安靜地給出錯誤答案,而且
1647
+ // 每次執行還不一定一樣)。寧可整個行程死掉,也不要產出「看起來正常」
1648
+ // 的錯誤結果。
1649
+ fprintf(stderr,
1650
+ "[sdq] 致命:無法取得 expert 槽位。為了不給出靜默錯誤的結果,"
1651
+ "直接中止。請回報這個情況(多半是預取把槽位佔成 busy 太久)。\n");
1652
+ std::fflush(stderr);
1653
+ abort();
1654
+ }
1655
+ }
1656
+ slot_state & st = slots_[s];
1657
+ if (st.layer >= 0) {
1658
+ page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
1659
+ stats_.evictions.fetch_add(1, std::memory_order_relaxed);
1660
+ used_.fetch_sub(1, std::memory_order_relaxed);
1661
+ }
1662
+ st.layer = il;
1663
+ st.expert = ie;
1664
+ st.last_use = ++clock_;
1665
+ st.pin = 1;
1666
+ st.is_hot = hot_lookup(il, ie);
1667
+ // **先標 busy,再 publish 到 page_。**
1668
+ // 順序反過來的話(先 publish 再標 busy 中間有一個窗口)另一條執行緒會把它
1669
+ // 當成命中,然後在資料還沒讀完時就拿去做矩陣乘法 → 非決定性的錯誤輸出。
1670
+ // acquire() 看到 busy 會等;admit/victim 本來就會跳過 busy 的槽位。
1671
+ st.busy = true;
1672
+ page_[((int64_t) il << 20) | (int64_t) ie] = s;
1673
+
1674
+ lk.unlock();
1675
+
1676
+ const uint64_t t0 = now_us();
1677
+ const bool ok = read_expert_par(il, ie, slot_ptr(s));
1678
+ const uint64_t dt = now_us() - t0;
1679
+
1680
+ std::unique_lock<std::mutex> lk2(mu_);
1681
+ st.busy = false;
1682
+ if (!ok) {
1683
+ st.layer = -1;
1684
+ st.expert = -1;
1685
+ st.pin = 0;
1686
+ page_.erase(((int64_t) il << 20) | (int64_t) ie);
1687
+ return -1;
1688
+ }
1689
+ used_.fetch_add(1, std::memory_order_relaxed);
1690
+ stats_.demand_wait_us.fetch_add(dt, std::memory_order_relaxed);
1691
+ const layer_desc * L = layer(il);
1692
+ if (L) {
1693
+ const uint64_t nb = (uint64_t) L->experts[(size_t) ie].bytes;
1694
+ stats_.ssb_read_bytes_demand.fetch_add(nb, std::memory_order_relaxed);
1695
+ if (lstats_ && il >= 0 && il < (int) info_.n_layer) { // SDQ-0004
1696
+ lstats_[il].demand_bytes.fetch_add(nb, std::memory_order_relaxed);
1697
+ }
1698
+ }
1699
+ return s;
1700
+ }
1701
+
1702
+ void pager::prefetch_expert(int il, int ie) {
1703
+ if (!ready_ || !cfg_.prefetch) {
1704
+ return;
1705
+ }
1706
+ {
1707
+ std::unique_lock<std::mutex> lk(mu_);
1708
+ if (find_slot_locked(il, ie) >= 0) {
1709
+ stats_.prefetch_duplicate.fetch_add(1, std::memory_order_relaxed);
1710
+ return;
1711
+ }
1712
+ // 預取只能占用「空槽」或「已經很久沒被用到的頁」。
1713
+ // (實測:無差別淘汰會讓命中率從 90% 掉到 27%,而預取使用率是 0%,純粹有害。)
1714
+ //
1715
+ // **槽位一定要選對尺寸**:need_big_slot() 為真的 expert 拿到小槽位會寫出邊界。
1716
+ const uint64_t stale_before = clock_ - (uint64_t) env_size("SDQ_PREFETCH_STALE_TOKENS", 2) * 320;
1717
+ // 預取佇列也不能太深:SSD 讀不夠快時,排在後面的預取早就過期了
1718
+ if (pf_q_.size() > (size_t) env_size("SDQ_PREFETCH_QUEUE", 64)) {
1719
+ return;
1720
+ }
1721
+ // **必須留夠空的槽位給 demand。**
1722
+ // 預取會把槽位標成 busy 並佔著它讀取;大槽位本來就少(工作集下限是
1723
+ // n_big_layers × n_expert_used),如果預取把最後幾個空槽都吃掉,
1724
+ // 下一層的 demand 就會「找不到能放 il=34 ie=114 的槽位」而中止。
1725
+ // (這是 verify 新增的預取壓力測試 [2b] 抓到的實際情況。)
1726
+ // 所以:對應尺寸的空槽少於 n_expert_used 個時,這一筆預取直接不做。
1727
+ const size_t reserve = (size_t) info_.n_expert_used;
1728
+ const size_t n_free_right = need_big_slot(il, ie) ? free_big_slots_.size() : free_slots_.size();
1729
+ if (n_free_right < reserve) {
1730
+ stats_.prefetch_duplicate.fetch_add(1, std::memory_order_relaxed);
1731
+ return;
1732
+ }
1733
+ const int32_t s = victim_stale_for(il, ie, stale_before);
1734
+ if (s < 0) {
1735
+ return;
1736
+ }
1737
+ slot_state & st = slots_[s];
1738
+ if (st.layer >= 0) {
1739
+ page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
1740
+ stats_.evictions.fetch_add(1, std::memory_order_relaxed);
1741
+ used_.fetch_sub(1, std::memory_order_relaxed);
1742
+ }
1743
+ st.layer = il;
1744
+ st.expert = ie;
1745
+ st.busy = true;
1746
+ st.pin = 0;
1747
+ pf_q_.push_back({s, il, ie, now_us()});
1748
+ stats_.prefetch_issued.fetch_add(1, std::memory_order_relaxed);
1749
+ }
1750
+ pf_cv_.notify_one();
1751
+ }
1752
+
1753
+ void pager::pf_worker() {
1754
+ std::unique_lock<std::mutex> lk(pf_mu_);
1755
+ for (;;) {
1756
+ pf_cv_.wait(lk, [this] { return pf_stop_ || !pf_q_.empty(); });
1757
+ if (pf_stop_ && pf_q_.empty()) {
1758
+ return;
1759
+ }
1760
+ const pf_item item = pf_q_.front();
1761
+ pf_q_.pop_front();
1762
+ lk.unlock();
1763
+
1764
+ // 這個槽可能已經被淘汰/重用 → 檢查 generation
1765
+ {
1766
+ std::unique_lock<std::mutex> g(mu_);
1767
+ const slot_state & st = slots_[item.slot];
1768
+ const bool still = st.busy && st.layer == item.il && st.expert == item.ie;
1769
+ if (!still) {
1770
+ lk.lock();
1771
+ continue;
1772
+ }
1773
+ }
1774
+
1775
+ // 預取也走 I/O 段平行:單獨一條執行緒循序讀三段,是這條路徑唯一的瓶頸。
1776
+ const bool ok = cfg_.io_parallel ? read_expert_par(item.il, item.ie, slot_ptr(item.slot))
1777
+ : read_expert(item.il, item.ie, slot_ptr(item.slot));
1778
+ const layer_desc * L = layer(item.il);
1779
+ {
1780
+ std::unique_lock<std::mutex> g(mu_);
1781
+ slot_state & st = slots_[item.slot];
1782
+ if (st.busy && st.layer == item.il && st.expert == item.ie) {
1783
+ st.busy = false;
1784
+ st.last_use = ++clock_;
1785
+ if (ok) {
1786
+ page_[((int64_t) item.il << 20) | (int64_t) item.ie] = item.slot;
1787
+ used_.fetch_add(1, std::memory_order_relaxed);
1788
+ stats_.prefetch_completed.fetch_add(1, std::memory_order_relaxed);
1789
+ // SDQ-0004: tag the page so the demand path can credit it,
1790
+ // and so unused prefetched bytes can be reported as waste.
1791
+ st.from_prefetch = true;
1792
+ if (lstats_ && item.il >= 0 && item.il < (int) info_.n_layer) {
1793
+ lstats_[item.il].prefetch_bytes.fetch_add(
1794
+ (uint64_t) slot_size_of(item.slot), std::memory_order_relaxed);
1795
+ }
1796
+ if (L) {
1797
+ stats_.ssb_read_bytes_prefetch.fetch_add(
1798
+ (uint64_t) L->experts[(size_t) item.ie].bytes, std::memory_order_relaxed);
1799
+ }
1800
+ } else {
1801
+ st.layer = -1;
1802
+ st.expert = -1;
1803
+ }
1804
+ }
1805
+ }
1806
+ lk.lock();
1807
+ }
1808
+ }
1809
+
1810
+ void pager::release(int s) {
1811
+ if (s < 0 || (size_t) s >= n_slots_) {
1812
+ return;
1813
+ }
1814
+ std::lock_guard<std::mutex> lk(mu_);
1815
+ slot_state & st = slots_[(size_t) s];
1816
+ if (st.pin > 0) {
1817
+ st.pin--;
1818
+ }
1819
+ }
1820
+
1821
+ void pager::unpin_all() {
1822
+ std::unique_lock<std::mutex> lk(mu_);
1823
+ for (auto & st : slots_) {
1824
+ st.pin = 0;
1825
+ }
1826
+ }
1827
+
1828
+ void pager::note_used(int il, int ie) {
1829
+ if (il < 0 || ie < 0) {
1830
+ return;
1831
+ }
1832
+ std::lock_guard<std::mutex> lk(hist_mu_);
1833
+ if (il >= (int) cur_used_.size()) {
1834
+ return;
1835
+ }
1836
+ auto & v = cur_used_[(size_t) il];
1837
+ if (std::find(v.begin(), v.end(), ie) == v.end()) {
1838
+ v.push_back(ie);
1839
+ }
1840
+ }
1841
+
1842
+ // 「下一層」預取:只在 layer L 的 op 開頭呼叫一次。
1843
+ //
1844
+ // 為什麼需要這個函式:end_token() 是**整個 token 的 40 層都算完之後**才被呼叫的
1845
+ // (sdq_cli.cpp 在 llama_decode 之後呼叫),所以它排的預取完全沒有和任何計算重疊
1846
+ // —— 量測上 prefetch 使用率只有 0~8 %,等於整條預取路徑是關的。
1847
+ // 在 layer L 開始時就替 layer L+1 排預取,才有一整層的時間(讀+算,約 4 ms)
1848
+ // 可以讓 SSD 讀取躲在後面。
1849
+ //
1850
+ // 預測用的是 prev_used_[il+1](上一個 token 在該層選的專家)—— MoE 的路由在
1851
+ // 鄰近 token 之間非常穩定,這是最便宜且不用訓練的預測器。
1852
+ void pager::prefetch_layer_ahead(int il) {
1853
+ if (!ready_ || !cfg_.prefetch || il < 0 || il >= (int) layers_.size()) {
1854
+ return;
1855
+ }
1856
+ int32_t ids[64];
1857
+ const int k = (int) info_.n_expert_used;
1858
+ if (k <= 0 || k > 64) {
1859
+ return;
1860
+ }
1861
+ predict(il, ids, k);
1862
+ for (int i = 0; i < k; i++) {
1863
+ prefetch_expert(il, ids[i]);
1864
+ }
1865
+ }
1866
+
1867
+ // SDQ-0005: called once per llama_decode() from any front end.
1868
+ //
1869
+ // sdq-chat drives this itself from main(); llama-server does not, which left
1870
+ // tokens at 0, ssd_bytes_per_token undefined and SDQ_STATS_FILE empty. This is
1871
+ // the single place that closes a decode step, so hook it here.
1872
+ //
1873
+ // n_tokens > 1 means prefill: a whole prompt was just paged in. Counting that as
1874
+ // one "token" would make ssd_bytes_per_token meaningless, and feeding it to the
1875
+ // Markov table would poison the transition counts with a whole-batch transition.
1876
+ // So prefill advances the byte accounting by n_tokens but leaves the routing
1877
+ // history alone -- it is not a single-token transition.
1878
+ void pager::note_decode(int n_tokens) {
1879
+ if (!ready_ || n_tokens <= 0) {
1880
+ return;
1881
+ }
1882
+ stats_.tokens.fetch_add((uint64_t) n_tokens, std::memory_order_relaxed);
1883
+ if (n_tokens == 1) {
1884
+ end_token();
1885
+ }
1886
+ dump_stats("decode");
1887
+ }
1888
+
1889
+ void pager::end_token() {
1890
+ if (!ready_) {
1891
+ return;
1892
+ }
1893
+ // 用「上個 token 的選擇」更新一階 Markov 轉移表
1894
+ {
1895
+ std::lock_guard<std::mutex> lk(hist_mu_);
1896
+ for (size_t il = 0; il < cur_used_.size(); il++) {
1897
+ auto & cur = cur_used_[il];
1898
+ if (cur.empty()) {
1899
+ continue;
1900
+ }
1901
+ auto & layer_hist = hist_[il];
1902
+ // 以每層最近一次(或平均)的前一次選擇為前驅
1903
+ if (!prev_used_.empty() && il < prev_used_.size() && !prev_used_[il].empty()) {
1904
+ for (int32_t from : prev_used_[il]) {
1905
+ trans & t = layer_hist[(size_t) from];
1906
+ for (int32_t to : cur) {
1907
+ int found = -1;
1908
+ for (int32_t i = 0; i < t.n; i++) {
1909
+ if (t.to[i] == to) {
1910
+ t.cnt[i]++;
1911
+ found = i;
1912
+ break;
1913
+ }
1914
+ }
1915
+ if (found < 0 && t.n < 64) {
1916
+ t.to[t.n] = to;
1917
+ t.cnt[t.n] = 1;
1918
+ t.n++;
1919
+ }
1920
+ }
1921
+ }
1922
+ }
1923
+ prev_used_[il] = cur;
1924
+ cur.clear();
1925
+ }
1926
+ }
1927
+
1928
+ // 依預測排預取
1929
+ if (!cfg_.prefetch) {
1930
+ return;
1931
+ }
1932
+ int32_t ids[64];
1933
+ for (size_t il = 0; il < layers_.size(); il++) {
1934
+ const int k = (int) info_.n_expert_used;
1935
+ predict((int) il, ids, k);
1936
+ for (int i = 0; i < k; i++) {
1937
+ prefetch_expert((int) il, ids[i]);
1938
+ }
1939
+ }
1940
+ }
1941
+
1942
+ void pager::predict(int il, int32_t * out, int k) {
1943
+ if (il < 0 || il >= (int) prev_used_.size()) {
1944
+ for (int i = 0; i < k; i++) {
1945
+ out[i] = i;
1946
+ }
1947
+ return;
1948
+ }
1949
+ // 以最近一次選擇為前驅;沒有歷史就直接沿用上一次的選擇
1950
+ const std::vector<int32_t> & prev = prev_used_[(size_t) il];
1951
+ std::vector<std::pair<uint32_t, int32_t>> scored;
1952
+ if (prev.empty()) {
1953
+ for (int i = 0; i < k; i++) {
1954
+ out[i] = i;
1955
+ }
1956
+ return;
1957
+ }
1958
+ for (int32_t from : prev) {
1959
+ const trans & t = hist_[(size_t) il][(size_t) from];
1960
+ for (int32_t i = 0; i < t.n; i++) {
1961
+ scored.push_back({t.cnt[i], t.to[i]});
1962
+ }
1963
+ }
1964
+ if (scored.empty()) {
1965
+ for (size_t i = 0; i < prev.size() && i < (size_t) k; i++) {
1966
+ out[i] = prev[i];
1967
+ }
1968
+ for (int i = (int) prev.size(); i < k; i++) {
1969
+ out[i] = i;
1970
+ }
1971
+ return;
1972
+ }
1973
+ std::sort(scored.begin(), scored.end(), [](auto & a, auto & b) {
1974
+ return a.first > b.first;
1975
+ });
1976
+ for (int i = 0; i < k && i < (int) scored.size(); i++) {
1977
+ out[i] = scored[(size_t) i].second;
1978
+ }
1979
+ }
1980
+
1981
+ double pager::hit_rate() const {
1982
+ const uint64_t req = stats_.expert_requests.load();
1983
+ const uint64_t hit = stats_.expert_hits.load();
1984
+ return req ? (double) hit / (double) req : 0.0;
1985
+ }
1986
+
1987
+ void pager::shutdown() {
1988
+ if (!ready_) {
1989
+ return;
1990
+ }
1991
+ if (!profile_out_.empty()) {
1992
+ std::lock_guard<std::mutex> lk(mu_);
1993
+ dump_profile(profile_out_);
1994
+ }
1995
+ if (pf_.joinable()) {
1996
+ {
1997
+ std::unique_lock<std::mutex> lk(pf_mu_);
1998
+ pf_stop_ = true;
1999
+ }
2000
+ pf_cv_.notify_all();
2001
+ pf_.join();
2002
+ }
2003
+ io_pool_stop();
2004
+ for (auto & fd : shard_fd_) {
2005
+ if (fd >= 0) {
2006
+ close(fd);
2007
+ }
2008
+ }
2009
+ for (auto & fd : shard_fd_direct_) {
2010
+ if (fd >= 0) {
2011
+ close(fd);
2012
+ }
2013
+ }
2014
+ shard_fd_.clear();
2015
+ shard_fd_direct_.clear();
2016
+ ready_ = false;
2017
+ }
2018
+
2019
+ pager::~pager() {
2020
+ shutdown();
2021
+ if (arena_) {
2022
+ // 注意:arena 是 posix_memalign 配置的,必須用 free() 釋放,
2023
+ // 用 munmap() 會破壞 glibc 的 heap(之後任何 free 都會報 double free)。
2024
+ free(arena_);
2025
+ arena_ = nullptr;
2026
+ }
2027
+ }
2028
+
2029
+ static double pct(uint64_t a, uint64_t b) {
2030
+ return b ? 100.0 * (double) a / (double) b : 0.0;
2031
+ }
2032
+
2033
+ void pager::print_summary() const {
2034
+ if (!ready_) {
2035
+ return;
2036
+ }
2037
+ const layer_desc * L0 = layer(0);
2038
+ if (L0 && !L0->experts.empty()) {
2039
+ const expert_desc & e = L0->experts[0];
2040
+ fprintf(stderr, "[sdq] page-map: n_layer=%lld n_expert=%lld n_expert_used=%lld n_embd=%lld n_ff_exp=%lld"
2041
+ " | L0E0 gate=%llu/%llu up=%llu/%llu down=%llu/%llu (off/size) ne0=%lld ne1=%lld d_ne0=%lld d_ne1=%lld\n",
2042
+ (long long) info_.n_layer, (long long) info_.n_expert, (long long) info_.n_expert_used,
2043
+ (long long) info_.n_embd, (long long) info_.n_ff_exp,
2044
+ (unsigned long long) e.seg[part::GATE].off, (unsigned long long) e.seg[part::GATE].size,
2045
+ (unsigned long long) e.seg[part::UP].off, (unsigned long long) e.seg[part::UP].size,
2046
+ (unsigned long long) e.seg[part::DOWN].off, (unsigned long long) e.seg[part::DOWN].size,
2047
+ (long long) e.ne0, (long long) e.ne1, (long long) e.down_ne0, (long long) e.down_ne1);
2048
+ }
2049
+ fprintf(stderr,
2050
+ "[sdq] expert pager ready: %s\n"
2051
+ "[sdq] 熱門 expert 釘選:%d 個%s\n"
2052
+ "[sdq] slot=%.2f MiB (%zu B) slots=%zu arena=%.2f GiB (RAM 實測 RSS=%.2f GiB)\n",
2053
+ cfg_.model_path.c_str(),
2054
+ n_hot_, n_hot_ > 0 ? "(永不淘汰)" : "(無剖面,用動態 LRU/LFU)",
2055
+ slot_bytes_ / 1048576.0, slot_bytes_, n_slots_, arena_bytes_ / 1073741824.0,
2056
+ process_rss_bytes() / 1073741824.0);
2057
+ if (n_big_) {
2058
+ // 大小兩種槽位:把配置結果講清楚,否則只看「slots=N」會誤以為每個槽一樣大。
2059
+ fprintf(stderr,
2060
+ "[sdq] 槽位分兩種:小 %.2f MiB × %zu + 大 %.2f MiB × %zu(平均 %.2f MiB)\n",
2061
+ small_bytes_ / 1048576.0, n_small_,
2062
+ big_bytes_ / 1048576.0, n_big_,
2063
+ (double) arena_bytes_ / (double) (n_slots_ ? n_slots_ : 1) / 1048576.0);
2064
+ }
2065
+ fprintf(stderr,
2066
+ "[sdq] prefetch=%s fadvise_dontneed=%d\n",
2067
+ cfg_.prefetch ? "on" : "off", (int) cfg_.fadvise_dontneed);
2068
+ }
2069
+
2070
+ void pager::dump_stats(const char * phase) {
2071
+ if (cfg_.stats_path.empty()) {
2072
+ return;
2073
+ }
2074
+ const uint64_t tokens = stats_.tokens.load();
2075
+ const uint64_t req = stats_.expert_requests.load();
2076
+ const uint64_t hit = stats_.expert_hits.load();
2077
+ const uint64_t issued = stats_.prefetch_issued.load();
2078
+ const uint64_t used = stats_.prefetch_used.load();
2079
+ const uint64_t b_dem = stats_.ssb_read_bytes_demand.load();
2080
+ const uint64_t b_pre = stats_.ssb_read_bytes_prefetch.load();
2081
+ // SDQ-0004: prefetched bytes that were consumed, so the rest is real waste.
2082
+ const uint64_t b_pre_used = stats_.prefetch_bytes_used.load();
2083
+
2084
+ std::ostringstream o;
2085
+ o << "{\"phase\":\"" << phase << "\""
2086
+ << ",\"tokens\":" << tokens
2087
+ << ",\"expert_requests\":" << req
2088
+ << ",\"expert_hits\":" << hit
2089
+ << ",\"expert_misses\":" << (req - hit)
2090
+ << ",\"hit_rate\":" << pct(hit, req)
2091
+ << ",\"ssb_demand_bytes\":" << b_dem
2092
+ << ",\"ssb_prefetch_bytes\":" << b_pre
2093
+ << ",\"ssb_total_bytes\":" << (b_dem + b_pre)
2094
+ << ",\"ssd_bytes_per_token\":" << (tokens ? (double) (b_dem + b_pre) / (double) tokens : 0.0)
2095
+ << ",\"prefetch_issued\":" << issued
2096
+ << ",\"prefetch_used\":" << used
2097
+ << ",\"prefetch_used_rate\":" << pct(used, issued)
2098
+ // SDQ-0004: prefetch waste. b_pre counts every byte the background reader
2099
+ // pulled in; b_pre_used counts the ones a demand request actually hit.
2100
+ // The difference is what made docs/07's negative result worse than "slower".
2101
+ << ",\"prefetch_bytes_used\":" << b_pre_used
2102
+ << ",\"prefetch_bytes_wasted\":" << (b_pre > b_pre_used ? b_pre - b_pre_used : 0)
2103
+ << ",\"prefetch_waste_rate\":" << pct(b_pre > b_pre_used ? b_pre - b_pre_used : 0, b_pre)
2104
+ << ",\"prefetch_duplicate\":" << stats_.prefetch_duplicate.load()
2105
+ << ",\"evictions\":" << stats_.evictions.load()
2106
+ << ",\"demand_wait_us\":" << stats_.demand_wait_us.load()
2107
+ << ",\"prefetch_wait_us\":" << stats_.prefetch_wait_us.load()
2108
+ << ",\"arena_bytes\":" << arena_bytes_
2109
+ // 大小兩種槽位之後,「用掉幾個槽位」不能直接乘 slot_bytes_(那是大的那種)。
2110
+ // dump_stats 只在階段轉換時呼叫一次,遍歷兩千幾個槽位的成本可以忽略。
2111
+ << ",\"arena_used_bytes\":" << used_bytes_snapshot()
2112
+ << ",\"rss_bytes\":" << process_rss_bytes()
2113
+ << ",\"n_slots\":" << n_slots_
2114
+ << ",\"n_small_slots\":" << n_small_
2115
+ << ",\"n_big_slots\":" << n_big_
2116
+ << ",\"small_slot_bytes\":" << small_bytes_
2117
+ << ",\"big_slot_bytes\":" << big_bytes_
2118
+ << ",\"slot_bytes\":" << slot_bytes_
2119
+ // SDQ-0004: per-layer breakdown. docs/10 needs this to tell "the device is
2120
+ // slow" apart from "one layer keeps evicting everyone else's experts".
2121
+ << ",\"per_layer\":[";
2122
+ for (int64_t il = 0; lstats_ && il < (int64_t) info_.n_layer; il++) {
2123
+ const uint64_t lreq = lstats_[il].req.load();
2124
+ const uint64_t lhit = lstats_[il].hit.load();
2125
+ o << (il ? "," : "")
2126
+ << "{\"layer\":" << il
2127
+ << ",\"req\":" << lreq
2128
+ << ",\"hit\":" << lhit
2129
+ << ",\"miss\":" << (lreq - lhit)
2130
+ << ",\"hit_rate\":" << pct(lhit, lreq)
2131
+ << ",\"demand_bytes\":" << lstats_[il].demand_bytes.load()
2132
+ << ",\"prefetch_bytes\":" << lstats_[il].prefetch_bytes.load()
2133
+ << "}";
2134
+ }
2135
+ o << "]" << "}";
2136
+ std::ofstream f(cfg_.stats_path, std::ios::app);
2137
+ if (f) {
2138
+ f << o.str() << "\n";
2139
+ }
2140
+ }
2141
+
2142
+ } // namespace sdq
2143
+ namespace sdq {
2144
+ // 由 CLI 呼叫:依環境變數/參數初始化 pager
2145
+ bool sdq_pager_active_for_layer(int il) {
2146
+ pager & P = pager::instance();
2147
+ if (!P.ready() || P.layer(il) == nullptr) {
2148
+ // ⚠️ 分頁開著、但某一層沒有頁面表 —— 千萬不要「靜靜地」走回原生路徑。
2149
+ // 這時 expert 權重是 sdq_buft 的假 buffer(內容恆為 0),
2150
+ // mul_mat_id 會算出一堆 0,然後看起來像「模型壞了」而不是「沒分頁」。
2151
+ // 直接當場出局,比輸出亂碼好診斷。
2152
+ if (P.ready() && il >= 0) {
2153
+ static std::mutex m;
2154
+ static bool warned = false;
2155
+ std::lock_guard<std::mutex> lk(m);
2156
+ if (!warned) {
2157
+ warned = true;
2158
+ fprintf(stderr,
2159
+ "[sdq] 致命錯誤:分頁已啟用,但第 %d 層沒有 expert 頁面表"
2160
+ "(模型只有 %lld 層)。若這是 MTP / 投機解碼圖,"
2161
+ "這個模型的分頁路徑還沒支援它;請改用 --no-mtp 或不要開啟 MTP。\n",
2162
+ il, (long long) P.info().n_layer);
2163
+ }
2164
+ std::abort();
2165
+ }
2166
+ return false;
2167
+ }
2168
+ // 除錯用:SDQ_MOE_LO / SDQ_MOE_HI 可以只讓部分層走分頁路徑,
2169
+ // 用來二分搜尋第一個發生數值差異的層。
2170
+ static const int lo = [] { const char * e = getenv("SDQ_MOE_LO"); return e ? atoi(e) : 0; }();
2171
+ static const int hi = [] { const char * e = getenv("SDQ_MOE_HI"); return e ? atoi(e) : 1 << 30; }();
2172
+ return il >= lo && il < hi;
2173
+ }
2174
+
2175
+ void sdq_on_decode(int n_tokens) {
2176
+ auto & inst = pager::instance();
2177
+ if (inst.ready()) {
2178
+ inst.note_decode(n_tokens);
2179
+ }
2180
+ }
2181
+
2182
+ bool sdq_init(const std::string & model_path) {
2183
+ config c;
2184
+ c.model_path = model_path;
2185
+ c.arena_bytes = env_size("SDQ_ARENA_MB", 0) << 20;
2186
+ c.ram_budget_bytes = env_size("SDQ_RAM_BUDGET_MB", 0) << 20;
2187
+ c.slot_bytes = env_size("SDQ_SLOT_KB", 0) << 10;
2188
+ // 預設關閉。docs/07 的實測是「開 5.68 / 關 6.20 tok/s」的負結果,而且
2189
+ // 預取中的槽位會讓 acquire() 靜默跳過 expert,曾造成輸出不一致(stage S7)。
2190
+ // 這裡原本是 true,等於讓那個負結果從來沒有真正生效。
2191
+ c.prefetch = env_flag("SDQ_PREFETCH", false);
2192
+ // ---- OPT-1:熱門 expert 剖面(Strata 式)----
2193
+ if (const char * pi = getenv("SDQ_PROFILE_IN")) { c.profile_in = pi; }
2194
+ if (const char * po = getenv("SDQ_PROFILE_OUT")) { c.profile_out = po; }
2195
+ c.hot_pin = (int) env_size("SDQ_HOT_PIN", 0);
2196
+ c.admit_min_hits = (int) env_size("SDQ_ADMIT_MIN_HITS", 1);
2197
+ c.fadvise_dontneed = env_flag("SDQ_FADV_DONTNEED", true);
2198
+ c.io_parallel = env_flag("SDQ_IO_PARALLEL", true);
2199
+ // I/O 讀取執行緒數。sdq-chat 會在載入模型「之前」自己 setenv 這個值
2200
+ // (3 × 計算執行緒,上限 48,docs/05 量到序列 3.79 → 24 條 6.68 tok/s)。
2201
+ // 但 cfg_.n_threads 從來沒有被賦值,所以任何沒有自行設定的前端
2202
+ // (llama-server 就是)會拿到 0,然後在 io_pool_start() 被夾到下限 4 條 ——
2203
+ // 等於一直跑在「序列讀」那一端。改成在 pager 自己算,量測與 sdq-chat 一致。
2204
+ if (!getenv("SDQ_IO_THREADS")) {
2205
+ const int hw = (int) std::max(1u, std::thread::hardware_concurrency());
2206
+ const int compute = hw < 8 ? hw : 8; // 與 sdq-chat 同一個上限 8
2207
+ c.io_threads = compute * 3;
2208
+ } else {
2209
+ c.io_threads = (int) env_size("SDQ_IO_THREADS", 0);
2210
+ }
2211
+ c.io_split = (int) env_size("SDQ_IO_SPLIT", 1);
2212
+ c.verbose = env_flag("SDQ_VERBOSE", false);
2213
+ const char * sp = getenv("SDQ_STATS_FILE");
2214
+ if (sp) {
2215
+ c.stats_path = sp;
2216
+ }
2217
+ sdq_set_config(c);
2218
+ try {
2219
+ pager::instance().init(c, nullptr);
2220
+ } catch (const std::exception & e) {
2221
+ fprintf(stderr, "[sdq] pager 初始化失敗:%s\n", e.what());
2222
+ return false;
2223
+ }
2224
+ return pager::instance().ready();
2225
+ }
2226
+
2227
+
2228
+ // ---------------------------------------------------------------- 除錯:dump 任意 F32 張量
2229
+ // 目的:讓「原生 mul_mat_id 路徑」與「SDQ 分頁路徑」輸出同一層的 MoE 結果,
2230
+ // 兩邊各自 dump 一次就能直接 A/B 對照,判定到底是哪一邊算錯。
2231
+ namespace {
2232
+ struct dump_arg { std::string path; };
2233
+
2234
+ void dump_op(ggml_tensor * dst, int ith, int /*nth*/, void * userdata) {
2235
+ if (ith != 0) {
2236
+ return;
2237
+ }
2238
+ const ggml_tensor * src = dst->src[0];
2239
+ dump_arg * a = (dump_arg *) userdata;
2240
+ FILE * f = fopen(a->path.c_str(), "wb");
2241
+ if (!f) {
2242
+ fprintf(stderr, "[sdq-dump] 無法開啟 %s\n", a->path.c_str());
2243
+ return;
2244
+ }
2245
+ const int64_t meta[6] = { src->ne[0], src->ne[1], src->ne[2], src->ne[3],
2246
+ (int64_t) ggml_nbytes(src), 0 };
2247
+ fwrite(meta, sizeof(int64_t), 6, f);
2248
+ // 只支援 F32 contiguous;其他情況就明確報錯,不要假設
2249
+ if (src->type != GGML_TYPE_F32 || !ggml_is_contiguous(src)) {
2250
+ fprintf(stderr, "[sdq-dump] %s 不是 contiguous F32(type=%d)\n",
2251
+ a->path.c_str(), (int) src->type);
2252
+ } else {
2253
+ fwrite(src->data, sizeof(float), (size_t) (ggml_nelements(src)), f);
2254
+ }
2255
+ fclose(f);
2256
+ fprintf(stderr, "[sdq-dump] %s (%lld 個元素)\n", a->path.c_str(),
2257
+ (long long) ggml_nelements(src));
2258
+ }
2259
+ } // namespace
2260
+
2261
+ ggml_tensor * sdq_maybe_dump_tensor(ggml_context * ctx, ggml_tensor * t, int il) {
2262
+ const char * prefix = getenv("SDQ_MOE_OUT_DUMP");
2263
+ if (!prefix || !t) {
2264
+ return t;
2265
+ }
2266
+ int layer = getenv("SDQ_MOE_OUT_DUMP_LAYER") ? atoi(getenv("SDQ_MOE_OUT_DUMP_LAYER")) : -1;
2267
+ if (layer >= 0 && il != layer) {
2268
+ return t;
2269
+ }
2270
+ dump_arg * a = new dump_arg{ std::string(prefix) + "." + std::to_string(il) };
2271
+ return ggml_custom_4d(ctx, GGML_TYPE_F32, t->ne[0], t->ne[1], t->ne[2], t->ne[3],
2272
+ &t, 1, dump_op, 1, a);
2273
+ }
2274
+
2275
+ void sdq_shutdown() {
2276
+ pager & P = pager::instance();
2277
+ sdq_print_time_profile();
2278
+ P.dump_stats("shutdown");
2279
+ P.print_summary();
2280
+ P.shutdown();
2281
+ }
2282
+ } // namespace sdq
sdq-dsv4/sdq_pager.h ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SDQ expert pager:SSD → RAM → CPU 的 MoE 專家分層快取
2
+ //
3
+ // 設計要點
4
+ // * 分頁單位 = 一個 expert(layer, expert id),大小 ~1.8 MB(三個矩陣:gate/up/down)
5
+ // * RAM 端是一個固定槽位的 arena(slab),每槽 2 MiB,O(1) 配置、不碎片化
6
+ // * 淘汰策略:LRU + 本 token 使用中(pinned)的保護
7
+ // * 預取:依「上一個 token 的專家選擇」用一階 Markov 轉移預測下一個 token 的專家,
8
+ // 交給背景執行緒 pread(),量測「預取命中率 / 預取浪費量」
9
+ // * 指標:hit/miss、SSD 讀取位元組、demand 停滯時間、prefetch 領先時間、arena 使用量
10
+ //
11
+ // 不使用 swap:所有權重資料只存在 arena(實體 RAM)與檔案(SSD)兩處。
12
+
13
+ #pragma once
14
+
15
+ #include <ggml.h>
16
+ #include <ggml-backend.h>
17
+ #include "llama.h"
18
+
19
+ #include <atomic>
20
+ #include <chrono>
21
+ #include <condition_variable>
22
+ #include <cstddef>
23
+ #include <cstdint>
24
+ #include <deque>
25
+ #include <memory>
26
+ #include <mutex>
27
+ #include <string>
28
+ #include <thread>
29
+ #include <unordered_map>
30
+ #include <vector>
31
+
32
+ namespace sdq {
33
+
34
+ // ---------------------------------------------------------------- GGUF 讀取
35
+ struct gguf_info {
36
+ int64_t n_layer = 0;
37
+ int64_t n_expert = 0;
38
+ int64_t n_expert_used = 0;
39
+ int64_t n_embd = 0;
40
+ int64_t n_ff_exp = 0;
41
+ int64_t n_ff_shexp = 0;
42
+ int64_t n_expert_shared = 0; // deepseek4 有 1 個 shared expert(不分頁)
43
+ int64_t n_head = 0;
44
+ int64_t n_head_kv = 0;
45
+ int64_t n_embd_head_k = 0;
46
+ int64_t n_embd_head_v = 0;
47
+ int64_t context_length = 0;
48
+ int64_t full_attention_interval = 4;
49
+ int64_t is_recr[512] = {0};
50
+ std::string arch;
51
+ std::string name;
52
+ };
53
+
54
+ // 一個 expert 槽位:gate/up/down 三段(有些模型把 gate+up 合成 gate_up)
55
+ struct seg_desc {
56
+ uint64_t off = 0; // 在「所屬 shard 檔案內」的位元組偏移
57
+ uint64_t size = 0; // 位元組數
58
+ // sharded GGUF:同一層的三段不一定在同一個檔案裡(unsplit 後 100+ GB 的模型一定會被切),
59
+ // 所以每一段都要記得自己屬於哪一個 shard,pread 才知道要開哪個 fd。
60
+ int shard = 0;
61
+ };
62
+
63
+ enum part : int { GATE = 0, UP = 1, DOWN = 2, COUNT = 3 };
64
+
65
+ struct expert_desc {
66
+ seg_desc seg[3];
67
+ int64_t slot_off[3] = {0, 0, 0}; // 三段在 arena 槽位內的相對偏移
68
+ int64_t ne0 = 0; // gate/up row 長度(= n_embd)
69
+ int64_t ne1 = 0; // gate/up row 數(= n_ff_exp)
70
+ int64_t down_ne0 = 0; // down row 長度(= n_ff_exp)
71
+ int64_t down_ne1 = 0; // down row 數(= n_embd)
72
+ ggml_type type[3] = {GGML_TYPE_COUNT, GGML_TYPE_COUNT, GGML_TYPE_COUNT};
73
+ int64_t bytes = 0; // 三段總大小
74
+ bool fused_gate_up = false;
75
+ };
76
+
77
+ // 一層的所有 expert 描述(40 層 × 256 個)
78
+ struct layer_desc {
79
+ std::vector<expert_desc> experts;
80
+ int64_t bytes_per_expert = 0;
81
+ // 這個層的 expert 比「大多數層」大(Q4_K/Q5_K vs Q6_K)→ 需要大槽位。
82
+ // 不然的話所有槽位都得按最大的那種配,白白浪費 6.8% 的 arena。
83
+ bool needs_big_slot = false;
84
+ };
85
+
86
+ // ---------------------------------------------------------------- 指標
87
+ struct stats {
88
+ std::atomic<uint64_t> expert_requests{0}; // 每個 (layer, token) 需要幾個 expert
89
+ std::atomic<uint64_t> expert_hits{0};
90
+ std::atomic<uint64_t> expert_misses{0};
91
+ std::atomic<uint64_t> ssb_read_bytes_demand{0};
92
+ std::atomic<uint64_t> ssb_read_bytes_prefetch{0};
93
+ std::atomic<uint64_t> prefetch_issued{0};
94
+ std::atomic<uint64_t> prefetch_completed{0};
95
+ std::atomic<uint64_t> prefetch_used{0}; // 預取後真的被用到
96
+ std::atomic<uint64_t> prefetch_duplicate{0};// 預取時已經在 RAM 裡
97
+ std::atomic<uint64_t> evictions{0};
98
+ std::atomic<uint64_t> demand_wait_us{0}; // 因為缺頁而停下來等 SSD 的時間
99
+ std::atomic<uint64_t> prefetch_wait_us{0};
100
+ std::atomic<uint64_t> prefetch_bytes_used{0}; // prefetched bytes a demand request actually hit
101
+ std::atomic<uint64_t> tokens{0};
102
+ std::atomic<uint64_t> moe_calls{0};
103
+ std::atomic<uint64_t> flops{0};
104
+ };
105
+
106
+ // ---- SDQ-0004: per-layer cache counters (Phase 0: is any layer starved?)
107
+ struct layer_stat {
108
+ std::atomic<uint64_t> req{0};
109
+ std::atomic<uint64_t> hit{0};
110
+ std::atomic<uint64_t> miss{0};
111
+ std::atomic<uint64_t> demand_bytes{0};
112
+ std::atomic<uint64_t> prefetch_bytes{0};
113
+ };
114
+
115
+
116
+ // ---------------------------------------------------------------- pager
117
+ struct config {
118
+ size_t arena_bytes = 0; // 0 = 自動(依 RAM 預算)
119
+ size_t slot_bytes = 0; // 0 = 自動(依模型推導)
120
+ int n_threads = 0;
121
+ bool prefetch = true;
122
+ int prefetch_ahead_layers = 0; // >0 時限制同時在飛的預取數量
123
+ bool fadvise_dontneed = true; // 讀完丟掉 kernel page cache,讓 RAM 帳目誠實
124
+ bool verbose = false;
125
+ uint64_t ram_budget_bytes = 0; // 0 = 自動(實體 RAM 的一小部分)
126
+ std::string model_path;
127
+ std::string stats_path;
128
+ int slot_bias = 0;
129
+
130
+ // ---- I/O 執行緒池 ----
131
+ // 為什麼需要:一個 expert 在檔案上是 gate / up / down 三段「不相鄰」的資料,
132
+ // 循序讀要 3 次 pread,而單一 pread 在本機量到約 7 ms(1.9 MB / 275 MB/s)。
133
+ // 也就是說「讀一個 expert」的延遲完全由這三段循序決定,執行緒再多也沒用。
134
+ // 把三段交給三條執行緒同時讀,單一 expert 的延遵掉到約 1/2~1/3。
135
+ // (實測:8 個 expert 全部 miss 的一層,序列讀 12.6 ms → 段平行 7.6 ms。)
136
+ bool io_parallel = true; // SDQ_IO_PARALLEL=0 可關(回到序列讀)
137
+ int io_threads = 0; // 0 = 自動(3 × n_threads,上限 48)
138
+ int io_split = 1; // SDQ_IO_SPLIT:每一段再切成幾塊並行讀(實測 1 最好)
139
+
140
+ // ---- Strata 式「熱門 expert 剖面」(OPT-1)----
141
+ // 離線用代表性文字跑一次,把每個 (layer, expert) 的請求次數記成剖面;
142
+ // 執行時把排行榜前 hot_pin 個釘成「熱頁」:熱頁可以被讀取,但**永遠不被淘汰**,
143
+ // 淘汰時一律先從冷頁挑。這解決的是「prefill 的一次性專家把熱區洗掉」。
144
+ std::string profile_in; // 讀取剖面(SDQ_PROFILE_IN)
145
+ std::string profile_out; // 結束時輸出剖面(SDQ_PROFILE_OUT)
146
+ int hot_pin = 0; // 釘選前幾名(SDQ_HOT_PIN)
147
+ int admit_min_hits = 1; // 請求次數低於此值就不收進快取(SDQ_ADMIT_MIN_HITS)
148
+ };
149
+
150
+ class pager {
151
+ public:
152
+ static pager & instance();
153
+
154
+ // 從 GGUF 檔頭建立 expert 頁面表,並配置 RAM arena
155
+ void init(const config & cfg, ggml_context * ctx);
156
+
157
+ bool ready() const { return ready_; }
158
+ size_t arena_bytes() const { return arena_bytes_; }
159
+ size_t slot_bytes() const { return slot_bytes_; }
160
+ size_t n_slots() const { return n_slots_; }
161
+ size_t small_bytes() const { return small_bytes_; }
162
+ size_t big_bytes() const { return big_bytes_; }
163
+ size_t n_small_slots() const { return n_small_; }
164
+ size_t n_big_slots() const { return n_big_; }
165
+ const gguf_info & info() const { return info_; }
166
+
167
+ // 取得 layer 描述(idx 越界或沒資料回 nullptr)
168
+ const layer_desc * layer(int il) const;
169
+
170
+ // 一次把「連續編號的一串 expert」讀進 RAM:同一層的三個矩陣(gate/up/down)本來
171
+ // 就各自連續存放,所以 k 個連續 expert 只需要 3 次大讀取(而不是 3k 次)。
172
+ // 對高延遲的儲存裝置(每個 request 都貴)這是最大的單項加速。
173
+ // experts 必須已依編號遞增排序;slots[i] 是 experts[i] 對應的槽位(由呼叫端配置)
174
+ // 回傳實際讀取的位元組數。
175
+ uint64_t read_expert_span(int il, const int32_t * experts, int n, const int32_t * slots);
176
+
177
+ // 只配置槽位(不讀資料):回傳槽位索引,-1 表示沒有空槽
178
+ int reserve_slot(int il, int ie);
179
+
180
+ // 查這個 expert 是否已經在 RAM:命中則 pin 並回傳槽位,未命中回傳 -1(不配置)
181
+ int probe(int il, int ie);
182
+
183
+ // 記錄一次合併讀取的位元組數(給指標用)
184
+ void note_span_read(uint64_t bytes);
185
+
186
+ // 確保 (layer, expert) 在 RAM,回傳槽位索引;-1 表示失敗
187
+ // pinned: 本 token 使用中,不可被淘汰;blocking: 缺頁時是否同步讀 SSD
188
+ int acquire(int il, int ie, bool blocking);
189
+
190
+ // 背景預取:把 page 讀進 arena(非同步),依歷史預測下一個 token 會用到的 expert
191
+ void prefetch_expert(int il, int ie);
192
+ // 「下一層」預取:在 layer L 的 op 開頭呼叫,替 layer L+1 排預取。
193
+ // 與 end_token() 的差別在時機 —— 後者在一個 token 的 40 層全算完才跑,
194
+ // 那時排的預取沒有任何計算可以重疊,實測使用率只有 0~8 %。
195
+ void prefetch_layer_ahead(int il);
196
+
197
+ // 每層計算開始時呼叫:解除上一層的 pin(本層用到的槽位才受保護)
198
+ void unpin_all();
199
+ // 用完一個槽位後立刻放掉 pin(成對呼叫 acquire())。
200
+ //
201
+ // 為什麼需要:原本 pin 是「整層釘住、下一層開頭才 unpin_all」。mode C 的
202
+ // expert_forward 回傳之後就再也不讀那個槽位(最後的歸約只讀各執行緒自己的
203
+ // partial 累加緩衝),所以繼續釘著只是白白把工作集撐大。
204
+ // 撐大的後果很具體:prefill 一個 ubatch 有 128 個 token,一層最多需要
205
+ // 256 個不同的 expert → 整層釘住 = 256 個槽位不能回收。大小兩種槽位之下,
206
+ // 大槽位只有約 n_big_layers × n_expert_used 個,於是**必定**在 prefill 時
207
+ // 「找不到能放 il=34 ie=… 的槽位」而中止。
208
+ void release(int s);
209
+ void note_used(int il, int ie);
210
+ void end_token();
211
+ // ---- SDQ-0005: one call per llama_decode() from any front end.
212
+ void note_decode(int n_tokens);
213
+
214
+ // 依歷史預測某層下一個 token 的 top-k expert(回傳信心 0..1)
215
+ void predict(int il, int32_t * out, int k);
216
+
217
+ void shutdown();
218
+ stats & get_stats() { return stats_; }
219
+
220
+ // 把當前指標寫成一行 JSON(append 到 stats_path)
221
+ void dump_stats(const char * phase);
222
+ void print_summary() const;
223
+
224
+ // arena 內的槽位指標
225
+ // 槽位索引空間是統一的:[0, n_small_) 是小槽位(連續),
226
+ // [n_small_, n_slots_) 是大槽位(接在小槽位區之後)。
227
+ uint8_t * slot_ptr(int s) {
228
+ return (size_t) s < n_small_
229
+ ? arena_ + (size_t) s * small_bytes_
230
+ : arena_ + small_bytes_ * n_small_ + (size_t)(s - (int) n_small_) * big_bytes_;
231
+ }
232
+ size_t slot_size_of(int s) const {
233
+ return (size_t) s < n_small_ ? small_bytes_ : big_bytes_;
234
+ }
235
+ bool need_big_slot(int il, int ie) const;
236
+
237
+ // 這個槽位算不算「大槽位」。
238
+ //
239
+ // 判斷依據是**槽位的實際大小**,不是索引 >= n_small_:
240
+ // 統一尺寸(SDQ_FORCE_UNIFORM_SLOT=1,或模型只有一種 expert 大小)時,
241
+ // 所有槽位的索引都 < n_small_ 但大小都是大的 —— 用索引判斷會讓大 expert
242
+ // 永遠配不到槽位,整個推論卡在 admit 的重試迴圈裡。
243
+ bool is_big_slot(int s) const {
244
+ return big_bytes_ && slot_size_of(s) >= big_bytes_;
245
+ }
246
+
247
+ size_t used_bytes() const { return used_bytes_snapshot(); }
248
+ double hit_rate() const;
249
+
250
+ // arena 裡實際被占用的位元組數。大小兩種槽位之後不能用「槽位數 × slot_bytes_」
251
+ // 計算(slot_bytes_ 是大的那一種),所以真的把槽位走一遍。
252
+ // 呼叫端:dump_stats(每次解碼階段結束)與 print_summary,不是熱路徑。
253
+ //
254
+ // ⚠️ 一定要拿 mu_:slots_ 是被 I/O 執行緒與 op 的執行緒同時寫的。
255
+ // 舊版沒有鎖,是一個實際的資料競爭(TSan 會報,實務上在 x86 上讀到的是
256
+ // 撕裂的 int32,數字會亂但不會改變推論結果 —— 這裡只是把它修掉)。
257
+ size_t used_bytes_snapshot() const {
258
+ std::lock_guard<std::mutex> lk(mu_);
259
+ size_t b = 0;
260
+ for (size_t s = 0; s < slots_.size(); s++) {
261
+ if (slots_[s].layer >= 0) {
262
+ b += slot_size_of((int) s);
263
+ }
264
+ }
265
+ return b;
266
+ }
267
+
268
+ // 讀 SSD(同步,給 acquire 用)
269
+ bool read_expert(int il, int ie, uint8_t * dst);
270
+
271
+ // ---- I/O 執行緒池(見 config::io_parallel 的說明)----
272
+ // 一個「讀取批次」的完成狀態;放在呼叫端的 stack 上,worker 只在 remaining==0
273
+ // 之後才會再碰它,所以離開 read_expert_par() 之後不會有 dangling pointer。
274
+ struct io_ctx {
275
+ std::atomic<int> remaining{0};
276
+ bool ok = true;
277
+ };
278
+ struct io_job {
279
+ int shard;
280
+ uint64_t off;
281
+ uint8_t * dst;
282
+ size_t size;
283
+ io_ctx * ctx;
284
+ };
285
+
286
+ void io_pool_start();
287
+ void io_pool_stop();
288
+ void io_worker(size_t wid);
289
+ bool read_expert_par(int il, int ie, uint8_t * dst);
290
+ bool read_range_staged(int shard, uint64_t off, uint8_t * dst, size_t size,
291
+ uint8_t * stage, size_t stage_sz);
292
+
293
+ // I/O 執行緒池每條 worker 自帶的 O_DIRECT 對齊緩衝(啟動時配置,已計入 RSS)
294
+ struct io_stage { uint8_t * buf = nullptr; size_t sz = 0; };
295
+ std::vector<io_stage> io_stage_;
296
+
297
+ bool io_pool_on_ = false;
298
+ int io_threads_ = 0;
299
+ int io_split_ = 1;
300
+ std::vector<std::thread> io_pool_;
301
+ std::mutex io_mu_;
302
+ std::condition_variable io_cv_; // 有工作 / 要停
303
+ std::condition_variable io_done_cv_; // 某個批次讀完了
304
+ std::deque<io_job> io_q_;
305
+ bool io_stop_ = false;
306
+ std::atomic<uint64_t> io_jobs_{0};
307
+ std::atomic<uint64_t> io_wait_ns_{0};
308
+
309
+ private:
310
+ pager() = default;
311
+ ~pager();
312
+
313
+ struct slot_state {
314
+ int32_t layer = -1;
315
+ int32_t expert = -1;
316
+ uint64_t last_use = 0; // LRU 記時
317
+ uint32_t freq = 0; // 這個 expert 被用到幾次(衰減式;用來做 LFU 混合策略)
318
+ uint32_t pin = 0; // >0 時不可淘汰
319
+ bool busy = false; // 背景執行緒正在寫入
320
+ bool is_hot = false; // 屬於剖面排行榜:淘汰時最後才會被選中
321
+ bool from_prefetch = false; // page was filled by the prefetcher
322
+ };
323
+
324
+ struct pf_item {
325
+ int slot;
326
+ int il;
327
+ int ie;
328
+ uint64_t issue_time_us;
329
+ };
330
+
331
+ void touch_freq_locked(slot_state & st);
332
+ // ---- OPT-1:剖面 / 熱頁 ----
333
+ void count_request(int il, int ie); // 記一次請求(給剖面用)
334
+ bool load_profile(const std::string & path); // 讀剖面並標記�� hot_pin 名
335
+ void dump_profile(const std::string & path); // 輸出排行榜
336
+ bool hot_lookup(int il, int ie) const;
337
+ void load_gguf_header(const std::string & path);
338
+ void build_page_map(const std::string & path);
339
+ void alloc_arena();
340
+ int find_slot_locked(int il, int ie);
341
+ int admit_slot_locked(int il, int ie);
342
+ int victim_locked() { return admit_slot_locked(-1, -1); }
343
+ int victim_stale_locked(uint64_t stale_before);
344
+ int victim_stale_for(int il, int ie, uint64_t stale_before);
345
+ void pf_worker();
346
+ bool read_range(int shard, uint64_t off, uint8_t * dst, size_t size);
347
+ bool read_range_direct(int shard, uint64_t off, uint8_t * dst, size_t size);
348
+ void open_model_fd();
349
+
350
+ // ---- sharded GGUF:模型被切成多個 -0000N-of-000M.gguf,每個檔有自己的
351
+ // header、自己的 data_start 與自己的張量子集。pager 必須同時開著全部的 fd,
352
+ // 並把每個張量的「檔內偏移」記成 (shard, off)。
353
+ std::vector<std::string> shard_paths_;
354
+ std::vector<int> shard_fd_;
355
+ std::vector<int> shard_fd_direct_;
356
+ std::vector<uint64_t> shard_data_start_;
357
+ bool use_direct_ = false;
358
+ std::string shard_path(int i) const {
359
+ return (i >= 0 && i < (int) shard_paths_.size()) ? shard_paths_[(size_t) i] : std::string();
360
+ }
361
+ // 依 GGUF 的 split 命名規則列舉同目錄的所有 shard
362
+ static std::vector<std::string> list_splits(const std::string & first_path, int n_split);
363
+
364
+ bool ready_ = false;
365
+ config cfg_;
366
+ gguf_info info_;
367
+ std::vector<layer_desc> layers_;
368
+
369
+ uint8_t * arena_ = nullptr;
370
+ size_t arena_bytes_ = 0;
371
+ size_t slot_bytes_ = 0; // = max(small_bytes_, big_bytes_),保留給舊的訊息輸出
372
+ size_t small_bytes_ = 0; // 小槽位大小(大多數層用)
373
+ size_t big_bytes_ = 0; // 大槽位大小;0 = 這個模型沒有大層
374
+ size_t n_small_ = 0;
375
+ size_t n_big_ = 0;
376
+ size_t n_slots_ = 0;
377
+ mutable std::mutex mu_;
378
+ std::vector<slot_state> slots_;
379
+ std::unordered_map<int64_t, int32_t> page_; // (layer<<16|expert) -> slot
380
+ std::vector<int32_t> free_slots_; // 空的小槽位
381
+ std::vector<int32_t> free_big_slots_;
382
+
383
+ // ---- OPT-1:剖面 / 熱頁(Strata 式:靜態排行榜 + 熱頁不淘汰)----
384
+ std::vector<uint32_t> prof_; // 每個 (layer, expert) 的累計請求次數
385
+ std::unique_ptr<layer_stat[]> lstats_; // SDQ-0004
386
+ void ensure_layer_stats(int n_layer); // SDQ-0004
387
+ std::vector<uint8_t> hot_; // 是否在排行榜前 hot_pin 名
388
+ int n_hot_ = 0;
389
+ std::string profile_out_;
390
+ uint32_t freq_tick_ = 0;
391
+ std::atomic<size_t> used_{0};
392
+ uint64_t clock_ = 0;
393
+
394
+ // 背景預取
395
+ std::thread pf_;
396
+ std::mutex pf_mu_;
397
+ std::condition_variable pf_cv_;
398
+ std::deque<pf_item> pf_q_;
399
+ bool pf_stop_ = false;
400
+ std::atomic<uint64_t> pf_inflight_{0};
401
+
402
+ // 路由歷史(一階 Markov:上個 token 的 expert -> 這次被選到的次數)
403
+ struct trans { int32_t to[64]; uint32_t cnt[64]; int32_t n = 0; };
404
+ std::vector<std::vector<trans>> hist_; // [layer][prev_expert]
405
+ std::vector<std::vector<int32_t>> prev_used_; // [layer] 上一個 token 用到的 expert(預測用)
406
+ std::vector<std::vector<int32_t>> cur_used_; // [layer] 當前 token 用到的 expert
407
+ std::mutex hist_mu_; // 保護 cur_used_ / prev_used_ / hist_(多執行緒會同時 note_used)
408
+ std::atomic<uint64_t> hist_lock_{0};
409
+
410
+ stats stats_;
411
+ };
412
+
413
+ // ---------------------------------------------------------------- 工具
414
+ uint64_t now_us();
415
+ size_t process_rss_bytes();
416
+ size_t physical_ram_bytes();
417
+
418
+ // ---------------------------------------------------------------- ggml 介接
419
+ // 自訂 buffer type:expert 權重佔 0 個 RAM 位元組,資料留在檔案裡
420
+ ggml_backend_buffer_type_t sdq_expert_buft();
421
+
422
+ // 註冊 model params 的 tensor buffer override(expert 權重 → 我們的 buffer)
423
+ void sdq_install_buft_overrides(llama_model_params * params);
424
+
425
+ // 設定 arena 大小 / 預取開關等(CLI 在載入模型前呼叫)
426
+ bool sdq_init(const std::string & model_path);
427
+ void sdq_set_config(const config & c);
428
+ const config & sdq_get_config();
429
+ void sdq_set_enabled(bool on);
430
+ void sdq_shutdown();
431
+
432
+ bool sdq_enabled();
433
+
434
+ // SDQ-0005: bridge for front ends that are not sdq-chat (llama-server).
435
+ // Defined in sdq_pager.cpp; safe to call when the pager was never initialised.
436
+ void sdq_on_decode(int n_tokens);
437
+
438
+ // 效能計時探針的結果(SDQ_PROFILE_TIME=1)
439
+ void sdq_print_time_profile();
440
+
441
+ // 自我測試:算一次指定 expert 的 MoE 輸出(給 NumPy 對照用)
442
+ int sdq_moe_selftest(int il, int ie, const float * x, float * out_io,
443
+ int64_t n_embd, int64_t n_ff, int n_tok,
444
+ float swiglu_limit, int swiglu_mode);
445
+
446
+ // 建立我們的 MoE ��訂節點。
447
+ // **必須把 gf 傳進來並呼叫 ggml_build_forward_expand(gf, out)**:
448
+ // ggml 的排程器只會為「進了圖的節點」配置 compute buffer,沒註冊的節點其輸出
449
+ // 緩衝區會被 liveness 分析判成可重用,導致 layer>=1 的結果被其他張量覆蓋
450
+ // (實測症狀:layer 0 正確、layer 1 起整個錯掉)。
451
+ ggml_tensor * sdq_build_moe(ggml_cgraph * gf, ggml_context * ctx, ggml_tensor * cur,
452
+ ggml_tensor * ids, ggml_tensor * weights, int il,
453
+ int64_t n_expert, int64_t n_expert_used,
454
+ float swiglu_limit, int swiglu_mode);
455
+
456
+ // SiLU-gated 激活的 clamp 語意(見 sdq_moe.cpp 的說明)。
457
+ // deepseek4 走 ggml_swiglu_clamp 的語意(只夾 gate 上界),與 SDQ 原版不同。
458
+ enum sdq_swiglu_mode : int {
459
+ SDQ_SWIGLU_LEGACY_POST_SILU = 0, // SDQ 原版:對稱夾 silu(gate)
460
+ SDQ_SWIGLU_GGML_FUSED_CLAMP = 1, // ggml_swiglu_clamp:gate = min(gate, limit)
461
+ };
462
+
463
+ // 這一層的分頁是否可用(graph build 時呼叫)
464
+ bool sdq_pager_active_for_layer(int il);
465
+
466
+ // 除錯/驗證用:把某個 F32 張量 dump 到檔案。
467
+ // 必須在「原生 mul_mat_id 路徑」與「SDQ 分頁路徑」兩邊都呼叫,才能做真正的 A/B 對照
468
+ // (env: SDQ_MOE_OUT_DUMP=prefix、SDQ_MOE_OUT_DUMP_LAYER=層編號,-1 = 全部)
469
+ ggml_tensor * sdq_maybe_dump_tensor(ggml_context * ctx, ggml_tensor * t, int il);
470
+
471
+
472
+ } // namespace sdq
sdq-dsv4/verify_dequant_dsv4.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """第二層證據:把 verify_moe_dsv4.py 的 NumPy dequant 與 **ggml 自己的**
3
+ dequantize_row_* 逐位元組比對。
4
+
5
+ 為什麼需要:verify_moe_dsv4.py 裡的 IQ3_S / MXFP4 還原是照著 ggml 原始碼手寫的。
6
+ 如果那個手寫版本有錯,後面「SDQ 的 C++ kernel 對得上 NumPy」的結論就只是在
7
+ 一份錯誤的 reference 上自我一致。這支腳本用 ggml_deq(直接呼叫 ggml 的 C 函式)
8
+ 當獨立實作,兩邊必須**逐位元組相同**(|diff| 必須精確為 0,不是「很小」)。
9
+
10
+ 前置:
11
+ c++ -O2 -o /tmp/opencode/ggml_deq ggml_deq.cpp \
12
+ -I<llama.cpp>/ggml/include -I<llama.cpp>/ggml/src \
13
+ -L<llama.cpp>/build/bin -lggml-base -Wl,-rpath,<llama.cpp>/build/bin
14
+ """
15
+ import os
16
+ import subprocess
17
+ import sys
18
+
19
+ import numpy as np
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
+ import verify_moe_dsv4 as V # noqa: E402
23
+
24
+ GGML_DEQ = os.environ.get('GGML_DEQ', '/tmp/opencode/ggml_deq')
25
+
26
+ # (tensor suffix, ggml type id, 每列的元素數)
27
+ TARGETS = [
28
+ ('ffn_gate_exps.weight', 21, 4096), # IQ3_S(layer 0)
29
+ ('ffn_up_exps.weight', 21, 4096), # IQ3_S(layer 0)
30
+ ('ffn_down_exps.weight', 39, 2048), # MXFP4(layer 0)
31
+ ('ffn_gate_exps.weight', 39, 4096), # MXFP4(layer 26 的 gate/up)
32
+ ('ffn_down_exps.weight', 39, 2048), # MXFP4(layer 26 的 down)
33
+ ]
34
+
35
+
36
+ def main() -> int:
37
+ kv, tensors = V.parse_headers(V.SHARDS)
38
+
39
+ rows = []
40
+ all_ok = True
41
+ for il in (0, 26):
42
+ for suffix, tt, n in TARGETS:
43
+ name = 'blk.%d.%s' % (il, suffix)
44
+ if name not in tensors:
45
+ continue
46
+ si, off, dims, ttt = tensors[name]
47
+ if ttt != tt:
48
+ continue
49
+ nrow = V.row_bytes(n, tt)
50
+ # 取該 (layer, tensor) 的第 0 個 expert 的前幾列
51
+ for row in (0, 1, 137):
52
+ byte_off = off + row * nrow
53
+ raw = V.SHARDS[si]
54
+ got = subprocess.run([GGML_DEQ, str(tt), raw, str(byte_off), str(n)],
55
+ capture_output=True, check=True).stdout
56
+ ref = np.frombuffer(got, dtype=np.float32)
57
+ # ggml_deq 讀的是整列(一個 weight 向量),NumPy 這邊用同一段
58
+ # 位元組還原 —— 兩邊必須對齊同一個 offset。
59
+ with open(raw, 'rb') as f:
60
+ f.seek(byte_off)
61
+ mine = V.dequant(f.read(nrow), n, tt)
62
+ assert ref.shape == mine.shape == (n,)
63
+ d = np.abs(ref - mine)
64
+ exact = bool(np.array_equal(ref, mine))
65
+ all_ok &= exact
66
+ rows.append((il, suffix, tt, row, float(d.max()), exact))
67
+ print('%-34s type=%-2d row=%-4d max|NumPy - ggml| = %g %s'
68
+ % (name, tt, row, d.max(), 'EXACT' if exact else 'MISMATCH'))
69
+
70
+ print()
71
+ print('%d/%d bit-exact' % (sum(1 for r in rows if r[5]), len(rows)))
72
+ print('ALL BIT-EXACT' if all_ok else 'MISMATCH FOUND')
73
+ return 0 if all_ok else 1
74
+
75
+
76
+ if __name__ == '__main__':
77
+ sys.exit(main())
sdq-dsv4/verify_moe_dsv4.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """驗證 SDQ 分頁 + 自訂 MoE kernel 的數學正確性(純 NumPy 對照)。
3
+
4
+ 對象:DeepSeek-V4-Flash-0731 UD-IQ4_NL(deepseek4)
5
+ gate/up : IQ3_S(layer 26 是 MXFP4)
6
+ down : MXFP4
7
+
8
+ 做法:直接從 GGUF 的 4 個 shard 讀出該 (layer, expert) 的三段權重 bytes,
9
+ 依 ggml 的 dequantize_row_iq3_s / dequantize_row_mxfp4 還原成 fp32,
10
+ 用 fp32 純重算一次 SiLU-gated MoE 前向,再與
11
+ `sdq-chat --selftest IL:IE:SEED:OUT.bin` 的輸出比對。
12
+
13
+ SDQ 這邊走的是「量化 activation + ggml_vec_dot_*」,這邊是 fp32,
14
+ 所以殘差應該是 activation 量化誤差量級(~1%),不是結構性錯誤。
15
+
16
+ swiglu 的 clamp 語意照 ggml_swiglu_clamp(deepseek4 實際走的那條):
17
+ gate = min(gate, limit)
18
+ up = clamp(up, -limit, limit)
19
+ out = silu(gate) * up
20
+ """
21
+ import os
22
+ import struct
23
+ import sys
24
+
25
+ import numpy as np
26
+
27
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
28
+ from ggml_tables import IQ3S_GRID, KVALUES_MXFP4, KMASK_IQ2XS # noqa: E402
29
+
30
+ QK_K = 256
31
+ QK_MXFP4 = 32
32
+
33
+ MODEL_DIR = os.environ.get(
34
+ 'DSV4_MODEL_DIR', '/root/app/work/models/UD-IQ4_NL')
35
+ MODEL = os.path.join(
36
+ MODEL_DIR, 'DeepSeek-V4-Flash-0731-UD-IQ4_NL-00001-of-00004.gguf')
37
+ SHARDS = [MODEL.replace('00001-of-00004', '%05d-of-00004' % i) for i in range(1, 5)]
38
+
39
+ # (block_size, type_size) per ggml type id
40
+ BLK = {
41
+ 8: (32, 34), # Q8_0
42
+ 12: (256, 144), # Q4_K
43
+ 13: (256, 176), # Q5_K
44
+ 7: (32, 24), # Q5_1
45
+ 2: (32, 18), # Q4_0
46
+ 21: (256, 110), # IQ3_S
47
+ 39: (32, 17), # MXFP4
48
+ 14: (256, 210), # Q6_K
49
+ }
50
+ TYNAME = {8: 'q8_0', 12: 'q4_K', 13: 'q5_K', 7: 'q5_1', 2: 'q4_0',
51
+ 21: 'iq3_s', 39: 'mxfp4', 14: 'q6_K'}
52
+
53
+ TS = {2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8}
54
+ FMT = {0: '<B', 1: '<b', 2: '<H', 3: '<h', 4: '<I', 5: '<i', 6: '<f',
55
+ 7: '<B', 10: '<Q', 11: '<q', 12: '<d'}
56
+
57
+
58
+ # ------------------------------------------------------------------ GGUF header
59
+ def rstr(f):
60
+ n = struct.unpack('<Q', f.read(8))[0]
61
+ return f.read(n)
62
+
63
+
64
+ def read_val(f, t):
65
+ if t == 8:
66
+ return rstr(f).decode('utf-8', 'replace')
67
+ if t == 9:
68
+ et = struct.unpack('<I', f.read(4))[0]
69
+ n = struct.unpack('<Q', f.read(8))[0]
70
+ if et == 8:
71
+ return [rstr(f).decode('utf-8', 'replace') for _ in range(n)]
72
+ sz = TS[et]
73
+ buf = f.read(n * sz)
74
+ return np.frombuffer(buf, dtype=np.dtype(FMT[et])).tolist()
75
+ buf = f.read(TS[t])
76
+ return np.frombuffer(buf, dtype=np.dtype(FMT[t]))[0].item()
77
+
78
+
79
+ def parse_headers(paths):
80
+ kv = {}
81
+ tensors = {}
82
+ for si, path in enumerate(paths):
83
+ with open(path, 'rb') as f:
84
+ assert f.read(4) == b'GGUF'
85
+ f.read(4)
86
+ n_tensors = struct.unpack('<Q', f.read(8))[0]
87
+ n_kv = struct.unpack('<Q', f.read(8))[0]
88
+ for _ in range(n_kv):
89
+ k = rstr(f).decode()
90
+ t = struct.unpack('<I', f.read(4))[0]
91
+ kv[k] = read_val(f, t)
92
+ align = kv.get('general.alignment', 32)
93
+ pending = []
94
+ for _ in range(n_tensors):
95
+ name = rstr(f).decode()
96
+ nd = struct.unpack('<I', f.read(4))[0]
97
+ dims = struct.unpack('<%dQ' % nd, f.read(8 * nd))
98
+ tt = struct.unpack('<I', f.read(4))[0]
99
+ off = struct.unpack('<Q', f.read(8))[0]
100
+ pending.append((name, dims, tt, off))
101
+ # data_start 必須在讀完 tensor 資訊表之後才算,否則整體錯開約 18 KB。
102
+ data_start = (f.tell() + align - 1) // align * align
103
+ for name, dims, tt, off in pending:
104
+ tensors[name] = (si, data_start + off, dims, tt)
105
+ return kv, tensors
106
+
107
+
108
+ # ------------------------------------------------------------------ dequant
109
+ def e8m0_to_fp32_half_vec(e):
110
+ """ggml_e8m0_to_fp32_half:x<2 是 denormal 2^(-128+x),否則 2^(x-128)。"""
111
+ e = e.astype(np.int64)
112
+ small = np.where(e < 2, (0x00200000 << np.maximum(e, 0)), 0)
113
+ norm = np.where(e >= 2, ((e - 1) << 23), 0)
114
+ return (small | norm).astype(np.uint32).view(np.float32)
115
+
116
+
117
+ def dequant_iq3_s(raw, n):
118
+ """block_iq3_s = { half d; uint8 qs[64]; uint8 qh[8]; uint8 signs[32];
119
+ uint8 scales[4]; } = 110 B / 256 elements。
120
+
121
+ 逐字對應 ggml dequantize_row_iq3_s(ggml/src/ggml-quants.c),
122
+ 以「一次處理全部 block」的方式向量化(輸出順序與 C 版完全相同)。
123
+ """
124
+ nb = n // QK_K
125
+ u = np.frombuffer(raw, dtype=np.uint8, count=nb * 110).reshape(nb, 110)
126
+ d = u[:, 0:2].copy().view(np.float16).reshape(nb).astype(np.float32)
127
+ qs = u[:, 2:66].astype(np.int32)
128
+ qh = u[:, 66:74].astype(np.int32)
129
+ signs = u[:, 74:106].astype(np.int32)
130
+ scales = u[:, 106:110].astype(np.int32)
131
+
132
+ grid = np.array(IQ3S_GRID, dtype=np.int64) # (512,) uint32
133
+ mask_lo = np.array(KMASK_IQ2XS[:4], dtype=np.int32) # j = 0..3
134
+ mask_hi = np.array(KMASK_IQ2XS[4:], dtype=np.int32) # j = 4..7
135
+
136
+ out = np.empty((nb, QK_K), dtype=np.float32)
137
+ for k in range(QK_K // 64): # ib32 = 0,2,4,6
138
+ db1 = d * (1 + 2 * (scales[:, k] & 0xF))
139
+ db2 = d * (1 + 2 * (scales[:, k] >> 4))
140
+ for half in (0, 1):
141
+ db = db1 if half == 0 else db2
142
+ h = qh[:, 2 * k + half]
143
+ q0 = qs[:, k * 16 + half * 8: k * 16 + half * 8 + 8]
144
+ s0 = signs[:, k * 8 + half * 4: k * 8 + half * 4 + 4]
145
+ for l in range(4):
146
+ i1 = q0[:, 2 * l] | ((h << (8 - 2 * l)) & 256)
147
+ i2 = q0[:, 2 * l + 1] | ((h << (7 - 2 * l)) & 256)
148
+ w1 = np.right_shift(grid[i1, None], 8 * np.arange(4)) & 0xFF
149
+ w2 = np.right_shift(grid[i2, None], 8 * np.arange(4)) & 0xFF
150
+ sgn = s0[:, l][:, None]
151
+ col = k * 64 + half * 32 + l * 8
152
+ out[:, col:col + 4] = db[:, None] * np.where(
153
+ (sgn & mask_lo) != 0, -1.0, 1.0) * w1
154
+ out[:, col + 4:col + 8] = db[:, None] * np.where(
155
+ (sgn & mask_hi) != 0, -1.0, 1.0) * w2
156
+ return out.reshape(-1)
157
+
158
+
159
+ def dequant_mxfp4(raw, n):
160
+ """block_mxfp4 = { uint8 e (E8M0); uint8 qs[16]; } = 17 B / 32 elements。
161
+
162
+ 逐字對應 ggml dequantize_row_mxfp4。注意 qs[j] 的低/高 nibble 分別對應
163
+ block 的前 16 個與後 16 個元素(不是交錯),而且 E8M0 的 scale 已經是
164
+ 「E2M1 值 ×2」(kvalues_fp4 是 doubled 的),所以這裡不要再乘 0.5。
165
+ """
166
+ nb = n // QK_MXFP4
167
+ u = np.frombuffer(raw, dtype=np.uint8, count=nb * 17).reshape(nb, 17)
168
+ d = e8m0_to_fp32_half_vec(u[:, 0])[:, None]
169
+ qs = u[:, 1:17].astype(np.int32)
170
+ kv = np.array(KVALUES_MXFP4, dtype=np.float32)
171
+ out = np.empty((nb, QK_MXFP4), dtype=np.float32)
172
+ out[:, 0:16] = kv[qs & 0xF] * d
173
+ out[:, 16:32] = kv[qs >> 4] * d
174
+ return out.reshape(-1)
175
+
176
+
177
+ def dequant_q8_0(raw, n):
178
+ nb = n // 32
179
+ u = np.frombuffer(raw, dtype=np.uint8, count=nb * 34).reshape(nb, 34)
180
+ d = u[:, 0:2].copy().view(np.float16).reshape(nb).astype(np.float32)[:, None]
181
+ return (u[:, 2:34].view(np.int8).astype(np.float32) * d).reshape(-1)
182
+
183
+
184
+ def dequant(raw, n, tt):
185
+ if tt == 21:
186
+ return dequant_iq3_s(raw, n)
187
+ if tt == 39:
188
+ return dequant_mxfp4(raw, n)
189
+ if tt == 8:
190
+ return dequant_q8_0(raw, n)
191
+ raise ValueError('unsupported type %s (%d)' % (TYNAME.get(tt, '?'), tt))
192
+
193
+
194
+ def row_bytes(n, tt):
195
+ bs, ts = BLK[tt]
196
+ assert n % bs == 0, (n, tt)
197
+ return n // bs * ts
198
+
199
+
200
+ # ------------------------------------------------------------------ expert I/O
201
+ def read_expert(tensors, il, ie):
202
+ """回傳 (gate, up, down) 三段;每段是 (raw, n_elem, type, n_elem_per_row, n_rows)。"""
203
+ def seg(name):
204
+ si, off, dims, tt = tensors['blk.%d.%s' % (il, name)]
205
+ ne0, ne1 = int(dims[0]), int(dims[1]) # GGUF dims = [ne0, ne1, n_expert]
206
+ nrow = row_bytes(ne0, tt)
207
+ n = nrow * ne1
208
+ with open(SHARDS[si], 'rb') as f:
209
+ f.seek(off + ie * n)
210
+ raw = f.read(n)
211
+ assert len(raw) == n, (len(raw), n)
212
+ return raw, ne0 * ne1, tt, ne0, ne1
213
+ return (seg('ffn_gate_exps.weight'), seg('ffn_up_exps.weight'),
214
+ seg('ffn_down_exps.weight'))
215
+
216
+
217
+ def swiglu_clamp(g, u, limit):
218
+ """ggml_swiglu_clamp:gate 只夾上界、up 對稱夾,而且夾在 silu 之前。"""
219
+ if limit <= 1e-6:
220
+ return (g / (1.0 + np.exp(-g))) * u
221
+ gc = np.minimum(g, limit)
222
+ uc = np.clip(u, -limit, limit)
223
+ return (gc / (1.0 + np.exp(-gc))) * uc
224
+
225
+
226
+ def swiglu_legacy(g, u, limit):
227
+ """SDQ 原版(qwen4exp 那條分支):對稱夾 silu 之後的 gate。"""
228
+ if limit <= 1e-6:
229
+ return (g / (1.0 + np.exp(-g))) * u
230
+ gc = np.clip(g / (1.0 + np.exp(-g)), -limit, limit)
231
+ return gc * np.clip(u, -limit, limit)
232
+
233
+
234
+ # ------------------------------------------------------------------ main
235
+ def main():
236
+ limit = float(os.environ.get('DSV4_LIMIT', '0'))
237
+ mode = int(os.environ.get('DSV4_MODE', '1'))
238
+ st_dir = os.environ.get('DSV4_ST_DIR', '/tmp/opencode')
239
+ st_pfx = os.environ.get('DSV4_ST_PREFIX', 'st_')
240
+ rel_tol = float(os.environ.get('DSV4_REL_TOL', '0.05'))
241
+ corr_min = float(os.environ.get('DSV4_CORR_MIN', '0.99'))
242
+
243
+ kv, tensors = parse_headers(SHARDS)
244
+ arch = kv['general.architecture']
245
+ assert arch == 'deepseek4', arch
246
+ n_embd = int(kv[arch + '.embedding_length'])
247
+ n_ff = int(kv[arch + '.expert_feed_forward_length'])
248
+ n_expert = int(kv[arch + '.expert_count'])
249
+ n_used = int(kv[arch + '.expert_used_count'])
250
+
251
+ print('model = %s' % os.path.basename(MODEL))
252
+ print('arch = %s n_layer=%d n_expert=%d n_used=%d n_embd=%d n_ff=%d'
253
+ % (arch, int(kv[arch + '.block_count']), n_expert, n_used, n_embd, n_ff))
254
+ print('swiglu = limit=%g mode=%d (%s)'
255
+ % (limit, mode, 'ggml_swiglu_clamp' if mode == 1 else 'SDQ legacy'))
256
+ print()
257
+
258
+ cases = []
259
+ for il in (0, 1, 2, 3, 12, 25, 26, 27, 41, 42):
260
+ for ie in (0, 7, 123, 255):
261
+ path = os.path.join(st_dir, '%sl%de%d.bin' % (st_pfx, il, ie))
262
+ if os.path.exists(path):
263
+ cases.append((il, ie, path))
264
+ break
265
+
266
+ if not cases:
267
+ print('找不到 selftest 輸出(%s/st_l*e*.bin);先跑 sdq-chat --selftest' % st_dir)
268
+ return 1
269
+
270
+ act = swiglu_clamp if mode == 1 else swiglu_legacy
271
+ all_ok = True
272
+ n_pass = 0
273
+ for il, ie, path in cases:
274
+ raw = open(path, 'rb').read()
275
+ n_tok = len(raw) // 4 // (2 * n_embd)
276
+ got = np.frombuffer(raw[:n_tok * n_embd * 4], dtype=np.float32).reshape(n_tok, n_embd)
277
+ x = np.frombuffer(raw[n_tok * n_embd * 4:], dtype=np.float32).reshape(n_tok, n_embd)
278
+
279
+ (gr, gn, g_t, g_ne0, g_ne1), (ur, un, u_t, _, _), (dr, dn, d_t, d_ne0, d_ne1) = \
280
+ read_expert(tensors, il, ie)
281
+ Wg = dequant(gr, gn, g_t).reshape(g_ne1, g_ne0)
282
+ Wu = dequant(ur, un, u_t).reshape(g_ne1, g_ne0)
283
+ Wd = dequant(dr, dn, d_t).reshape(d_ne1, d_ne0)
284
+ del gr, ur, dr
285
+
286
+ rels, corrs = [], []
287
+ for t in range(n_tok):
288
+ xt = x[t]
289
+ h = act(Wg @ xt, Wu @ xt, limit)
290
+ ref = Wd @ h
291
+ gt = got[t]
292
+ denom = max(float(np.abs(ref).max()), 1e-12)
293
+ rels.append(float(np.abs(gt - ref).max()) / denom)
294
+ corrs.append(float(np.corrcoef(gt, ref)[0, 1]))
295
+
296
+ rel = max(rels)
297
+ corr = min(corrs)
298
+ ok = rel < rel_tol and corr > corr_min
299
+ all_ok &= ok
300
+ n_pass += int(ok)
301
+ print('L%-2d E%-3d gate=%-6s up=%-6s down=%-6s | max_rel_err=%7.4f corr=%.6f | %s'
302
+ % (il, ie, TYNAME[g_t], TYNAME[u_t], TYNAME[d_t], rel, corr,
303
+ 'PASS' if ok else 'FAIL'))
304
+ del Wg, Wu, Wd
305
+
306
+ print()
307
+ print('%d/%d PASS' % (n_pass, len(cases)))
308
+ print('ALL PASS' if all_ok else 'SOME FAILED')
309
+ return 0 if all_ok else 1
310
+
311
+
312
+ if __name__ == '__main__':
313
+ sys.exit(main())