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