EPM: expert paging CPU inference for MiniCPM-V-4.7-35B-A3B Q4_K_M
Browse filesPort the 8 GiB expert pager to openbmb/MiniCPM-V-4.7-35B-A3B-Q4_K_M.gguf
(19.71 GiB, qwen35moe, 40 layers, 256 experts, top-8).
Measured under an 8 GiB / no-swap ceiling enforced by tools/strict8g.py:
5.83 tok/s median (N=9), peak anon RSS 7799 MiB, VmSwap 0, 9/9 strict_ok.
Unmodified llama.cpp cannot load the model at that ceiling.
Correctness: 3-layer verification passes; 5 runs x 48 tokens x 248144 vocab
logits bit-identical.
Port-specific changes: GGUF BOOL-array header fix, calibrated SDQ_RESERVE_MB,
two expert size classes, hybrid SSM/attention thread behaviour.
- .gitignore +8 -0
- README.md +363 -0
- bench/expert_index.json +1836 -0
- bench/results/ram_sweep.json +227 -0
- bench/results/ram_sweep_n9.json +584 -0
- bench/results/strict_budgets.json +527 -0
- bench/results/strict_headline.json +214 -0
- bench/results/strict_headline.md +20 -0
- bench/results/strict_prefetch.json +268 -0
- bench/results/strict_threads.json +247 -0
- bench/results/summary.json +83 -0
- bench/run_budget_curve.py +133 -0
- bench/run_n9.py +168 -0
- bench/run_negative.py +155 -0
- bench/run_ramsweep.py +135 -0
- bench/summarize.py +117 -0
- bench/upload.py +59 -0
- cpp/sdq_buft.cpp +225 -0
- cpp/sdq_cli.cpp +821 -0
- cpp/sdq_moe.cpp +755 -0
- cpp/sdq_mtp.cpp +101 -0
- cpp/sdq_pager.cpp +2091 -0
- cpp/sdq_pager.h +433 -0
- demo.py +398 -0
- docs/CORRECTNESS.md +123 -0
- docs/DESIGN.md +138 -0
- docs/NEGATIVE_RESULTS.md +62 -0
- docs/STRICT_8GIB.md +130 -0
- patches/0001-sdq-expert-pager.patch +0 -0
- sdq/__init__.py +0 -0
- sdq/bench.py +188 -0
- sdq/config.py +54 -0
- sdq/experiments.py +395 -0
- sdq/hf_sync.py +175 -0
- sdq/strict_bench.py +317 -0
- sdq/verify.py +294 -0
- tools/determinism_check.py +124 -0
- tools/drop_model_cache.py +46 -0
- tools/epq_lock.so.c +104 -0
- tools/gguf_index.py +289 -0
- tools/io_probe.py +116 -0
- tools/moe_layer_check.py +88 -0
- tools/moe_ref.py +225 -0
- tools/route_overlap.py +137 -0
- tools/strict8g.py +311 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
# built by tools/strict8g.py at run time
|
| 4 |
+
tools/*.so
|
| 5 |
+
# transient per-run records written by the bench scripts
|
| 6 |
+
bench/results/.*
|
| 7 |
+
*.gguf
|
| 8 |
+
*.bin
|
README.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: gguf
|
| 4 |
+
base_model: openbmb/MiniCPM-V-4.7-35B-A3B
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
tags:
|
| 7 |
+
- minicpm
|
| 8 |
+
- qwen35moe
|
| 9 |
+
- gguf
|
| 10 |
+
- moe
|
| 11 |
+
- cpu
|
| 12 |
+
- low-memory
|
| 13 |
+
- expert-offloading
|
| 14 |
+
- no-swap
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# EPM:MiniCPM-V-4.7-35B-A3B 的 8 GiB 專家分頁 CPU 推論
|
| 18 |
+
|
| 19 |
+
在 **8 GiB RAM、沒有 swap** 的條件下,用 CPU 跑 19.71 GiB 的
|
| 20 |
+
`MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf`。
|
| 21 |
+
|
| 22 |
+
同一個模型、同樣的 8 GiB 上限之下,**未修改的 llama.cpp 連模型都載不進去**
|
| 23 |
+
(它要配置一個 14.4 GB 的 CPU_REPACK buffer,在載入階段就失敗)。EPM 把路由
|
| 24 |
+
專家權重留在儲存裝置,只把當前選中的專家分頁載入受限的 CPU arena:
|
| 25 |
+
**5.83 tok/s,峰值匿名 RSS 7799 MiB,VmSwap 全程 0 MiB。**
|
| 26 |
+
|
| 27 |
+
這個 repo 收錄實作(`cpp/` + `patches/`)、量測工具與全部證據。
|
| 28 |
+
**不包含模型權重。**
|
| 29 |
+
|
| 30 |
+
> **名稱說明**:EPM = Expert Paging for MiniCPM。本專案**不是**論文中的 SDQ
|
| 31 |
+
> (Sparse Decomposed Quantization):沒有 2:4 結構化稀疏跳零計算,也不宣稱
|
| 32 |
+
> GPU 稀疏加速。`sdq/` Python package、`sdq_*` C++ 檔案與 `SDQ_*` 環境變數是
|
| 33 |
+
> 歷史 implementation identifier,保留以免破壞既有驗證工具;對外產品名稱與
|
| 34 |
+
> 執行入口是 EPM / `epq-chat`。
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 快速開始
|
| 39 |
+
|
| 40 |
+
需求:Linux x86-64、C++ 工具鏈、CMake、Git、Python 3、19.71 GB 磁碟空間。
|
| 41 |
+
已測 llama.cpp commit `9d7d5049804be03fce6575cc9f49c058b3c48788`。
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
python3 demo.py --once
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
從頭開始時,腳本會取得本 repo 原始碼、從 `openbmb/MiniCPM-V-4.7-35B-A3B-gguf`
|
| 48 |
+
下載 GGUF、套用 patch、編譯,然後跑一句示範。模型已下載的話:
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
python3 demo.py --model-path /path/to/MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf \
|
| 52 |
+
--once --ctx 4096 --threads 8 --n-predict 48
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
互動式聊天:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
python3 demo.py --model-path /path/to/MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf \
|
| 59 |
+
--ctx 4096 --threads 8
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
**記憶體預算與 reserve**:8 GiB 目標用 `--ram-budget-mb 8192` +
|
| 63 |
+
`SDQ_RESERVE_MB=512`(這個模型在 ctx=4096 / ubatch=128 下校正出來的值,
|
| 64 |
+
見 `docs/DESIGN.md` §3)。換 RAM 預算要重新校正。
|
| 65 |
+
|
| 66 |
+
此 GGUF 不含 vision projector,本路徑只支援文字推論。已驗證的 context 是 4K;
|
| 67 |
+
模型原生支援 262 K,但在這個 RAM 預算下沒有驗證過。
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 在 8 GiB / no-swap 下量到的結果
|
| 72 |
+
|
| 73 |
+
**每次執行都由 `tools/strict8g.py` 管治**:所有**匿名**記憶體映射被 `mlock()`
|
| 74 |
+
鎖住(被鎖的頁永遠不會被寫到 swap,所以 no-swap 是核心保證的),
|
| 75 |
+
實體常駐量有硬上限,超標立刻 `SIGKILL`,另外 `RLIMIT_DATA` 當第二道閘門。
|
| 76 |
+
**`strict_ok=False` 的樣本一律不採用。**
|
| 77 |
+
|
| 78 |
+
### 標配(N=9,配對交錯,提示詞中英交替)
|
| 79 |
+
|
| 80 |
+
| 指標 | 值 |
|
| 81 |
+
| --- | ---: |
|
| 82 |
+
| **Decode tok/s(中位數)** | **5.83** |
|
| 83 |
+
| 單輪全距 | 5.19 – 6.63(抖動 25%) |
|
| 84 |
+
| Expert cache hit rate | 68.68% |
|
| 85 |
+
| SSD 讀取 / decode token | 214.1 MB |
|
| 86 |
+
| Arena | 6.35 GiB(3714 slots) |
|
| 87 |
+
| **峰值匿名 RSS** | **7799 MiB** / 8192 MiB 上限 |
|
| 88 |
+
| **峰值 VmSwap** | **0.000 MiB** |
|
| 89 |
+
| strict_ok | 9/9 |
|
| 90 |
+
|
| 91 |
+
條件:2× Intel Xeon Platinum 8559C(AVX-512 / AMX),`ctx=4096`、`ubatch=128`、
|
| 92 |
+
8 threads、greedy、48 requested tokens。
|
| 93 |
+
|
| 94 |
+
> 這是**共享主機**,同一組設定的單輪抖動可達 25%。數字足以支撐「能不能跑」
|
| 95 |
+
> 與「大概多快」,不足以支撐 5% 等級的宣稱。
|
| 96 |
+
|
| 97 |
+
### 同一個 8 GiB 上限下的傳統 llama.cpp
|
| 98 |
+
|
| 99 |
+
| 路徑 | 結果 |
|
| 100 |
+
| --- | --- |
|
| 101 |
+
| 傳統 llama.cpp `llama-completion`(**未套 patch**) | **無法載入模型** |
|
| 102 |
+
| EPM `epq-chat` | **5.83 tok/s**,峰值匿名 RSS 7799 MiB,VmSwap 0 |
|
| 103 |
+
|
| 104 |
+
失敗點:
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
ggml_aligned_malloc: insufficient memory (attempted to allocate 14400.00 MB)
|
| 108 |
+
ggml_backend_buft_alloc_buffer_n_default: failed to allocate CPU_REPACK buffer of size 15099494400
|
| 109 |
+
llama_model_load: error loading model: unable to allocate CPU_REPACK buffer
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
對照必須在**同一個上限**下建立。先量「兩邊都沒記憶體限制」再比較,是把 RAM
|
| 113 |
+
這個變數漏掉了。
|
| 114 |
+
|
| 115 |
+
### Pager budget 曲線(實體上限固定 8 GiB,只有 arena 大小在變,N=5)
|
| 116 |
+
|
| 117 |
+
| Budget | Arena | 槽位 | 命中率 | SSD MB/token | Decode tok/s | 峰值匿名 RSS | VmSwap |
|
| 118 |
+
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
| 119 |
+
| 2 GiB | 0.57 GiB | 332 | 22.72% | 526.2 | 4.32 | 1842 MiB | 0 |
|
| 120 |
+
| 3 GiB | 1.53 GiB | 895 | 50.19% | 340.0 | 5.16 | 2828 MiB | 0 |
|
| 121 |
+
| 4 GiB | 2.50 GiB | 1459 | 59.81% | 275.8 | 5.48 | 3817 MiB | 0 |
|
| 122 |
+
| 5 GiB | 3.46 GiB | 2023 | 64.88% | 240.8 | 5.82 | 4805 MiB | 0 |
|
| 123 |
+
| 6 GiB | 4.43 GiB | 2587 | 67.52% | 222.2 | 5.38 | 5793 MiB | 0 |
|
| 124 |
+
| 7 GiB | 5.39 GiB | 3150 | 68.60% | 214.0 | 5.10 | 6779 MiB | 0 |
|
| 125 |
+
| **8 GiB** | **6.35 GiB** | **3714** | **70.74%** | **198.9** | **5.45** | **7766 MiB** | **0** |
|
| 126 |
+
|
| 127 |
+
2 GiB 只有 332 個槽位。單一 token 的工作集是 320 個 layer-expert,但**槽位還分兩種
|
| 128 |
+
��寸**:20 層用大槽位、20 層用小槽位,所以每個 token 需要的是 160 個大槽位加
|
| 129 |
+
160 個小槽位,而不是 320 個任一尺寸的槽位。2 GiB 之下 arena 只有 0.57 GiB,
|
| 130 |
+
湊不出這個組合,命中率因此崩到 22.7%(8 GiB 之下 3714 個槽位裡約四成是大槽位,
|
| 131 |
+
命中率回到 70.7%)。
|
| 132 |
+
|
| 133 |
+
命中率在 8 GiB 這個端點**仍然在爬升**(8 GiB 70.7% → 16 GiB 77.5%)→
|
| 134 |
+
**瓶頸是記憶體上限,不是命中率飽和。**
|
| 135 |
+
|
| 136 |
+
> 這一欄與上面標配的命中率(68.68%)不同是正常的:這裡是 N=5、提示詞輪替不同,
|
| 137 |
+
> 標配是 N=9。兩者都受同一個 8 GiB 上限管治。
|
| 138 |
+
|
| 139 |
+
### RAM 擴張曲線:4 / 8 / 16 / 64 GiB
|
| 140 |
+
|
| 141 |
+
`--ram-budget-mb` 從 4 GiB 拉到 64 GiB。每次執行都由 `tools/strict8g.py` 管治,
|
| 142 |
+
36/36 輪 `strict_ok=True`,`VmSwap` 全程 0。
|
| 143 |
+
|
| 144 |
+
**兩點方法學說明,讀表前請先看:**
|
| 145 |
+
|
| 146 |
+
1. **這條曲線不用 `mlock`。** `mlock()` 會強制整個映射常駐 —— 對一個 60 GiB 的
|
| 147 |
+
arena,它會讓 RSS 直接變成 60 GiB,量到的就不是真實機器的行為。no-swap 的
|
| 148 |
+
核心保證保留給 8 GiB 這個**部署目標**(那一點另外有完整 mlock 證據,見
|
| 149 |
+
[`docs/STRICT_8GIB.md`](docs/STRICT_8GIB.md))。
|
| 150 |
+
2. **arena 不預先碰滿。** 全部路由專家權重只有 18.16 GiB,所以預先碰觸上限就是
|
| 151 |
+
18.17 GiB;超出來的部分等到真的用到才 fault in。這就是為什麼 64 GiB 預算的
|
| 152 |
+
「峰值 RSS」會停在約 24.0 GiB 而不是 62 GiB —— **那個數字才是真實的記憶體佔用。**
|
| 153 |
+
|
| 154 |
+
結構指標(槽位、命中率、SSD 讀取量、峰值 RSS)都是設定的直接函數,不受執行抖動
|
| 155 |
+
影響,四個點都列。吞吐另外做 **N=9 配對交錯** 並用 Welch t 檢定。
|
| 156 |
+
|
| 157 |
+
| Budget | Arena | 槽位 | 命中率 | SSD MB/token | 平均 tok/s | 單輪標準差 | 峰值 RSS | VmSwap |
|
| 158 |
+
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
| 159 |
+
| 4 GiB | 2.74 GiB | 1600 | 61.50% | 275.4 | 5.43 ± 0.37 | 0.37 | 3963 MiB | 0 |
|
| 160 |
+
| **8 GiB** | **6.36 GiB** | **3715** | **71.98%** | **199.1** | **6.33 ± 0.33** | 0.33 | **7669 MiB** | **0** |
|
| 161 |
+
| 16 GiB | 13.59 GiB | 7943 | 77.54% | 157.9 | 6.28 ± 0.41 | 0.41 | 15076 MiB | 0 |
|
| 162 |
+
| 64 GiB | 59.89 GiB | 35003 | 77.54% | 157.6 | 6.32 ± 0.51 | 0.51 | 23969 MiB | 0 |
|
| 163 |
+
|
| 164 |
+
| 比較 | 結果 | Welch t | p | 判定 |
|
| 165 |
+
| --- | ---: | ---: | ---: | --- |
|
| 166 |
+
| **4 GiB 比 8 GiB** | 5.43 → 6.33(**+16.6%**) | **−5.44** | **0.0001** | **統計顯著** |
|
| 167 |
+
| 8 GiB 比 16 GiB | 6.33 → 6.28(−0.8%) | −0.30 | 0.76 | 無法分辨 |
|
| 168 |
+
| 8 GiB 比 64 GiB | 6.33 → 6.32(−0.2%) | −0.05 | 0.96 | 無法分辨 |
|
| 169 |
+
|
| 170 |
+
#### 這條曲線的重點
|
| 171 |
+
|
| 172 |
+
* **4 GiB → 8 GiB 是真實且顯著的提升:+16.6%**(t = −5.44,p = 0.0001)。4 GiB 時
|
| 173 |
+
命中率只有 61.5%、SSD/token 275.4 MB(8 GiB 的 1.4 倍),因為 arena 湊不出
|
| 174 |
+
「160 大 + 160 小」的工作集。
|
| 175 |
+
* **8 GiB 以上再加記憶體完全沒有可量測的收益**:8 vs 16(t = −0.30, p = 0.76)與
|
| 176 |
+
8 vs 64(t = −0.05, p = 0.96)全部無法分辨,而記憶體用量是 2 倍到 8 倍。
|
| 177 |
+
結構指標也說明原因:命中率在 **16 GiB 就飽和在 77.54%**,SSD/token 飽和在她
|
| 178 |
+
~158 MB。
|
| 179 |
+
* **真正的記憶體需求飽和在約 24.0 GiB 峰值**(18.16 GiB 全部專家權重 + 1.54 GiB
|
| 180 |
+
非專家權重 + KV cache / compute buffer / runtime)。
|
| 181 |
+
* **部署結論:8 GiB 就是對的容量。** 它拿到飽和命中率的 93%(72.0% 對 77.5%),
|
| 182 |
+
而且是整條曲線裡 throughput 顯著優於更低設定、又與所有更高設定打平的唯一一點。
|
| 183 |
+
|
| 184 |
+
> 逐輪原始資料:`bench/results/ram_sweep_n9.json`(N=9)與
|
| 185 |
+
> `bench/results/ram_sweep.json`(N=3,含 32 GiB 點)。
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
## 記憶體怎麼配置的
|
| 190 |
+
|
| 191 |
+
模型 metadata:每個 MoE 層 256 個路由專家,每個 token 選 8 個。
|
| 192 |
+
GGUF 21,166,255,008 B,其中路由專家權重 **18.16 GiB**、非專家常駐權重
|
| 193 |
+
**1.54 GiB**。
|
| 194 |
+
|
| 195 |
+
EPM 把未啟用的專家留在檔案裡,依需求把選中的專家載入記憶體 arena。關鍵實作:
|
| 196 |
+
|
| 197 |
+
* **三段並行讀取**:一個 expert 的 gate/up/down 在檔案上互不相鄰,循序讀是
|
| 198 |
+
3 × 延遲的鏈;改成三段同時讀之後明顯變快。
|
| 199 |
+
* **大小分級槽位**:**這個模型有兩種 expert 大小** —— 20 層的 `ffn_down_exps`
|
| 200 |
+
是 Q6_K(2,039,808 B/expert),另外 20 層是 Q4_K(1,769,472 B/expert)。
|
| 201 |
+
依分佈分大小槽位,而不是全部按最大的配。
|
| 202 |
+
* **需求觸發的 LRU 淘汰**,加上對「本 token 使用中」的 pin 保護。
|
| 203 |
+
* **讀取與計算重疊**(mode C),每個 expert 算完立刻釋放槽位。
|
| 204 |
+
* **跨層預取預設關閉**:它是負結果(見 `docs/NEGATIVE_RESULTS.md`)。
|
| 205 |
+
|
| 206 |
+
### Arena sizing
|
| 207 |
+
|
| 208 |
+
```
|
| 209 |
+
arena = RAM 預算 − 匿名 RSS − SDQ_RESERVE_MB
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
1. **扣的是「匿名 RSS」,不是「總 RSS」。** `statm` 的 resident 把 mmap 進來的
|
| 213 |
+
模型權重頁也算成已用,但那些是乾淨的 file-backed 頁,核心會回��。
|
| 214 |
+
2. **`RLIMIT_DATA` 只當上界,不當預算來源。** 它算的是匿名**虛擬**記憶體,
|
| 215 |
+
而 llama.cpp 會配置大量「映射了但永遠不會 touch」的 buffer。
|
| 216 |
+
|
| 217 |
+
`SDQ_RESERVE_MB` 是**逐模型、逐 ctx/ubatch 校正**的。本模型 ctx=4096 /
|
| 218 |
+
ubatch=128 下,512 MiB 留 446 MiB 餘量。峰值匿名 RSS 幾乎與 reserve 成 1:1
|
| 219 |
+
反比。**換 RAM 預算必須重新校正** —— 用固定的 512 MiB 跑 4 GiB 會超出上限。
|
| 220 |
+
完整校正表見 [`docs/DESIGN.md`](docs/DESIGN.md) §3。
|
| 221 |
+
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
## 正確性
|
| 225 |
+
|
| 226 |
+
```bash
|
| 227 |
+
python3 -m sdq.verify --model <gguf> --bin <epq-chat> --fast
|
| 228 |
+
python3 tools/determinism_check.py --model <gguf> --bin <epq-chat> --runs 5 -n 48
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
| 層 | 內容 | 結果 |
|
| 232 |
+
| --- | --- | --- |
|
| 233 |
+
| 1 kernel | 單一 expert 前向 vs NumPy 純重算(L0 E3 / L17 E200 / L33 E7 / **L39 E100**) | 全 PASS,max abs diff ≤ 3.7e-09 |
|
| 234 |
+
| 2 整層 | 真實路由輸入 vs NumPy,L0 / L39 | L0 PASS(1.1e-08);L39 PASS(8.8e-05,落在單一 Q8_K LSB 翻轉上界 3.2e-02 之內) |
|
| 235 |
+
| 2b 預取壓力 | 預取開 + 極小 arena 強制回收,兩種槽位配置 | 4/4 PASS |
|
| 236 |
+
| 3 端到端 A/B | vs llama.cpp 原生 `mul_mat_id` | **在 8 GiB 上不可能執行** —— 需要整個 19.71 GiB 模型常駐 |
|
| 237 |
+
|
| 238 |
+
決定性:**5 次執行 × 48 token × 248,144 vocab 的 logits 全部逐位元組相同。**
|
| 239 |
+
|
| 240 |
+
過程中抓到並修正的問題包括一個 GGUF 標頭解析在 **BOOL 陣列**上錯位的 bug
|
| 241 |
+
(`qwen35moe.attention.recurrent_layers`,這個混合架構模型才有)。
|
| 242 |
+
|
| 243 |
+
L39 那一組 8.8e-05 的差異被完整追查到根因,並證明是 **Q8_K 量化邊界**造成、
|
| 244 |
+
不是 pager 的錯誤:把 C++ 端自己的 SiLU 中間值餵回 NumPy 參考,輸出立刻從
|
| 245 |
+
7.9e-04 掉到 **6.7e-08**。完整證據鏈見 [`docs/CORRECTNESS.md`](docs/CORRECTNESS.md) §4。
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## 這個模型特有的兩件事
|
| 250 |
+
|
| 251 |
+
相對於同系列的 Qwen3.6-35B-A3B 實作,本模型有兩個結構差異,
|
| 252 |
+
**移植時必須重新推導、不能照抄**:
|
| 253 |
+
|
| 254 |
+
1. **兩種 expert 大小**(20 層 2,039,808 B / 20 層 1,769,472 B)。槽位尺寸必須
|
| 255 |
+
從 metadata 推導。參考實作是 37 層 + 3 層,比例完全不同。
|
| 256 |
+
2. **混合架構**:40 層裡只有 10 層是全注意力,30 層是 SSM 層。直接後果是
|
| 257 |
+
**執行緒數的結論相反** —— 這裡 12 執行緒比 8 快約 6.5%,參考實作則是
|
| 258 |
+
8 之後開始變慢。SSM 層不受序列維度限制,能吃滿更多執行緒。
|
| 259 |
+
|
| 260 |
+
另外模型 metadata 裡多了一個 BOOL 陣列(`recurrent_layers`),
|
| 261 |
+
參考實作的 GGUF 標頭解析器會在那裡錯位。
|
| 262 |
+
|
| 263 |
+
---
|
| 264 |
+
|
| 265 |
+
## 其他結果
|
| 266 |
+
|
| 267 |
+
### 執行緒數
|
| 268 |
+
|
| 269 |
+
| threads | 4 | 8 | 10 | 12 | 16 |
|
| 270 |
+
| --- | ---: | ---: | ---: | ---: | ---: |
|
| 271 |
+
| 平均 tok/s | 4.65 | 5.96 | 6.16 | **6.35** | 6.27 |
|
| 272 |
+
|
| 273 |
+
4 → 8 是真實且顯著的提升(+28%)。8 之後的差異都在單輪抖動之內。
|
| 274 |
+
預設 8,**建議 12**(理由見上一節)。
|
| 275 |
+
|
| 276 |
+
### 預取(負結果)
|
| 277 |
+
|
| 278 |
+
跨層預取 5.95 ± 0.37、僅 end_token 5.78 ± 0.33、關閉 6.03 ± 0.44 ——
|
| 279 |
+
**差異遠小於單輪抖動,統計上無法分辨**,而且多數輪次 SSD 讀取量上升。
|
| 280 |
+
**預設關閉。**
|
| 281 |
+
|
| 282 |
+
### 儲存裝置會改變結論
|
| 283 |
+
|
| 284 |
+
本機是本地 NVMe。慢速儲存上 I/O 排程是瓶頸,預取與並行讀取才有意義;
|
| 285 |
+
快速 NVMe 上命中率(arena 大小)才是瓶頸。**本 repo 的 tok/s 不能直接套用到
|
| 286 |
+
慢速儲存的機器上。** 請先跑 `tools/io_probe.py` 再決定要最佳化哪一邊。
|
| 287 |
+
|
| 288 |
+
---
|
| 289 |
+
|
| 290 |
+
## 這個量測**沒有**證明的事
|
| 291 |
+
|
| 292 |
+
1. **不是 cgroup 驗證。** 這個環境的 cgroup2 掛載唯讀、沒有 `CAP_SYS_ADMIN`。
|
| 293 |
+
目前的保證是「kernel `RLIMIT_DATA` + 取樣殺行程」與「mlock 匿名頁」兩條腿。
|
| 294 |
+
2. **主機 swap 是開啟的。** no-swap 由 `mlock` 讓它不可能發生,不是把主機的
|
| 295 |
+
swap 關掉。
|
| 296 |
+
3. **`brk()` 主堆疊未被鎖**(glibc 內部直接下 syscall)。大型配置都走 `mmap`。
|
| 297 |
+
4. **共享主機**,單輪抖動可達 25%。
|
| 298 |
+
5. **只驗到 4K context**;模型原生支援 262 K。
|
| 299 |
+
6. **只做文字推論**,vision projector 是另一個檔案。
|
| 300 |
+
7. **儲存裝置比歷史量測的機器快一到兩個數量級。**
|
| 301 |
+
|
| 302 |
+
完整方法與證據見 [`docs/STRICT_8GIB.md`](docs/STRICT_8GIB.md)。
|
| 303 |
+
|
| 304 |
+
---
|
| 305 |
+
|
| 306 |
+
## Repository layout
|
| 307 |
+
|
| 308 |
+
| 路徑 | 內容 |
|
| 309 |
+
| --- | --- |
|
| 310 |
+
| `cpp/` | C++ expert pager(`sdq_pager.*`、`sdq_moe.cpp`、`sdq_buft.cpp`、`sdq_cli.cpp`、`sdq_mtp.cpp`) |
|
| 311 |
+
| `patches/` | 把 pager 接進 llama.cpp 的 patch(針對固定 commit `9d7d5049`) |
|
| 312 |
+
| `sdq/` | 設定、受管治的基準測量、實驗、驗證、HF sync |
|
| 313 |
+
| `tools/` | 8 GiB/no-swap 管治、GGUF 索引、決定性檢查、儲存 probe |
|
| 314 |
+
| `bench/` | 量測腳本與 `bench/results/` 原始輸出(JSON) |
|
| 315 |
+
| `docs/` | 見下 |
|
| 316 |
+
|
| 317 |
+
文件:
|
| 318 |
+
|
| 319 |
+
| 文件 | 內容 |
|
| 320 |
+
| --- | --- |
|
| 321 |
+
| [`docs/STRICT_8GIB.md`](docs/STRICT_8GIB.md) | **8 GiB / no-swap 的方法、證據與限制**(先讀這份) |
|
| 322 |
+
| [`docs/DESIGN.md`](docs/DESIGN.md) | 記憶體帳目、SSD→RAM→CPU 設計、reserve 校正 |
|
| 323 |
+
| [`docs/CORRECTNESS.md`](docs/CORRECTNESS.md) | 三層驗證、決定性證據、Q8_K 量化邊界分析 |
|
| 324 |
+
| [`docs/NEGATIVE_RESULTS.md`](docs/NEGATIVE_RESULTS.md) | 試過但沒留下的最佳化 |
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
## 重現
|
| 329 |
+
|
| 330 |
+
```bash
|
| 331 |
+
# 受管治的單次執行
|
| 332 |
+
python3 tools/strict8g.py --limit-mb 8192 --record /tmp/one.json -- \
|
| 333 |
+
epq-chat -m model.gguf -p "..." -n 48 -c 4096 -t 8 -b 128 \
|
| 334 |
+
--temp 0 --no-chat --no-interactive --ram-budget-mb 8192
|
| 335 |
+
|
| 336 |
+
# 標配基準測量(9 輪,配對交錯)
|
| 337 |
+
python3 -m sdq.strict_bench --model model.gguf --bins base=./build/bin/epq-chat \
|
| 338 |
+
--rounds 9 --strict-mb 8192 --label headline
|
| 339 |
+
|
| 340 |
+
# 正確性與決定性
|
| 341 |
+
python3 -m sdq.verify --model model.gguf --bin ./build/bin/epq-chat --fast
|
| 342 |
+
python3 tools/determinism_check.py --model model.gguf --bin ./build/bin/epq-chat --runs 5 -n 48
|
| 343 |
+
|
| 344 |
+
# 曲線與負結果
|
| 345 |
+
python3 bench/run_budget_curve.py --model model.gguf --bin ./build/bin/epq-chat --rounds 5
|
| 346 |
+
python3 bench/run_ramsweep.py --model model.gguf --bin ./build/bin/epq-chat --rounds 3
|
| 347 |
+
python3 bench/run_n9.py --model model.gguf --bin ./build/bin/epq-chat --rounds 9
|
| 348 |
+
python3 bench/run_negative.py --which prefetch --model model.gguf --bin ./build/bin/epq-chat
|
| 349 |
+
python3 bench/run_negative.py --which threads --model model.gguf --bin ./build/bin/epq-chat
|
| 350 |
+
|
| 351 |
+
# 儲存裝置特性、路由重疊、GGUF 結構
|
| 352 |
+
python3 tools/io_probe.py --model model.gguf
|
| 353 |
+
python3 tools/route_overlap.py --model model.gguf
|
| 354 |
+
python3 tools/gguf_index.py model.gguf --json bench/expert_index.json
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## License
|
| 360 |
+
|
| 361 |
+
MiniCPM-V base model 由 upstream 發布;請以 upstream model card 的目前條款為準。
|
| 362 |
+
本 repository 包含推論軟體與量測結果,**不包含模型權重**。
|
| 363 |
+
llama.cpp 與其相依套件保留各自授權條款。
|
bench/expert_index.json
ADDED
|
@@ -0,0 +1,1836 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"file": "/root/models/minicpm-v4.7-35b-a3b-q4_k_m.gguf",
|
| 3 |
+
"file_size": 21166255008,
|
| 4 |
+
"gguf_version": 3,
|
| 5 |
+
"architecture": "qwen35moe",
|
| 6 |
+
"model_name": "35a3_hf_v47_0923",
|
| 7 |
+
"quant_file_type": 15,
|
| 8 |
+
"n_layer": 40,
|
| 9 |
+
"n_expert": 256,
|
| 10 |
+
"n_expert_used": 8,
|
| 11 |
+
"n_embd": 2048,
|
| 12 |
+
"n_ff_exp": 512,
|
| 13 |
+
"n_ff_shexp": 512,
|
| 14 |
+
"context_length": 262144,
|
| 15 |
+
"n_head": 16,
|
| 16 |
+
"n_head_kv": 2,
|
| 17 |
+
"head_dim": 256,
|
| 18 |
+
"rope_sections": [
|
| 19 |
+
11,
|
| 20 |
+
11,
|
| 21 |
+
10,
|
| 22 |
+
0
|
| 23 |
+
],
|
| 24 |
+
"full_attention_interval": 4,
|
| 25 |
+
"data_start": 10984608,
|
| 26 |
+
"alignment": 32,
|
| 27 |
+
"resident_ram_bytes": 1651756800,
|
| 28 |
+
"expert_total_bytes": 19503513600,
|
| 29 |
+
"accounted_bytes": 21155270400,
|
| 30 |
+
"per_layer": [
|
| 31 |
+
{
|
| 32 |
+
"layer": 0,
|
| 33 |
+
"experts": {
|
| 34 |
+
"ffn_down_exps": {
|
| 35 |
+
"file_offset": 732225952,
|
| 36 |
+
"file_end": 952426912,
|
| 37 |
+
"type": "Q6_K",
|
| 38 |
+
"type_id": 14,
|
| 39 |
+
"dims": [
|
| 40 |
+
512,
|
| 41 |
+
2048,
|
| 42 |
+
256
|
| 43 |
+
],
|
| 44 |
+
"bytes_per_expert": 860160,
|
| 45 |
+
"n_expert": 256
|
| 46 |
+
},
|
| 47 |
+
"ffn_gate_exps": {
|
| 48 |
+
"file_offset": 953287072,
|
| 49 |
+
"file_end": 1104282016,
|
| 50 |
+
"type": "Q4_K",
|
| 51 |
+
"type_id": 12,
|
| 52 |
+
"dims": [
|
| 53 |
+
2048,
|
| 54 |
+
512,
|
| 55 |
+
256
|
| 56 |
+
],
|
| 57 |
+
"bytes_per_expert": 589824,
|
| 58 |
+
"n_expert": 256
|
| 59 |
+
},
|
| 60 |
+
"ffn_up_exps": {
|
| 61 |
+
"file_offset": 1106977184,
|
| 62 |
+
"file_end": 1257972128,
|
| 63 |
+
"type": "Q4_K",
|
| 64 |
+
"type_id": 12,
|
| 65 |
+
"dims": [
|
| 66 |
+
2048,
|
| 67 |
+
512,
|
| 68 |
+
256
|
| 69 |
+
],
|
| 70 |
+
"bytes_per_expert": 589824,
|
| 71 |
+
"n_expert": 256
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"bytes_per_expert_total": 2039808
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"layer": 1,
|
| 78 |
+
"experts": {
|
| 79 |
+
"ffn_down_exps": {
|
| 80 |
+
"file_offset": 1281983648,
|
| 81 |
+
"file_end": 1502184608,
|
| 82 |
+
"type": "Q6_K",
|
| 83 |
+
"type_id": 14,
|
| 84 |
+
"dims": [
|
| 85 |
+
512,
|
| 86 |
+
2048,
|
| 87 |
+
256
|
| 88 |
+
],
|
| 89 |
+
"bytes_per_expert": 860160,
|
| 90 |
+
"n_expert": 256
|
| 91 |
+
},
|
| 92 |
+
"ffn_gate_exps": {
|
| 93 |
+
"file_offset": 1503044768,
|
| 94 |
+
"file_end": 1654039712,
|
| 95 |
+
"type": "Q4_K",
|
| 96 |
+
"type_id": 12,
|
| 97 |
+
"dims": [
|
| 98 |
+
2048,
|
| 99 |
+
512,
|
| 100 |
+
256
|
| 101 |
+
],
|
| 102 |
+
"bytes_per_expert": 589824,
|
| 103 |
+
"n_expert": 256
|
| 104 |
+
},
|
| 105 |
+
"ffn_up_exps": {
|
| 106 |
+
"file_offset": 1656734880,
|
| 107 |
+
"file_end": 1807729824,
|
| 108 |
+
"type": "Q4_K",
|
| 109 |
+
"type_id": 12,
|
| 110 |
+
"dims": [
|
| 111 |
+
2048,
|
| 112 |
+
512,
|
| 113 |
+
256
|
| 114 |
+
],
|
| 115 |
+
"bytes_per_expert": 589824,
|
| 116 |
+
"n_expert": 256
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"bytes_per_expert_total": 2039808
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"layer": 2,
|
| 123 |
+
"experts": {
|
| 124 |
+
"ffn_down_exps": {
|
| 125 |
+
"file_offset": 1831741344,
|
| 126 |
+
"file_end": 2051942304,
|
| 127 |
+
"type": "Q6_K",
|
| 128 |
+
"type_id": 14,
|
| 129 |
+
"dims": [
|
| 130 |
+
512,
|
| 131 |
+
2048,
|
| 132 |
+
256
|
| 133 |
+
],
|
| 134 |
+
"bytes_per_expert": 860160,
|
| 135 |
+
"n_expert": 256
|
| 136 |
+
},
|
| 137 |
+
"ffn_gate_exps": {
|
| 138 |
+
"file_offset": 2052802464,
|
| 139 |
+
"file_end": 2203797408,
|
| 140 |
+
"type": "Q4_K",
|
| 141 |
+
"type_id": 12,
|
| 142 |
+
"dims": [
|
| 143 |
+
2048,
|
| 144 |
+
512,
|
| 145 |
+
256
|
| 146 |
+
],
|
| 147 |
+
"bytes_per_expert": 589824,
|
| 148 |
+
"n_expert": 256
|
| 149 |
+
},
|
| 150 |
+
"ffn_up_exps": {
|
| 151 |
+
"file_offset": 2206492576,
|
| 152 |
+
"file_end": 2357487520,
|
| 153 |
+
"type": "Q4_K",
|
| 154 |
+
"type_id": 12,
|
| 155 |
+
"dims": [
|
| 156 |
+
2048,
|
| 157 |
+
512,
|
| 158 |
+
256
|
| 159 |
+
],
|
| 160 |
+
"bytes_per_expert": 589824,
|
| 161 |
+
"n_expert": 256
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
"bytes_per_expert_total": 2039808
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"layer": 3,
|
| 168 |
+
"experts": {
|
| 169 |
+
"ffn_down_exps": {
|
| 170 |
+
"file_offset": 2378625696,
|
| 171 |
+
"file_end": 2598826656,
|
| 172 |
+
"type": "Q6_K",
|
| 173 |
+
"type_id": 14,
|
| 174 |
+
"dims": [
|
| 175 |
+
512,
|
| 176 |
+
2048,
|
| 177 |
+
256
|
| 178 |
+
],
|
| 179 |
+
"bytes_per_expert": 860160,
|
| 180 |
+
"n_expert": 256
|
| 181 |
+
},
|
| 182 |
+
"ffn_gate_exps": {
|
| 183 |
+
"file_offset": 2599686816,
|
| 184 |
+
"file_end": 2750681760,
|
| 185 |
+
"type": "Q4_K",
|
| 186 |
+
"type_id": 12,
|
| 187 |
+
"dims": [
|
| 188 |
+
2048,
|
| 189 |
+
512,
|
| 190 |
+
256
|
| 191 |
+
],
|
| 192 |
+
"bytes_per_expert": 589824,
|
| 193 |
+
"n_expert": 256
|
| 194 |
+
},
|
| 195 |
+
"ffn_up_exps": {
|
| 196 |
+
"file_offset": 2753376928,
|
| 197 |
+
"file_end": 2904371872,
|
| 198 |
+
"type": "Q4_K",
|
| 199 |
+
"type_id": 12,
|
| 200 |
+
"dims": [
|
| 201 |
+
2048,
|
| 202 |
+
512,
|
| 203 |
+
256
|
| 204 |
+
],
|
| 205 |
+
"bytes_per_expert": 589824,
|
| 206 |
+
"n_expert": 256
|
| 207 |
+
}
|
| 208 |
+
},
|
| 209 |
+
"bytes_per_expert_total": 2039808
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"layer": 4,
|
| 213 |
+
"experts": {
|
| 214 |
+
"ffn_down_exps": {
|
| 215 |
+
"file_offset": 2923459232,
|
| 216 |
+
"file_end": 3143660192,
|
| 217 |
+
"type": "Q6_K",
|
| 218 |
+
"type_id": 14,
|
| 219 |
+
"dims": [
|
| 220 |
+
512,
|
| 221 |
+
2048,
|
| 222 |
+
256
|
| 223 |
+
],
|
| 224 |
+
"bytes_per_expert": 860160,
|
| 225 |
+
"n_expert": 256
|
| 226 |
+
},
|
| 227 |
+
"ffn_gate_exps": {
|
| 228 |
+
"file_offset": 3144520352,
|
| 229 |
+
"file_end": 3295515296,
|
| 230 |
+
"type": "Q4_K",
|
| 231 |
+
"type_id": 12,
|
| 232 |
+
"dims": [
|
| 233 |
+
2048,
|
| 234 |
+
512,
|
| 235 |
+
256
|
| 236 |
+
],
|
| 237 |
+
"bytes_per_expert": 589824,
|
| 238 |
+
"n_expert": 256
|
| 239 |
+
},
|
| 240 |
+
"ffn_up_exps": {
|
| 241 |
+
"file_offset": 3298210464,
|
| 242 |
+
"file_end": 3449205408,
|
| 243 |
+
"type": "Q4_K",
|
| 244 |
+
"type_id": 12,
|
| 245 |
+
"dims": [
|
| 246 |
+
2048,
|
| 247 |
+
512,
|
| 248 |
+
256
|
| 249 |
+
],
|
| 250 |
+
"bytes_per_expert": 589824,
|
| 251 |
+
"n_expert": 256
|
| 252 |
+
}
|
| 253 |
+
},
|
| 254 |
+
"bytes_per_expert_total": 2039808
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"layer": 5,
|
| 258 |
+
"experts": {
|
| 259 |
+
"ffn_down_exps": {
|
| 260 |
+
"file_offset": 3468891552,
|
| 261 |
+
"file_end": 3619886496,
|
| 262 |
+
"type": "Q4_K",
|
| 263 |
+
"type_id": 12,
|
| 264 |
+
"dims": [
|
| 265 |
+
512,
|
| 266 |
+
2048,
|
| 267 |
+
256
|
| 268 |
+
],
|
| 269 |
+
"bytes_per_expert": 589824,
|
| 270 |
+
"n_expert": 256
|
| 271 |
+
},
|
| 272 |
+
"ffn_gate_exps": {
|
| 273 |
+
"file_offset": 3620476320,
|
| 274 |
+
"file_end": 3771471264,
|
| 275 |
+
"type": "Q4_K",
|
| 276 |
+
"type_id": 12,
|
| 277 |
+
"dims": [
|
| 278 |
+
2048,
|
| 279 |
+
512,
|
| 280 |
+
256
|
| 281 |
+
],
|
| 282 |
+
"bytes_per_expert": 589824,
|
| 283 |
+
"n_expert": 256
|
| 284 |
+
},
|
| 285 |
+
"ffn_up_exps": {
|
| 286 |
+
"file_offset": 3774166432,
|
| 287 |
+
"file_end": 3925161376,
|
| 288 |
+
"type": "Q4_K",
|
| 289 |
+
"type_id": 12,
|
| 290 |
+
"dims": [
|
| 291 |
+
2048,
|
| 292 |
+
512,
|
| 293 |
+
256
|
| 294 |
+
],
|
| 295 |
+
"bytes_per_expert": 589824,
|
| 296 |
+
"n_expert": 256
|
| 297 |
+
}
|
| 298 |
+
},
|
| 299 |
+
"bytes_per_expert_total": 1769472
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"layer": 6,
|
| 303 |
+
"experts": {
|
| 304 |
+
"ffn_down_exps": {
|
| 305 |
+
"file_offset": 3944847520,
|
| 306 |
+
"file_end": 4095842464,
|
| 307 |
+
"type": "Q4_K",
|
| 308 |
+
"type_id": 12,
|
| 309 |
+
"dims": [
|
| 310 |
+
512,
|
| 311 |
+
2048,
|
| 312 |
+
256
|
| 313 |
+
],
|
| 314 |
+
"bytes_per_expert": 589824,
|
| 315 |
+
"n_expert": 256
|
| 316 |
+
},
|
| 317 |
+
"ffn_gate_exps": {
|
| 318 |
+
"file_offset": 4096432288,
|
| 319 |
+
"file_end": 4247427232,
|
| 320 |
+
"type": "Q4_K",
|
| 321 |
+
"type_id": 12,
|
| 322 |
+
"dims": [
|
| 323 |
+
2048,
|
| 324 |
+
512,
|
| 325 |
+
256
|
| 326 |
+
],
|
| 327 |
+
"bytes_per_expert": 589824,
|
| 328 |
+
"n_expert": 256
|
| 329 |
+
},
|
| 330 |
+
"ffn_up_exps": {
|
| 331 |
+
"file_offset": 4250122400,
|
| 332 |
+
"file_end": 4401117344,
|
| 333 |
+
"type": "Q4_K",
|
| 334 |
+
"type_id": 12,
|
| 335 |
+
"dims": [
|
| 336 |
+
2048,
|
| 337 |
+
512,
|
| 338 |
+
256
|
| 339 |
+
],
|
| 340 |
+
"bytes_per_expert": 589824,
|
| 341 |
+
"n_expert": 256
|
| 342 |
+
}
|
| 343 |
+
},
|
| 344 |
+
"bytes_per_expert_total": 1769472
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"layer": 7,
|
| 348 |
+
"experts": {
|
| 349 |
+
"ffn_down_exps": {
|
| 350 |
+
"file_offset": 4422255520,
|
| 351 |
+
"file_end": 4642456480,
|
| 352 |
+
"type": "Q6_K",
|
| 353 |
+
"type_id": 14,
|
| 354 |
+
"dims": [
|
| 355 |
+
512,
|
| 356 |
+
2048,
|
| 357 |
+
256
|
| 358 |
+
],
|
| 359 |
+
"bytes_per_expert": 860160,
|
| 360 |
+
"n_expert": 256
|
| 361 |
+
},
|
| 362 |
+
"ffn_gate_exps": {
|
| 363 |
+
"file_offset": 4643316640,
|
| 364 |
+
"file_end": 4794311584,
|
| 365 |
+
"type": "Q4_K",
|
| 366 |
+
"type_id": 12,
|
| 367 |
+
"dims": [
|
| 368 |
+
2048,
|
| 369 |
+
512,
|
| 370 |
+
256
|
| 371 |
+
],
|
| 372 |
+
"bytes_per_expert": 589824,
|
| 373 |
+
"n_expert": 256
|
| 374 |
+
},
|
| 375 |
+
"ffn_up_exps": {
|
| 376 |
+
"file_offset": 4797006752,
|
| 377 |
+
"file_end": 4948001696,
|
| 378 |
+
"type": "Q4_K",
|
| 379 |
+
"type_id": 12,
|
| 380 |
+
"dims": [
|
| 381 |
+
2048,
|
| 382 |
+
512,
|
| 383 |
+
256
|
| 384 |
+
],
|
| 385 |
+
"bytes_per_expert": 589824,
|
| 386 |
+
"n_expert": 256
|
| 387 |
+
}
|
| 388 |
+
},
|
| 389 |
+
"bytes_per_expert_total": 2039808
|
| 390 |
+
},
|
| 391 |
+
{
|
| 392 |
+
"layer": 8,
|
| 393 |
+
"experts": {
|
| 394 |
+
"ffn_down_exps": {
|
| 395 |
+
"file_offset": 4962763680,
|
| 396 |
+
"file_end": 5113758624,
|
| 397 |
+
"type": "Q4_K",
|
| 398 |
+
"type_id": 12,
|
| 399 |
+
"dims": [
|
| 400 |
+
512,
|
| 401 |
+
2048,
|
| 402 |
+
256
|
| 403 |
+
],
|
| 404 |
+
"bytes_per_expert": 589824,
|
| 405 |
+
"n_expert": 256
|
| 406 |
+
},
|
| 407 |
+
"ffn_gate_exps": {
|
| 408 |
+
"file_offset": 5114348448,
|
| 409 |
+
"file_end": 5265343392,
|
| 410 |
+
"type": "Q4_K",
|
| 411 |
+
"type_id": 12,
|
| 412 |
+
"dims": [
|
| 413 |
+
2048,
|
| 414 |
+
512,
|
| 415 |
+
256
|
| 416 |
+
],
|
| 417 |
+
"bytes_per_expert": 589824,
|
| 418 |
+
"n_expert": 256
|
| 419 |
+
},
|
| 420 |
+
"ffn_up_exps": {
|
| 421 |
+
"file_offset": 5268038560,
|
| 422 |
+
"file_end": 5419033504,
|
| 423 |
+
"type": "Q4_K",
|
| 424 |
+
"type_id": 12,
|
| 425 |
+
"dims": [
|
| 426 |
+
2048,
|
| 427 |
+
512,
|
| 428 |
+
256
|
| 429 |
+
],
|
| 430 |
+
"bytes_per_expert": 589824,
|
| 431 |
+
"n_expert": 256
|
| 432 |
+
}
|
| 433 |
+
},
|
| 434 |
+
"bytes_per_expert_total": 1769472
|
| 435 |
+
},
|
| 436 |
+
{
|
| 437 |
+
"layer": 9,
|
| 438 |
+
"experts": {
|
| 439 |
+
"ffn_down_exps": {
|
| 440 |
+
"file_offset": 5438719648,
|
| 441 |
+
"file_end": 5589714592,
|
| 442 |
+
"type": "Q4_K",
|
| 443 |
+
"type_id": 12,
|
| 444 |
+
"dims": [
|
| 445 |
+
512,
|
| 446 |
+
2048,
|
| 447 |
+
256
|
| 448 |
+
],
|
| 449 |
+
"bytes_per_expert": 589824,
|
| 450 |
+
"n_expert": 256
|
| 451 |
+
},
|
| 452 |
+
"ffn_gate_exps": {
|
| 453 |
+
"file_offset": 5590304416,
|
| 454 |
+
"file_end": 5741299360,
|
| 455 |
+
"type": "Q4_K",
|
| 456 |
+
"type_id": 12,
|
| 457 |
+
"dims": [
|
| 458 |
+
2048,
|
| 459 |
+
512,
|
| 460 |
+
256
|
| 461 |
+
],
|
| 462 |
+
"bytes_per_expert": 589824,
|
| 463 |
+
"n_expert": 256
|
| 464 |
+
},
|
| 465 |
+
"ffn_up_exps": {
|
| 466 |
+
"file_offset": 5743994528,
|
| 467 |
+
"file_end": 5894989472,
|
| 468 |
+
"type": "Q4_K",
|
| 469 |
+
"type_id": 12,
|
| 470 |
+
"dims": [
|
| 471 |
+
2048,
|
| 472 |
+
512,
|
| 473 |
+
256
|
| 474 |
+
],
|
| 475 |
+
"bytes_per_expert": 589824,
|
| 476 |
+
"n_expert": 256
|
| 477 |
+
}
|
| 478 |
+
},
|
| 479 |
+
"bytes_per_expert_total": 1769472
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"layer": 10,
|
| 483 |
+
"experts": {
|
| 484 |
+
"ffn_down_exps": {
|
| 485 |
+
"file_offset": 5919000992,
|
| 486 |
+
"file_end": 6139201952,
|
| 487 |
+
"type": "Q6_K",
|
| 488 |
+
"type_id": 14,
|
| 489 |
+
"dims": [
|
| 490 |
+
512,
|
| 491 |
+
2048,
|
| 492 |
+
256
|
| 493 |
+
],
|
| 494 |
+
"bytes_per_expert": 860160,
|
| 495 |
+
"n_expert": 256
|
| 496 |
+
},
|
| 497 |
+
"ffn_gate_exps": {
|
| 498 |
+
"file_offset": 6140062112,
|
| 499 |
+
"file_end": 6291057056,
|
| 500 |
+
"type": "Q4_K",
|
| 501 |
+
"type_id": 12,
|
| 502 |
+
"dims": [
|
| 503 |
+
2048,
|
| 504 |
+
512,
|
| 505 |
+
256
|
| 506 |
+
],
|
| 507 |
+
"bytes_per_expert": 589824,
|
| 508 |
+
"n_expert": 256
|
| 509 |
+
},
|
| 510 |
+
"ffn_up_exps": {
|
| 511 |
+
"file_offset": 6293752224,
|
| 512 |
+
"file_end": 6444747168,
|
| 513 |
+
"type": "Q4_K",
|
| 514 |
+
"type_id": 12,
|
| 515 |
+
"dims": [
|
| 516 |
+
2048,
|
| 517 |
+
512,
|
| 518 |
+
256
|
| 519 |
+
],
|
| 520 |
+
"bytes_per_expert": 589824,
|
| 521 |
+
"n_expert": 256
|
| 522 |
+
}
|
| 523 |
+
},
|
| 524 |
+
"bytes_per_expert_total": 2039808
|
| 525 |
+
},
|
| 526 |
+
{
|
| 527 |
+
"layer": 11,
|
| 528 |
+
"experts": {
|
| 529 |
+
"ffn_down_exps": {
|
| 530 |
+
"file_offset": 6465615008,
|
| 531 |
+
"file_end": 6616609952,
|
| 532 |
+
"type": "Q4_K",
|
| 533 |
+
"type_id": 12,
|
| 534 |
+
"dims": [
|
| 535 |
+
512,
|
| 536 |
+
2048,
|
| 537 |
+
256
|
| 538 |
+
],
|
| 539 |
+
"bytes_per_expert": 589824,
|
| 540 |
+
"n_expert": 256
|
| 541 |
+
},
|
| 542 |
+
"ffn_gate_exps": {
|
| 543 |
+
"file_offset": 6617199776,
|
| 544 |
+
"file_end": 6768194720,
|
| 545 |
+
"type": "Q4_K",
|
| 546 |
+
"type_id": 12,
|
| 547 |
+
"dims": [
|
| 548 |
+
2048,
|
| 549 |
+
512,
|
| 550 |
+
256
|
| 551 |
+
],
|
| 552 |
+
"bytes_per_expert": 589824,
|
| 553 |
+
"n_expert": 256
|
| 554 |
+
},
|
| 555 |
+
"ffn_up_exps": {
|
| 556 |
+
"file_offset": 6770889888,
|
| 557 |
+
"file_end": 6921884832,
|
| 558 |
+
"type": "Q4_K",
|
| 559 |
+
"type_id": 12,
|
| 560 |
+
"dims": [
|
| 561 |
+
2048,
|
| 562 |
+
512,
|
| 563 |
+
256
|
| 564 |
+
],
|
| 565 |
+
"bytes_per_expert": 589824,
|
| 566 |
+
"n_expert": 256
|
| 567 |
+
}
|
| 568 |
+
},
|
| 569 |
+
"bytes_per_expert_total": 1769472
|
| 570 |
+
},
|
| 571 |
+
{
|
| 572 |
+
"layer": 12,
|
| 573 |
+
"experts": {
|
| 574 |
+
"ffn_down_exps": {
|
| 575 |
+
"file_offset": 6936646816,
|
| 576 |
+
"file_end": 7087641760,
|
| 577 |
+
"type": "Q4_K",
|
| 578 |
+
"type_id": 12,
|
| 579 |
+
"dims": [
|
| 580 |
+
512,
|
| 581 |
+
2048,
|
| 582 |
+
256
|
| 583 |
+
],
|
| 584 |
+
"bytes_per_expert": 589824,
|
| 585 |
+
"n_expert": 256
|
| 586 |
+
},
|
| 587 |
+
"ffn_gate_exps": {
|
| 588 |
+
"file_offset": 7088231584,
|
| 589 |
+
"file_end": 7239226528,
|
| 590 |
+
"type": "Q4_K",
|
| 591 |
+
"type_id": 12,
|
| 592 |
+
"dims": [
|
| 593 |
+
2048,
|
| 594 |
+
512,
|
| 595 |
+
256
|
| 596 |
+
],
|
| 597 |
+
"bytes_per_expert": 589824,
|
| 598 |
+
"n_expert": 256
|
| 599 |
+
},
|
| 600 |
+
"ffn_up_exps": {
|
| 601 |
+
"file_offset": 7241921696,
|
| 602 |
+
"file_end": 7392916640,
|
| 603 |
+
"type": "Q4_K",
|
| 604 |
+
"type_id": 12,
|
| 605 |
+
"dims": [
|
| 606 |
+
2048,
|
| 607 |
+
512,
|
| 608 |
+
256
|
| 609 |
+
],
|
| 610 |
+
"bytes_per_expert": 589824,
|
| 611 |
+
"n_expert": 256
|
| 612 |
+
}
|
| 613 |
+
},
|
| 614 |
+
"bytes_per_expert_total": 1769472
|
| 615 |
+
},
|
| 616 |
+
{
|
| 617 |
+
"layer": 13,
|
| 618 |
+
"experts": {
|
| 619 |
+
"ffn_down_exps": {
|
| 620 |
+
"file_offset": 7416928160,
|
| 621 |
+
"file_end": 7637129120,
|
| 622 |
+
"type": "Q6_K",
|
| 623 |
+
"type_id": 14,
|
| 624 |
+
"dims": [
|
| 625 |
+
512,
|
| 626 |
+
2048,
|
| 627 |
+
256
|
| 628 |
+
],
|
| 629 |
+
"bytes_per_expert": 860160,
|
| 630 |
+
"n_expert": 256
|
| 631 |
+
},
|
| 632 |
+
"ffn_gate_exps": {
|
| 633 |
+
"file_offset": 7637989280,
|
| 634 |
+
"file_end": 7788984224,
|
| 635 |
+
"type": "Q4_K",
|
| 636 |
+
"type_id": 12,
|
| 637 |
+
"dims": [
|
| 638 |
+
2048,
|
| 639 |
+
512,
|
| 640 |
+
256
|
| 641 |
+
],
|
| 642 |
+
"bytes_per_expert": 589824,
|
| 643 |
+
"n_expert": 256
|
| 644 |
+
},
|
| 645 |
+
"ffn_up_exps": {
|
| 646 |
+
"file_offset": 7791679392,
|
| 647 |
+
"file_end": 7942674336,
|
| 648 |
+
"type": "Q4_K",
|
| 649 |
+
"type_id": 12,
|
| 650 |
+
"dims": [
|
| 651 |
+
2048,
|
| 652 |
+
512,
|
| 653 |
+
256
|
| 654 |
+
],
|
| 655 |
+
"bytes_per_expert": 589824,
|
| 656 |
+
"n_expert": 256
|
| 657 |
+
}
|
| 658 |
+
},
|
| 659 |
+
"bytes_per_expert_total": 2039808
|
| 660 |
+
},
|
| 661 |
+
{
|
| 662 |
+
"layer": 14,
|
| 663 |
+
"experts": {
|
| 664 |
+
"ffn_down_exps": {
|
| 665 |
+
"file_offset": 7962360480,
|
| 666 |
+
"file_end": 8113355424,
|
| 667 |
+
"type": "Q4_K",
|
| 668 |
+
"type_id": 12,
|
| 669 |
+
"dims": [
|
| 670 |
+
512,
|
| 671 |
+
2048,
|
| 672 |
+
256
|
| 673 |
+
],
|
| 674 |
+
"bytes_per_expert": 589824,
|
| 675 |
+
"n_expert": 256
|
| 676 |
+
},
|
| 677 |
+
"ffn_gate_exps": {
|
| 678 |
+
"file_offset": 8113945248,
|
| 679 |
+
"file_end": 8264940192,
|
| 680 |
+
"type": "Q4_K",
|
| 681 |
+
"type_id": 12,
|
| 682 |
+
"dims": [
|
| 683 |
+
2048,
|
| 684 |
+
512,
|
| 685 |
+
256
|
| 686 |
+
],
|
| 687 |
+
"bytes_per_expert": 589824,
|
| 688 |
+
"n_expert": 256
|
| 689 |
+
},
|
| 690 |
+
"ffn_up_exps": {
|
| 691 |
+
"file_offset": 8267635360,
|
| 692 |
+
"file_end": 8418630304,
|
| 693 |
+
"type": "Q4_K",
|
| 694 |
+
"type_id": 12,
|
| 695 |
+
"dims": [
|
| 696 |
+
2048,
|
| 697 |
+
512,
|
| 698 |
+
256
|
| 699 |
+
],
|
| 700 |
+
"bytes_per_expert": 589824,
|
| 701 |
+
"n_expert": 256
|
| 702 |
+
}
|
| 703 |
+
},
|
| 704 |
+
"bytes_per_expert_total": 1769472
|
| 705 |
+
},
|
| 706 |
+
{
|
| 707 |
+
"layer": 15,
|
| 708 |
+
"experts": {
|
| 709 |
+
"ffn_down_exps": {
|
| 710 |
+
"file_offset": 8439498144,
|
| 711 |
+
"file_end": 8590493088,
|
| 712 |
+
"type": "Q4_K",
|
| 713 |
+
"type_id": 12,
|
| 714 |
+
"dims": [
|
| 715 |
+
512,
|
| 716 |
+
2048,
|
| 717 |
+
256
|
| 718 |
+
],
|
| 719 |
+
"bytes_per_expert": 589824,
|
| 720 |
+
"n_expert": 256
|
| 721 |
+
},
|
| 722 |
+
"ffn_gate_exps": {
|
| 723 |
+
"file_offset": 8591082912,
|
| 724 |
+
"file_end": 8742077856,
|
| 725 |
+
"type": "Q4_K",
|
| 726 |
+
"type_id": 12,
|
| 727 |
+
"dims": [
|
| 728 |
+
2048,
|
| 729 |
+
512,
|
| 730 |
+
256
|
| 731 |
+
],
|
| 732 |
+
"bytes_per_expert": 589824,
|
| 733 |
+
"n_expert": 256
|
| 734 |
+
},
|
| 735 |
+
"ffn_up_exps": {
|
| 736 |
+
"file_offset": 8744773024,
|
| 737 |
+
"file_end": 8895767968,
|
| 738 |
+
"type": "Q4_K",
|
| 739 |
+
"type_id": 12,
|
| 740 |
+
"dims": [
|
| 741 |
+
2048,
|
| 742 |
+
512,
|
| 743 |
+
256
|
| 744 |
+
],
|
| 745 |
+
"bytes_per_expert": 589824,
|
| 746 |
+
"n_expert": 256
|
| 747 |
+
}
|
| 748 |
+
},
|
| 749 |
+
"bytes_per_expert_total": 1769472
|
| 750 |
+
},
|
| 751 |
+
{
|
| 752 |
+
"layer": 16,
|
| 753 |
+
"experts": {
|
| 754 |
+
"ffn_down_exps": {
|
| 755 |
+
"file_offset": 8914855328,
|
| 756 |
+
"file_end": 9135056288,
|
| 757 |
+
"type": "Q6_K",
|
| 758 |
+
"type_id": 14,
|
| 759 |
+
"dims": [
|
| 760 |
+
512,
|
| 761 |
+
2048,
|
| 762 |
+
256
|
| 763 |
+
],
|
| 764 |
+
"bytes_per_expert": 860160,
|
| 765 |
+
"n_expert": 256
|
| 766 |
+
},
|
| 767 |
+
"ffn_gate_exps": {
|
| 768 |
+
"file_offset": 9135916448,
|
| 769 |
+
"file_end": 9286911392,
|
| 770 |
+
"type": "Q4_K",
|
| 771 |
+
"type_id": 12,
|
| 772 |
+
"dims": [
|
| 773 |
+
2048,
|
| 774 |
+
512,
|
| 775 |
+
256
|
| 776 |
+
],
|
| 777 |
+
"bytes_per_expert": 589824,
|
| 778 |
+
"n_expert": 256
|
| 779 |
+
},
|
| 780 |
+
"ffn_up_exps": {
|
| 781 |
+
"file_offset": 9289606560,
|
| 782 |
+
"file_end": 9440601504,
|
| 783 |
+
"type": "Q4_K",
|
| 784 |
+
"type_id": 12,
|
| 785 |
+
"dims": [
|
| 786 |
+
2048,
|
| 787 |
+
512,
|
| 788 |
+
256
|
| 789 |
+
],
|
| 790 |
+
"bytes_per_expert": 589824,
|
| 791 |
+
"n_expert": 256
|
| 792 |
+
}
|
| 793 |
+
},
|
| 794 |
+
"bytes_per_expert_total": 2039808
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"layer": 17,
|
| 798 |
+
"experts": {
|
| 799 |
+
"ffn_down_exps": {
|
| 800 |
+
"file_offset": 9460287648,
|
| 801 |
+
"file_end": 9611282592,
|
| 802 |
+
"type": "Q4_K",
|
| 803 |
+
"type_id": 12,
|
| 804 |
+
"dims": [
|
| 805 |
+
512,
|
| 806 |
+
2048,
|
| 807 |
+
256
|
| 808 |
+
],
|
| 809 |
+
"bytes_per_expert": 589824,
|
| 810 |
+
"n_expert": 256
|
| 811 |
+
},
|
| 812 |
+
"ffn_gate_exps": {
|
| 813 |
+
"file_offset": 9611872416,
|
| 814 |
+
"file_end": 9762867360,
|
| 815 |
+
"type": "Q4_K",
|
| 816 |
+
"type_id": 12,
|
| 817 |
+
"dims": [
|
| 818 |
+
2048,
|
| 819 |
+
512,
|
| 820 |
+
256
|
| 821 |
+
],
|
| 822 |
+
"bytes_per_expert": 589824,
|
| 823 |
+
"n_expert": 256
|
| 824 |
+
},
|
| 825 |
+
"ffn_up_exps": {
|
| 826 |
+
"file_offset": 9765562528,
|
| 827 |
+
"file_end": 9916557472,
|
| 828 |
+
"type": "Q4_K",
|
| 829 |
+
"type_id": 12,
|
| 830 |
+
"dims": [
|
| 831 |
+
2048,
|
| 832 |
+
512,
|
| 833 |
+
256
|
| 834 |
+
],
|
| 835 |
+
"bytes_per_expert": 589824,
|
| 836 |
+
"n_expert": 256
|
| 837 |
+
}
|
| 838 |
+
},
|
| 839 |
+
"bytes_per_expert_total": 1769472
|
| 840 |
+
},
|
| 841 |
+
{
|
| 842 |
+
"layer": 18,
|
| 843 |
+
"experts": {
|
| 844 |
+
"ffn_down_exps": {
|
| 845 |
+
"file_offset": 9936243616,
|
| 846 |
+
"file_end": 10087238560,
|
| 847 |
+
"type": "Q4_K",
|
| 848 |
+
"type_id": 12,
|
| 849 |
+
"dims": [
|
| 850 |
+
512,
|
| 851 |
+
2048,
|
| 852 |
+
256
|
| 853 |
+
],
|
| 854 |
+
"bytes_per_expert": 589824,
|
| 855 |
+
"n_expert": 256
|
| 856 |
+
},
|
| 857 |
+
"ffn_gate_exps": {
|
| 858 |
+
"file_offset": 10087828384,
|
| 859 |
+
"file_end": 10238823328,
|
| 860 |
+
"type": "Q4_K",
|
| 861 |
+
"type_id": 12,
|
| 862 |
+
"dims": [
|
| 863 |
+
2048,
|
| 864 |
+
512,
|
| 865 |
+
256
|
| 866 |
+
],
|
| 867 |
+
"bytes_per_expert": 589824,
|
| 868 |
+
"n_expert": 256
|
| 869 |
+
},
|
| 870 |
+
"ffn_up_exps": {
|
| 871 |
+
"file_offset": 10241518496,
|
| 872 |
+
"file_end": 10392513440,
|
| 873 |
+
"type": "Q4_K",
|
| 874 |
+
"type_id": 12,
|
| 875 |
+
"dims": [
|
| 876 |
+
2048,
|
| 877 |
+
512,
|
| 878 |
+
256
|
| 879 |
+
],
|
| 880 |
+
"bytes_per_expert": 589824,
|
| 881 |
+
"n_expert": 256
|
| 882 |
+
}
|
| 883 |
+
},
|
| 884 |
+
"bytes_per_expert_total": 1769472
|
| 885 |
+
},
|
| 886 |
+
{
|
| 887 |
+
"layer": 19,
|
| 888 |
+
"experts": {
|
| 889 |
+
"ffn_down_exps": {
|
| 890 |
+
"file_offset": 10413651616,
|
| 891 |
+
"file_end": 10633852576,
|
| 892 |
+
"type": "Q6_K",
|
| 893 |
+
"type_id": 14,
|
| 894 |
+
"dims": [
|
| 895 |
+
512,
|
| 896 |
+
2048,
|
| 897 |
+
256
|
| 898 |
+
],
|
| 899 |
+
"bytes_per_expert": 860160,
|
| 900 |
+
"n_expert": 256
|
| 901 |
+
},
|
| 902 |
+
"ffn_gate_exps": {
|
| 903 |
+
"file_offset": 10634712736,
|
| 904 |
+
"file_end": 10785707680,
|
| 905 |
+
"type": "Q4_K",
|
| 906 |
+
"type_id": 12,
|
| 907 |
+
"dims": [
|
| 908 |
+
2048,
|
| 909 |
+
512,
|
| 910 |
+
256
|
| 911 |
+
],
|
| 912 |
+
"bytes_per_expert": 589824,
|
| 913 |
+
"n_expert": 256
|
| 914 |
+
},
|
| 915 |
+
"ffn_up_exps": {
|
| 916 |
+
"file_offset": 10788402848,
|
| 917 |
+
"file_end": 10939397792,
|
| 918 |
+
"type": "Q4_K",
|
| 919 |
+
"type_id": 12,
|
| 920 |
+
"dims": [
|
| 921 |
+
2048,
|
| 922 |
+
512,
|
| 923 |
+
256
|
| 924 |
+
],
|
| 925 |
+
"bytes_per_expert": 589824,
|
| 926 |
+
"n_expert": 256
|
| 927 |
+
}
|
| 928 |
+
},
|
| 929 |
+
"bytes_per_expert_total": 2039808
|
| 930 |
+
},
|
| 931 |
+
{
|
| 932 |
+
"layer": 20,
|
| 933 |
+
"experts": {
|
| 934 |
+
"ffn_down_exps": {
|
| 935 |
+
"file_offset": 10954159776,
|
| 936 |
+
"file_end": 11105154720,
|
| 937 |
+
"type": "Q4_K",
|
| 938 |
+
"type_id": 12,
|
| 939 |
+
"dims": [
|
| 940 |
+
512,
|
| 941 |
+
2048,
|
| 942 |
+
256
|
| 943 |
+
],
|
| 944 |
+
"bytes_per_expert": 589824,
|
| 945 |
+
"n_expert": 256
|
| 946 |
+
},
|
| 947 |
+
"ffn_gate_exps": {
|
| 948 |
+
"file_offset": 11105744544,
|
| 949 |
+
"file_end": 11256739488,
|
| 950 |
+
"type": "Q4_K",
|
| 951 |
+
"type_id": 12,
|
| 952 |
+
"dims": [
|
| 953 |
+
2048,
|
| 954 |
+
512,
|
| 955 |
+
256
|
| 956 |
+
],
|
| 957 |
+
"bytes_per_expert": 589824,
|
| 958 |
+
"n_expert": 256
|
| 959 |
+
},
|
| 960 |
+
"ffn_up_exps": {
|
| 961 |
+
"file_offset": 11259434656,
|
| 962 |
+
"file_end": 11410429600,
|
| 963 |
+
"type": "Q4_K",
|
| 964 |
+
"type_id": 12,
|
| 965 |
+
"dims": [
|
| 966 |
+
2048,
|
| 967 |
+
512,
|
| 968 |
+
256
|
| 969 |
+
],
|
| 970 |
+
"bytes_per_expert": 589824,
|
| 971 |
+
"n_expert": 256
|
| 972 |
+
}
|
| 973 |
+
},
|
| 974 |
+
"bytes_per_expert_total": 1769472
|
| 975 |
+
},
|
| 976 |
+
{
|
| 977 |
+
"layer": 21,
|
| 978 |
+
"experts": {
|
| 979 |
+
"ffn_down_exps": {
|
| 980 |
+
"file_offset": 11430115744,
|
| 981 |
+
"file_end": 11581110688,
|
| 982 |
+
"type": "Q4_K",
|
| 983 |
+
"type_id": 12,
|
| 984 |
+
"dims": [
|
| 985 |
+
512,
|
| 986 |
+
2048,
|
| 987 |
+
256
|
| 988 |
+
],
|
| 989 |
+
"bytes_per_expert": 589824,
|
| 990 |
+
"n_expert": 256
|
| 991 |
+
},
|
| 992 |
+
"ffn_gate_exps": {
|
| 993 |
+
"file_offset": 11581700512,
|
| 994 |
+
"file_end": 11732695456,
|
| 995 |
+
"type": "Q4_K",
|
| 996 |
+
"type_id": 12,
|
| 997 |
+
"dims": [
|
| 998 |
+
2048,
|
| 999 |
+
512,
|
| 1000 |
+
256
|
| 1001 |
+
],
|
| 1002 |
+
"bytes_per_expert": 589824,
|
| 1003 |
+
"n_expert": 256
|
| 1004 |
+
},
|
| 1005 |
+
"ffn_up_exps": {
|
| 1006 |
+
"file_offset": 11735390624,
|
| 1007 |
+
"file_end": 11886385568,
|
| 1008 |
+
"type": "Q4_K",
|
| 1009 |
+
"type_id": 12,
|
| 1010 |
+
"dims": [
|
| 1011 |
+
2048,
|
| 1012 |
+
512,
|
| 1013 |
+
256
|
| 1014 |
+
],
|
| 1015 |
+
"bytes_per_expert": 589824,
|
| 1016 |
+
"n_expert": 256
|
| 1017 |
+
}
|
| 1018 |
+
},
|
| 1019 |
+
"bytes_per_expert_total": 1769472
|
| 1020 |
+
},
|
| 1021 |
+
{
|
| 1022 |
+
"layer": 22,
|
| 1023 |
+
"experts": {
|
| 1024 |
+
"ffn_down_exps": {
|
| 1025 |
+
"file_offset": 11910397088,
|
| 1026 |
+
"file_end": 12130598048,
|
| 1027 |
+
"type": "Q6_K",
|
| 1028 |
+
"type_id": 14,
|
| 1029 |
+
"dims": [
|
| 1030 |
+
512,
|
| 1031 |
+
2048,
|
| 1032 |
+
256
|
| 1033 |
+
],
|
| 1034 |
+
"bytes_per_expert": 860160,
|
| 1035 |
+
"n_expert": 256
|
| 1036 |
+
},
|
| 1037 |
+
"ffn_gate_exps": {
|
| 1038 |
+
"file_offset": 12131458208,
|
| 1039 |
+
"file_end": 12282453152,
|
| 1040 |
+
"type": "Q4_K",
|
| 1041 |
+
"type_id": 12,
|
| 1042 |
+
"dims": [
|
| 1043 |
+
2048,
|
| 1044 |
+
512,
|
| 1045 |
+
256
|
| 1046 |
+
],
|
| 1047 |
+
"bytes_per_expert": 589824,
|
| 1048 |
+
"n_expert": 256
|
| 1049 |
+
},
|
| 1050 |
+
"ffn_up_exps": {
|
| 1051 |
+
"file_offset": 12285148320,
|
| 1052 |
+
"file_end": 12436143264,
|
| 1053 |
+
"type": "Q4_K",
|
| 1054 |
+
"type_id": 12,
|
| 1055 |
+
"dims": [
|
| 1056 |
+
2048,
|
| 1057 |
+
512,
|
| 1058 |
+
256
|
| 1059 |
+
],
|
| 1060 |
+
"bytes_per_expert": 589824,
|
| 1061 |
+
"n_expert": 256
|
| 1062 |
+
}
|
| 1063 |
+
},
|
| 1064 |
+
"bytes_per_expert_total": 2039808
|
| 1065 |
+
},
|
| 1066 |
+
{
|
| 1067 |
+
"layer": 23,
|
| 1068 |
+
"experts": {
|
| 1069 |
+
"ffn_down_exps": {
|
| 1070 |
+
"file_offset": 12457011104,
|
| 1071 |
+
"file_end": 12608006048,
|
| 1072 |
+
"type": "Q4_K",
|
| 1073 |
+
"type_id": 12,
|
| 1074 |
+
"dims": [
|
| 1075 |
+
512,
|
| 1076 |
+
2048,
|
| 1077 |
+
256
|
| 1078 |
+
],
|
| 1079 |
+
"bytes_per_expert": 589824,
|
| 1080 |
+
"n_expert": 256
|
| 1081 |
+
},
|
| 1082 |
+
"ffn_gate_exps": {
|
| 1083 |
+
"file_offset": 12608595872,
|
| 1084 |
+
"file_end": 12759590816,
|
| 1085 |
+
"type": "Q4_K",
|
| 1086 |
+
"type_id": 12,
|
| 1087 |
+
"dims": [
|
| 1088 |
+
2048,
|
| 1089 |
+
512,
|
| 1090 |
+
256
|
| 1091 |
+
],
|
| 1092 |
+
"bytes_per_expert": 589824,
|
| 1093 |
+
"n_expert": 256
|
| 1094 |
+
},
|
| 1095 |
+
"ffn_up_exps": {
|
| 1096 |
+
"file_offset": 12762285984,
|
| 1097 |
+
"file_end": 12913280928,
|
| 1098 |
+
"type": "Q4_K",
|
| 1099 |
+
"type_id": 12,
|
| 1100 |
+
"dims": [
|
| 1101 |
+
2048,
|
| 1102 |
+
512,
|
| 1103 |
+
256
|
| 1104 |
+
],
|
| 1105 |
+
"bytes_per_expert": 589824,
|
| 1106 |
+
"n_expert": 256
|
| 1107 |
+
}
|
| 1108 |
+
},
|
| 1109 |
+
"bytes_per_expert_total": 1769472
|
| 1110 |
+
},
|
| 1111 |
+
{
|
| 1112 |
+
"layer": 24,
|
| 1113 |
+
"experts": {
|
| 1114 |
+
"ffn_down_exps": {
|
| 1115 |
+
"file_offset": 12928042912,
|
| 1116 |
+
"file_end": 13079037856,
|
| 1117 |
+
"type": "Q4_K",
|
| 1118 |
+
"type_id": 12,
|
| 1119 |
+
"dims": [
|
| 1120 |
+
512,
|
| 1121 |
+
2048,
|
| 1122 |
+
256
|
| 1123 |
+
],
|
| 1124 |
+
"bytes_per_expert": 589824,
|
| 1125 |
+
"n_expert": 256
|
| 1126 |
+
},
|
| 1127 |
+
"ffn_gate_exps": {
|
| 1128 |
+
"file_offset": 13079627680,
|
| 1129 |
+
"file_end": 13230622624,
|
| 1130 |
+
"type": "Q4_K",
|
| 1131 |
+
"type_id": 12,
|
| 1132 |
+
"dims": [
|
| 1133 |
+
2048,
|
| 1134 |
+
512,
|
| 1135 |
+
256
|
| 1136 |
+
],
|
| 1137 |
+
"bytes_per_expert": 589824,
|
| 1138 |
+
"n_expert": 256
|
| 1139 |
+
},
|
| 1140 |
+
"ffn_up_exps": {
|
| 1141 |
+
"file_offset": 13233317792,
|
| 1142 |
+
"file_end": 13384312736,
|
| 1143 |
+
"type": "Q4_K",
|
| 1144 |
+
"type_id": 12,
|
| 1145 |
+
"dims": [
|
| 1146 |
+
2048,
|
| 1147 |
+
512,
|
| 1148 |
+
256
|
| 1149 |
+
],
|
| 1150 |
+
"bytes_per_expert": 589824,
|
| 1151 |
+
"n_expert": 256
|
| 1152 |
+
}
|
| 1153 |
+
},
|
| 1154 |
+
"bytes_per_expert_total": 1769472
|
| 1155 |
+
},
|
| 1156 |
+
{
|
| 1157 |
+
"layer": 25,
|
| 1158 |
+
"experts": {
|
| 1159 |
+
"ffn_down_exps": {
|
| 1160 |
+
"file_offset": 13408324256,
|
| 1161 |
+
"file_end": 13628525216,
|
| 1162 |
+
"type": "Q6_K",
|
| 1163 |
+
"type_id": 14,
|
| 1164 |
+
"dims": [
|
| 1165 |
+
512,
|
| 1166 |
+
2048,
|
| 1167 |
+
256
|
| 1168 |
+
],
|
| 1169 |
+
"bytes_per_expert": 860160,
|
| 1170 |
+
"n_expert": 256
|
| 1171 |
+
},
|
| 1172 |
+
"ffn_gate_exps": {
|
| 1173 |
+
"file_offset": 13629385376,
|
| 1174 |
+
"file_end": 13780380320,
|
| 1175 |
+
"type": "Q4_K",
|
| 1176 |
+
"type_id": 12,
|
| 1177 |
+
"dims": [
|
| 1178 |
+
2048,
|
| 1179 |
+
512,
|
| 1180 |
+
256
|
| 1181 |
+
],
|
| 1182 |
+
"bytes_per_expert": 589824,
|
| 1183 |
+
"n_expert": 256
|
| 1184 |
+
},
|
| 1185 |
+
"ffn_up_exps": {
|
| 1186 |
+
"file_offset": 13783075488,
|
| 1187 |
+
"file_end": 13934070432,
|
| 1188 |
+
"type": "Q4_K",
|
| 1189 |
+
"type_id": 12,
|
| 1190 |
+
"dims": [
|
| 1191 |
+
2048,
|
| 1192 |
+
512,
|
| 1193 |
+
256
|
| 1194 |
+
],
|
| 1195 |
+
"bytes_per_expert": 589824,
|
| 1196 |
+
"n_expert": 256
|
| 1197 |
+
}
|
| 1198 |
+
},
|
| 1199 |
+
"bytes_per_expert_total": 2039808
|
| 1200 |
+
},
|
| 1201 |
+
{
|
| 1202 |
+
"layer": 26,
|
| 1203 |
+
"experts": {
|
| 1204 |
+
"ffn_down_exps": {
|
| 1205 |
+
"file_offset": 13953756576,
|
| 1206 |
+
"file_end": 14104751520,
|
| 1207 |
+
"type": "Q4_K",
|
| 1208 |
+
"type_id": 12,
|
| 1209 |
+
"dims": [
|
| 1210 |
+
512,
|
| 1211 |
+
2048,
|
| 1212 |
+
256
|
| 1213 |
+
],
|
| 1214 |
+
"bytes_per_expert": 589824,
|
| 1215 |
+
"n_expert": 256
|
| 1216 |
+
},
|
| 1217 |
+
"ffn_gate_exps": {
|
| 1218 |
+
"file_offset": 14105341344,
|
| 1219 |
+
"file_end": 14256336288,
|
| 1220 |
+
"type": "Q4_K",
|
| 1221 |
+
"type_id": 12,
|
| 1222 |
+
"dims": [
|
| 1223 |
+
2048,
|
| 1224 |
+
512,
|
| 1225 |
+
256
|
| 1226 |
+
],
|
| 1227 |
+
"bytes_per_expert": 589824,
|
| 1228 |
+
"n_expert": 256
|
| 1229 |
+
},
|
| 1230 |
+
"ffn_up_exps": {
|
| 1231 |
+
"file_offset": 14259031456,
|
| 1232 |
+
"file_end": 14410026400,
|
| 1233 |
+
"type": "Q4_K",
|
| 1234 |
+
"type_id": 12,
|
| 1235 |
+
"dims": [
|
| 1236 |
+
2048,
|
| 1237 |
+
512,
|
| 1238 |
+
256
|
| 1239 |
+
],
|
| 1240 |
+
"bytes_per_expert": 589824,
|
| 1241 |
+
"n_expert": 256
|
| 1242 |
+
}
|
| 1243 |
+
},
|
| 1244 |
+
"bytes_per_expert_total": 1769472
|
| 1245 |
+
},
|
| 1246 |
+
{
|
| 1247 |
+
"layer": 27,
|
| 1248 |
+
"experts": {
|
| 1249 |
+
"ffn_down_exps": {
|
| 1250 |
+
"file_offset": 14430894240,
|
| 1251 |
+
"file_end": 14581889184,
|
| 1252 |
+
"type": "Q4_K",
|
| 1253 |
+
"type_id": 12,
|
| 1254 |
+
"dims": [
|
| 1255 |
+
512,
|
| 1256 |
+
2048,
|
| 1257 |
+
256
|
| 1258 |
+
],
|
| 1259 |
+
"bytes_per_expert": 589824,
|
| 1260 |
+
"n_expert": 256
|
| 1261 |
+
},
|
| 1262 |
+
"ffn_gate_exps": {
|
| 1263 |
+
"file_offset": 14582479008,
|
| 1264 |
+
"file_end": 14733473952,
|
| 1265 |
+
"type": "Q4_K",
|
| 1266 |
+
"type_id": 12,
|
| 1267 |
+
"dims": [
|
| 1268 |
+
2048,
|
| 1269 |
+
512,
|
| 1270 |
+
256
|
| 1271 |
+
],
|
| 1272 |
+
"bytes_per_expert": 589824,
|
| 1273 |
+
"n_expert": 256
|
| 1274 |
+
},
|
| 1275 |
+
"ffn_up_exps": {
|
| 1276 |
+
"file_offset": 14736169120,
|
| 1277 |
+
"file_end": 14887164064,
|
| 1278 |
+
"type": "Q4_K",
|
| 1279 |
+
"type_id": 12,
|
| 1280 |
+
"dims": [
|
| 1281 |
+
2048,
|
| 1282 |
+
512,
|
| 1283 |
+
256
|
| 1284 |
+
],
|
| 1285 |
+
"bytes_per_expert": 589824,
|
| 1286 |
+
"n_expert": 256
|
| 1287 |
+
}
|
| 1288 |
+
},
|
| 1289 |
+
"bytes_per_expert_total": 1769472
|
| 1290 |
+
},
|
| 1291 |
+
{
|
| 1292 |
+
"layer": 28,
|
| 1293 |
+
"experts": {
|
| 1294 |
+
"ffn_down_exps": {
|
| 1295 |
+
"file_offset": 14906251424,
|
| 1296 |
+
"file_end": 15126452384,
|
| 1297 |
+
"type": "Q6_K",
|
| 1298 |
+
"type_id": 14,
|
| 1299 |
+
"dims": [
|
| 1300 |
+
512,
|
| 1301 |
+
2048,
|
| 1302 |
+
256
|
| 1303 |
+
],
|
| 1304 |
+
"bytes_per_expert": 860160,
|
| 1305 |
+
"n_expert": 256
|
| 1306 |
+
},
|
| 1307 |
+
"ffn_gate_exps": {
|
| 1308 |
+
"file_offset": 15127312544,
|
| 1309 |
+
"file_end": 15278307488,
|
| 1310 |
+
"type": "Q4_K",
|
| 1311 |
+
"type_id": 12,
|
| 1312 |
+
"dims": [
|
| 1313 |
+
2048,
|
| 1314 |
+
512,
|
| 1315 |
+
256
|
| 1316 |
+
],
|
| 1317 |
+
"bytes_per_expert": 589824,
|
| 1318 |
+
"n_expert": 256
|
| 1319 |
+
},
|
| 1320 |
+
"ffn_up_exps": {
|
| 1321 |
+
"file_offset": 15281002656,
|
| 1322 |
+
"file_end": 15431997600,
|
| 1323 |
+
"type": "Q4_K",
|
| 1324 |
+
"type_id": 12,
|
| 1325 |
+
"dims": [
|
| 1326 |
+
2048,
|
| 1327 |
+
512,
|
| 1328 |
+
256
|
| 1329 |
+
],
|
| 1330 |
+
"bytes_per_expert": 589824,
|
| 1331 |
+
"n_expert": 256
|
| 1332 |
+
}
|
| 1333 |
+
},
|
| 1334 |
+
"bytes_per_expert_total": 2039808
|
| 1335 |
+
},
|
| 1336 |
+
{
|
| 1337 |
+
"layer": 29,
|
| 1338 |
+
"experts": {
|
| 1339 |
+
"ffn_down_exps": {
|
| 1340 |
+
"file_offset": 15451683744,
|
| 1341 |
+
"file_end": 15602678688,
|
| 1342 |
+
"type": "Q4_K",
|
| 1343 |
+
"type_id": 12,
|
| 1344 |
+
"dims": [
|
| 1345 |
+
512,
|
| 1346 |
+
2048,
|
| 1347 |
+
256
|
| 1348 |
+
],
|
| 1349 |
+
"bytes_per_expert": 589824,
|
| 1350 |
+
"n_expert": 256
|
| 1351 |
+
},
|
| 1352 |
+
"ffn_gate_exps": {
|
| 1353 |
+
"file_offset": 15603268512,
|
| 1354 |
+
"file_end": 15754263456,
|
| 1355 |
+
"type": "Q4_K",
|
| 1356 |
+
"type_id": 12,
|
| 1357 |
+
"dims": [
|
| 1358 |
+
2048,
|
| 1359 |
+
512,
|
| 1360 |
+
256
|
| 1361 |
+
],
|
| 1362 |
+
"bytes_per_expert": 589824,
|
| 1363 |
+
"n_expert": 256
|
| 1364 |
+
},
|
| 1365 |
+
"ffn_up_exps": {
|
| 1366 |
+
"file_offset": 15756958624,
|
| 1367 |
+
"file_end": 15907953568,
|
| 1368 |
+
"type": "Q4_K",
|
| 1369 |
+
"type_id": 12,
|
| 1370 |
+
"dims": [
|
| 1371 |
+
2048,
|
| 1372 |
+
512,
|
| 1373 |
+
256
|
| 1374 |
+
],
|
| 1375 |
+
"bytes_per_expert": 589824,
|
| 1376 |
+
"n_expert": 256
|
| 1377 |
+
}
|
| 1378 |
+
},
|
| 1379 |
+
"bytes_per_expert_total": 1769472
|
| 1380 |
+
},
|
| 1381 |
+
{
|
| 1382 |
+
"layer": 30,
|
| 1383 |
+
"experts": {
|
| 1384 |
+
"ffn_down_exps": {
|
| 1385 |
+
"file_offset": 15927639712,
|
| 1386 |
+
"file_end": 16078634656,
|
| 1387 |
+
"type": "Q4_K",
|
| 1388 |
+
"type_id": 12,
|
| 1389 |
+
"dims": [
|
| 1390 |
+
512,
|
| 1391 |
+
2048,
|
| 1392 |
+
256
|
| 1393 |
+
],
|
| 1394 |
+
"bytes_per_expert": 589824,
|
| 1395 |
+
"n_expert": 256
|
| 1396 |
+
},
|
| 1397 |
+
"ffn_gate_exps": {
|
| 1398 |
+
"file_offset": 16079224480,
|
| 1399 |
+
"file_end": 16230219424,
|
| 1400 |
+
"type": "Q4_K",
|
| 1401 |
+
"type_id": 12,
|
| 1402 |
+
"dims": [
|
| 1403 |
+
2048,
|
| 1404 |
+
512,
|
| 1405 |
+
256
|
| 1406 |
+
],
|
| 1407 |
+
"bytes_per_expert": 589824,
|
| 1408 |
+
"n_expert": 256
|
| 1409 |
+
},
|
| 1410 |
+
"ffn_up_exps": {
|
| 1411 |
+
"file_offset": 16232914592,
|
| 1412 |
+
"file_end": 16383909536,
|
| 1413 |
+
"type": "Q4_K",
|
| 1414 |
+
"type_id": 12,
|
| 1415 |
+
"dims": [
|
| 1416 |
+
2048,
|
| 1417 |
+
512,
|
| 1418 |
+
256
|
| 1419 |
+
],
|
| 1420 |
+
"bytes_per_expert": 589824,
|
| 1421 |
+
"n_expert": 256
|
| 1422 |
+
}
|
| 1423 |
+
},
|
| 1424 |
+
"bytes_per_expert_total": 1769472
|
| 1425 |
+
},
|
| 1426 |
+
{
|
| 1427 |
+
"layer": 31,
|
| 1428 |
+
"experts": {
|
| 1429 |
+
"ffn_down_exps": {
|
| 1430 |
+
"file_offset": 16405047712,
|
| 1431 |
+
"file_end": 16625248672,
|
| 1432 |
+
"type": "Q6_K",
|
| 1433 |
+
"type_id": 14,
|
| 1434 |
+
"dims": [
|
| 1435 |
+
512,
|
| 1436 |
+
2048,
|
| 1437 |
+
256
|
| 1438 |
+
],
|
| 1439 |
+
"bytes_per_expert": 860160,
|
| 1440 |
+
"n_expert": 256
|
| 1441 |
+
},
|
| 1442 |
+
"ffn_gate_exps": {
|
| 1443 |
+
"file_offset": 16626108832,
|
| 1444 |
+
"file_end": 16777103776,
|
| 1445 |
+
"type": "Q4_K",
|
| 1446 |
+
"type_id": 12,
|
| 1447 |
+
"dims": [
|
| 1448 |
+
2048,
|
| 1449 |
+
512,
|
| 1450 |
+
256
|
| 1451 |
+
],
|
| 1452 |
+
"bytes_per_expert": 589824,
|
| 1453 |
+
"n_expert": 256
|
| 1454 |
+
},
|
| 1455 |
+
"ffn_up_exps": {
|
| 1456 |
+
"file_offset": 16779798944,
|
| 1457 |
+
"file_end": 16930793888,
|
| 1458 |
+
"type": "Q4_K",
|
| 1459 |
+
"type_id": 12,
|
| 1460 |
+
"dims": [
|
| 1461 |
+
2048,
|
| 1462 |
+
512,
|
| 1463 |
+
256
|
| 1464 |
+
],
|
| 1465 |
+
"bytes_per_expert": 589824,
|
| 1466 |
+
"n_expert": 256
|
| 1467 |
+
}
|
| 1468 |
+
},
|
| 1469 |
+
"bytes_per_expert_total": 2039808
|
| 1470 |
+
},
|
| 1471 |
+
{
|
| 1472 |
+
"layer": 32,
|
| 1473 |
+
"experts": {
|
| 1474 |
+
"ffn_down_exps": {
|
| 1475 |
+
"file_offset": 16945555872,
|
| 1476 |
+
"file_end": 17096550816,
|
| 1477 |
+
"type": "Q4_K",
|
| 1478 |
+
"type_id": 12,
|
| 1479 |
+
"dims": [
|
| 1480 |
+
512,
|
| 1481 |
+
2048,
|
| 1482 |
+
256
|
| 1483 |
+
],
|
| 1484 |
+
"bytes_per_expert": 589824,
|
| 1485 |
+
"n_expert": 256
|
| 1486 |
+
},
|
| 1487 |
+
"ffn_gate_exps": {
|
| 1488 |
+
"file_offset": 17097140640,
|
| 1489 |
+
"file_end": 17248135584,
|
| 1490 |
+
"type": "Q4_K",
|
| 1491 |
+
"type_id": 12,
|
| 1492 |
+
"dims": [
|
| 1493 |
+
2048,
|
| 1494 |
+
512,
|
| 1495 |
+
256
|
| 1496 |
+
],
|
| 1497 |
+
"bytes_per_expert": 589824,
|
| 1498 |
+
"n_expert": 256
|
| 1499 |
+
},
|
| 1500 |
+
"ffn_up_exps": {
|
| 1501 |
+
"file_offset": 17250830752,
|
| 1502 |
+
"file_end": 17401825696,
|
| 1503 |
+
"type": "Q4_K",
|
| 1504 |
+
"type_id": 12,
|
| 1505 |
+
"dims": [
|
| 1506 |
+
2048,
|
| 1507 |
+
512,
|
| 1508 |
+
256
|
| 1509 |
+
],
|
| 1510 |
+
"bytes_per_expert": 589824,
|
| 1511 |
+
"n_expert": 256
|
| 1512 |
+
}
|
| 1513 |
+
},
|
| 1514 |
+
"bytes_per_expert_total": 1769472
|
| 1515 |
+
},
|
| 1516 |
+
{
|
| 1517 |
+
"layer": 33,
|
| 1518 |
+
"experts": {
|
| 1519 |
+
"ffn_down_exps": {
|
| 1520 |
+
"file_offset": 17421511840,
|
| 1521 |
+
"file_end": 17572506784,
|
| 1522 |
+
"type": "Q4_K",
|
| 1523 |
+
"type_id": 12,
|
| 1524 |
+
"dims": [
|
| 1525 |
+
512,
|
| 1526 |
+
2048,
|
| 1527 |
+
256
|
| 1528 |
+
],
|
| 1529 |
+
"bytes_per_expert": 589824,
|
| 1530 |
+
"n_expert": 256
|
| 1531 |
+
},
|
| 1532 |
+
"ffn_gate_exps": {
|
| 1533 |
+
"file_offset": 17573096608,
|
| 1534 |
+
"file_end": 17724091552,
|
| 1535 |
+
"type": "Q4_K",
|
| 1536 |
+
"type_id": 12,
|
| 1537 |
+
"dims": [
|
| 1538 |
+
2048,
|
| 1539 |
+
512,
|
| 1540 |
+
256
|
| 1541 |
+
],
|
| 1542 |
+
"bytes_per_expert": 589824,
|
| 1543 |
+
"n_expert": 256
|
| 1544 |
+
},
|
| 1545 |
+
"ffn_up_exps": {
|
| 1546 |
+
"file_offset": 17726786720,
|
| 1547 |
+
"file_end": 17877781664,
|
| 1548 |
+
"type": "Q4_K",
|
| 1549 |
+
"type_id": 12,
|
| 1550 |
+
"dims": [
|
| 1551 |
+
2048,
|
| 1552 |
+
512,
|
| 1553 |
+
256
|
| 1554 |
+
],
|
| 1555 |
+
"bytes_per_expert": 589824,
|
| 1556 |
+
"n_expert": 256
|
| 1557 |
+
}
|
| 1558 |
+
},
|
| 1559 |
+
"bytes_per_expert_total": 1769472
|
| 1560 |
+
},
|
| 1561 |
+
{
|
| 1562 |
+
"layer": 34,
|
| 1563 |
+
"experts": {
|
| 1564 |
+
"ffn_down_exps": {
|
| 1565 |
+
"file_offset": 17901793184,
|
| 1566 |
+
"file_end": 18121994144,
|
| 1567 |
+
"type": "Q6_K",
|
| 1568 |
+
"type_id": 14,
|
| 1569 |
+
"dims": [
|
| 1570 |
+
512,
|
| 1571 |
+
2048,
|
| 1572 |
+
256
|
| 1573 |
+
],
|
| 1574 |
+
"bytes_per_expert": 860160,
|
| 1575 |
+
"n_expert": 256
|
| 1576 |
+
},
|
| 1577 |
+
"ffn_gate_exps": {
|
| 1578 |
+
"file_offset": 18122854304,
|
| 1579 |
+
"file_end": 18273849248,
|
| 1580 |
+
"type": "Q4_K",
|
| 1581 |
+
"type_id": 12,
|
| 1582 |
+
"dims": [
|
| 1583 |
+
2048,
|
| 1584 |
+
512,
|
| 1585 |
+
256
|
| 1586 |
+
],
|
| 1587 |
+
"bytes_per_expert": 589824,
|
| 1588 |
+
"n_expert": 256
|
| 1589 |
+
},
|
| 1590 |
+
"ffn_up_exps": {
|
| 1591 |
+
"file_offset": 18276544416,
|
| 1592 |
+
"file_end": 18427539360,
|
| 1593 |
+
"type": "Q4_K",
|
| 1594 |
+
"type_id": 12,
|
| 1595 |
+
"dims": [
|
| 1596 |
+
2048,
|
| 1597 |
+
512,
|
| 1598 |
+
256
|
| 1599 |
+
],
|
| 1600 |
+
"bytes_per_expert": 589824,
|
| 1601 |
+
"n_expert": 256
|
| 1602 |
+
}
|
| 1603 |
+
},
|
| 1604 |
+
"bytes_per_expert_total": 2039808
|
| 1605 |
+
},
|
| 1606 |
+
{
|
| 1607 |
+
"layer": 35,
|
| 1608 |
+
"experts": {
|
| 1609 |
+
"ffn_down_exps": {
|
| 1610 |
+
"file_offset": 18448677536,
|
| 1611 |
+
"file_end": 18668878496,
|
| 1612 |
+
"type": "Q6_K",
|
| 1613 |
+
"type_id": 14,
|
| 1614 |
+
"dims": [
|
| 1615 |
+
512,
|
| 1616 |
+
2048,
|
| 1617 |
+
256
|
| 1618 |
+
],
|
| 1619 |
+
"bytes_per_expert": 860160,
|
| 1620 |
+
"n_expert": 256
|
| 1621 |
+
},
|
| 1622 |
+
"ffn_gate_exps": {
|
| 1623 |
+
"file_offset": 18669738656,
|
| 1624 |
+
"file_end": 18820733600,
|
| 1625 |
+
"type": "Q4_K",
|
| 1626 |
+
"type_id": 12,
|
| 1627 |
+
"dims": [
|
| 1628 |
+
2048,
|
| 1629 |
+
512,
|
| 1630 |
+
256
|
| 1631 |
+
],
|
| 1632 |
+
"bytes_per_expert": 589824,
|
| 1633 |
+
"n_expert": 256
|
| 1634 |
+
},
|
| 1635 |
+
"ffn_up_exps": {
|
| 1636 |
+
"file_offset": 18823428768,
|
| 1637 |
+
"file_end": 18974423712,
|
| 1638 |
+
"type": "Q4_K",
|
| 1639 |
+
"type_id": 12,
|
| 1640 |
+
"dims": [
|
| 1641 |
+
2048,
|
| 1642 |
+
512,
|
| 1643 |
+
256
|
| 1644 |
+
],
|
| 1645 |
+
"bytes_per_expert": 589824,
|
| 1646 |
+
"n_expert": 256
|
| 1647 |
+
}
|
| 1648 |
+
},
|
| 1649 |
+
"bytes_per_expert_total": 2039808
|
| 1650 |
+
},
|
| 1651 |
+
{
|
| 1652 |
+
"layer": 36,
|
| 1653 |
+
"experts": {
|
| 1654 |
+
"ffn_down_exps": {
|
| 1655 |
+
"file_offset": 18993511072,
|
| 1656 |
+
"file_end": 19213712032,
|
| 1657 |
+
"type": "Q6_K",
|
| 1658 |
+
"type_id": 14,
|
| 1659 |
+
"dims": [
|
| 1660 |
+
512,
|
| 1661 |
+
2048,
|
| 1662 |
+
256
|
| 1663 |
+
],
|
| 1664 |
+
"bytes_per_expert": 860160,
|
| 1665 |
+
"n_expert": 256
|
| 1666 |
+
},
|
| 1667 |
+
"ffn_gate_exps": {
|
| 1668 |
+
"file_offset": 19214572192,
|
| 1669 |
+
"file_end": 19365567136,
|
| 1670 |
+
"type": "Q4_K",
|
| 1671 |
+
"type_id": 12,
|
| 1672 |
+
"dims": [
|
| 1673 |
+
2048,
|
| 1674 |
+
512,
|
| 1675 |
+
256
|
| 1676 |
+
],
|
| 1677 |
+
"bytes_per_expert": 589824,
|
| 1678 |
+
"n_expert": 256
|
| 1679 |
+
},
|
| 1680 |
+
"ffn_up_exps": {
|
| 1681 |
+
"file_offset": 19368262304,
|
| 1682 |
+
"file_end": 19519257248,
|
| 1683 |
+
"type": "Q4_K",
|
| 1684 |
+
"type_id": 12,
|
| 1685 |
+
"dims": [
|
| 1686 |
+
2048,
|
| 1687 |
+
512,
|
| 1688 |
+
256
|
| 1689 |
+
],
|
| 1690 |
+
"bytes_per_expert": 589824,
|
| 1691 |
+
"n_expert": 256
|
| 1692 |
+
}
|
| 1693 |
+
},
|
| 1694 |
+
"bytes_per_expert_total": 2039808
|
| 1695 |
+
},
|
| 1696 |
+
{
|
| 1697 |
+
"layer": 37,
|
| 1698 |
+
"experts": {
|
| 1699 |
+
"ffn_down_exps": {
|
| 1700 |
+
"file_offset": 19543268768,
|
| 1701 |
+
"file_end": 19763469728,
|
| 1702 |
+
"type": "Q6_K",
|
| 1703 |
+
"type_id": 14,
|
| 1704 |
+
"dims": [
|
| 1705 |
+
512,
|
| 1706 |
+
2048,
|
| 1707 |
+
256
|
| 1708 |
+
],
|
| 1709 |
+
"bytes_per_expert": 860160,
|
| 1710 |
+
"n_expert": 256
|
| 1711 |
+
},
|
| 1712 |
+
"ffn_gate_exps": {
|
| 1713 |
+
"file_offset": 19764329888,
|
| 1714 |
+
"file_end": 19915324832,
|
| 1715 |
+
"type": "Q4_K",
|
| 1716 |
+
"type_id": 12,
|
| 1717 |
+
"dims": [
|
| 1718 |
+
2048,
|
| 1719 |
+
512,
|
| 1720 |
+
256
|
| 1721 |
+
],
|
| 1722 |
+
"bytes_per_expert": 589824,
|
| 1723 |
+
"n_expert": 256
|
| 1724 |
+
},
|
| 1725 |
+
"ffn_up_exps": {
|
| 1726 |
+
"file_offset": 19918020000,
|
| 1727 |
+
"file_end": 20069014944,
|
| 1728 |
+
"type": "Q4_K",
|
| 1729 |
+
"type_id": 12,
|
| 1730 |
+
"dims": [
|
| 1731 |
+
2048,
|
| 1732 |
+
512,
|
| 1733 |
+
256
|
| 1734 |
+
],
|
| 1735 |
+
"bytes_per_expert": 589824,
|
| 1736 |
+
"n_expert": 256
|
| 1737 |
+
}
|
| 1738 |
+
},
|
| 1739 |
+
"bytes_per_expert_total": 2039808
|
| 1740 |
+
},
|
| 1741 |
+
{
|
| 1742 |
+
"layer": 38,
|
| 1743 |
+
"experts": {
|
| 1744 |
+
"ffn_down_exps": {
|
| 1745 |
+
"file_offset": 20093026464,
|
| 1746 |
+
"file_end": 20313227424,
|
| 1747 |
+
"type": "Q6_K",
|
| 1748 |
+
"type_id": 14,
|
| 1749 |
+
"dims": [
|
| 1750 |
+
512,
|
| 1751 |
+
2048,
|
| 1752 |
+
256
|
| 1753 |
+
],
|
| 1754 |
+
"bytes_per_expert": 860160,
|
| 1755 |
+
"n_expert": 256
|
| 1756 |
+
},
|
| 1757 |
+
"ffn_gate_exps": {
|
| 1758 |
+
"file_offset": 20314087584,
|
| 1759 |
+
"file_end": 20465082528,
|
| 1760 |
+
"type": "Q4_K",
|
| 1761 |
+
"type_id": 12,
|
| 1762 |
+
"dims": [
|
| 1763 |
+
2048,
|
| 1764 |
+
512,
|
| 1765 |
+
256
|
| 1766 |
+
],
|
| 1767 |
+
"bytes_per_expert": 589824,
|
| 1768 |
+
"n_expert": 256
|
| 1769 |
+
},
|
| 1770 |
+
"ffn_up_exps": {
|
| 1771 |
+
"file_offset": 20467777696,
|
| 1772 |
+
"file_end": 20618772640,
|
| 1773 |
+
"type": "Q4_K",
|
| 1774 |
+
"type_id": 12,
|
| 1775 |
+
"dims": [
|
| 1776 |
+
2048,
|
| 1777 |
+
512,
|
| 1778 |
+
256
|
| 1779 |
+
],
|
| 1780 |
+
"bytes_per_expert": 589824,
|
| 1781 |
+
"n_expert": 256
|
| 1782 |
+
}
|
| 1783 |
+
},
|
| 1784 |
+
"bytes_per_expert_total": 2039808
|
| 1785 |
+
},
|
| 1786 |
+
{
|
| 1787 |
+
"layer": 39,
|
| 1788 |
+
"experts": {
|
| 1789 |
+
"ffn_down_exps": {
|
| 1790 |
+
"file_offset": 20639910816,
|
| 1791 |
+
"file_end": 20860111776,
|
| 1792 |
+
"type": "Q6_K",
|
| 1793 |
+
"type_id": 14,
|
| 1794 |
+
"dims": [
|
| 1795 |
+
512,
|
| 1796 |
+
2048,
|
| 1797 |
+
256
|
| 1798 |
+
],
|
| 1799 |
+
"bytes_per_expert": 860160,
|
| 1800 |
+
"n_expert": 256
|
| 1801 |
+
},
|
| 1802 |
+
"ffn_gate_exps": {
|
| 1803 |
+
"file_offset": 20860971936,
|
| 1804 |
+
"file_end": 21011966880,
|
| 1805 |
+
"type": "Q4_K",
|
| 1806 |
+
"type_id": 12,
|
| 1807 |
+
"dims": [
|
| 1808 |
+
2048,
|
| 1809 |
+
512,
|
| 1810 |
+
256
|
| 1811 |
+
],
|
| 1812 |
+
"bytes_per_expert": 589824,
|
| 1813 |
+
"n_expert": 256
|
| 1814 |
+
},
|
| 1815 |
+
"ffn_up_exps": {
|
| 1816 |
+
"file_offset": 21014662048,
|
| 1817 |
+
"file_end": 21165656992,
|
| 1818 |
+
"type": "Q4_K",
|
| 1819 |
+
"type_id": 12,
|
| 1820 |
+
"dims": [
|
| 1821 |
+
2048,
|
| 1822 |
+
512,
|
| 1823 |
+
256
|
| 1824 |
+
],
|
| 1825 |
+
"bytes_per_expert": 589824,
|
| 1826 |
+
"n_expert": 256
|
| 1827 |
+
}
|
| 1828 |
+
},
|
| 1829 |
+
"bytes_per_expert_total": 2039808
|
| 1830 |
+
}
|
| 1831 |
+
],
|
| 1832 |
+
"decode_working_set_bytes_max": 16318464,
|
| 1833 |
+
"decode_working_set_bytes_mean": 15237120.0,
|
| 1834 |
+
"expert_slot_count": 120,
|
| 1835 |
+
"kv_bytes_per_token_f16": 20480
|
| 1836 |
+
}
|
bench/results/ram_sweep.json
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"budget_mb": 4096,
|
| 4 |
+
"reserve_mb": 256,
|
| 5 |
+
"rc": 0,
|
| 6 |
+
"strict_ok": true,
|
| 7 |
+
"decode_tps": 4.97,
|
| 8 |
+
"hit_pct": 59.9155,
|
| 9 |
+
"mb_per_token": 275.32386779785156,
|
| 10 |
+
"slots": 1600,
|
| 11 |
+
"arena_gib": 2.74,
|
| 12 |
+
"peak_anon_mib": 3963.3,
|
| 13 |
+
"peak_swap_mb": 0.0,
|
| 14 |
+
"round": 0,
|
| 15 |
+
"prompt": "Explain in one sentence why SSDs are slow."
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"budget_mb": 8192,
|
| 19 |
+
"reserve_mb": 512,
|
| 20 |
+
"rc": 0,
|
| 21 |
+
"strict_ok": true,
|
| 22 |
+
"decode_tps": 6.66,
|
| 23 |
+
"hit_pct": 70.722,
|
| 24 |
+
"mb_per_token": 199.07283782958984,
|
| 25 |
+
"slots": 3715,
|
| 26 |
+
"arena_gib": 6.36,
|
| 27 |
+
"peak_anon_mib": 7668.3,
|
| 28 |
+
"peak_swap_mb": 0.0,
|
| 29 |
+
"round": 0,
|
| 30 |
+
"prompt": "Explain in one sentence why SSDs are slow."
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"budget_mb": 16384,
|
| 34 |
+
"reserve_mb": 1024,
|
| 35 |
+
"rc": 0,
|
| 36 |
+
"strict_ok": true,
|
| 37 |
+
"decode_tps": 5.87,
|
| 38 |
+
"hit_pct": 76.4665,
|
| 39 |
+
"mb_per_token": 157.9723358154297,
|
| 40 |
+
"slots": 7943,
|
| 41 |
+
"arena_gib": 13.59,
|
| 42 |
+
"peak_anon_mib": 15076.2,
|
| 43 |
+
"peak_swap_mb": 0.0,
|
| 44 |
+
"round": 0,
|
| 45 |
+
"prompt": "Explain in one sentence why SSDs are slow."
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"budget_mb": 32768,
|
| 49 |
+
"reserve_mb": 1024,
|
| 50 |
+
"rc": 0,
|
| 51 |
+
"strict_ok": true,
|
| 52 |
+
"decode_tps": 6.72,
|
| 53 |
+
"hit_pct": 76.4522,
|
| 54 |
+
"mb_per_token": 157.84645080566406,
|
| 55 |
+
"slots": 16963,
|
| 56 |
+
"arena_gib": 29.02,
|
| 57 |
+
"peak_anon_mib": 23959.0,
|
| 58 |
+
"peak_swap_mb": 0.0,
|
| 59 |
+
"round": 0,
|
| 60 |
+
"prompt": "Explain in one sentence why SSDs are slow."
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"budget_mb": 65536,
|
| 64 |
+
"reserve_mb": 1024,
|
| 65 |
+
"rc": 0,
|
| 66 |
+
"strict_ok": true,
|
| 67 |
+
"decode_tps": 6.8,
|
| 68 |
+
"hit_pct": 76.5186,
|
| 69 |
+
"mb_per_token": 157.60040283203125,
|
| 70 |
+
"slots": 35003,
|
| 71 |
+
"arena_gib": 59.89,
|
| 72 |
+
"peak_anon_mib": 23969.2,
|
| 73 |
+
"peak_swap_mb": 0.0,
|
| 74 |
+
"round": 0,
|
| 75 |
+
"prompt": "Explain in one sentence why SSDs are slow."
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"budget_mb": 4096,
|
| 79 |
+
"reserve_mb": 256,
|
| 80 |
+
"rc": 0,
|
| 81 |
+
"strict_ok": true,
|
| 82 |
+
"decode_tps": 5.56,
|
| 83 |
+
"hit_pct": 63.7432,
|
| 84 |
+
"mb_per_token": 239.48001861572266,
|
| 85 |
+
"slots": 1600,
|
| 86 |
+
"arena_gib": 2.74,
|
| 87 |
+
"peak_anon_mib": 3946.2,
|
| 88 |
+
"peak_swap_mb": 0.0,
|
| 89 |
+
"round": 1,
|
| 90 |
+
"prompt": "\u8acb\u7528\u4e00\u53e5\u8a71\u4ecb\u7d39\u4f60\u81ea\u5df1\u3002"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"budget_mb": 8192,
|
| 94 |
+
"reserve_mb": 512,
|
| 95 |
+
"rc": 0,
|
| 96 |
+
"strict_ok": true,
|
| 97 |
+
"decode_tps": 6.44,
|
| 98 |
+
"hit_pct": 75.0542,
|
| 99 |
+
"mb_per_token": 162.54615783691406,
|
| 100 |
+
"slots": 3715,
|
| 101 |
+
"arena_gib": 6.36,
|
| 102 |
+
"peak_anon_mib": 7652.6,
|
| 103 |
+
"peak_swap_mb": 0.0,
|
| 104 |
+
"round": 1,
|
| 105 |
+
"prompt": "\u8acb\u7528\u4e00\u53e5\u8a71\u4ecb\u7d39\u4f60\u81ea\u5df1\u3002"
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"budget_mb": 16384,
|
| 109 |
+
"reserve_mb": 1024,
|
| 110 |
+
"rc": 0,
|
| 111 |
+
"strict_ok": true,
|
| 112 |
+
"decode_tps": 6.75,
|
| 113 |
+
"hit_pct": 79.9662,
|
| 114 |
+
"mb_per_token": 128.8137435913086,
|
| 115 |
+
"slots": 7943,
|
| 116 |
+
"arena_gib": 13.59,
|
| 117 |
+
"peak_anon_mib": 15059.6,
|
| 118 |
+
"peak_swap_mb": 0.0,
|
| 119 |
+
"round": 1,
|
| 120 |
+
"prompt": "\u8acb\u7528\u4e00\u53e5\u8a71\u4ecb\u7d39\u4f60\u81ea\u5df1\u3002"
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"budget_mb": 32768,
|
| 124 |
+
"reserve_mb": 1024,
|
| 125 |
+
"rc": 0,
|
| 126 |
+
"strict_ok": true,
|
| 127 |
+
"decode_tps": 6.49,
|
| 128 |
+
"hit_pct": 79.572,
|
| 129 |
+
"mb_per_token": 131.12545013427734,
|
| 130 |
+
"slots": 16963,
|
| 131 |
+
"arena_gib": 29.02,
|
| 132 |
+
"peak_anon_mib": 23093.5,
|
| 133 |
+
"peak_swap_mb": 0.0,
|
| 134 |
+
"round": 1,
|
| 135 |
+
"prompt": "\u8acb\u7528\u4e00\u53e5\u8a71\u4ecb\u7d39\u4f60\u81ea\u5df1\u3002"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"budget_mb": 65536,
|
| 139 |
+
"reserve_mb": 1024,
|
| 140 |
+
"rc": 0,
|
| 141 |
+
"strict_ok": true,
|
| 142 |
+
"decode_tps": 6.46,
|
| 143 |
+
"hit_pct": 79.572,
|
| 144 |
+
"mb_per_token": 131.12545013427734,
|
| 145 |
+
"slots": 35003,
|
| 146 |
+
"arena_gib": 59.89,
|
| 147 |
+
"peak_anon_mib": 23094.1,
|
| 148 |
+
"peak_swap_mb": 0.0,
|
| 149 |
+
"round": 1,
|
| 150 |
+
"prompt": "\u8acb\u7528\u4e00\u53e5\u8a71\u4ecb\u7d39\u4f60\u81ea\u5df1\u3002"
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"budget_mb": 4096,
|
| 154 |
+
"reserve_mb": 256,
|
| 155 |
+
"rc": 0,
|
| 156 |
+
"strict_ok": true,
|
| 157 |
+
"decode_tps": 4.92,
|
| 158 |
+
"hit_pct": 61.5016,
|
| 159 |
+
"mb_per_token": 264.0085220336914,
|
| 160 |
+
"slots": 1600,
|
| 161 |
+
"arena_gib": 2.74,
|
| 162 |
+
"peak_anon_mib": 3949.7,
|
| 163 |
+
"peak_swap_mb": 0.0,
|
| 164 |
+
"round": 2,
|
| 165 |
+
"prompt": "Write a short paragraph about SSD caching."
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"budget_mb": 8192,
|
| 169 |
+
"reserve_mb": 512,
|
| 170 |
+
"rc": 0,
|
| 171 |
+
"strict_ok": true,
|
| 172 |
+
"decode_tps": 6.44,
|
| 173 |
+
"hit_pct": 71.9877,
|
| 174 |
+
"mb_per_token": 190.2914047241211,
|
| 175 |
+
"slots": 3715,
|
| 176 |
+
"arena_gib": 6.36,
|
| 177 |
+
"peak_anon_mib": 7654.6,
|
| 178 |
+
"peak_swap_mb": 0.0,
|
| 179 |
+
"round": 2,
|
| 180 |
+
"prompt": "Write a short paragraph about SSD caching."
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"budget_mb": 16384,
|
| 184 |
+
"reserve_mb": 1024,
|
| 185 |
+
"rc": 0,
|
| 186 |
+
"strict_ok": true,
|
| 187 |
+
"decode_tps": 6.2,
|
| 188 |
+
"hit_pct": 77.5357,
|
| 189 |
+
"mb_per_token": 150.6662368774414,
|
| 190 |
+
"slots": 7943,
|
| 191 |
+
"arena_gib": 13.59,
|
| 192 |
+
"peak_anon_mib": 15062.1,
|
| 193 |
+
"peak_swap_mb": 0.0,
|
| 194 |
+
"round": 2,
|
| 195 |
+
"prompt": "Write a short paragraph about SSD caching."
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"budget_mb": 32768,
|
| 199 |
+
"reserve_mb": 1024,
|
| 200 |
+
"rc": 0,
|
| 201 |
+
"strict_ok": true,
|
| 202 |
+
"decode_tps": 6.99,
|
| 203 |
+
"hit_pct": 76.6347,
|
| 204 |
+
"mb_per_token": 156.62002563476562,
|
| 205 |
+
"slots": 16963,
|
| 206 |
+
"arena_gib": 29.02,
|
| 207 |
+
"peak_anon_mib": 23875.6,
|
| 208 |
+
"peak_swap_mb": 0.0,
|
| 209 |
+
"round": 2,
|
| 210 |
+
"prompt": "Write a short paragraph about SSD caching."
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"budget_mb": 65536,
|
| 214 |
+
"reserve_mb": 1024,
|
| 215 |
+
"rc": 0,
|
| 216 |
+
"strict_ok": true,
|
| 217 |
+
"decode_tps": 6.16,
|
| 218 |
+
"hit_pct": 77.5416,
|
| 219 |
+
"mb_per_token": 150.62332153320312,
|
| 220 |
+
"slots": 35003,
|
| 221 |
+
"arena_gib": 59.89,
|
| 222 |
+
"peak_anon_mib": 23731.4,
|
| 223 |
+
"peak_swap_mb": 0.0,
|
| 224 |
+
"round": 2,
|
| 225 |
+
"prompt": "Write a short paragraph about SSD caching."
|
| 226 |
+
}
|
| 227 |
+
]
|
bench/results/ram_sweep_n9.json
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"per_budget": {
|
| 3 |
+
"4096": {
|
| 4 |
+
"n": 9,
|
| 5 |
+
"mean_tps": 5.4255555555555555,
|
| 6 |
+
"stdev_tps": 0.3725624481583963,
|
| 7 |
+
"median_tps": 5.45,
|
| 8 |
+
"min_tps": 4.78,
|
| 9 |
+
"max_tps": 5.84,
|
| 10 |
+
"slots": 1600,
|
| 11 |
+
"arena_gib": 2.74,
|
| 12 |
+
"hit_pct": 61.4956,
|
| 13 |
+
"mb_per_token": 275.4068374633789,
|
| 14 |
+
"peak_anon_mb": 3963.4,
|
| 15 |
+
"peak_swap_mb": 0.0,
|
| 16 |
+
"all_strict_ok": true
|
| 17 |
+
},
|
| 18 |
+
"8192": {
|
| 19 |
+
"n": 9,
|
| 20 |
+
"mean_tps": 6.328888888888889,
|
| 21 |
+
"stdev_tps": 0.3313398725042175,
|
| 22 |
+
"median_tps": 6.27,
|
| 23 |
+
"min_tps": 5.96,
|
| 24 |
+
"max_tps": 6.88,
|
| 25 |
+
"slots": 3715,
|
| 26 |
+
"arena_gib": 6.36,
|
| 27 |
+
"hit_pct": 71.9818,
|
| 28 |
+
"mb_per_token": 199.07283782958984,
|
| 29 |
+
"peak_anon_mb": 7668.7,
|
| 30 |
+
"peak_swap_mb": 0.0,
|
| 31 |
+
"all_strict_ok": true
|
| 32 |
+
},
|
| 33 |
+
"16384": {
|
| 34 |
+
"n": 9,
|
| 35 |
+
"mean_tps": 6.275555555555556,
|
| 36 |
+
"stdev_tps": 0.4072502643065782,
|
| 37 |
+
"median_tps": 6.38,
|
| 38 |
+
"min_tps": 5.5,
|
| 39 |
+
"max_tps": 6.73,
|
| 40 |
+
"slots": 7943,
|
| 41 |
+
"arena_gib": 13.59,
|
| 42 |
+
"hit_pct": 77.5357,
|
| 43 |
+
"mb_per_token": 157.93132781982422,
|
| 44 |
+
"peak_anon_mb": 15076.2,
|
| 45 |
+
"peak_swap_mb": 0.0,
|
| 46 |
+
"all_strict_ok": true
|
| 47 |
+
},
|
| 48 |
+
"65536": {
|
| 49 |
+
"n": 9,
|
| 50 |
+
"mean_tps": 6.318888888888889,
|
| 51 |
+
"stdev_tps": 0.5079233319223591,
|
| 52 |
+
"median_tps": 6.37,
|
| 53 |
+
"min_tps": 5.19,
|
| 54 |
+
"max_tps": 6.81,
|
| 55 |
+
"slots": 35003,
|
| 56 |
+
"arena_gib": 59.89,
|
| 57 |
+
"hit_pct": 77.5416,
|
| 58 |
+
"mb_per_token": 157.60040283203125,
|
| 59 |
+
"peak_anon_mb": 23968.9,
|
| 60 |
+
"peak_swap_mb": 0.0,
|
| 61 |
+
"all_strict_ok": true
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"welch_vs_8192": {
|
| 65 |
+
"4096_vs_8192": {
|
| 66 |
+
"t": -5.435361505492692,
|
| 67 |
+
"p": 5.766235313608975e-05
|
| 68 |
+
},
|
| 69 |
+
"16384_vs_8192": {
|
| 70 |
+
"t": -0.3047542264956529,
|
| 71 |
+
"p": 0.7646425989688621
|
| 72 |
+
},
|
| 73 |
+
"65536_vs_8192": {
|
| 74 |
+
"t": -0.049468847526827016,
|
| 75 |
+
"p": 0.96125630480275
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
"rows": [
|
| 79 |
+
{
|
| 80 |
+
"budget_mb": 4096,
|
| 81 |
+
"reserve_mb": 256,
|
| 82 |
+
"rc": 0,
|
| 83 |
+
"strict_ok": true,
|
| 84 |
+
"decode_tps": 5.84,
|
| 85 |
+
"hit_pct": 59.904,
|
| 86 |
+
"mb_per_token": 275.4068374633789,
|
| 87 |
+
"slots": 1600,
|
| 88 |
+
"arena_gib": 2.74,
|
| 89 |
+
"peak_anon_mib": 3963.0,
|
| 90 |
+
"peak_swap_mb": 0.0,
|
| 91 |
+
"round": 0
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"budget_mb": 8192,
|
| 95 |
+
"reserve_mb": 512,
|
| 96 |
+
"rc": 0,
|
| 97 |
+
"strict_ok": true,
|
| 98 |
+
"decode_tps": 6.28,
|
| 99 |
+
"hit_pct": 70.722,
|
| 100 |
+
"mb_per_token": 199.07283782958984,
|
| 101 |
+
"slots": 3715,
|
| 102 |
+
"arena_gib": 6.36,
|
| 103 |
+
"peak_anon_mib": 7668.3,
|
| 104 |
+
"peak_swap_mb": 0.0,
|
| 105 |
+
"round": 0
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"budget_mb": 16384,
|
| 109 |
+
"reserve_mb": 1024,
|
| 110 |
+
"rc": 0,
|
| 111 |
+
"strict_ok": true,
|
| 112 |
+
"decode_tps": 6.57,
|
| 113 |
+
"hit_pct": 76.4723,
|
| 114 |
+
"mb_per_token": 157.93132781982422,
|
| 115 |
+
"slots": 7943,
|
| 116 |
+
"arena_gib": 13.59,
|
| 117 |
+
"peak_anon_mib": 15076.2,
|
| 118 |
+
"peak_swap_mb": 0.0,
|
| 119 |
+
"round": 0
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"budget_mb": 65536,
|
| 123 |
+
"reserve_mb": 1024,
|
| 124 |
+
"rc": 0,
|
| 125 |
+
"strict_ok": true,
|
| 126 |
+
"decode_tps": 6.64,
|
| 127 |
+
"hit_pct": 76.5186,
|
| 128 |
+
"mb_per_token": 157.60040283203125,
|
| 129 |
+
"slots": 35003,
|
| 130 |
+
"arena_gib": 59.89,
|
| 131 |
+
"peak_anon_mib": 23968.8,
|
| 132 |
+
"peak_swap_mb": 0.0,
|
| 133 |
+
"round": 0
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"budget_mb": 4096,
|
| 137 |
+
"reserve_mb": 256,
|
| 138 |
+
"rc": 0,
|
| 139 |
+
"strict_ok": true,
|
| 140 |
+
"decode_tps": 5.78,
|
| 141 |
+
"hit_pct": 63.7613,
|
| 142 |
+
"mb_per_token": 239.35604095458984,
|
| 143 |
+
"slots": 1600,
|
| 144 |
+
"arena_gib": 2.74,
|
| 145 |
+
"peak_anon_mib": 3946.2,
|
| 146 |
+
"peak_swap_mb": 0.0,
|
| 147 |
+
"round": 1
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"budget_mb": 8192,
|
| 151 |
+
"reserve_mb": 512,
|
| 152 |
+
"rc": 0,
|
| 153 |
+
"strict_ok": true,
|
| 154 |
+
"decode_tps": 6.09,
|
| 155 |
+
"hit_pct": 75.0603,
|
| 156 |
+
"mb_per_token": 162.5041961669922,
|
| 157 |
+
"slots": 3715,
|
| 158 |
+
"arena_gib": 6.36,
|
| 159 |
+
"peak_anon_mib": 7652.6,
|
| 160 |
+
"peak_swap_mb": 0.0,
|
| 161 |
+
"round": 1
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"budget_mb": 16384,
|
| 165 |
+
"reserve_mb": 1024,
|
| 166 |
+
"rc": 0,
|
| 167 |
+
"strict_ok": true,
|
| 168 |
+
"decode_tps": 6.63,
|
| 169 |
+
"hit_pct": 79.9662,
|
| 170 |
+
"mb_per_token": 128.8137435913086,
|
| 171 |
+
"slots": 7943,
|
| 172 |
+
"arena_gib": 13.59,
|
| 173 |
+
"peak_anon_mib": 15059.6,
|
| 174 |
+
"peak_swap_mb": 0.0,
|
| 175 |
+
"round": 1
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"budget_mb": 65536,
|
| 179 |
+
"reserve_mb": 1024,
|
| 180 |
+
"rc": 0,
|
| 181 |
+
"strict_ok": true,
|
| 182 |
+
"decode_tps": 6.72,
|
| 183 |
+
"hit_pct": 79.572,
|
| 184 |
+
"mb_per_token": 131.12545013427734,
|
| 185 |
+
"slots": 35003,
|
| 186 |
+
"arena_gib": 59.89,
|
| 187 |
+
"peak_anon_mib": 23094.1,
|
| 188 |
+
"peak_swap_mb": 0.0,
|
| 189 |
+
"round": 1
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"budget_mb": 4096,
|
| 193 |
+
"reserve_mb": 256,
|
| 194 |
+
"rc": 0,
|
| 195 |
+
"strict_ok": true,
|
| 196 |
+
"decode_tps": 4.99,
|
| 197 |
+
"hit_pct": 61.4838,
|
| 198 |
+
"mb_per_token": 264.13536071777344,
|
| 199 |
+
"slots": 1600,
|
| 200 |
+
"arena_gib": 2.74,
|
| 201 |
+
"peak_anon_mib": 3949.3,
|
| 202 |
+
"peak_swap_mb": 0.0,
|
| 203 |
+
"round": 2
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"budget_mb": 8192,
|
| 207 |
+
"reserve_mb": 512,
|
| 208 |
+
"rc": 0,
|
| 209 |
+
"strict_ok": true,
|
| 210 |
+
"decode_tps": 6.88,
|
| 211 |
+
"hit_pct": 71.9877,
|
| 212 |
+
"mb_per_token": 190.2914047241211,
|
| 213 |
+
"slots": 3715,
|
| 214 |
+
"arena_gib": 6.36,
|
| 215 |
+
"peak_anon_mib": 7654.6,
|
| 216 |
+
"peak_swap_mb": 0.0,
|
| 217 |
+
"round": 2
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"budget_mb": 16384,
|
| 221 |
+
"reserve_mb": 1024,
|
| 222 |
+
"rc": 0,
|
| 223 |
+
"strict_ok": true,
|
| 224 |
+
"decode_tps": 6.15,
|
| 225 |
+
"hit_pct": 77.5357,
|
| 226 |
+
"mb_per_token": 150.6662368774414,
|
| 227 |
+
"slots": 7943,
|
| 228 |
+
"arena_gib": 13.59,
|
| 229 |
+
"peak_anon_mib": 15062.1,
|
| 230 |
+
"peak_swap_mb": 0.0,
|
| 231 |
+
"round": 2
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"budget_mb": 65536,
|
| 235 |
+
"reserve_mb": 1024,
|
| 236 |
+
"rc": 0,
|
| 237 |
+
"strict_ok": true,
|
| 238 |
+
"decode_tps": 5.19,
|
| 239 |
+
"hit_pct": 77.5416,
|
| 240 |
+
"mb_per_token": 150.62332153320312,
|
| 241 |
+
"slots": 35003,
|
| 242 |
+
"arena_gib": 59.89,
|
| 243 |
+
"peak_anon_mib": 23731.8,
|
| 244 |
+
"peak_swap_mb": 0.0,
|
| 245 |
+
"round": 2
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"budget_mb": 4096,
|
| 249 |
+
"reserve_mb": 256,
|
| 250 |
+
"rc": 0,
|
| 251 |
+
"strict_ok": true,
|
| 252 |
+
"decode_tps": 5.84,
|
| 253 |
+
"hit_pct": 59.8982,
|
| 254 |
+
"mb_per_token": 275.4373550415039,
|
| 255 |
+
"slots": 1600,
|
| 256 |
+
"arena_gib": 2.74,
|
| 257 |
+
"peak_anon_mib": 3963.0,
|
| 258 |
+
"peak_swap_mb": 0.0,
|
| 259 |
+
"round": 3
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"budget_mb": 8192,
|
| 263 |
+
"reserve_mb": 512,
|
| 264 |
+
"rc": 0,
|
| 265 |
+
"strict_ok": true,
|
| 266 |
+
"decode_tps": 6.27,
|
| 267 |
+
"hit_pct": 70.7278,
|
| 268 |
+
"mb_per_token": 199.03087615966797,
|
| 269 |
+
"slots": 3715,
|
| 270 |
+
"arena_gib": 6.36,
|
| 271 |
+
"peak_anon_mib": 7668.7,
|
| 272 |
+
"peak_swap_mb": 0.0,
|
| 273 |
+
"round": 3
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"budget_mb": 16384,
|
| 277 |
+
"reserve_mb": 1024,
|
| 278 |
+
"rc": 0,
|
| 279 |
+
"strict_ok": true,
|
| 280 |
+
"decode_tps": 5.87,
|
| 281 |
+
"hit_pct": 76.4723,
|
| 282 |
+
"mb_per_token": 157.93132781982422,
|
| 283 |
+
"slots": 7943,
|
| 284 |
+
"arena_gib": 13.59,
|
| 285 |
+
"peak_anon_mib": 15075.8,
|
| 286 |
+
"peak_swap_mb": 0.0,
|
| 287 |
+
"round": 3
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"budget_mb": 65536,
|
| 291 |
+
"reserve_mb": 1024,
|
| 292 |
+
"rc": 0,
|
| 293 |
+
"strict_ok": true,
|
| 294 |
+
"decode_tps": 6.63,
|
| 295 |
+
"hit_pct": 76.5186,
|
| 296 |
+
"mb_per_token": 157.60040283203125,
|
| 297 |
+
"slots": 35003,
|
| 298 |
+
"arena_gib": 59.89,
|
| 299 |
+
"peak_anon_mib": 23968.9,
|
| 300 |
+
"peak_swap_mb": 0.0,
|
| 301 |
+
"round": 3
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"budget_mb": 4096,
|
| 305 |
+
"reserve_mb": 256,
|
| 306 |
+
"rc": 0,
|
| 307 |
+
"strict_ok": true,
|
| 308 |
+
"decode_tps": 5.39,
|
| 309 |
+
"hit_pct": 63.7432,
|
| 310 |
+
"mb_per_token": 239.48001861572266,
|
| 311 |
+
"slots": 1600,
|
| 312 |
+
"arena_gib": 2.74,
|
| 313 |
+
"peak_anon_mib": 3946.2,
|
| 314 |
+
"peak_swap_mb": 0.0,
|
| 315 |
+
"round": 4
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"budget_mb": 8192,
|
| 319 |
+
"reserve_mb": 512,
|
| 320 |
+
"rc": 0,
|
| 321 |
+
"strict_ok": true,
|
| 322 |
+
"decode_tps": 6.17,
|
| 323 |
+
"hit_pct": 75.0542,
|
| 324 |
+
"mb_per_token": 162.54615783691406,
|
| 325 |
+
"slots": 3715,
|
| 326 |
+
"arena_gib": 6.36,
|
| 327 |
+
"peak_anon_mib": 7652.2,
|
| 328 |
+
"peak_swap_mb": 0.0,
|
| 329 |
+
"round": 4
|
| 330 |
+
},
|
| 331 |
+
{
|
| 332 |
+
"budget_mb": 16384,
|
| 333 |
+
"reserve_mb": 1024,
|
| 334 |
+
"rc": 0,
|
| 335 |
+
"strict_ok": true,
|
| 336 |
+
"decode_tps": 5.5,
|
| 337 |
+
"hit_pct": 79.9662,
|
| 338 |
+
"mb_per_token": 128.8137435913086,
|
| 339 |
+
"slots": 7943,
|
| 340 |
+
"arena_gib": 13.59,
|
| 341 |
+
"peak_anon_mib": 15059.6,
|
| 342 |
+
"peak_swap_mb": 0.0,
|
| 343 |
+
"round": 4
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"budget_mb": 65536,
|
| 347 |
+
"reserve_mb": 1024,
|
| 348 |
+
"rc": 0,
|
| 349 |
+
"strict_ok": true,
|
| 350 |
+
"decode_tps": 5.89,
|
| 351 |
+
"hit_pct": 79.572,
|
| 352 |
+
"mb_per_token": 131.12545013427734,
|
| 353 |
+
"slots": 35003,
|
| 354 |
+
"arena_gib": 59.89,
|
| 355 |
+
"peak_anon_mib": 23094.1,
|
| 356 |
+
"peak_swap_mb": 0.0,
|
| 357 |
+
"round": 4
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"budget_mb": 4096,
|
| 361 |
+
"reserve_mb": 256,
|
| 362 |
+
"rc": 0,
|
| 363 |
+
"strict_ok": true,
|
| 364 |
+
"decode_tps": 4.78,
|
| 365 |
+
"hit_pct": 61.5016,
|
| 366 |
+
"mb_per_token": 264.0085220336914,
|
| 367 |
+
"slots": 1600,
|
| 368 |
+
"arena_gib": 2.74,
|
| 369 |
+
"peak_anon_mib": 3949.7,
|
| 370 |
+
"peak_swap_mb": 0.0,
|
| 371 |
+
"round": 5
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"budget_mb": 8192,
|
| 375 |
+
"reserve_mb": 512,
|
| 376 |
+
"rc": 0,
|
| 377 |
+
"strict_ok": true,
|
| 378 |
+
"decode_tps": 5.99,
|
| 379 |
+
"hit_pct": 71.9699,
|
| 380 |
+
"mb_per_token": 190.41824340820312,
|
| 381 |
+
"slots": 3715,
|
| 382 |
+
"arena_gib": 6.36,
|
| 383 |
+
"peak_anon_mib": 7655.0,
|
| 384 |
+
"peak_swap_mb": 0.0,
|
| 385 |
+
"round": 5
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"budget_mb": 16384,
|
| 389 |
+
"reserve_mb": 1024,
|
| 390 |
+
"rc": 0,
|
| 391 |
+
"strict_ok": true,
|
| 392 |
+
"decode_tps": 6.09,
|
| 393 |
+
"hit_pct": 77.5357,
|
| 394 |
+
"mb_per_token": 150.6662368774414,
|
| 395 |
+
"slots": 7943,
|
| 396 |
+
"arena_gib": 13.59,
|
| 397 |
+
"peak_anon_mib": 15062.0,
|
| 398 |
+
"peak_swap_mb": 0.0,
|
| 399 |
+
"round": 5
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"budget_mb": 65536,
|
| 403 |
+
"reserve_mb": 1024,
|
| 404 |
+
"rc": 0,
|
| 405 |
+
"strict_ok": true,
|
| 406 |
+
"decode_tps": 6.33,
|
| 407 |
+
"hit_pct": 77.5416,
|
| 408 |
+
"mb_per_token": 150.62332153320312,
|
| 409 |
+
"slots": 35003,
|
| 410 |
+
"arena_gib": 59.89,
|
| 411 |
+
"peak_anon_mib": 23731.8,
|
| 412 |
+
"peak_swap_mb": 0.0,
|
| 413 |
+
"round": 5
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"budget_mb": 4096,
|
| 417 |
+
"reserve_mb": 256,
|
| 418 |
+
"rc": 0,
|
| 419 |
+
"strict_ok": true,
|
| 420 |
+
"decode_tps": 5.26,
|
| 421 |
+
"hit_pct": 59.8982,
|
| 422 |
+
"mb_per_token": 275.4487991333008,
|
| 423 |
+
"slots": 1600,
|
| 424 |
+
"arena_gib": 2.74,
|
| 425 |
+
"peak_anon_mib": 3963.4,
|
| 426 |
+
"peak_swap_mb": 0.0,
|
| 427 |
+
"round": 6
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"budget_mb": 8192,
|
| 431 |
+
"reserve_mb": 512,
|
| 432 |
+
"rc": 0,
|
| 433 |
+
"strict_ok": true,
|
| 434 |
+
"decode_tps": 5.96,
|
| 435 |
+
"hit_pct": 70.7278,
|
| 436 |
+
"mb_per_token": 199.03087615966797,
|
| 437 |
+
"slots": 3715,
|
| 438 |
+
"arena_gib": 6.36,
|
| 439 |
+
"peak_anon_mib": 7668.3,
|
| 440 |
+
"peak_swap_mb": 0.0,
|
| 441 |
+
"round": 6
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"budget_mb": 16384,
|
| 445 |
+
"reserve_mb": 1024,
|
| 446 |
+
"rc": 0,
|
| 447 |
+
"strict_ok": true,
|
| 448 |
+
"decode_tps": 6.38,
|
| 449 |
+
"hit_pct": 76.4723,
|
| 450 |
+
"mb_per_token": 157.93132781982422,
|
| 451 |
+
"slots": 7943,
|
| 452 |
+
"arena_gib": 13.59,
|
| 453 |
+
"peak_anon_mib": 15076.2,
|
| 454 |
+
"peak_swap_mb": 0.0,
|
| 455 |
+
"round": 6
|
| 456 |
+
},
|
| 457 |
+
{
|
| 458 |
+
"budget_mb": 65536,
|
| 459 |
+
"reserve_mb": 1024,
|
| 460 |
+
"rc": 0,
|
| 461 |
+
"strict_ok": true,
|
| 462 |
+
"decode_tps": 6.29,
|
| 463 |
+
"hit_pct": 76.5186,
|
| 464 |
+
"mb_per_token": 157.60040283203125,
|
| 465 |
+
"slots": 35003,
|
| 466 |
+
"arena_gib": 59.89,
|
| 467 |
+
"peak_anon_mib": 23968.8,
|
| 468 |
+
"peak_swap_mb": 0.0,
|
| 469 |
+
"round": 6
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"budget_mb": 4096,
|
| 473 |
+
"reserve_mb": 256,
|
| 474 |
+
"rc": 0,
|
| 475 |
+
"strict_ok": true,
|
| 476 |
+
"decode_tps": 5.45,
|
| 477 |
+
"hit_pct": 63.7553,
|
| 478 |
+
"mb_per_token": 239.3970489501953,
|
| 479 |
+
"slots": 1600,
|
| 480 |
+
"arena_gib": 2.74,
|
| 481 |
+
"peak_anon_mib": 3946.2,
|
| 482 |
+
"peak_swap_mb": 0.0,
|
| 483 |
+
"round": 7
|
| 484 |
+
},
|
| 485 |
+
{
|
| 486 |
+
"budget_mb": 8192,
|
| 487 |
+
"reserve_mb": 512,
|
| 488 |
+
"rc": 0,
|
| 489 |
+
"strict_ok": true,
|
| 490 |
+
"decode_tps": 6.75,
|
| 491 |
+
"hit_pct": 75.0603,
|
| 492 |
+
"mb_per_token": 162.5041961669922,
|
| 493 |
+
"slots": 3715,
|
| 494 |
+
"arena_gib": 6.36,
|
| 495 |
+
"peak_anon_mib": 7652.6,
|
| 496 |
+
"peak_swap_mb": 0.0,
|
| 497 |
+
"round": 7
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"budget_mb": 16384,
|
| 501 |
+
"reserve_mb": 1024,
|
| 502 |
+
"rc": 0,
|
| 503 |
+
"strict_ok": true,
|
| 504 |
+
"decode_tps": 6.56,
|
| 505 |
+
"hit_pct": 79.9662,
|
| 506 |
+
"mb_per_token": 128.8137435913086,
|
| 507 |
+
"slots": 7943,
|
| 508 |
+
"arena_gib": 13.59,
|
| 509 |
+
"peak_anon_mib": 15059.9,
|
| 510 |
+
"peak_swap_mb": 0.0,
|
| 511 |
+
"round": 7
|
| 512 |
+
},
|
| 513 |
+
{
|
| 514 |
+
"budget_mb": 65536,
|
| 515 |
+
"reserve_mb": 1024,
|
| 516 |
+
"rc": 0,
|
| 517 |
+
"strict_ok": true,
|
| 518 |
+
"decode_tps": 6.37,
|
| 519 |
+
"hit_pct": 79.572,
|
| 520 |
+
"mb_per_token": 131.12545013427734,
|
| 521 |
+
"slots": 35003,
|
| 522 |
+
"arena_gib": 59.89,
|
| 523 |
+
"peak_anon_mib": 23094.1,
|
| 524 |
+
"peak_swap_mb": 0.0,
|
| 525 |
+
"round": 7
|
| 526 |
+
},
|
| 527 |
+
{
|
| 528 |
+
"budget_mb": 4096,
|
| 529 |
+
"reserve_mb": 256,
|
| 530 |
+
"rc": 0,
|
| 531 |
+
"strict_ok": true,
|
| 532 |
+
"decode_tps": 5.5,
|
| 533 |
+
"hit_pct": 61.4956,
|
| 534 |
+
"mb_per_token": 264.0504837036133,
|
| 535 |
+
"slots": 1600,
|
| 536 |
+
"arena_gib": 2.74,
|
| 537 |
+
"peak_anon_mib": 3949.4,
|
| 538 |
+
"peak_swap_mb": 0.0,
|
| 539 |
+
"round": 8
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"budget_mb": 8192,
|
| 543 |
+
"reserve_mb": 512,
|
| 544 |
+
"rc": 0,
|
| 545 |
+
"strict_ok": true,
|
| 546 |
+
"decode_tps": 6.57,
|
| 547 |
+
"hit_pct": 71.9818,
|
| 548 |
+
"mb_per_token": 190.33336639404297,
|
| 549 |
+
"slots": 3715,
|
| 550 |
+
"arena_gib": 6.36,
|
| 551 |
+
"peak_anon_mib": 7654.6,
|
| 552 |
+
"peak_swap_mb": 0.0,
|
| 553 |
+
"round": 8
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"budget_mb": 16384,
|
| 557 |
+
"reserve_mb": 1024,
|
| 558 |
+
"rc": 0,
|
| 559 |
+
"strict_ok": true,
|
| 560 |
+
"decode_tps": 6.73,
|
| 561 |
+
"hit_pct": 77.5357,
|
| 562 |
+
"mb_per_token": 150.6662368774414,
|
| 563 |
+
"slots": 7943,
|
| 564 |
+
"arena_gib": 13.59,
|
| 565 |
+
"peak_anon_mib": 15062.1,
|
| 566 |
+
"peak_swap_mb": 0.0,
|
| 567 |
+
"round": 8
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"budget_mb": 65536,
|
| 571 |
+
"reserve_mb": 1024,
|
| 572 |
+
"rc": 0,
|
| 573 |
+
"strict_ok": true,
|
| 574 |
+
"decode_tps": 6.81,
|
| 575 |
+
"hit_pct": 77.5416,
|
| 576 |
+
"mb_per_token": 150.62332153320312,
|
| 577 |
+
"slots": 35003,
|
| 578 |
+
"arena_gib": 59.89,
|
| 579 |
+
"peak_anon_mib": 23731.4,
|
| 580 |
+
"peak_swap_mb": 0.0,
|
| 581 |
+
"round": 8
|
| 582 |
+
}
|
| 583 |
+
]
|
| 584 |
+
}
|
bench/results/strict_budgets.json
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"budget_mb": 2048,
|
| 4 |
+
"reserve_mb": 512,
|
| 5 |
+
"rc": 0,
|
| 6 |
+
"strict_ok": true,
|
| 7 |
+
"decode_tps": 4.3,
|
| 8 |
+
"hit_pct": 20.3054,
|
| 9 |
+
"mb_per_token": 542.1533584594727,
|
| 10 |
+
"slots": 332,
|
| 11 |
+
"arena_gib": 0.57,
|
| 12 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 249 + \u5927 1.95 MiB \u00d7 83\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 13 |
+
"peak_anon_mb": 1855.1,
|
| 14 |
+
"peak_swap_mb": 0.0,
|
| 15 |
+
"round": 0
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"budget_mb": 3072,
|
| 19 |
+
"reserve_mb": 512,
|
| 20 |
+
"rc": 0,
|
| 21 |
+
"strict_ok": true,
|
| 22 |
+
"decode_tps": 5.06,
|
| 23 |
+
"hit_pct": 48.2818,
|
| 24 |
+
"mb_per_token": 353.35350036621094,
|
| 25 |
+
"slots": 895,
|
| 26 |
+
"arena_gib": 1.53,
|
| 27 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 672 + \u5927 1.95 MiB \u00d7 223\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 28 |
+
"peak_anon_mb": 2841.2,
|
| 29 |
+
"peak_swap_mb": 0.0,
|
| 30 |
+
"round": 0
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"budget_mb": 4096,
|
| 34 |
+
"reserve_mb": 512,
|
| 35 |
+
"rc": 0,
|
| 36 |
+
"strict_ok": true,
|
| 37 |
+
"decode_tps": 5.31,
|
| 38 |
+
"hit_pct": 57.5263,
|
| 39 |
+
"mb_per_token": 291.71085357666016,
|
| 40 |
+
"slots": 1459,
|
| 41 |
+
"arena_gib": 2.5,
|
| 42 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1095 + \u5927 1.95 MiB \u00d7 364\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 43 |
+
"peak_anon_mb": 3829.7,
|
| 44 |
+
"peak_swap_mb": 0.0,
|
| 45 |
+
"round": 0
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"budget_mb": 5120,
|
| 49 |
+
"reserve_mb": 512,
|
| 50 |
+
"rc": 0,
|
| 51 |
+
"strict_ok": true,
|
| 52 |
+
"decode_tps": 4.45,
|
| 53 |
+
"hit_pct": 60.0741,
|
| 54 |
+
"mb_per_token": 274.1832733154297,
|
| 55 |
+
"slots": 2023,
|
| 56 |
+
"arena_gib": 3.46,
|
| 57 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1518 + \u5927 1.95 MiB \u00d7 505\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 58 |
+
"peak_anon_mb": 4811.1,
|
| 59 |
+
"peak_swap_mb": 0.0,
|
| 60 |
+
"round": 0
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"budget_mb": 6144,
|
| 64 |
+
"reserve_mb": 512,
|
| 65 |
+
"rc": 0,
|
| 66 |
+
"strict_ok": true,
|
| 67 |
+
"decode_tps": 5.38,
|
| 68 |
+
"hit_pct": 63.7169,
|
| 69 |
+
"mb_per_token": 248.86226654052734,
|
| 70 |
+
"slots": 2587,
|
| 71 |
+
"arena_gib": 4.43,
|
| 72 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1941 + \u5927 1.95 MiB \u00d7 646\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 73 |
+
"peak_anon_mb": 5799.2,
|
| 74 |
+
"peak_swap_mb": 0.0,
|
| 75 |
+
"round": 0
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"budget_mb": 7168,
|
| 79 |
+
"reserve_mb": 512,
|
| 80 |
+
"rc": 0,
|
| 81 |
+
"strict_ok": true,
|
| 82 |
+
"decode_tps": 4.87,
|
| 83 |
+
"hit_pct": 65.686,
|
| 84 |
+
"mb_per_token": 234.78984832763672,
|
| 85 |
+
"slots": 3150,
|
| 86 |
+
"arena_gib": 5.39,
|
| 87 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2363 + \u5927 1.95 MiB \u00d7 787\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 88 |
+
"peak_anon_mb": 6786.0,
|
| 89 |
+
"peak_swap_mb": 0.0,
|
| 90 |
+
"round": 0
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"budget_mb": 8192,
|
| 94 |
+
"reserve_mb": 512,
|
| 95 |
+
"rc": 0,
|
| 96 |
+
"strict_ok": true,
|
| 97 |
+
"decode_tps": 5.45,
|
| 98 |
+
"hit_pct": 70.7393,
|
| 99 |
+
"mb_per_token": 198.94886016845703,
|
| 100 |
+
"slots": 3714,
|
| 101 |
+
"arena_gib": 6.35,
|
| 102 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2786 + \u5927 1.95 MiB \u00d7 928\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 103 |
+
"peak_anon_mb": 7780.2,
|
| 104 |
+
"peak_swap_mb": 0.0,
|
| 105 |
+
"round": 0
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"budget_mb": 2048,
|
| 109 |
+
"reserve_mb": 512,
|
| 110 |
+
"rc": 0,
|
| 111 |
+
"strict_ok": true,
|
| 112 |
+
"decode_tps": 4.68,
|
| 113 |
+
"hit_pct": 23.7283,
|
| 114 |
+
"mb_per_token": 499.88555908203125,
|
| 115 |
+
"slots": 332,
|
| 116 |
+
"arena_gib": 0.57,
|
| 117 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 249 + \u5927 1.95 MiB \u00d7 83\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 118 |
+
"peak_anon_mb": 1839.9,
|
| 119 |
+
"peak_swap_mb": 0.0,
|
| 120 |
+
"round": 1
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"budget_mb": 3072,
|
| 124 |
+
"reserve_mb": 512,
|
| 125 |
+
"rc": 0,
|
| 126 |
+
"strict_ok": true,
|
| 127 |
+
"decode_tps": 5.44,
|
| 128 |
+
"hit_pct": 54.8276,
|
| 129 |
+
"mb_per_token": 296.50211334228516,
|
| 130 |
+
"slots": 895,
|
| 131 |
+
"arena_gib": 1.53,
|
| 132 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 672 + \u5927 1.95 MiB \u00d7 223\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 133 |
+
"peak_anon_mb": 2826.4,
|
| 134 |
+
"peak_swap_mb": 0.0,
|
| 135 |
+
"round": 1
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"budget_mb": 4096,
|
| 139 |
+
"reserve_mb": 512,
|
| 140 |
+
"rc": 0,
|
| 141 |
+
"strict_ok": true,
|
| 142 |
+
"decode_tps": 5.48,
|
| 143 |
+
"hit_pct": 64.953,
|
| 144 |
+
"mb_per_token": 229.83646392822266,
|
| 145 |
+
"slots": 1459,
|
| 146 |
+
"arena_gib": 2.5,
|
| 147 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1095 + \u5927 1.95 MiB \u00d7 364\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 148 |
+
"peak_anon_mb": 3814.6,
|
| 149 |
+
"peak_swap_mb": 0.0,
|
| 150 |
+
"round": 1
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"budget_mb": 5120,
|
| 154 |
+
"reserve_mb": 512,
|
| 155 |
+
"rc": 0,
|
| 156 |
+
"strict_ok": true,
|
| 157 |
+
"decode_tps": 5.65,
|
| 158 |
+
"hit_pct": 70.9499,
|
| 159 |
+
"mb_per_token": 190.71578979492188,
|
| 160 |
+
"slots": 2023,
|
| 161 |
+
"arena_gib": 3.46,
|
| 162 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1518 + \u5927 1.95 MiB \u00d7 505\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 163 |
+
"peak_anon_mb": 4802.7,
|
| 164 |
+
"peak_swap_mb": 0.0,
|
| 165 |
+
"round": 1
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"budget_mb": 6144,
|
| 169 |
+
"reserve_mb": 512,
|
| 170 |
+
"rc": 0,
|
| 171 |
+
"strict_ok": true,
|
| 172 |
+
"decode_tps": 5.74,
|
| 173 |
+
"hit_pct": 72.758,
|
| 174 |
+
"mb_per_token": 178.3151626586914,
|
| 175 |
+
"slots": 2587,
|
| 176 |
+
"arena_gib": 4.43,
|
| 177 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1941 + \u5927 1.95 MiB \u00d7 646\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 178 |
+
"peak_anon_mb": 5790.4,
|
| 179 |
+
"peak_swap_mb": 0.0,
|
| 180 |
+
"round": 1
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"budget_mb": 7168,
|
| 184 |
+
"reserve_mb": 512,
|
| 185 |
+
"rc": 0,
|
| 186 |
+
"strict_ok": true,
|
| 187 |
+
"decode_tps": 5.1,
|
| 188 |
+
"hit_pct": 74.2587,
|
| 189 |
+
"mb_per_token": 168.00975799560547,
|
| 190 |
+
"slots": 3150,
|
| 191 |
+
"arena_gib": 5.39,
|
| 192 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2363 + \u5927 1.95 MiB \u00d7 787\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 193 |
+
"peak_anon_mb": 6777.3,
|
| 194 |
+
"peak_swap_mb": 0.0,
|
| 195 |
+
"round": 1
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"budget_mb": 8192,
|
| 199 |
+
"reserve_mb": 512,
|
| 200 |
+
"rc": 0,
|
| 201 |
+
"strict_ok": true,
|
| 202 |
+
"decode_tps": 5.87,
|
| 203 |
+
"hit_pct": 76.2146,
|
| 204 |
+
"mb_per_token": 154.17957305908203,
|
| 205 |
+
"slots": 3714,
|
| 206 |
+
"arena_gib": 6.35,
|
| 207 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2786 + \u5927 1.95 MiB \u00d7 928\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 208 |
+
"peak_anon_mb": 7764.4,
|
| 209 |
+
"peak_swap_mb": 0.0,
|
| 210 |
+
"round": 1
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"budget_mb": 2048,
|
| 214 |
+
"reserve_mb": 512,
|
| 215 |
+
"rc": 0,
|
| 216 |
+
"strict_ok": true,
|
| 217 |
+
"decode_tps": 4.44,
|
| 218 |
+
"hit_pct": 22.7189,
|
| 219 |
+
"mb_per_token": 526.1802673339844,
|
| 220 |
+
"slots": 332,
|
| 221 |
+
"arena_gib": 0.57,
|
| 222 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 249 + \u5927 1.95 MiB \u00d7 83\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 223 |
+
"peak_anon_mb": 1842.3,
|
| 224 |
+
"peak_swap_mb": 0.0,
|
| 225 |
+
"round": 2
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"budget_mb": 3072,
|
| 229 |
+
"reserve_mb": 512,
|
| 230 |
+
"rc": 0,
|
| 231 |
+
"strict_ok": true,
|
| 232 |
+
"decode_tps": 5.28,
|
| 233 |
+
"hit_pct": 50.1924,
|
| 234 |
+
"mb_per_token": 339.9505615234375,
|
| 235 |
+
"slots": 895,
|
| 236 |
+
"arena_gib": 1.53,
|
| 237 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 672 + \u5927 1.95 MiB \u00d7 223\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 238 |
+
"peak_anon_mb": 2828.5,
|
| 239 |
+
"peak_swap_mb": 0.0,
|
| 240 |
+
"round": 2
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"budget_mb": 4096,
|
| 244 |
+
"reserve_mb": 512,
|
| 245 |
+
"rc": 0,
|
| 246 |
+
"strict_ok": true,
|
| 247 |
+
"decode_tps": 5.83,
|
| 248 |
+
"hit_pct": 59.8082,
|
| 249 |
+
"mb_per_token": 275.8455276489258,
|
| 250 |
+
"slots": 1459,
|
| 251 |
+
"arena_gib": 2.5,
|
| 252 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1095 + \u5927 1.95 MiB \u00d7 364\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 253 |
+
"peak_anon_mb": 3817.0,
|
| 254 |
+
"peak_swap_mb": 0.0,
|
| 255 |
+
"round": 2
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"budget_mb": 5120,
|
| 259 |
+
"reserve_mb": 512,
|
| 260 |
+
"rc": 0,
|
| 261 |
+
"strict_ok": true,
|
| 262 |
+
"decode_tps": 5.83,
|
| 263 |
+
"hit_pct": 64.8765,
|
| 264 |
+
"mb_per_token": 240.75603485107422,
|
| 265 |
+
"slots": 2023,
|
| 266 |
+
"arena_gib": 3.46,
|
| 267 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1518 + \u5927 1.95 MiB \u00d7 505\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 268 |
+
"peak_anon_mb": 4804.7,
|
| 269 |
+
"peak_swap_mb": 0.0,
|
| 270 |
+
"round": 2
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"budget_mb": 6144,
|
| 274 |
+
"reserve_mb": 512,
|
| 275 |
+
"rc": 0,
|
| 276 |
+
"strict_ok": true,
|
| 277 |
+
"decode_tps": 5.73,
|
| 278 |
+
"hit_pct": 67.5173,
|
| 279 |
+
"mb_per_token": 222.21946716308594,
|
| 280 |
+
"slots": 2587,
|
| 281 |
+
"arena_gib": 4.43,
|
| 282 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1941 + \u5927 1.95 MiB \u00d7 646\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 283 |
+
"peak_anon_mb": 5793.3,
|
| 284 |
+
"peak_swap_mb": 0.0,
|
| 285 |
+
"round": 2
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"budget_mb": 7168,
|
| 289 |
+
"reserve_mb": 512,
|
| 290 |
+
"rc": 0,
|
| 291 |
+
"strict_ok": true,
|
| 292 |
+
"decode_tps": 5.91,
|
| 293 |
+
"hit_pct": 68.5975,
|
| 294 |
+
"mb_per_token": 214.0064239501953,
|
| 295 |
+
"slots": 3150,
|
| 296 |
+
"arena_gib": 5.39,
|
| 297 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2363 + \u5927 1.95 MiB \u00d7 787\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 298 |
+
"peak_anon_mb": 6778.7,
|
| 299 |
+
"peak_swap_mb": 0.0,
|
| 300 |
+
"round": 2
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"budget_mb": 8192,
|
| 304 |
+
"reserve_mb": 512,
|
| 305 |
+
"rc": 0,
|
| 306 |
+
"strict_ok": true,
|
| 307 |
+
"decode_tps": 5.58,
|
| 308 |
+
"hit_pct": 70.0723,
|
| 309 |
+
"mb_per_token": 203.4769058227539,
|
| 310 |
+
"slots": 3714,
|
| 311 |
+
"arena_gib": 6.35,
|
| 312 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2786 + \u5927 1.95 MiB \u00d7 928\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 313 |
+
"peak_anon_mb": 7766.4,
|
| 314 |
+
"peak_swap_mb": 0.0,
|
| 315 |
+
"round": 2
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"budget_mb": 2048,
|
| 319 |
+
"reserve_mb": 512,
|
| 320 |
+
"rc": 0,
|
| 321 |
+
"strict_ok": true,
|
| 322 |
+
"decode_tps": 4.32,
|
| 323 |
+
"hit_pct": 20.2997,
|
| 324 |
+
"mb_per_token": 542.1895980834961,
|
| 325 |
+
"slots": 332,
|
| 326 |
+
"arena_gib": 0.57,
|
| 327 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 249 + \u5927 1.95 MiB \u00d7 83\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 328 |
+
"peak_anon_mb": 1855.2,
|
| 329 |
+
"peak_swap_mb": 0.0,
|
| 330 |
+
"round": 3
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"budget_mb": 3072,
|
| 334 |
+
"reserve_mb": 512,
|
| 335 |
+
"rc": 0,
|
| 336 |
+
"strict_ok": true,
|
| 337 |
+
"decode_tps": 5.04,
|
| 338 |
+
"hit_pct": 48.2876,
|
| 339 |
+
"mb_per_token": 353.32298278808594,
|
| 340 |
+
"slots": 895,
|
| 341 |
+
"arena_gib": 1.53,
|
| 342 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 672 + \u5927 1.95 MiB \u00d7 223\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 343 |
+
"peak_anon_mb": 2841.5,
|
| 344 |
+
"peak_swap_mb": 0.0,
|
| 345 |
+
"round": 3
|
| 346 |
+
},
|
| 347 |
+
{
|
| 348 |
+
"budget_mb": 4096,
|
| 349 |
+
"reserve_mb": 512,
|
| 350 |
+
"rc": 0,
|
| 351 |
+
"strict_ok": true,
|
| 352 |
+
"decode_tps": 5.92,
|
| 353 |
+
"hit_pct": 57.509,
|
| 354 |
+
"mb_per_token": 291.83483123779297,
|
| 355 |
+
"slots": 1459,
|
| 356 |
+
"arena_gib": 2.5,
|
| 357 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1095 + \u5927 1.95 MiB \u00d7 364\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 358 |
+
"peak_anon_mb": 3829.6,
|
| 359 |
+
"peak_swap_mb": 0.0,
|
| 360 |
+
"round": 3
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"budget_mb": 5120,
|
| 364 |
+
"reserve_mb": 512,
|
| 365 |
+
"rc": 0,
|
| 366 |
+
"strict_ok": true,
|
| 367 |
+
"decode_tps": 5.82,
|
| 368 |
+
"hit_pct": 60.0452,
|
| 369 |
+
"mb_per_token": 274.39022064208984,
|
| 370 |
+
"slots": 2023,
|
| 371 |
+
"arena_gib": 3.46,
|
| 372 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1518 + \u5927 1.95 MiB \u00d7 505\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 373 |
+
"peak_anon_mb": 4811.5,
|
| 374 |
+
"peak_swap_mb": 0.0,
|
| 375 |
+
"round": 3
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
"budget_mb": 6144,
|
| 379 |
+
"reserve_mb": 512,
|
| 380 |
+
"rc": 0,
|
| 381 |
+
"strict_ok": true,
|
| 382 |
+
"decode_tps": 4.92,
|
| 383 |
+
"hit_pct": 63.7053,
|
| 384 |
+
"mb_per_token": 248.94428253173828,
|
| 385 |
+
"slots": 2587,
|
| 386 |
+
"arena_gib": 4.43,
|
| 387 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1941 + \u5927 1.95 MiB \u00d7 646\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 388 |
+
"peak_anon_mb": 5799.2,
|
| 389 |
+
"peak_swap_mb": 0.0,
|
| 390 |
+
"round": 3
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"budget_mb": 7168,
|
| 394 |
+
"reserve_mb": 512,
|
| 395 |
+
"rc": 0,
|
| 396 |
+
"strict_ok": true,
|
| 397 |
+
"decode_tps": 4.75,
|
| 398 |
+
"hit_pct": 65.686,
|
| 399 |
+
"mb_per_token": 234.78984832763672,
|
| 400 |
+
"slots": 3150,
|
| 401 |
+
"arena_gib": 5.39,
|
| 402 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2363 + \u5927 1.95 MiB \u00d7 787\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 403 |
+
"peak_anon_mb": 6786.0,
|
| 404 |
+
"peak_swap_mb": 0.0,
|
| 405 |
+
"round": 3
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"budget_mb": 8192,
|
| 409 |
+
"reserve_mb": 512,
|
| 410 |
+
"rc": 0,
|
| 411 |
+
"strict_ok": true,
|
| 412 |
+
"decode_tps": 5.39,
|
| 413 |
+
"hit_pct": 70.7393,
|
| 414 |
+
"mb_per_token": 198.94886016845703,
|
| 415 |
+
"slots": 3714,
|
| 416 |
+
"arena_gib": 6.35,
|
| 417 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2786 + \u5927 1.95 MiB \u00d7 928\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 418 |
+
"peak_anon_mb": 7780.6,
|
| 419 |
+
"peak_swap_mb": 0.0,
|
| 420 |
+
"round": 3
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"budget_mb": 2048,
|
| 424 |
+
"reserve_mb": 512,
|
| 425 |
+
"rc": 0,
|
| 426 |
+
"strict_ok": true,
|
| 427 |
+
"decode_tps": 4.0,
|
| 428 |
+
"hit_pct": 23.6861,
|
| 429 |
+
"mb_per_token": 500.1373291015625,
|
| 430 |
+
"slots": 332,
|
| 431 |
+
"arena_gib": 0.57,
|
| 432 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 249 + \u5927 1.95 MiB \u00d7 83\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 433 |
+
"peak_anon_mb": 1839.9,
|
| 434 |
+
"peak_swap_mb": 0.0,
|
| 435 |
+
"round": 4
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"budget_mb": 3072,
|
| 439 |
+
"reserve_mb": 512,
|
| 440 |
+
"rc": 0,
|
| 441 |
+
"strict_ok": true,
|
| 442 |
+
"decode_tps": 5.16,
|
| 443 |
+
"hit_pct": 54.8156,
|
| 444 |
+
"mb_per_token": 296.5574264526367,
|
| 445 |
+
"slots": 895,
|
| 446 |
+
"arena_gib": 1.53,
|
| 447 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 672 + \u5927 1.95 MiB \u00d7 223\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 448 |
+
"peak_anon_mb": 2826.1,
|
| 449 |
+
"peak_swap_mb": 0.0,
|
| 450 |
+
"round": 4
|
| 451 |
+
},
|
| 452 |
+
{
|
| 453 |
+
"budget_mb": 4096,
|
| 454 |
+
"reserve_mb": 512,
|
| 455 |
+
"rc": 0,
|
| 456 |
+
"strict_ok": true,
|
| 457 |
+
"decode_tps": 5.35,
|
| 458 |
+
"hit_pct": 64.9409,
|
| 459 |
+
"mb_per_token": 229.9184799194336,
|
| 460 |
+
"slots": 1459,
|
| 461 |
+
"arena_gib": 2.5,
|
| 462 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1095 + \u5927 1.95 MiB \u00d7 364\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 463 |
+
"peak_anon_mb": 3814.2,
|
| 464 |
+
"peak_swap_mb": 0.0,
|
| 465 |
+
"round": 4
|
| 466 |
+
},
|
| 467 |
+
{
|
| 468 |
+
"budget_mb": 5120,
|
| 469 |
+
"reserve_mb": 512,
|
| 470 |
+
"rc": 0,
|
| 471 |
+
"strict_ok": true,
|
| 472 |
+
"decode_tps": 5.89,
|
| 473 |
+
"hit_pct": 70.9438,
|
| 474 |
+
"mb_per_token": 190.75775146484375,
|
| 475 |
+
"slots": 2023,
|
| 476 |
+
"arena_gib": 3.46,
|
| 477 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1518 + \u5927 1.95 MiB \u00d7 505\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 478 |
+
"peak_anon_mb": 4802.4,
|
| 479 |
+
"peak_swap_mb": 0.0,
|
| 480 |
+
"round": 4
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"budget_mb": 6144,
|
| 484 |
+
"reserve_mb": 512,
|
| 485 |
+
"rc": 0,
|
| 486 |
+
"strict_ok": true,
|
| 487 |
+
"decode_tps": 5.37,
|
| 488 |
+
"hit_pct": 72.7459,
|
| 489 |
+
"mb_per_token": 178.39813232421875,
|
| 490 |
+
"slots": 2587,
|
| 491 |
+
"arena_gib": 4.43,
|
| 492 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 1941 + \u5927 1.95 MiB \u00d7 646\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 493 |
+
"peak_anon_mb": 5790.5,
|
| 494 |
+
"peak_swap_mb": 0.0,
|
| 495 |
+
"round": 4
|
| 496 |
+
},
|
| 497 |
+
{
|
| 498 |
+
"budget_mb": 7168,
|
| 499 |
+
"reserve_mb": 512,
|
| 500 |
+
"rc": 0,
|
| 501 |
+
"strict_ok": true,
|
| 502 |
+
"decode_tps": 5.67,
|
| 503 |
+
"hit_pct": 74.2466,
|
| 504 |
+
"mb_per_token": 168.0927276611328,
|
| 505 |
+
"slots": 3150,
|
| 506 |
+
"arena_gib": 5.39,
|
| 507 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2363 + \u5927 1.95 MiB \u00d7 787\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 508 |
+
"peak_anon_mb": 6777.3,
|
| 509 |
+
"peak_swap_mb": 0.0,
|
| 510 |
+
"round": 4
|
| 511 |
+
},
|
| 512 |
+
{
|
| 513 |
+
"budget_mb": 8192,
|
| 514 |
+
"reserve_mb": 512,
|
| 515 |
+
"rc": 0,
|
| 516 |
+
"strict_ok": true,
|
| 517 |
+
"decode_tps": 5.42,
|
| 518 |
+
"hit_pct": 76.2146,
|
| 519 |
+
"mb_per_token": 154.17957305908203,
|
| 520 |
+
"slots": 3714,
|
| 521 |
+
"arena_gib": 6.35,
|
| 522 |
+
"slot_split": "\u5c0f 1.69 MiB \u00d7 2786 + \u5927 1.95 MiB \u00d7 928\uff08\u5e73\u5747 1.75 MiB\uff09",
|
| 523 |
+
"peak_anon_mb": 7764.4,
|
| 524 |
+
"peak_swap_mb": 0.0,
|
| 525 |
+
"round": 4
|
| 526 |
+
}
|
| 527 |
+
]
|
bench/results/strict_headline.json
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"config": {
|
| 3 |
+
"model": "/root/models/minicpm-v4.7-35b-a3b-q4_k_m.gguf",
|
| 4 |
+
"bins": "['base=/root/app/llama.cpp/build/bin/epq-chat']",
|
| 5 |
+
"bin": "",
|
| 6 |
+
"rounds": "9",
|
| 7 |
+
"strict_mb": "8192",
|
| 8 |
+
"strict_data_mb": "0",
|
| 9 |
+
"metric": "anon",
|
| 10 |
+
"cli": "chat",
|
| 11 |
+
"ram_budget_mb": "0",
|
| 12 |
+
"ctx": "4096",
|
| 13 |
+
"threads": "8",
|
| 14 |
+
"ubatch": "128",
|
| 15 |
+
"n_predict": "48",
|
| 16 |
+
"no_prefetch": "False",
|
| 17 |
+
"no_lock": "False",
|
| 18 |
+
"extra": "[]",
|
| 19 |
+
"label": "headline",
|
| 20 |
+
"workdir": "/tmp/bench",
|
| 21 |
+
"keep_text": "False"
|
| 22 |
+
},
|
| 23 |
+
"results": [
|
| 24 |
+
{
|
| 25 |
+
"arm": "base",
|
| 26 |
+
"round": 0,
|
| 27 |
+
"wall_s": 12.26,
|
| 28 |
+
"returncode": 0,
|
| 29 |
+
"strict_ok": true,
|
| 30 |
+
"peak_anon_mb": 7781.2,
|
| 31 |
+
"peak_swap_mb": 0.0,
|
| 32 |
+
"vm_lck_mb": 7642.9,
|
| 33 |
+
"swap_samples_over_zero": 0,
|
| 34 |
+
"peak_file_mb": 1321.9,
|
| 35 |
+
"decode_tokens": 47,
|
| 36 |
+
"decode_s": 7.09,
|
| 37 |
+
"decode_tok_s": 6.63,
|
| 38 |
+
"slots": 3714,
|
| 39 |
+
"arena_gib": 6.35,
|
| 40 |
+
"hit_rate_pct": 68.6775,
|
| 41 |
+
"ssd_mb_per_token": 220.29495239257812,
|
| 42 |
+
"prefetch_used_pct": 0,
|
| 43 |
+
"stdout_tail": "ixture-of-Experts (MoE) models route tokens by using a lightweight gating network to dynamically select a small subset of specialized \"expert\"在 $100 \\mathrm{~m"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"arm": "base",
|
| 47 |
+
"round": 1,
|
| 48 |
+
"wall_s": 13.43,
|
| 49 |
+
"returncode": 0,
|
| 50 |
+
"strict_ok": true,
|
| 51 |
+
"peak_anon_mb": 7799.5,
|
| 52 |
+
"peak_swap_mb": 0.0,
|
| 53 |
+
"vm_lck_mb": 7650.5,
|
| 54 |
+
"swap_samples_over_zero": 0,
|
| 55 |
+
"peak_file_mb": 1321.9,
|
| 56 |
+
"decode_tokens": 47,
|
| 57 |
+
"decode_s": 7.57,
|
| 58 |
+
"decode_tok_s": 6.21,
|
| 59 |
+
"slots": 3714,
|
| 60 |
+
"arena_gib": 6.35,
|
| 61 |
+
"hit_rate_pct": 72.1095,
|
| 62 |
+
"ssd_mb_per_token": 192.59357452392578,
|
| 63 |
+
"prefetch_used_pct": 0,
|
| 64 |
+
"stdout_tail": ".gguf\n[sdq] ctx=4096 threads=8 ubatch=128 RAM(RSS)=8568 MB 模型載入 0.7s\nassistant: 1. **CPU** 是電腦的「大腦」,負責執行所有運算與指令,但本身不儲存大量資料。\n2. **RAM** 是「短期工作記憶」,存放 CPU 正在處理"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"arm": "base",
|
| 68 |
+
"round": 2,
|
| 69 |
+
"wall_s": 12.62,
|
| 70 |
+
"returncode": 0,
|
| 71 |
+
"strict_ok": true,
|
| 72 |
+
"peak_anon_mb": 7799.5,
|
| 73 |
+
"peak_swap_mb": 0.0,
|
| 74 |
+
"vm_lck_mb": 7650.5,
|
| 75 |
+
"swap_samples_over_zero": 0,
|
| 76 |
+
"peak_file_mb": 1322.1,
|
| 77 |
+
"decode_tokens": 47,
|
| 78 |
+
"decode_s": 7.42,
|
| 79 |
+
"decode_tok_s": 6.34,
|
| 80 |
+
"slots": 3714,
|
| 81 |
+
"arena_gib": 6.35,
|
| 82 |
+
"hit_rate_pct": 68.6112,
|
| 83 |
+
"ssd_mb_per_token": 214.11705017089844,
|
| 84 |
+
"prefetch_used_pct": 0,
|
| 85 |
+
"stdout_tail": "*Analyze the Request:**\n * Task: List five daily work tasks that involve using a computer.\n * Constraint: Each task must be explained in one sentence."
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"arm": "base",
|
| 89 |
+
"round": 3,
|
| 90 |
+
"wall_s": 15.36,
|
| 91 |
+
"returncode": 0,
|
| 92 |
+
"strict_ok": true,
|
| 93 |
+
"peak_anon_mb": 7781.6,
|
| 94 |
+
"peak_swap_mb": 0.0,
|
| 95 |
+
"vm_lck_mb": 7642.9,
|
| 96 |
+
"swap_samples_over_zero": 0,
|
| 97 |
+
"peak_file_mb": 1321.8,
|
| 98 |
+
"decode_tokens": 47,
|
| 99 |
+
"decode_s": 9.05,
|
| 100 |
+
"decode_tok_s": 5.19,
|
| 101 |
+
"slots": 3714,
|
| 102 |
+
"arena_gib": 6.35,
|
| 103 |
+
"hit_rate_pct": 68.6775,
|
| 104 |
+
"ssd_mb_per_token": 220.29495239257812,
|
| 105 |
+
"prefetch_used_pct": 0,
|
| 106 |
+
"stdout_tail": "ixture-of-Experts (MoE) models route tokens by using a lightweight gating network to dynamically select a small subset of specialized \"expert\"在 $100 \\mathrm{~m"
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"arm": "base",
|
| 110 |
+
"round": 4,
|
| 111 |
+
"wall_s": 13.22,
|
| 112 |
+
"returncode": 0,
|
| 113 |
+
"strict_ok": true,
|
| 114 |
+
"peak_anon_mb": 7799.1,
|
| 115 |
+
"peak_swap_mb": 0.0,
|
| 116 |
+
"vm_lck_mb": 7650.5,
|
| 117 |
+
"swap_samples_over_zero": 0,
|
| 118 |
+
"peak_file_mb": 1322.1,
|
| 119 |
+
"decode_tokens": 47,
|
| 120 |
+
"decode_s": 8.13,
|
| 121 |
+
"decode_tok_s": 5.78,
|
| 122 |
+
"slots": 3714,
|
| 123 |
+
"arena_gib": 6.35,
|
| 124 |
+
"hit_rate_pct": 72.1152,
|
| 125 |
+
"ssd_mb_per_token": 192.5516128540039,
|
| 126 |
+
"prefetch_used_pct": 0,
|
| 127 |
+
"stdout_tail": ".gguf\n[sdq] ctx=4096 threads=8 ubatch=128 RAM(RSS)=8568 MB 模型載入 0.7s\nassistant: 1. **CPU** 是電腦的「大腦」,負責執行所有運算與指令,但本身不儲存大量資料。\n2. **RAM** 是「短期工作記憶」,存放 CPU 正在處理"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"arm": "base",
|
| 131 |
+
"round": 5,
|
| 132 |
+
"wall_s": 14.53,
|
| 133 |
+
"returncode": 0,
|
| 134 |
+
"strict_ok": true,
|
| 135 |
+
"peak_anon_mb": 7799.2,
|
| 136 |
+
"peak_swap_mb": 0.0,
|
| 137 |
+
"vm_lck_mb": 7650.5,
|
| 138 |
+
"swap_samples_over_zero": 0,
|
| 139 |
+
"peak_file_mb": 1322.1,
|
| 140 |
+
"decode_tokens": 47,
|
| 141 |
+
"decode_s": 8.67,
|
| 142 |
+
"decode_tok_s": 5.42,
|
| 143 |
+
"slots": 3714,
|
| 144 |
+
"arena_gib": 6.35,
|
| 145 |
+
"hit_rate_pct": 68.617,
|
| 146 |
+
"ssd_mb_per_token": 214.07604217529297,
|
| 147 |
+
"prefetch_used_pct": 0,
|
| 148 |
+
"stdout_tail": "*Analyze the Request:**\n * Task: List five daily work tasks that involve using a computer.\n * Constraint: Each task must be explained in one sentence."
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"arm": "base",
|
| 152 |
+
"round": 6,
|
| 153 |
+
"wall_s": 15.05,
|
| 154 |
+
"returncode": 0,
|
| 155 |
+
"strict_ok": true,
|
| 156 |
+
"peak_anon_mb": 7781.2,
|
| 157 |
+
"peak_swap_mb": 0.0,
|
| 158 |
+
"vm_lck_mb": 7642.9,
|
| 159 |
+
"swap_samples_over_zero": 0,
|
| 160 |
+
"peak_file_mb": 1321.6,
|
| 161 |
+
"decode_tokens": 47,
|
| 162 |
+
"decode_s": 8.9,
|
| 163 |
+
"decode_tok_s": 5.28,
|
| 164 |
+
"slots": 3714,
|
| 165 |
+
"arena_gib": 6.35,
|
| 166 |
+
"hit_rate_pct": 68.6717,
|
| 167 |
+
"ssd_mb_per_token": 220.3378677368164,
|
| 168 |
+
"prefetch_used_pct": 0,
|
| 169 |
+
"stdout_tail": "ixture-of-Experts (MoE) models route tokens by using a lightweight gating network to dynamically select a small subset of specialized \"expert\"在 $100 \\mathrm{~m"
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"arm": "base",
|
| 173 |
+
"round": 7,
|
| 174 |
+
"wall_s": 13.28,
|
| 175 |
+
"returncode": 0,
|
| 176 |
+
"strict_ok": true,
|
| 177 |
+
"peak_anon_mb": 7799.3,
|
| 178 |
+
"peak_swap_mb": 0.0,
|
| 179 |
+
"vm_lck_mb": 7650.5,
|
| 180 |
+
"swap_samples_over_zero": 0,
|
| 181 |
+
"peak_file_mb": 1322.1,
|
| 182 |
+
"decode_tokens": 47,
|
| 183 |
+
"decode_s": 7.51,
|
| 184 |
+
"decode_tok_s": 6.26,
|
| 185 |
+
"slots": 3714,
|
| 186 |
+
"arena_gib": 6.35,
|
| 187 |
+
"hit_rate_pct": 72.1209,
|
| 188 |
+
"ssd_mb_per_token": 192.51060485839844,
|
| 189 |
+
"prefetch_used_pct": 0,
|
| 190 |
+
"stdout_tail": ".gguf\n[sdq] ctx=4096 threads=8 ubatch=128 RAM(RSS)=8568 MB 模型載入 0.8s\nassistant: 1. **CPU** 是電腦的「大腦」,負責執行所有運算與指令,但本身不儲存大量資料。\n2. **RAM** 是「短期工作記憶」,存放 CPU 正在處理"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"arm": "base",
|
| 194 |
+
"round": 8,
|
| 195 |
+
"wall_s": 14.18,
|
| 196 |
+
"returncode": 0,
|
| 197 |
+
"strict_ok": true,
|
| 198 |
+
"peak_anon_mb": 7799.1,
|
| 199 |
+
"peak_swap_mb": 0.0,
|
| 200 |
+
"vm_lck_mb": 7650.5,
|
| 201 |
+
"swap_samples_over_zero": 0,
|
| 202 |
+
"peak_file_mb": 1322.4,
|
| 203 |
+
"decode_tokens": 47,
|
| 204 |
+
"decode_s": 8.07,
|
| 205 |
+
"decode_tok_s": 5.83,
|
| 206 |
+
"slots": 3714,
|
| 207 |
+
"arena_gib": 6.35,
|
| 208 |
+
"hit_rate_pct": 68.617,
|
| 209 |
+
"ssd_mb_per_token": 214.07604217529297,
|
| 210 |
+
"prefetch_used_pct": 0,
|
| 211 |
+
"stdout_tail": "*Analyze the Request:**\n * Task: List five daily work tasks that involve using a computer.\n * Constraint: Each task must be explained in one sentence."
|
| 212 |
+
}
|
| 213 |
+
]
|
| 214 |
+
}
|
bench/results/strict_headline.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# strict 8192 MiB / no-swap — headline
|
| 2 |
+
|
| 3 |
+
- 模型:`/root/models/minicpm-v4.7-35b-a3b-q4_k_m.gguf`
|
| 4 |
+
- ctx=4096 ubatch=128 threads=8 n_predict=48 temp=0
|
| 5 |
+
- 上限:8192 MiB(anon),每次執行都由 `tools/strict8g.py` 管治;VmSwap 必須全程為 0,匿名記憶體全程 mlock
|
| 6 |
+
|
| 7 |
+
| 指標 | base |
|
| 8 |
+
| --- | ---: |
|
| 9 |
+
| Decode tok/s (median) | 5.83 |
|
| 10 |
+
| expert cache hit % | 68.68 |
|
| 11 |
+
| SSD MB / decode token | 214.1 |
|
| 12 |
+
| slots | 3714 |
|
| 13 |
+
| arena GiB | 6.35 |
|
| 14 |
+
| peak anon RSS MiB | 7799 |
|
| 15 |
+
| peak VmSwap MiB | 0.000 |
|
| 16 |
+
| draft acceptance % (min-max) | — |
|
| 17 |
+
|
| 18 |
+
每個 arm 9 輪;第一個 arm 的單輪 decode tok/s 全距 5.19–6.63,抖動幅度 25%。 請以中位數比較,不要用單輪數字。
|
| 19 |
+
|
| 20 |
+
完整逐輪資料:`strict_headline.json`
|
bench/results/strict_prefetch.json
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"label": "prefetch",
|
| 3 |
+
"rows": [
|
| 4 |
+
{
|
| 5 |
+
"rc": 0,
|
| 6 |
+
"strict_ok": true,
|
| 7 |
+
"tps": 5.55,
|
| 8 |
+
"hit_pct": 70.7335,
|
| 9 |
+
"mb_per_token": 198.9898681640625,
|
| 10 |
+
"prefetch_issued": 0,
|
| 11 |
+
"prefetch_used": 0,
|
| 12 |
+
"peak_anon_mb": 7780.5,
|
| 13 |
+
"peak_swap_mb": 0.0,
|
| 14 |
+
"arm": "prefetch_off",
|
| 15 |
+
"round": 0
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"rc": 0,
|
| 19 |
+
"strict_ok": true,
|
| 20 |
+
"tps": 6.22,
|
| 21 |
+
"hit_pct": 71.0633,
|
| 22 |
+
"mb_per_token": 208.33301544189453,
|
| 23 |
+
"prefetch_issued": 296,
|
| 24 |
+
"prefetch_used": 9,
|
| 25 |
+
"peak_anon_mb": 7780.6,
|
| 26 |
+
"peak_swap_mb": 0.0,
|
| 27 |
+
"arm": "prefetch_layer_ahead",
|
| 28 |
+
"round": 0
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"rc": 0,
|
| 32 |
+
"strict_ok": true,
|
| 33 |
+
"tps": 5.47,
|
| 34 |
+
"hit_pct": 67.7361,
|
| 35 |
+
"mb_per_token": 220.1375961303711,
|
| 36 |
+
"prefetch_issued": 0,
|
| 37 |
+
"prefetch_used": 0,
|
| 38 |
+
"peak_anon_mb": 7773.8,
|
| 39 |
+
"peak_swap_mb": 0.0,
|
| 40 |
+
"arm": "prefetch_end_token",
|
| 41 |
+
"round": 0
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"rc": 0,
|
| 45 |
+
"strict_ok": true,
|
| 46 |
+
"tps": 5.66,
|
| 47 |
+
"hit_pct": 75.0542,
|
| 48 |
+
"mb_per_token": 162.54615783691406,
|
| 49 |
+
"prefetch_issued": 0,
|
| 50 |
+
"prefetch_used": 0,
|
| 51 |
+
"peak_anon_mb": 7765.4,
|
| 52 |
+
"peak_swap_mb": 0.0,
|
| 53 |
+
"arm": "prefetch_off",
|
| 54 |
+
"round": 1
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"rc": 0,
|
| 58 |
+
"strict_ok": true,
|
| 59 |
+
"tps": 5.38,
|
| 60 |
+
"hit_pct": 75.3737,
|
| 61 |
+
"mb_per_token": 172.6512908935547,
|
| 62 |
+
"prefetch_issued": 312,
|
| 63 |
+
"prefetch_used": 15,
|
| 64 |
+
"peak_anon_mb": 7765.0,
|
| 65 |
+
"peak_swap_mb": 0.0,
|
| 66 |
+
"arm": "prefetch_layer_ahead",
|
| 67 |
+
"round": 1
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"rc": 0,
|
| 71 |
+
"strict_ok": true,
|
| 72 |
+
"tps": 5.45,
|
| 73 |
+
"hit_pct": 75.0603,
|
| 74 |
+
"mb_per_token": 162.5041961669922,
|
| 75 |
+
"prefetch_issued": 0,
|
| 76 |
+
"prefetch_used": 0,
|
| 77 |
+
"peak_anon_mb": 7765.0,
|
| 78 |
+
"peak_swap_mb": 0.0,
|
| 79 |
+
"arm": "prefetch_end_token",
|
| 80 |
+
"round": 1
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"rc": 0,
|
| 84 |
+
"strict_ok": true,
|
| 85 |
+
"tps": 5.88,
|
| 86 |
+
"hit_pct": 71.9818,
|
| 87 |
+
"mb_per_token": 190.33336639404297,
|
| 88 |
+
"prefetch_issued": 0,
|
| 89 |
+
"prefetch_used": 0,
|
| 90 |
+
"peak_anon_mb": 7767.8,
|
| 91 |
+
"peak_swap_mb": 0.0,
|
| 92 |
+
"arm": "prefetch_off",
|
| 93 |
+
"round": 2
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"rc": 0,
|
| 97 |
+
"strict_ok": true,
|
| 98 |
+
"tps": 5.8,
|
| 99 |
+
"hit_pct": 72.2186,
|
| 100 |
+
"mb_per_token": 201.25865936279297,
|
| 101 |
+
"prefetch_issued": 312,
|
| 102 |
+
"prefetch_used": 10,
|
| 103 |
+
"peak_anon_mb": 7767.8,
|
| 104 |
+
"peak_swap_mb": 0.0,
|
| 105 |
+
"arm": "prefetch_layer_ahead",
|
| 106 |
+
"round": 2
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"rc": 0,
|
| 110 |
+
"strict_ok": true,
|
| 111 |
+
"tps": 6.04,
|
| 112 |
+
"hit_pct": 71.9877,
|
| 113 |
+
"mb_per_token": 190.2914047241211,
|
| 114 |
+
"prefetch_issued": 0,
|
| 115 |
+
"prefetch_used": 0,
|
| 116 |
+
"peak_anon_mb": 7767.8,
|
| 117 |
+
"peak_swap_mb": 0.0,
|
| 118 |
+
"arm": "prefetch_end_token",
|
| 119 |
+
"round": 2
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"rc": 0,
|
| 123 |
+
"strict_ok": true,
|
| 124 |
+
"tps": 6.42,
|
| 125 |
+
"hit_pct": 70.7393,
|
| 126 |
+
"mb_per_token": 198.94886016845703,
|
| 127 |
+
"prefetch_issued": 0,
|
| 128 |
+
"prefetch_used": 0,
|
| 129 |
+
"peak_anon_mb": 7780.6,
|
| 130 |
+
"peak_swap_mb": 0.0,
|
| 131 |
+
"arm": "prefetch_off",
|
| 132 |
+
"round": 3
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"rc": 0,
|
| 136 |
+
"strict_ok": true,
|
| 137 |
+
"tps": 6.26,
|
| 138 |
+
"hit_pct": 71.0401,
|
| 139 |
+
"mb_per_token": 208.49895477294922,
|
| 140 |
+
"prefetch_issued": 296,
|
| 141 |
+
"prefetch_used": 0,
|
| 142 |
+
"peak_anon_mb": 7780.6,
|
| 143 |
+
"peak_swap_mb": 0.0,
|
| 144 |
+
"arm": "prefetch_layer_ahead",
|
| 145 |
+
"round": 3
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"rc": 0,
|
| 149 |
+
"strict_ok": true,
|
| 150 |
+
"tps": 5.8,
|
| 151 |
+
"hit_pct": 67.7361,
|
| 152 |
+
"mb_per_token": 220.1375961303711,
|
| 153 |
+
"prefetch_issued": 0,
|
| 154 |
+
"prefetch_used": 0,
|
| 155 |
+
"peak_anon_mb": 7774.2,
|
| 156 |
+
"peak_swap_mb": 0.0,
|
| 157 |
+
"arm": "prefetch_end_token",
|
| 158 |
+
"round": 3
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"rc": 0,
|
| 162 |
+
"strict_ok": true,
|
| 163 |
+
"tps": 6.01,
|
| 164 |
+
"hit_pct": 75.0542,
|
| 165 |
+
"mb_per_token": 162.54615783691406,
|
| 166 |
+
"prefetch_issued": 0,
|
| 167 |
+
"prefetch_used": 0,
|
| 168 |
+
"peak_anon_mb": 7765.4,
|
| 169 |
+
"peak_swap_mb": 0.0,
|
| 170 |
+
"arm": "prefetch_off",
|
| 171 |
+
"round": 4
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"rc": 0,
|
| 175 |
+
"strict_ok": true,
|
| 176 |
+
"tps": 5.75,
|
| 177 |
+
"hit_pct": 75.3797,
|
| 178 |
+
"mb_per_token": 172.61028289794922,
|
| 179 |
+
"prefetch_issued": 312,
|
| 180 |
+
"prefetch_used": 15,
|
| 181 |
+
"peak_anon_mb": 7765.1,
|
| 182 |
+
"peak_swap_mb": 0.0,
|
| 183 |
+
"arm": "prefetch_layer_ahead",
|
| 184 |
+
"round": 4
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"rc": 0,
|
| 188 |
+
"strict_ok": true,
|
| 189 |
+
"tps": 5.63,
|
| 190 |
+
"hit_pct": 75.0542,
|
| 191 |
+
"mb_per_token": 162.54615783691406,
|
| 192 |
+
"prefetch_issued": 0,
|
| 193 |
+
"prefetch_used": 0,
|
| 194 |
+
"peak_anon_mb": 7765.4,
|
| 195 |
+
"peak_swap_mb": 0.0,
|
| 196 |
+
"arm": "prefetch_end_token",
|
| 197 |
+
"round": 4
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"rc": 0,
|
| 201 |
+
"strict_ok": true,
|
| 202 |
+
"tps": 6.68,
|
| 203 |
+
"hit_pct": 71.9936,
|
| 204 |
+
"mb_per_token": 190.2484893798828,
|
| 205 |
+
"prefetch_issued": 0,
|
| 206 |
+
"prefetch_used": 0,
|
| 207 |
+
"peak_anon_mb": 7767.5,
|
| 208 |
+
"peak_swap_mb": 0.0,
|
| 209 |
+
"arm": "prefetch_off",
|
| 210 |
+
"round": 5
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"rc": 0,
|
| 214 |
+
"strict_ok": true,
|
| 215 |
+
"tps": 6.29,
|
| 216 |
+
"hit_pct": 72.2127,
|
| 217 |
+
"mb_per_token": 201.30157470703125,
|
| 218 |
+
"prefetch_issued": 312,
|
| 219 |
+
"prefetch_used": 10,
|
| 220 |
+
"peak_anon_mb": 7767.5,
|
| 221 |
+
"peak_swap_mb": 0.0,
|
| 222 |
+
"arm": "prefetch_layer_ahead",
|
| 223 |
+
"round": 5
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"rc": 0,
|
| 227 |
+
"strict_ok": true,
|
| 228 |
+
"tps": 6.27,
|
| 229 |
+
"hit_pct": 71.964,
|
| 230 |
+
"mb_per_token": 190.460205078125,
|
| 231 |
+
"prefetch_issued": 0,
|
| 232 |
+
"prefetch_used": 0,
|
| 233 |
+
"peak_anon_mb": 7767.5,
|
| 234 |
+
"peak_swap_mb": 0.0,
|
| 235 |
+
"arm": "prefetch_end_token",
|
| 236 |
+
"round": 5
|
| 237 |
+
}
|
| 238 |
+
],
|
| 239 |
+
"summary": {
|
| 240 |
+
"prefetch_off": {
|
| 241 |
+
"n": 6,
|
| 242 |
+
"median_tps": 5.945,
|
| 243 |
+
"mean_tps": 6.033333333333333,
|
| 244 |
+
"stdev_tps": 0.43925694834193185,
|
| 245 |
+
"min_tps": 5.55,
|
| 246 |
+
"max_tps": 6.68,
|
| 247 |
+
"all_strict_ok": true
|
| 248 |
+
},
|
| 249 |
+
"prefetch_layer_ahead": {
|
| 250 |
+
"n": 6,
|
| 251 |
+
"median_tps": 6.01,
|
| 252 |
+
"mean_tps": 5.95,
|
| 253 |
+
"stdev_tps": 0.3666060555964672,
|
| 254 |
+
"min_tps": 5.38,
|
| 255 |
+
"max_tps": 6.29,
|
| 256 |
+
"all_strict_ok": true
|
| 257 |
+
},
|
| 258 |
+
"prefetch_end_token": {
|
| 259 |
+
"n": 6,
|
| 260 |
+
"median_tps": 5.715,
|
| 261 |
+
"mean_tps": 5.776666666666666,
|
| 262 |
+
"stdev_tps": 0.327271548819427,
|
| 263 |
+
"min_tps": 5.45,
|
| 264 |
+
"max_tps": 6.27,
|
| 265 |
+
"all_strict_ok": true
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
bench/results/strict_threads.json
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"label": "threads",
|
| 3 |
+
"rows": [
|
| 4 |
+
{
|
| 5 |
+
"rc": 0,
|
| 6 |
+
"strict_ok": true,
|
| 7 |
+
"tps": 4.7,
|
| 8 |
+
"hit_pct": 70.8227,
|
| 9 |
+
"mb_per_token": 198.34423065185547,
|
| 10 |
+
"prefetch_issued": 0,
|
| 11 |
+
"prefetch_used": 0,
|
| 12 |
+
"peak_anon_mb": 7764.7,
|
| 13 |
+
"peak_swap_mb": 0.0,
|
| 14 |
+
"arm": "threads_4",
|
| 15 |
+
"round": 0
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"rc": 0,
|
| 19 |
+
"strict_ok": true,
|
| 20 |
+
"tps": 5.44,
|
| 21 |
+
"hit_pct": 70.7335,
|
| 22 |
+
"mb_per_token": 198.9898681640625,
|
| 23 |
+
"prefetch_issued": 0,
|
| 24 |
+
"prefetch_used": 0,
|
| 25 |
+
"peak_anon_mb": 7780.2,
|
| 26 |
+
"peak_swap_mb": 0.0,
|
| 27 |
+
"arm": "threads_8",
|
| 28 |
+
"round": 0
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"rc": 0,
|
| 32 |
+
"strict_ok": true,
|
| 33 |
+
"tps": 6.68,
|
| 34 |
+
"hit_pct": 70.7424,
|
| 35 |
+
"mb_per_token": 198.8658905029297,
|
| 36 |
+
"prefetch_issued": 0,
|
| 37 |
+
"prefetch_used": 0,
|
| 38 |
+
"peak_anon_mb": 7788.2,
|
| 39 |
+
"peak_swap_mb": 0.0,
|
| 40 |
+
"arm": "threads_10",
|
| 41 |
+
"round": 0
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"rc": 0,
|
| 45 |
+
"strict_ok": true,
|
| 46 |
+
"tps": 6.61,
|
| 47 |
+
"hit_pct": 70.7451,
|
| 48 |
+
"mb_per_token": 198.9126205444336,
|
| 49 |
+
"prefetch_issued": 0,
|
| 50 |
+
"prefetch_used": 0,
|
| 51 |
+
"peak_anon_mb": 7796.0,
|
| 52 |
+
"peak_swap_mb": 0.0,
|
| 53 |
+
"arm": "threads_12",
|
| 54 |
+
"round": 0
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"rc": 0,
|
| 58 |
+
"strict_ok": true,
|
| 59 |
+
"tps": 6.59,
|
| 60 |
+
"hit_pct": 70.709,
|
| 61 |
+
"mb_per_token": 199.21588897705078,
|
| 62 |
+
"prefetch_issued": 0,
|
| 63 |
+
"prefetch_used": 0,
|
| 64 |
+
"peak_anon_mb": 7811.3,
|
| 65 |
+
"peak_swap_mb": 0.0,
|
| 66 |
+
"arm": "threads_16",
|
| 67 |
+
"round": 0
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"rc": 0,
|
| 71 |
+
"strict_ok": true,
|
| 72 |
+
"tps": 4.41,
|
| 73 |
+
"hit_pct": 75.7866,
|
| 74 |
+
"mb_per_token": 156.92615509033203,
|
| 75 |
+
"prefetch_issued": 0,
|
| 76 |
+
"prefetch_used": 0,
|
| 77 |
+
"peak_anon_mb": 7754.7,
|
| 78 |
+
"peak_swap_mb": 0.0,
|
| 79 |
+
"arm": "threads_4",
|
| 80 |
+
"round": 1
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"rc": 0,
|
| 84 |
+
"strict_ok": true,
|
| 85 |
+
"tps": 6.24,
|
| 86 |
+
"hit_pct": 75.0542,
|
| 87 |
+
"mb_per_token": 162.54615783691406,
|
| 88 |
+
"prefetch_issued": 0,
|
| 89 |
+
"prefetch_used": 0,
|
| 90 |
+
"peak_anon_mb": 7765.0,
|
| 91 |
+
"peak_swap_mb": 0.0,
|
| 92 |
+
"arm": "threads_8",
|
| 93 |
+
"round": 1
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"rc": 0,
|
| 97 |
+
"strict_ok": true,
|
| 98 |
+
"tps": 5.74,
|
| 99 |
+
"hit_pct": 76.2284,
|
| 100 |
+
"mb_per_token": 154.0384292602539,
|
| 101 |
+
"prefetch_issued": 0,
|
| 102 |
+
"prefetch_used": 0,
|
| 103 |
+
"peak_anon_mb": 7768.9,
|
| 104 |
+
"peak_swap_mb": 0.0,
|
| 105 |
+
"arm": "threads_10",
|
| 106 |
+
"round": 1
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"rc": 0,
|
| 110 |
+
"strict_ok": true,
|
| 111 |
+
"tps": 6.68,
|
| 112 |
+
"hit_pct": 75.0768,
|
| 113 |
+
"mb_per_token": 162.36400604248047,
|
| 114 |
+
"prefetch_issued": 0,
|
| 115 |
+
"prefetch_used": 0,
|
| 116 |
+
"peak_anon_mb": 7774.7,
|
| 117 |
+
"peak_swap_mb": 0.0,
|
| 118 |
+
"arm": "threads_12",
|
| 119 |
+
"round": 1
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"rc": 0,
|
| 123 |
+
"strict_ok": true,
|
| 124 |
+
"tps": 5.9,
|
| 125 |
+
"hit_pct": 76.657,
|
| 126 |
+
"mb_per_token": 151.58843994140625,
|
| 127 |
+
"prefetch_issued": 0,
|
| 128 |
+
"prefetch_used": 0,
|
| 129 |
+
"peak_anon_mb": 7783.7,
|
| 130 |
+
"peak_swap_mb": 0.0,
|
| 131 |
+
"arm": "threads_16",
|
| 132 |
+
"round": 1
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"rc": 0,
|
| 136 |
+
"strict_ok": true,
|
| 137 |
+
"tps": 4.83,
|
| 138 |
+
"hit_pct": 71.5758,
|
| 139 |
+
"mb_per_token": 193.1133270263672,
|
| 140 |
+
"prefetch_issued": 0,
|
| 141 |
+
"prefetch_used": 0,
|
| 142 |
+
"peak_anon_mb": 7756.9,
|
| 143 |
+
"peak_swap_mb": 0.0,
|
| 144 |
+
"arm": "threads_4",
|
| 145 |
+
"round": 2
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"rc": 0,
|
| 149 |
+
"strict_ok": true,
|
| 150 |
+
"tps": 6.2,
|
| 151 |
+
"hit_pct": 71.9877,
|
| 152 |
+
"mb_per_token": 190.2914047241211,
|
| 153 |
+
"prefetch_issued": 0,
|
| 154 |
+
"prefetch_used": 0,
|
| 155 |
+
"peak_anon_mb": 7767.8,
|
| 156 |
+
"peak_swap_mb": 0.0,
|
| 157 |
+
"arm": "threads_8",
|
| 158 |
+
"round": 2
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"rc": 0,
|
| 162 |
+
"strict_ok": true,
|
| 163 |
+
"tps": 6.07,
|
| 164 |
+
"hit_pct": 70.074,
|
| 165 |
+
"mb_per_token": 203.43780517578125,
|
| 166 |
+
"prefetch_issued": 0,
|
| 167 |
+
"prefetch_used": 0,
|
| 168 |
+
"peak_anon_mb": 7771.9,
|
| 169 |
+
"peak_swap_mb": 0.0,
|
| 170 |
+
"arm": "threads_10",
|
| 171 |
+
"round": 2
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"rc": 0,
|
| 175 |
+
"strict_ok": true,
|
| 176 |
+
"tps": 5.77,
|
| 177 |
+
"hit_pct": 71.9652,
|
| 178 |
+
"mb_per_token": 186.03134155273438,
|
| 179 |
+
"prefetch_issued": 0,
|
| 180 |
+
"prefetch_used": 0,
|
| 181 |
+
"peak_anon_mb": 7777.3,
|
| 182 |
+
"peak_swap_mb": 0.0,
|
| 183 |
+
"arm": "threads_12",
|
| 184 |
+
"round": 2
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"rc": 0,
|
| 188 |
+
"strict_ok": true,
|
| 189 |
+
"tps": 6.32,
|
| 190 |
+
"hit_pct": 70.0906,
|
| 191 |
+
"mb_per_token": 203.3071517944336,
|
| 192 |
+
"prefetch_issued": 0,
|
| 193 |
+
"prefetch_used": 0,
|
| 194 |
+
"peak_anon_mb": 7787.5,
|
| 195 |
+
"peak_swap_mb": 0.0,
|
| 196 |
+
"arm": "threads_16",
|
| 197 |
+
"round": 2
|
| 198 |
+
}
|
| 199 |
+
],
|
| 200 |
+
"summary": {
|
| 201 |
+
"threads_4": {
|
| 202 |
+
"n": 3,
|
| 203 |
+
"median_tps": 4.7,
|
| 204 |
+
"mean_tps": 4.6466666666666665,
|
| 205 |
+
"stdev_tps": 0.2150193789716018,
|
| 206 |
+
"min_tps": 4.41,
|
| 207 |
+
"max_tps": 4.83,
|
| 208 |
+
"all_strict_ok": true
|
| 209 |
+
},
|
| 210 |
+
"threads_8": {
|
| 211 |
+
"n": 3,
|
| 212 |
+
"median_tps": 6.2,
|
| 213 |
+
"mean_tps": 5.96,
|
| 214 |
+
"stdev_tps": 0.4507771067833857,
|
| 215 |
+
"min_tps": 5.44,
|
| 216 |
+
"max_tps": 6.24,
|
| 217 |
+
"all_strict_ok": true
|
| 218 |
+
},
|
| 219 |
+
"threads_10": {
|
| 220 |
+
"n": 3,
|
| 221 |
+
"median_tps": 6.07,
|
| 222 |
+
"mean_tps": 6.163333333333333,
|
| 223 |
+
"stdev_tps": 0.4768997099321125,
|
| 224 |
+
"min_tps": 5.74,
|
| 225 |
+
"max_tps": 6.68,
|
| 226 |
+
"all_strict_ok": true
|
| 227 |
+
},
|
| 228 |
+
"threads_12": {
|
| 229 |
+
"n": 3,
|
| 230 |
+
"median_tps": 6.61,
|
| 231 |
+
"mean_tps": 6.3533333333333335,
|
| 232 |
+
"stdev_tps": 0.5063924696649167,
|
| 233 |
+
"min_tps": 5.77,
|
| 234 |
+
"max_tps": 6.68,
|
| 235 |
+
"all_strict_ok": true
|
| 236 |
+
},
|
| 237 |
+
"threads_16": {
|
| 238 |
+
"n": 3,
|
| 239 |
+
"median_tps": 6.32,
|
| 240 |
+
"mean_tps": 6.2700000000000005,
|
| 241 |
+
"stdev_tps": 0.34770677301427394,
|
| 242 |
+
"min_tps": 5.9,
|
| 243 |
+
"max_tps": 6.59,
|
| 244 |
+
"all_strict_ok": true
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
}
|
bench/results/summary.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"budget_curve": "| Budget | Arena (GiB) | 槽位 | 命中率 | SSD MB/token | Decode tok/s | 峰值匿名 RSS | VmSwap |\n| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |\n| 2 GiB | 0.57 | 332 | 22.72% | 526.2 | 4.32 | 1842 | 0.000 |\n| 3 GiB | 1.53 | 895 | 50.19% | 340.0 | 5.16 | 2828 | 0.000 |\n| 4 GiB | 2.50 | 1459 | 59.81% | 275.8 | 5.48 | 3817 | 0.000 |\n| 5 GiB | 3.46 | 2023 | 64.88% | 240.8 | 5.82 | 4805 | 0.000 |\n| 6 GiB | 4.43 | 2587 | 67.52% | 222.2 | 5.38 | 5793 | 0.000 |\n| 7 GiB | 5.39 | 3150 | 68.60% | 214.0 | 5.10 | 6779 | 0.000 |\n| 8 GiB | 6.35 | 3714 | 70.74% | 198.9 | 5.45 | 7766 | 0.000 |",
|
| 3 |
+
"ram_sweep": "| Budget | Arena (GiB) | 槽位 | 命中率 | SSD MB/token | 平均 tok/s | 單輪標準差 | 峰值 RSS | VmSwap |\n| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |\n| 4 GiB | 2.74 | 1600 | 61.50% | 275.4 | 5.43 ± 0.37 | 3963 | 0.000 |\n| 8 GiB | 6.36 | 3715 | 71.98% | 199.1 | 6.33 ± 0.33 | 7669 | 0.000 |\n| 16 GiB | 13.59 | 7943 | 77.54% | 157.9 | 6.28 ± 0.41 | 15076 | 0.000 |\n| 64 GiB | 59.89 | 35003 | 77.54% | 157.6 | 6.32 ± 0.51 | 23969 | 0.000 |",
|
| 4 |
+
"ram_sweep_tests": "| 比較 | 只改變了 | 平均 tok/s | Welch t | p | 判定 |\n| --- | --- | ---: | ---: | ---: | --- |\n| 4 GiB 比 8 GiB | RAM 預算 | 5.43 → 6.33 (-14%) | -5.44 | 0.000 | 顯著 |\n| 16 GiB 比 8 GiB | RAM 預算 | 6.28 → 6.33 (-1%) | -0.30 | 0.765 | 無法分辨 |\n| 64 GiB 比 8 GiB | RAM 預算 | 6.32 → 6.33 (-0%) | -0.05 | 0.961 | 無法分辨 |",
|
| 5 |
+
"negatives": {
|
| 6 |
+
"threads": {
|
| 7 |
+
"threads_4": {
|
| 8 |
+
"n": 3,
|
| 9 |
+
"median_tps": 4.7,
|
| 10 |
+
"mean_tps": 4.6466666666666665,
|
| 11 |
+
"stdev_tps": 0.2150193789716018,
|
| 12 |
+
"min_tps": 4.41,
|
| 13 |
+
"max_tps": 4.83,
|
| 14 |
+
"all_strict_ok": true
|
| 15 |
+
},
|
| 16 |
+
"threads_8": {
|
| 17 |
+
"n": 3,
|
| 18 |
+
"median_tps": 6.2,
|
| 19 |
+
"mean_tps": 5.96,
|
| 20 |
+
"stdev_tps": 0.4507771067833857,
|
| 21 |
+
"min_tps": 5.44,
|
| 22 |
+
"max_tps": 6.24,
|
| 23 |
+
"all_strict_ok": true
|
| 24 |
+
},
|
| 25 |
+
"threads_10": {
|
| 26 |
+
"n": 3,
|
| 27 |
+
"median_tps": 6.07,
|
| 28 |
+
"mean_tps": 6.163333333333333,
|
| 29 |
+
"stdev_tps": 0.4768997099321125,
|
| 30 |
+
"min_tps": 5.74,
|
| 31 |
+
"max_tps": 6.68,
|
| 32 |
+
"all_strict_ok": true
|
| 33 |
+
},
|
| 34 |
+
"threads_12": {
|
| 35 |
+
"n": 3,
|
| 36 |
+
"median_tps": 6.61,
|
| 37 |
+
"mean_tps": 6.3533333333333335,
|
| 38 |
+
"stdev_tps": 0.5063924696649167,
|
| 39 |
+
"min_tps": 5.77,
|
| 40 |
+
"max_tps": 6.68,
|
| 41 |
+
"all_strict_ok": true
|
| 42 |
+
},
|
| 43 |
+
"threads_16": {
|
| 44 |
+
"n": 3,
|
| 45 |
+
"median_tps": 6.32,
|
| 46 |
+
"mean_tps": 6.2700000000000005,
|
| 47 |
+
"stdev_tps": 0.34770677301427394,
|
| 48 |
+
"min_tps": 5.9,
|
| 49 |
+
"max_tps": 6.59,
|
| 50 |
+
"all_strict_ok": true
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"prefetch": {
|
| 54 |
+
"prefetch_off": {
|
| 55 |
+
"n": 6,
|
| 56 |
+
"median_tps": 5.945,
|
| 57 |
+
"mean_tps": 6.033333333333333,
|
| 58 |
+
"stdev_tps": 0.43925694834193185,
|
| 59 |
+
"min_tps": 5.55,
|
| 60 |
+
"max_tps": 6.68,
|
| 61 |
+
"all_strict_ok": true
|
| 62 |
+
},
|
| 63 |
+
"prefetch_layer_ahead": {
|
| 64 |
+
"n": 6,
|
| 65 |
+
"median_tps": 6.01,
|
| 66 |
+
"mean_tps": 5.95,
|
| 67 |
+
"stdev_tps": 0.3666060555964672,
|
| 68 |
+
"min_tps": 5.38,
|
| 69 |
+
"max_tps": 6.29,
|
| 70 |
+
"all_strict_ok": true
|
| 71 |
+
},
|
| 72 |
+
"prefetch_end_token": {
|
| 73 |
+
"n": 6,
|
| 74 |
+
"median_tps": 5.715,
|
| 75 |
+
"mean_tps": 5.776666666666666,
|
| 76 |
+
"stdev_tps": 0.327271548819427,
|
| 77 |
+
"min_tps": 5.45,
|
| 78 |
+
"max_tps": 6.27,
|
| 79 |
+
"all_strict_ok": true
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
bench/run_budget_curve.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Pager budget curve: how hit rate and throughput move with arena size.
|
| 3 |
+
|
| 4 |
+
The physical ceiling stays at --strict-mb (8 GiB) for every point; only the
|
| 5 |
+
pager's RAM budget changes, so the arena shrinks and the cache holds fewer
|
| 6 |
+
experts. This isolates "does more arena still buy anything" from "does the
|
| 7 |
+
machine have more RAM".
|
| 8 |
+
|
| 9 |
+
Output: bench/results/strict_budgets.json
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import subprocess
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
HERE = Path(__file__).resolve().parent
|
| 23 |
+
REPO = HERE.parent
|
| 24 |
+
|
| 25 |
+
PROMPTS = [
|
| 26 |
+
"Explain in one sentence why SSDs are slow.",
|
| 27 |
+
"請用一句話介紹你自己。",
|
| 28 |
+
"Write a short paragraph about SSD caching.",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def run_one(strict8g: Path, binary: str, model: str, budget_mb: int, strict_mb: int,
|
| 33 |
+
prompt: str, n_predict: int, ctx: int, threads: int, ubatch: int,
|
| 34 |
+
record: Path, reserve_mb: int, extra_env: dict | None = None) -> dict:
|
| 35 |
+
cmd = [
|
| 36 |
+
sys.executable, str(strict8g),
|
| 37 |
+
"--limit-mb", str(strict_mb),
|
| 38 |
+
"--record", str(record),
|
| 39 |
+
"--",
|
| 40 |
+
binary, "-m", model, "-p", prompt, "-n", str(n_predict),
|
| 41 |
+
"-c", str(ctx), "-t", str(threads), "-b", str(ubatch),
|
| 42 |
+
"--temp", "0", "--no-chat", "--no-interactive",
|
| 43 |
+
"--ram-budget-mb", str(budget_mb),
|
| 44 |
+
# hit rate is only printed when a stats file is requested
|
| 45 |
+
"--stats-file", str(record.with_suffix(".stats")),
|
| 46 |
+
]
|
| 47 |
+
env = dict(os.environ, SDQ_RESERVE_MB=str(reserve_mb))
|
| 48 |
+
if extra_env:
|
| 49 |
+
env.update(extra_env)
|
| 50 |
+
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
| 51 |
+
out = p.stdout + p.stderr
|
| 52 |
+
|
| 53 |
+
def grab(pat, cast=float):
|
| 54 |
+
m = re.search(pat, out)
|
| 55 |
+
return cast(m.group(1)) if m else None
|
| 56 |
+
|
| 57 |
+
rec = {}
|
| 58 |
+
if record.exists():
|
| 59 |
+
try:
|
| 60 |
+
rec = json.loads(record.read_text())
|
| 61 |
+
except Exception:
|
| 62 |
+
rec = {}
|
| 63 |
+
# the pager's own counters (hit rate, SSD bytes, prefetch) go to --stats-file
|
| 64 |
+
stats = {}
|
| 65 |
+
sp = record.with_suffix(".stats")
|
| 66 |
+
if sp.exists():
|
| 67 |
+
try:
|
| 68 |
+
for line in sp.read_text().splitlines():
|
| 69 |
+
line = line.strip()
|
| 70 |
+
if line.startswith("{"):
|
| 71 |
+
stats = json.loads(line)
|
| 72 |
+
except Exception:
|
| 73 |
+
stats = {}
|
| 74 |
+
|
| 75 |
+
m = re.search(r"= ([\d.]+) tok/s", out)
|
| 76 |
+
return {
|
| 77 |
+
"budget_mb": budget_mb,
|
| 78 |
+
"reserve_mb": reserve_mb,
|
| 79 |
+
"rc": p.returncode,
|
| 80 |
+
"strict_ok": bool(rec.get("strict_ok", False)),
|
| 81 |
+
"decode_tps": float(m.group(1)) if m else None,
|
| 82 |
+
"hit_pct": stats.get("hit_rate"),
|
| 83 |
+
"mb_per_token": (stats["ssd_bytes_per_token"] / 1048576.0) if stats.get("ssd_bytes_per_token") else None,
|
| 84 |
+
"slots": int(grab(r"slots=(\d+)", str) or 0),
|
| 85 |
+
"arena_gib": grab(r"arena=([\d.]+) GiB"),
|
| 86 |
+
"slot_split": (re.search(r"槽位分兩種:(.+)", out).group(1).strip()
|
| 87 |
+
if re.search(r"槽位分兩種:(.+)", out) else None),
|
| 88 |
+
"peak_anon_mb": rec.get("peak_anon_mb"),
|
| 89 |
+
"peak_swap_mb": rec.get("peak_swap_mb"),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def main() -> None:
|
| 94 |
+
ap = argparse.ArgumentParser()
|
| 95 |
+
ap.add_argument("--model", required=True)
|
| 96 |
+
ap.add_argument("--bin", required=True)
|
| 97 |
+
ap.add_argument("--budgets", type=int, nargs="+",
|
| 98 |
+
default=[2048, 3072, 4096, 5120, 6144, 7168, 8192])
|
| 99 |
+
ap.add_argument("--strict-mb", type=int, default=8192)
|
| 100 |
+
ap.add_argument("--reserve-mb", type=int, default=512)
|
| 101 |
+
ap.add_argument("--rounds", type=int, default=5)
|
| 102 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 103 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 104 |
+
ap.add_argument("--ubatch", type=int, default=128)
|
| 105 |
+
ap.add_argument("--n-predict", type=int, default=48)
|
| 106 |
+
ap.add_argument("--out", default=str(HERE / "results" / "strict_budgets.json"))
|
| 107 |
+
a = ap.parse_args()
|
| 108 |
+
|
| 109 |
+
strict8g = REPO / "tools" / "strict8g.py"
|
| 110 |
+
(HERE / "results").mkdir(parents=True, exist_ok=True)
|
| 111 |
+
rows = []
|
| 112 |
+
for r in range(a.rounds):
|
| 113 |
+
for b in a.budgets:
|
| 114 |
+
prompt = PROMPTS[r % len(PROMPTS)]
|
| 115 |
+
rec = HERE / "results" / f".budget-{b}-{r}.json"
|
| 116 |
+
row = run_one(strict8g, a.bin, a.model, b, a.strict_mb, prompt,
|
| 117 |
+
a.n_predict, a.ctx, a.threads, a.ubatch, rec,
|
| 118 |
+
a.reserve_mb)
|
| 119 |
+
row["round"] = r
|
| 120 |
+
rows.append(row)
|
| 121 |
+
ok = "ok" if row["strict_ok"] else "FAIL"
|
| 122 |
+
print(f"[{r + 1}/{a.rounds}] budget={b:>5} tps={row['decode_tps']} "
|
| 123 |
+
f"hit={row['hit_pct']}% MB/tok={row['mb_per_token']} "
|
| 124 |
+
f"slots={row['slots']} anon={row['peak_anon_mb']} {ok}", flush=True)
|
| 125 |
+
rec.unlink(missing_ok=True)
|
| 126 |
+
|
| 127 |
+
out = Path(a.out)
|
| 128 |
+
out.write_text(json.dumps(rows, indent=2))
|
| 129 |
+
print(f"-> {out}")
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
if __name__ == "__main__":
|
| 133 |
+
main()
|
bench/run_n9.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Throughput at four RAM magnitudes with Welch t-tests.
|
| 3 |
+
|
| 4 |
+
N=3 runs cannot rank settings: the same configuration differs by tens of percent
|
| 5 |
+
between three rounds on a shared host. This script runs each budget 9 times,
|
| 6 |
+
interleaved so machine drift hits every budget equally, and reports Welch
|
| 7 |
+
t-tests for the comparisons that drive the deployment decision.
|
| 8 |
+
|
| 9 |
+
Output: bench/results/ram_sweep_n9.json
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import math
|
| 17 |
+
import statistics
|
| 18 |
+
import subprocess
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
HERE = Path(__file__).resolve().parent.parent / "bench"
|
| 23 |
+
REPO = HERE.parent
|
| 24 |
+
sys.path.insert(0, str(HERE))
|
| 25 |
+
|
| 26 |
+
from run_ramsweep import run_one # noqa: E402
|
| 27 |
+
|
| 28 |
+
PROMPTS = [
|
| 29 |
+
"Explain in one sentence why SSDs are slow.",
|
| 30 |
+
"請用一句話介紹你自己。",
|
| 31 |
+
"Write a short paragraph about SSD caching.",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def welch(a: list[float], b: list[float]) -> tuple[float, float]:
|
| 36 |
+
"""Returns (t, two-sided p) for the difference of means, Welch's t-test."""
|
| 37 |
+
na, nb = len(a), len(b)
|
| 38 |
+
ma, mb = statistics.mean(a), statistics.mean(b)
|
| 39 |
+
va = statistics.variance(a) if na > 1 else 0.0
|
| 40 |
+
vb = statistics.variance(b) if nb > 1 else 0.0
|
| 41 |
+
se2 = va / na + vb / nb
|
| 42 |
+
if se2 == 0:
|
| 43 |
+
return (0.0, 1.0)
|
| 44 |
+
t = (ma - mb) / math.sqrt(se2)
|
| 45 |
+
df_num = se2 ** 2
|
| 46 |
+
df_den = (va / na) ** 2 / (na - 1) + (vb / nb) ** 2 / (nb - 1)
|
| 47 |
+
df = df_num / df_den if df_den else float(na + nb - 2)
|
| 48 |
+
# two-sided p from the t distribution via the incomplete beta function
|
| 49 |
+
x = df / (df + t * t)
|
| 50 |
+
p = betainc(df / 2.0, 0.5, x)
|
| 51 |
+
return (t, p)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def betainc(a: float, b: float, x: float) -> float:
|
| 55 |
+
"""Regularized incomplete beta I_x(a, b)."""
|
| 56 |
+
if x <= 0:
|
| 57 |
+
return 0.0
|
| 58 |
+
if x >= 1:
|
| 59 |
+
return 1.0
|
| 60 |
+
lbeta = math.lgamma(a) + math.lgamma(b) - math.lgamma(a + b)
|
| 61 |
+
front = math.exp(math.log(x) * a + math.log(1 - x) * b - lbeta)
|
| 62 |
+
if x < (a + 1) / (a + b + 2):
|
| 63 |
+
return front * betacf(a, b, x) / a
|
| 64 |
+
return 1.0 - math.exp(math.log(1 - x) * b + math.log(x) * a - lbeta) * betacf(b, a, 1 - x) / b
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def betacf(a: float, b: float, x: float) -> float:
|
| 68 |
+
tiny = 1e-300
|
| 69 |
+
qab, qap, qam = a + b, a + 1.0, a - 1.0
|
| 70 |
+
c, d = 1.0, 1.0 - qab * x / qap
|
| 71 |
+
if abs(d) < tiny:
|
| 72 |
+
d = tiny
|
| 73 |
+
d = 1.0 / d
|
| 74 |
+
h = d
|
| 75 |
+
for m in range(1, 300):
|
| 76 |
+
m2 = 2 * m
|
| 77 |
+
aa = m * (b - m) * x / ((qam + m2) * (a + m2))
|
| 78 |
+
d = 1.0 + aa * d
|
| 79 |
+
if abs(d) < tiny:
|
| 80 |
+
d = tiny
|
| 81 |
+
c = 1.0 + aa / c
|
| 82 |
+
if abs(c) < tiny:
|
| 83 |
+
c = tiny
|
| 84 |
+
d = 1.0 / d
|
| 85 |
+
h *= d * c
|
| 86 |
+
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
|
| 87 |
+
d = 1.0 + aa * d
|
| 88 |
+
if abs(d) < tiny:
|
| 89 |
+
d = tiny
|
| 90 |
+
c = 1.0 + aa / c
|
| 91 |
+
if abs(c) < tiny:
|
| 92 |
+
c = tiny
|
| 93 |
+
d = 1.0 / d
|
| 94 |
+
de = d * c
|
| 95 |
+
h *= de
|
| 96 |
+
if abs(de - 1.0) < 3e-16:
|
| 97 |
+
break
|
| 98 |
+
return h
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def main() -> None:
|
| 102 |
+
ap = argparse.ArgumentParser()
|
| 103 |
+
ap.add_argument("--model", required=True)
|
| 104 |
+
ap.add_argument("--bin", required=True)
|
| 105 |
+
ap.add_argument("--budgets", type=int, nargs="+",
|
| 106 |
+
default=[4096, 8192, 16384, 65536])
|
| 107 |
+
ap.add_argument("--rounds", type=int, default=9)
|
| 108 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 109 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 110 |
+
ap.add_argument("--ubatch", type=int, default=128)
|
| 111 |
+
ap.add_argument("--n-predict", type=int, default=48)
|
| 112 |
+
a = ap.parse_args()
|
| 113 |
+
|
| 114 |
+
strict8g = REPO / "tools" / "strict8g.py"
|
| 115 |
+
(HERE / "results").mkdir(parents=True, exist_ok=True)
|
| 116 |
+
rows = []
|
| 117 |
+
for r in range(a.rounds):
|
| 118 |
+
for b in a.budgets:
|
| 119 |
+
prompt = PROMPTS[r % len(PROMPTS)]
|
| 120 |
+
rec = HERE / "results" / f".n9-{b}-{r}.json"
|
| 121 |
+
row = run_one(strict8g, a.bin, a.model, b, prompt, a.n_predict,
|
| 122 |
+
a.ctx, a.threads, a.ubatch, rec)
|
| 123 |
+
row["round"] = r
|
| 124 |
+
rows.append(row)
|
| 125 |
+
out_live = HERE / "results" / "ram_sweep_n9.json"
|
| 126 |
+
out_live.write_text(json.dumps({"partial": True, "rows": rows}, indent=2))
|
| 127 |
+
print(f"[{r + 1}/{a.rounds}] {b:>6} MiB tps={row['decode_tps']} "
|
| 128 |
+
f"slots={row['slots']} anon={row['peak_anon_mib']} "
|
| 129 |
+
f"ok={row['strict_ok']}", flush=True)
|
| 130 |
+
rec.unlink(missing_ok=True)
|
| 131 |
+
Path(str(rec)).with_suffix(".stats").unlink(missing_ok=True)
|
| 132 |
+
|
| 133 |
+
per = {}
|
| 134 |
+
for b in a.budgets:
|
| 135 |
+
sel = [r for r in rows if r["budget_mb"] == b and r["decode_tps"]]
|
| 136 |
+
tps = [r["decode_tps"] for r in sel]
|
| 137 |
+
per[b] = {
|
| 138 |
+
"n": len(tps),
|
| 139 |
+
"mean_tps": statistics.mean(tps),
|
| 140 |
+
"stdev_tps": statistics.stdev(tps) if len(tps) > 1 else None,
|
| 141 |
+
"median_tps": statistics.median(tps),
|
| 142 |
+
"min_tps": min(tps), "max_tps": max(tps),
|
| 143 |
+
"slots": sel[0]["slots"], "arena_gib": sel[0]["arena_gib"],
|
| 144 |
+
"hit_pct": statistics.median([r["hit_pct"] for r in sel if r["hit_pct"]]) if sel[0]["hit_pct"] else None,
|
| 145 |
+
"mb_per_token": sel[0]["mb_per_token"],
|
| 146 |
+
"peak_anon_mb": max((r["peak_anon_mib"] for r in sel if r["peak_anon_mib"] is not None), default=None),
|
| 147 |
+
"peak_swap_mb": max((r["peak_swap_mb"] for r in sel if r["peak_swap_mb"] is not None), default=None),
|
| 148 |
+
"all_strict_ok": all(r["strict_ok"] for r in sel),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
tests = {}
|
| 152 |
+
base = a.budgets[1] if len(a.budgets) > 1 else a.budgets[0]
|
| 153 |
+
for b in a.budgets:
|
| 154 |
+
if b == base:
|
| 155 |
+
continue
|
| 156 |
+
t, p = welch([r["decode_tps"] for r in rows if r["budget_mb"] == b and r["decode_tps"]],
|
| 157 |
+
[r["decode_tps"] for r in rows if r["budget_mb"] == base and r["decode_tps"]])
|
| 158 |
+
tests[f"{b}_vs_{base}"] = {"t": t, "p": p}
|
| 159 |
+
|
| 160 |
+
out = HERE / "results" / "ram_sweep_n9.json"
|
| 161 |
+
out.write_text(json.dumps({"per_budget": per, "welch_vs_%d" % base: tests,
|
| 162 |
+
"rows": rows}, indent=2))
|
| 163 |
+
print(json.dumps({"per_budget": per, "welch": tests}, indent=2))
|
| 164 |
+
print(f"-> {out}")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
main()
|
bench/run_negative.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Negative results: settings that looked promising and did not pay off.
|
| 3 |
+
|
| 4 |
+
Two knobs are measured end to end under the same 8 GiB ceiling as the headline:
|
| 5 |
+
|
| 6 |
+
prefetch
|
| 7 |
+
Paging the next layer's experts while the current layer computes. The pager
|
| 8 |
+
can predict them from the previous token's routing, but only a small share
|
| 9 |
+
of the predicted experts are actually used, so the extra reads are mostly
|
| 10 |
+
waste. ``SDQ_PREFETCH_LAYER_AHEAD=1`` turns it on.
|
| 11 |
+
|
| 12 |
+
threads
|
| 13 |
+
Only n_expert_used = 8 experts per token can run in parallel inside one
|
| 14 |
+
layer, so more threads mostly contend for the same rows.
|
| 15 |
+
|
| 16 |
+
Output: bench/results/strict_prefetch.json and bench/results/strict_threads.json
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import re
|
| 25 |
+
import statistics
|
| 26 |
+
import subprocess
|
| 27 |
+
import sys
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
HERE = Path(__file__).resolve().parent
|
| 31 |
+
REPO = HERE.parent
|
| 32 |
+
|
| 33 |
+
PROMPTS = [
|
| 34 |
+
"Explain in one sentence why SSDs are slow.",
|
| 35 |
+
"請用一句話介紹你自己。",
|
| 36 |
+
"Write a short paragraph about SSD caching.",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def run(strict8g: Path, binary: str, model: str, prompt: str, threads: int,
|
| 41 |
+
n_predict: int, ctx: int, ubatch: int, budget_mb: int,
|
| 42 |
+
record: Path, reserve_mb: int, env_extra: dict) -> dict:
|
| 43 |
+
cmd = [
|
| 44 |
+
sys.executable, str(strict8g),
|
| 45 |
+
"--limit-mb", str(budget_mb), "--record", str(record), "--",
|
| 46 |
+
binary, "-m", model, "-p", prompt, "-n", str(n_predict),
|
| 47 |
+
"-c", str(ctx), "-t", str(threads), "-b", str(ubatch),
|
| 48 |
+
"--temp", "0", "--no-chat", "--no-interactive",
|
| 49 |
+
"--ram-budget-mb", str(budget_mb),
|
| 50 |
+
"--stats-file", str(record.with_suffix(".stats")),
|
| 51 |
+
]
|
| 52 |
+
env = dict(os.environ, SDQ_RESERVE_MB=str(reserve_mb))
|
| 53 |
+
env.update(env_extra)
|
| 54 |
+
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
| 55 |
+
out = p.stdout + p.stderr
|
| 56 |
+
|
| 57 |
+
rec, stats = {}, {}
|
| 58 |
+
if record.exists():
|
| 59 |
+
try:
|
| 60 |
+
rec = json.loads(record.read_text())
|
| 61 |
+
except Exception:
|
| 62 |
+
pass
|
| 63 |
+
sp = record.with_suffix(".stats")
|
| 64 |
+
if sp.exists():
|
| 65 |
+
for line in sp.read_text().splitlines():
|
| 66 |
+
if line.strip().startswith("{"):
|
| 67 |
+
stats = json.loads(line)
|
| 68 |
+
|
| 69 |
+
m = re.search(r"= ([\d.]+) tok/s", out)
|
| 70 |
+
return {
|
| 71 |
+
"rc": p.returncode,
|
| 72 |
+
"strict_ok": bool(rec.get("strict_ok", False)),
|
| 73 |
+
"tps": float(m.group(1)) if m else None,
|
| 74 |
+
"hit_pct": stats.get("hit_rate"),
|
| 75 |
+
"mb_per_token": (stats["ssd_bytes_per_token"] / 1048576.0) if stats.get("ssd_bytes_per_token") else None,
|
| 76 |
+
"prefetch_issued": stats.get("prefetch_issued"),
|
| 77 |
+
"prefetch_used": stats.get("prefetch_used"),
|
| 78 |
+
"peak_anon_mb": rec.get("peak_anon_mb"),
|
| 79 |
+
"peak_swap_mb": rec.get("peak_swap_mb"),
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def sweep(arms: dict, a, label: str, out_name: str) -> None:
|
| 84 |
+
strict8g = REPO / "tools" / "strict8g.py"
|
| 85 |
+
(HERE / "results").mkdir(parents=True, exist_ok=True)
|
| 86 |
+
rows = []
|
| 87 |
+
for r in range(a.rounds):
|
| 88 |
+
for name, cfg in arms.items():
|
| 89 |
+
prompt = PROMPTS[r % len(PROMPTS)]
|
| 90 |
+
rec = HERE / "results" / f".{out_name}-{name}-{r}.json"
|
| 91 |
+
row = run(strict8g, a.bin, a.model, prompt, cfg["threads"],
|
| 92 |
+
a.n_predict, a.ctx, a.ubatch, a.budget_mb, rec,
|
| 93 |
+
cfg["reserve_mb"], cfg.get("env", {}))
|
| 94 |
+
row["arm"] = name
|
| 95 |
+
row["round"] = r
|
| 96 |
+
rows.append(row)
|
| 97 |
+
print(f"[{r + 1}/{a.rounds}] {name:<14} tps={row['tps']} "
|
| 98 |
+
f"hit={row['hit_pct']} MB/tok={row['mb_per_token']} "
|
| 99 |
+
f"ok={row['strict_ok']}", flush=True)
|
| 100 |
+
rec.unlink(missing_ok=True)
|
| 101 |
+
Path(str(rec) + ".stats").with_suffix(".stats").unlink(missing_ok=True)
|
| 102 |
+
|
| 103 |
+
summary = {}
|
| 104 |
+
for name in arms:
|
| 105 |
+
tps = [r["tps"] for r in rows if r["arm"] == name and r["tps"]]
|
| 106 |
+
summary[name] = {
|
| 107 |
+
"n": len(tps),
|
| 108 |
+
"median_tps": statistics.median(tps) if tps else None,
|
| 109 |
+
"mean_tps": statistics.mean(tps) if tps else None,
|
| 110 |
+
"stdev_tps": statistics.stdev(tps) if len(tps) > 1 else None,
|
| 111 |
+
"min_tps": min(tps) if tps else None,
|
| 112 |
+
"max_tps": max(tps) if tps else None,
|
| 113 |
+
"all_strict_ok": all(r["strict_ok"] for r in rows if r["arm"] == name),
|
| 114 |
+
}
|
| 115 |
+
out = HERE / "results" / out_name
|
| 116 |
+
out.write_text(json.dumps({"label": label, "rows": rows, "summary": summary},
|
| 117 |
+
indent=2, ensure_ascii=False))
|
| 118 |
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 119 |
+
print(f"-> {out}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def main() -> None:
|
| 123 |
+
ap = argparse.ArgumentParser()
|
| 124 |
+
ap.add_argument("--model", required=True)
|
| 125 |
+
ap.add_argument("--bin", required=True)
|
| 126 |
+
ap.add_argument("--which", choices=["prefetch", "threads"], required=True)
|
| 127 |
+
ap.add_argument("--rounds", type=int, default=6)
|
| 128 |
+
ap.add_argument("--budget-mb", type=int, default=8192)
|
| 129 |
+
ap.add_argument("--reserve-mb", type=int, default=512)
|
| 130 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 131 |
+
ap.add_argument("--ubatch", type=int, default=128)
|
| 132 |
+
ap.add_argument("--n-predict", type=int, default=48)
|
| 133 |
+
a = ap.parse_args()
|
| 134 |
+
|
| 135 |
+
if a.which == "prefetch":
|
| 136 |
+
arms = {
|
| 137 |
+
"prefetch_off": {"threads": 8,
|
| 138 |
+
"reserve_mb": a.reserve_mb, "env": {}},
|
| 139 |
+
"prefetch_layer_ahead": {"threads": 8, "reserve_mb": a.reserve_mb,
|
| 140 |
+
"env": {"SDQ_PREFETCH_LAYER_AHEAD": "1"}},
|
| 141 |
+
"prefetch_end_token": {"threads": 8, "reserve_mb": a.reserve_mb,
|
| 142 |
+
"env": {"SDQ_PREFETCH": "1",
|
| 143 |
+
"SDQ_PREFETCH_LAYER_AHEAD": "0"}},
|
| 144 |
+
}
|
| 145 |
+
sweep(arms, a, "prefetch", "strict_prefetch.json")
|
| 146 |
+
else:
|
| 147 |
+
arms = {
|
| 148 |
+
f"threads_{t}": {"threads": t, "reserve_mb": a.reserve_mb, "env": {}}
|
| 149 |
+
for t in (4, 8, 10, 12, 16)
|
| 150 |
+
}
|
| 151 |
+
sweep(arms, a, "threads", "strict_threads.json")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
main()
|
bench/run_ramsweep.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""RAM expansion sweep: run the paged model at several physical RAM budgets.
|
| 3 |
+
|
| 4 |
+
Every run is governed by tools/strict8g.py, so the ceiling is enforced while the
|
| 5 |
+
process runs rather than inferred afterwards. The curve deliberately runs without
|
| 6 |
+
mlock (see docs/STRICT_8GIB.md): mlock() forces the whole mapping resident, which
|
| 7 |
+
would report the configured arena size as RSS instead of real usage.
|
| 8 |
+
|
| 9 |
+
Output: bench/results/ram_sweep.json
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import subprocess
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
HERE = Path(__file__).resolve().parent
|
| 23 |
+
REPO = HERE.parent
|
| 24 |
+
|
| 25 |
+
PROMPTS = [
|
| 26 |
+
"Explain in one sentence why SSDs are slow.",
|
| 27 |
+
"請用一句話介紹你自己。",
|
| 28 |
+
"Write a short paragraph about SSD caching.",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def run_one(strict8g: Path, binary: str, model: str, budget_mb: int,
|
| 33 |
+
prompt: str, n_predict: int, ctx: int, threads: int, ubatch: int,
|
| 34 |
+
record: Path) -> dict:
|
| 35 |
+
cmd = [
|
| 36 |
+
sys.executable, str(strict8g),
|
| 37 |
+
"--limit-mb", str(budget_mb),
|
| 38 |
+
"--record", str(record),
|
| 39 |
+
"--no-lock",
|
| 40 |
+
"--",
|
| 41 |
+
binary, "-m", model, "-p", prompt, "-n", str(n_predict),
|
| 42 |
+
"-c", str(ctx), "-t", str(threads), "-b", str(ubatch),
|
| 43 |
+
"--temp", "0", "--no-chat", "--no-interactive",
|
| 44 |
+
"--ram-budget-mb", str(budget_mb),
|
| 45 |
+
"--stats-file", str(record.with_suffix(".stats")),
|
| 46 |
+
]
|
| 47 |
+
# Reserve scales with the budget. budget/16 reproduces the calibration
|
| 48 |
+
# measured at 8 GiB (512 MiB, 446 MiB of headroom) and keeps a comparable
|
| 49 |
+
# margin at 4 GiB, where a fixed 512 MiB reserve overruns the ceiling.
|
| 50 |
+
reserve = max(128, min(1024, budget_mb // 16))
|
| 51 |
+
env = dict(os.environ, SDQ_RESERVE_MB=str(reserve))
|
| 52 |
+
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
| 53 |
+
out = p.stdout + p.stderr
|
| 54 |
+
|
| 55 |
+
def grab(pat, cast=float):
|
| 56 |
+
m = re.search(pat, out)
|
| 57 |
+
return cast(m.group(1)) if m else None
|
| 58 |
+
|
| 59 |
+
rec = {}
|
| 60 |
+
if record.exists():
|
| 61 |
+
try:
|
| 62 |
+
rec = json.loads(record.read_text())
|
| 63 |
+
except Exception:
|
| 64 |
+
rec = {}
|
| 65 |
+
# the pager's own counters (hit rate, SSD bytes, prefetch) go to --stats-file
|
| 66 |
+
stats = {}
|
| 67 |
+
sp = record.with_suffix(".stats")
|
| 68 |
+
if sp.exists():
|
| 69 |
+
try:
|
| 70 |
+
for line in sp.read_text().splitlines():
|
| 71 |
+
line = line.strip()
|
| 72 |
+
if line.startswith("{"):
|
| 73 |
+
stats = json.loads(line)
|
| 74 |
+
except Exception:
|
| 75 |
+
stats = {}
|
| 76 |
+
|
| 77 |
+
return {
|
| 78 |
+
"budget_mb": budget_mb,
|
| 79 |
+
"reserve_mb": max(128, min(1024, budget_mb // 16)),
|
| 80 |
+
"rc": p.returncode,
|
| 81 |
+
"strict_ok": bool(rec.get("strict_ok", False)),
|
| 82 |
+
"decode_tps": grab(r"decode: (\d+) tok / ([\d.]+)s = ([\d.]+) tok/s", str) and
|
| 83 |
+
float(re.search(r"= ([\d.]+) tok/s", out).group(1)),
|
| 84 |
+
"hit_pct": stats.get("hit_rate"),
|
| 85 |
+
"mb_per_token": (stats["ssd_bytes_per_token"] / 1048576.0) if stats.get("ssd_bytes_per_token") else None,
|
| 86 |
+
"slots": int(grab(r"slots=(\d+)", str) or 0),
|
| 87 |
+
"arena_gib": grab(r"arena=([\d.]+) GiB"),
|
| 88 |
+
"peak_anon_mib": rec.get("peak_anon_mb") or grab(r"anon=([\d.]+) MiB"),
|
| 89 |
+
"peak_swap_mb": rec.get("peak_swap_mb"),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def main() -> None:
|
| 94 |
+
ap = argparse.ArgumentParser()
|
| 95 |
+
ap.add_argument("--model", required=True)
|
| 96 |
+
ap.add_argument("--bin", required=True)
|
| 97 |
+
ap.add_argument("--budgets", type=int, nargs="+",
|
| 98 |
+
default=[4096, 8192, 16384, 32768, 65536])
|
| 99 |
+
ap.add_argument("--rounds", type=int, default=3)
|
| 100 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 101 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 102 |
+
ap.add_argument("--ubatch", type=int, default=128)
|
| 103 |
+
ap.add_argument("--n-predict", type=int, default=48)
|
| 104 |
+
ap.add_argument("--out", default=str(HERE / "results" / "ram_sweep.json"))
|
| 105 |
+
a = ap.parse_args()
|
| 106 |
+
|
| 107 |
+
strict8g = REPO / "tools" / "strict8g.py"
|
| 108 |
+
rows = []
|
| 109 |
+
# Interleave budgets round-robin so drift in machine load hits every budget
|
| 110 |
+
# equally instead of penalising whichever one ran last.
|
| 111 |
+
for r in range(a.rounds):
|
| 112 |
+
for b in a.budgets:
|
| 113 |
+
prompt = PROMPTS[r % len(PROMPTS)]
|
| 114 |
+
rec = HERE / "results" / f".ramsweep-{b}-{r}.json"
|
| 115 |
+
rec.parent.mkdir(parents=True, exist_ok=True)
|
| 116 |
+
row = run_one(strict8g, a.bin, a.model, b, prompt, a.n_predict,
|
| 117 |
+
a.ctx, a.threads, a.ubatch, rec)
|
| 118 |
+
row["round"] = r
|
| 119 |
+
row["prompt"] = prompt
|
| 120 |
+
rows.append(row)
|
| 121 |
+
ok = "ok" if row["strict_ok"] else "FAIL"
|
| 122 |
+
print(f"[{r + 1}/{a.rounds}] budget={b:>6} MiB tps={row['decode_tps']} "
|
| 123 |
+
f"slots={row['slots']} arena={row['arena_gib']} "
|
| 124 |
+
f"anon={row['peak_anon_mib']} swap={row['peak_swap_mb']} {ok}",
|
| 125 |
+
flush=True)
|
| 126 |
+
rec.unlink(missing_ok=True)
|
| 127 |
+
|
| 128 |
+
out = Path(a.out)
|
| 129 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 130 |
+
out.write_text(json.dumps(rows, indent=2))
|
| 131 |
+
print(f"-> {out}")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
main()
|
bench/summarize.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Render the raw bench JSON into the markdown tables used by README.md and docs/."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import statistics
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
HERE = Path(__file__).resolve().parent
|
| 11 |
+
RES = HERE / "results"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def med(rows, key):
|
| 15 |
+
vals = [r[key] for r in rows if r.get(key) is not None]
|
| 16 |
+
return statistics.median(vals) if vals else None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def fmt(v, nd=2):
|
| 20 |
+
return "—" if v is None else f"{v:.{nd}f}"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def budget_curve():
|
| 24 |
+
f = RES / "strict_budgets.json"
|
| 25 |
+
if not f.exists():
|
| 26 |
+
return None
|
| 27 |
+
rows = json.loads(f.read_text())
|
| 28 |
+
budgets = sorted({r["budget_mb"] for r in rows})
|
| 29 |
+
lines = ["| Budget | Arena (GiB) | 槽位 | 命中率 | SSD MB/token | Decode tok/s | 峰值匿名 RSS | VmSwap |",
|
| 30 |
+
"| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"]
|
| 31 |
+
for b in budgets:
|
| 32 |
+
sel = [r for r in rows if r["budget_mb"] == b]
|
| 33 |
+
ok = [r for r in sel if r["strict_ok"]]
|
| 34 |
+
lines.append(
|
| 35 |
+
f"| {b // 1024} GiB | {fmt(med(sel, 'arena_gib'))} | "
|
| 36 |
+
f"{int(med(sel, 'slots') or 0)} | {fmt(med(sel, 'hit_pct'))}% | "
|
| 37 |
+
f"{fmt(med(sel, 'mb_per_token'), 1)} | {fmt(med(sel, 'decode_tps'))} | "
|
| 38 |
+
f"{fmt(med(sel, 'peak_anon_mb'), 0)} | {fmt(med(sel, 'peak_swap_mb'), 3)} |")
|
| 39 |
+
return "\n".join(lines)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def ram_sweep():
|
| 43 |
+
f = RES / "ram_sweep_n9.json"
|
| 44 |
+
src = f if f.exists() else (RES / "ram_sweep.json")
|
| 45 |
+
if not src.exists():
|
| 46 |
+
return None, None
|
| 47 |
+
data = json.loads(src.read_text())
|
| 48 |
+
if "per_budget" in data:
|
| 49 |
+
per = data["per_budget"]
|
| 50 |
+
tests = data.get("welch_vs_8192", {})
|
| 51 |
+
n = max(v["n"] for v in per.values())
|
| 52 |
+
lines = [f"| Budget | Arena (GiB) | 槽位 | 命中率 | SSD MB/token | 平均 tok/s | 單輪標準差 | 峰值 RSS | VmSwap |",
|
| 53 |
+
"| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"]
|
| 54 |
+
for b, v in sorted(per.items(), key=lambda kv: int(kv[0])):
|
| 55 |
+
lines.append(
|
| 56 |
+
f"| {int(b) // 1024} GiB | {fmt(v['arena_gib'])} | {int(v['slots'])} | "
|
| 57 |
+
f"{fmt(v['hit_pct'])}% | {fmt(v['mb_per_token'], 1)} | "
|
| 58 |
+
f"{fmt(v['mean_tps'])} ± {fmt(v['stdev_tps'])} | "
|
| 59 |
+
f"{fmt(v['peak_anon_mb'], 0)} | {fmt(v['peak_swap_mb'], 3)} |")
|
| 60 |
+
tlines = ["| 比較 | 只改變了 | 平均 tok/s | Welch t | p | 判定 |", "| --- | --- | ---: | ---: | ---: | --- |"]
|
| 61 |
+
base = sorted(per, key=lambda k: int(k))[1] if len(per) > 1 else None
|
| 62 |
+
for name, t in tests.items():
|
| 63 |
+
other, _, b = name.partition("_vs_")
|
| 64 |
+
o, bb = per[other], per[b]
|
| 65 |
+
verdict = "顯著" if t["p"] < 0.05 else "無法分辨"
|
| 66 |
+
tlines.append(
|
| 67 |
+
f"| {int(other) // 1024} GiB 比 {int(b) // 1024} GiB | RAM 預算 | "
|
| 68 |
+
f"{o['mean_tps']:.2f} → {bb['mean_tps']:.2f} "
|
| 69 |
+
f"({(o['mean_tps'] / bb['mean_tps'] - 1) * 100:+.0f}%) | "
|
| 70 |
+
f"{t['t']:+.2f} | {t['p']:.3f} | {verdict} |")
|
| 71 |
+
return "\n".join(lines), "\n".join(tlines)
|
| 72 |
+
rows = data["rows"] if isinstance(data, dict) else data
|
| 73 |
+
budgets = sorted({r["budget_mb"] for r in rows})
|
| 74 |
+
lines = ["| Budget | Arena (GiB) | 槽位 | 命中率 | SSD MB/token | Decode tok/s | 峰值 RSS | VmSwap |",
|
| 75 |
+
"| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"]
|
| 76 |
+
for b in budgets:
|
| 77 |
+
sel = [r for r in rows if r["budget_mb"] == b]
|
| 78 |
+
lines.append(
|
| 79 |
+
f"| {b // 1024} GiB | {fmt(med(sel, 'arena_gib'))} | {int(med(sel, 'slots') or 0)} | "
|
| 80 |
+
f"{fmt(med(sel, 'hit_pct'))}% | {fmt(med(sel, 'mb_per_token'), 1)} | "
|
| 81 |
+
f"{fmt(med(sel, 'decode_tps'))} | {fmt(med(sel, 'peak_anon_mib'), 0)} | "
|
| 82 |
+
f"{fmt(med(sel, 'peak_swap_mb'), 3)} |")
|
| 83 |
+
return "\n".join(lines), None
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def headline():
|
| 87 |
+
for name in ("strict_headline.json",):
|
| 88 |
+
f = RES / name
|
| 89 |
+
if not f.exists():
|
| 90 |
+
return None
|
| 91 |
+
return json.loads(f.read_text())
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def negatives():
|
| 95 |
+
out = {}
|
| 96 |
+
f = RES / "strict_threads.json"
|
| 97 |
+
if f.exists():
|
| 98 |
+
out["threads"] = json.loads(f.read_text())["summary"]
|
| 99 |
+
f = RES / "strict_prefetch.json"
|
| 100 |
+
if f.exists():
|
| 101 |
+
out["prefetch"] = json.loads(f.read_text())["summary"]
|
| 102 |
+
return out
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def main() -> None:
|
| 106 |
+
parts = {}
|
| 107 |
+
parts["budget_curve"] = budget_curve()
|
| 108 |
+
sweep, tests = ram_sweep()
|
| 109 |
+
parts["ram_sweep"] = sweep
|
| 110 |
+
parts["ram_sweep_tests"] = tests
|
| 111 |
+
parts["negatives"] = negatives()
|
| 112 |
+
print(json.dumps(parts, indent=2, ensure_ascii=False))
|
| 113 |
+
(RES / "summary.json").write_text(json.dumps(parts, indent=2, ensure_ascii=False))
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
if __name__ == "__main__":
|
| 117 |
+
main()
|
bench/upload.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Upload the project code, docs and measurement results to the HF repo."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from huggingface_hub import HfApi
|
| 12 |
+
|
| 13 |
+
# this script lives in bench/, the repo root is its parent
|
| 14 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 15 |
+
|
| 16 |
+
MSG = """EPM: expert paging CPU inference for MiniCPM-V-4.7-35B-A3B Q4_K_M
|
| 17 |
+
|
| 18 |
+
Port the 8 GiB expert pager to openbmb/MiniCPM-V-4.7-35B-A3B-Q4_K_M.gguf
|
| 19 |
+
(19.71 GiB, qwen35moe, 40 layers, 256 experts, top-8).
|
| 20 |
+
|
| 21 |
+
Measured under an 8 GiB / no-swap ceiling enforced by tools/strict8g.py:
|
| 22 |
+
5.83 tok/s median (N=9), peak anon RSS 7799 MiB, VmSwap 0, 9/9 strict_ok.
|
| 23 |
+
Unmodified llama.cpp cannot load the model at that ceiling.
|
| 24 |
+
|
| 25 |
+
Correctness: 3-layer verification passes; 5 runs x 48 tokens x 248144 vocab
|
| 26 |
+
logits bit-identical.
|
| 27 |
+
|
| 28 |
+
Port-specific changes: GGUF BOOL-array header fix, calibrated SDQ_RESERVE_MB,
|
| 29 |
+
two expert size classes, hybrid SSM/attention thread behaviour.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main() -> int:
|
| 34 |
+
ap = argparse.ArgumentParser()
|
| 35 |
+
ap.add_argument("--repo", default=os.environ.get("EPM_HF_REPO", "HelloSun/minicpm47woek"))
|
| 36 |
+
ap.add_argument("--folder", default=str(REPO))
|
| 37 |
+
ap.add_argument("--message", default=MSG)
|
| 38 |
+
ap.add_argument("--dry-run", action="store_true")
|
| 39 |
+
a = ap.parse_args()
|
| 40 |
+
|
| 41 |
+
api = HfApi()
|
| 42 |
+
print("whoami:", api.whoami()["name"])
|
| 43 |
+
if a.dry_run:
|
| 44 |
+
print("dry run, not uploading")
|
| 45 |
+
return 0
|
| 46 |
+
api.upload_folder(
|
| 47 |
+
folder_path=a.folder,
|
| 48 |
+
repo_id=a.repo,
|
| 49 |
+
repo_type="model",
|
| 50 |
+
ignore_patterns=[".git/*", "__pycache__/*", "*.pyc", "*.so",
|
| 51 |
+
"bench/results/.*", "*.gguf", "*.bin"],
|
| 52 |
+
commit_message=a.message,
|
| 53 |
+
)
|
| 54 |
+
print(f"uploaded -> https://huggingface.co/{a.repo}")
|
| 55 |
+
return 0
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
sys.exit(main())
|
cpp/sdq_buft.cpp
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 15 |
+
#include <cstdio>
|
| 16 |
+
#include <cstdlib>
|
| 17 |
+
#include <cstring>
|
| 18 |
+
#include <algorithm>
|
| 19 |
+
#include <mutex>
|
| 20 |
+
#include <unordered_map>
|
| 21 |
+
#include <vector>
|
| 22 |
+
|
| 23 |
+
namespace sdq {
|
| 24 |
+
|
| 25 |
+
// 給 tensor->data 的假位址。每個 expert 權重張量分到一小塊(預設 4 KB),
|
| 26 |
+
// 指標必須有效(ggml 會做指標運算),但內容永遠不會被讀。
|
| 27 |
+
struct expert_buffer {
|
| 28 |
+
std::vector<uint8_t> scratch;
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
static bool g_enabled = false;
|
| 32 |
+
|
| 33 |
+
void sdq_set_enabled(bool on) { g_enabled = on; }
|
| 34 |
+
bool sdq_enabled() { return g_enabled; }
|
| 35 |
+
|
| 36 |
+
static config g_cfg;
|
| 37 |
+
void sdq_set_config(const config & c) { g_cfg = c; }
|
| 38 |
+
const config & sdq_get_config() { return g_cfg; }
|
| 39 |
+
|
| 40 |
+
// ---------------------------------------------------------------- buffer iface
|
| 41 |
+
|
| 42 |
+
// ggml_backend_buffer_i 的欄位依賴下面的實作,先給宣告
|
| 43 |
+
struct ggml_backend_buffer_i sdq_buffer_iface();
|
| 44 |
+
|
| 45 |
+
static const char * sdq_buft_get_name(ggml_backend_buffer_type_t buft) {
|
| 46 |
+
(void) buft;
|
| 47 |
+
return "SDQ_ExpertSSD";
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
static size_t sdq_buft_get_alignment(ggml_backend_buffer_type_t buft) {
|
| 51 |
+
(void) buft;
|
| 52 |
+
return 4096;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
static size_t sdq_buft_get_max_size(ggml_backend_buffer_type_t buft) {
|
| 56 |
+
(void) buft;
|
| 57 |
+
return SIZE_MAX;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// 每個 expert 權重張量只回報 4 KB:18.2 GiB 的權重「不會」被載入 RAM 的關鍵。
|
| 61 |
+
static size_t sdq_buft_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) {
|
| 62 |
+
(void) buft;
|
| 63 |
+
(void) tensor;
|
| 64 |
+
return 4096;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
static bool sdq_buft_is_host(ggml_backend_buffer_type_t buft) {
|
| 68 |
+
(void) buft;
|
| 69 |
+
return false;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
static ggml_backend_buffer_t sdq_buft_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) {
|
| 73 |
+
// llama.cpp 會用 get_alloc_size() 計算總大小後呼叫本函式;
|
| 74 |
+
// 我們只保留真正需要的假位址空間(每個張量 4 KB)。
|
| 75 |
+
const size_t bytes = size < 4096 ? 4096 : size;
|
| 76 |
+
auto * buf = new expert_buffer();
|
| 77 |
+
buf->scratch.assign(bytes, 0);
|
| 78 |
+
auto * b = new ggml_backend_buffer{
|
| 79 |
+
/* .iface = */ sdq_buffer_iface(),
|
| 80 |
+
/* .buft = */ buft,
|
| 81 |
+
/* .context = */ buf,
|
| 82 |
+
/* .size = */ bytes,
|
| 83 |
+
/* .usage = */ GGML_BACKEND_BUFFER_USAGE_WEIGHTS,
|
| 84 |
+
};
|
| 85 |
+
return b;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// 一次配置整個 context 的張量:每個張量 4 KB 假位址,實際權重 0 RAM。
|
| 89 |
+
static ggml_backend_buffer_t sdq_buft_alloc_buffer_n(ggml_backend_buffer_type_t buft,
|
| 90 |
+
ggml_tensor ** tensors, int n_tensors) {
|
| 91 |
+
const size_t slot = 4096;
|
| 92 |
+
const size_t bytes = slot * (size_t) std::max(n_tensors, 1);
|
| 93 |
+
ggml_backend_buffer_t buffer = sdq_buft_alloc_buffer(buft, bytes);
|
| 94 |
+
if (!buffer) {
|
| 95 |
+
return nullptr;
|
| 96 |
+
}
|
| 97 |
+
uint8_t * base = (uint8_t *) buffer->iface.get_base(buffer);
|
| 98 |
+
size_t off = 0;
|
| 99 |
+
for (int i = 0; i < n_tensors; i++) {
|
| 100 |
+
ggml_tensor * t = tensors[i];
|
| 101 |
+
t->data = base + off;
|
| 102 |
+
t->buffer = buffer;
|
| 103 |
+
off += slot;
|
| 104 |
+
buffer->iface.init_tensor(buffer, t);
|
| 105 |
+
}
|
| 106 |
+
return buffer;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
static void sdq_buft_free_buffer(ggml_backend_buffer_t buffer) {
|
| 110 |
+
// 只釋放自己的資料;ggml_backend_buffer_free() 會負責 delete buffer 本體,
|
| 111 |
+
// 這裡再 delete 一次會造成 double free。
|
| 112 |
+
delete (expert_buffer *) buffer->context;
|
| 113 |
+
buffer->context = nullptr;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
static void * sdq_buft_get_base(ggml_backend_buffer_t buffer) {
|
| 117 |
+
return ((expert_buffer *) buffer->context)->scratch.data();
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
static ggml_status sdq_buft_init_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor) {
|
| 121 |
+
(void) buffer;
|
| 122 |
+
(void) tensor;
|
| 123 |
+
return GGML_STATUS_SUCCESS;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
static void sdq_buft_memset_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
|
| 127 |
+
uint8_t value, size_t offset, size_t size) {
|
| 128 |
+
(void) buffer; (void) tensor; (void) value; (void) offset; (void) size;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
static void sdq_buft_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor,
|
| 132 |
+
const void * data, size_t offset, size_t size) {
|
| 133 |
+
// 關鍵:忽略。llama.cpp 會把 mmap 的資料交給我們,這裡一個位元組都不落地,
|
| 134 |
+
// 專家權利永遠不會佔用 RAM。
|
| 135 |
+
(void) buffer; (void) tensor; (void) data; (void) offset; (void) size;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
static void sdq_buft_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * tensor,
|
| 139 |
+
void * data, size_t offset, size_t size) {
|
| 140 |
+
(void) buffer; (void) tensor; (void) offset;
|
| 141 |
+
memset(data, 0, size);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
static bool sdq_buft_cpy_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * src, ggml_tensor * dst) {
|
| 145 |
+
(void) buffer; (void) src; (void) dst;
|
| 146 |
+
return true;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
static void sdq_buft_clear(ggml_backend_buffer_t buffer, uint8_t value) {
|
| 150 |
+
(void) buffer; (void) value;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
struct ggml_backend_buffer_i sdq_buffer_iface() {
|
| 154 |
+
static ggml_backend_buffer_i i = [] {
|
| 155 |
+
ggml_backend_buffer_i x {};
|
| 156 |
+
x.free_buffer = sdq_buft_free_buffer;
|
| 157 |
+
x.get_base = sdq_buft_get_base;
|
| 158 |
+
x.init_tensor = sdq_buft_init_tensor;
|
| 159 |
+
x.memset_tensor = sdq_buft_memset_tensor;
|
| 160 |
+
x.set_tensor = sdq_buft_set_tensor;
|
| 161 |
+
x.get_tensor = sdq_buft_get_tensor;
|
| 162 |
+
x.cpy_tensor = sdq_buft_cpy_tensor;
|
| 163 |
+
x.clear = sdq_buft_clear;
|
| 164 |
+
return x;
|
| 165 |
+
}();
|
| 166 |
+
return i;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
ggml_backend_buffer_type_t sdq_expert_buft() {
|
| 170 |
+
static ggml_backend_buffer_type_t buft = [] {
|
| 171 |
+
return new ggml_backend_buffer_type{
|
| 172 |
+
/* .iface = */ {
|
| 173 |
+
/* .get_name = */ sdq_buft_get_name,
|
| 174 |
+
/* .alloc_buffer = */ sdq_buft_alloc_buffer,
|
| 175 |
+
/* .alloc_buffer_n = */ sdq_buft_alloc_buffer_n,
|
| 176 |
+
/* .get_alignment = */ sdq_buft_get_alignment,
|
| 177 |
+
/* .get_max_size = */ sdq_buft_get_max_size,
|
| 178 |
+
/* .get_alloc_size = */ sdq_buft_get_alloc_size,
|
| 179 |
+
/* .get_alloc_size_n = */ nullptr,
|
| 180 |
+
/* .is_host = */ sdq_buft_is_host,
|
| 181 |
+
},
|
| 182 |
+
/* .device = */ nullptr,
|
| 183 |
+
/* .context = */ nullptr,
|
| 184 |
+
};
|
| 185 |
+
}();
|
| 186 |
+
return buft;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// llama.cpp 端需要的介面宣告
|
| 190 |
+
void sdq_install_buft_overrides(llama_model_params * params);
|
| 191 |
+
|
| 192 |
+
static const llama_model_tensor_buft_override * sdq_overrides() {
|
| 193 |
+
static llama_model_tensor_buft_override ov[] = {
|
| 194 |
+
{ "blk\\.[0-9]+\\.ffn_(gate|up|down|gate_up)_exps\\.weight", sdq_expert_buft() },
|
| 195 |
+
{ nullptr, nullptr },
|
| 196 |
+
};
|
| 197 |
+
return ov;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
void sdq_install_buft_overrides(llama_model_params * params) {
|
| 201 |
+
if (!params) {
|
| 202 |
+
return;
|
| 203 |
+
}
|
| 204 |
+
if (params->tensor_buft_overrides == nullptr) {
|
| 205 |
+
params->tensor_buft_overrides = sdq_overrides();
|
| 206 |
+
} else {
|
| 207 |
+
// 使用者已有 override:把手動指定的順序原樣保留,附加在尾端
|
| 208 |
+
size_t n = 0;
|
| 209 |
+
while (params->tensor_buft_overrides[n].pattern != nullptr) {
|
| 210 |
+
n++;
|
| 211 |
+
}
|
| 212 |
+
static std::vector<llama_model_tensor_buft_override> merged;
|
| 213 |
+
merged.clear();
|
| 214 |
+
for (size_t i = 0; i < n; i++) {
|
| 215 |
+
merged.push_back(params->tensor_buft_overrides[i]);
|
| 216 |
+
}
|
| 217 |
+
for (size_t i = 0; sdq_overrides()[i].pattern != nullptr; i++) {
|
| 218 |
+
merged.push_back(sdq_overrides()[i]);
|
| 219 |
+
}
|
| 220 |
+
merged.push_back({ nullptr, nullptr });
|
| 221 |
+
params->tensor_buft_overrides = merged.data();
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
} // namespace sdq
|
cpp/sdq_cli.cpp
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// EPQ 聊天 / benchmark 主程式
|
| 2 |
+
//
|
| 3 |
+
// epq-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 |
+
std::string span_test;
|
| 50 |
+
std::string dump_tensors; // ggml eval callback:把指定張量的資料存檔(A/B 比對用)
|
| 51 |
+
bool no_pager = false; // A/B 測試用:不啟用分頁,走上游 mul_mat_id(整個模型在 RAM)
|
| 52 |
+
int32_t arena_mb = 0;
|
| 53 |
+
int32_t ram_budget_mb = 0;
|
| 54 |
+
bool fadvise = true;
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
double now_s() {
|
| 58 |
+
static auto t0 = std::chrono::steady_clock::now();
|
| 59 |
+
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
size_t rss_mb() { return process_rss_bytes() / (1024 * 1024); }
|
| 63 |
+
|
| 64 |
+
// 從 /proc/self/status 讀 VmHWM(峰值 RSS)/ VmSwap(本行程觀測到的 swap 用量;
|
| 65 |
+
// 這不代表系統 swap 已停用,也不構成總 RSS 硬上限)
|
| 66 |
+
size_t proc_status_kb(const char * key) {
|
| 67 |
+
FILE * f = fopen("/proc/self/status", "r");
|
| 68 |
+
if (!f) return 0;
|
| 69 |
+
char line[256];
|
| 70 |
+
const size_t klen = strlen(key);
|
| 71 |
+
size_t val = 0;
|
| 72 |
+
while (fgets(line, sizeof(line), f)) {
|
| 73 |
+
if (strncmp(line, key, klen) == 0) {
|
| 74 |
+
val = (size_t) strtoull(line + klen, nullptr, 10);
|
| 75 |
+
break;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
fclose(f);
|
| 79 |
+
return val;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
size_t peak_rss_mb() { return proc_status_kb("VmHWM:") / 1024; }
|
| 83 |
+
size_t swap_mb() { return proc_status_kb("VmSwap:") / 1024; }
|
| 84 |
+
|
| 85 |
+
// /proc/self/smaps_rollup:把 RSS 拆成「檔案對映」與「匿名」兩塊。
|
| 86 |
+
// 這很重要:模型檔是 mmap 進來的,核心預讀會讓「檔案對映頁」也算進 RSS,
|
| 87 |
+
// 報告時必須分開看,否則會誤以為 RAM 用量比實際少(或多)。
|
| 88 |
+
size_t smaps_kb(const char * key) {
|
| 89 |
+
FILE * f = fopen("/proc/self/smaps_rollup", "r");
|
| 90 |
+
if (!f) return 0;
|
| 91 |
+
char line[256];
|
| 92 |
+
const size_t klen = strlen(key);
|
| 93 |
+
size_t val = 0;
|
| 94 |
+
while (fgets(line, sizeof(line), f)) {
|
| 95 |
+
if (strncmp(line, key, klen) == 0) {
|
| 96 |
+
val = (size_t) strtoull(line + klen, nullptr, 10);
|
| 97 |
+
break;
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
fclose(f);
|
| 101 |
+
return val;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
void print_mem_report(const char * when) {
|
| 105 |
+
fprintf(stderr,
|
| 106 |
+
"[sdq] 記憶體(%s):匿名 RSS %zu MB / 檔案對映 RSS %zu MB / 總 RSS %zu MB / 峰值 %zu MB / 本行程 VmSwap %zu MB(非系統 swap 狀態)\n",
|
| 107 |
+
when, smaps_kb("Anonymous:") / 1024, smaps_kb("RssFile:") / 1024,
|
| 108 |
+
smaps_kb("Rss:") / 1024, peak_rss_mb(), swap_mb());
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// 讀 /proc/self/io:從 block device 實際讀了多少(SSD 讀取)
|
| 112 |
+
uint64_t read_bytes_io() {
|
| 113 |
+
FILE * f = fopen("/proc/self/io", "r");
|
| 114 |
+
if (!f) return 0;
|
| 115 |
+
char line[256];
|
| 116 |
+
uint64_t v = 0;
|
| 117 |
+
while (fgets(line, sizeof(line), f)) {
|
| 118 |
+
if (strncmp(line, "read_bytes:", 11) == 0) {
|
| 119 |
+
v = strtoull(line + 11, nullptr, 10);
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
fclose(f);
|
| 123 |
+
return v;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
struct chat_state {
|
| 127 |
+
std::vector<llama_chat_message> msgs;
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
std::string build_prompt(const options & opt, const chat_state & st) {
|
| 131 |
+
if (!opt.chat) {
|
| 132 |
+
return opt.prompt;
|
| 133 |
+
}
|
| 134 |
+
const char * tmpl = llama_model_chat_template(nullptr, nullptr);
|
| 135 |
+
// 沒有 model 指標時用內建模板;下面 main() 會用實際模型的模板重做
|
| 136 |
+
(void) tmpl;
|
| 137 |
+
size_t n_chars = 0;
|
| 138 |
+
for (const auto & m : st.msgs) {
|
| 139 |
+
n_chars += strlen(m.role) + strlen(m.content) + 32;
|
| 140 |
+
}
|
| 141 |
+
std::vector<char> buf(2 * n_chars + 1024);
|
| 142 |
+
const char * t = st.msgs.empty() ? nullptr : nullptr;
|
| 143 |
+
(void) t;
|
| 144 |
+
int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
|
| 145 |
+
buf.data(), (int32_t) buf.size());
|
| 146 |
+
if (len > (int32_t) buf.size()) {
|
| 147 |
+
buf.resize(len + 1);
|
| 148 |
+
llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
|
| 149 |
+
buf.data(), (int32_t) buf.size());
|
| 150 |
+
}
|
| 151 |
+
return std::string(buf.data());
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
} // namespace
|
| 155 |
+
|
| 156 |
+
#include <execinfo.h>
|
| 157 |
+
#include <signal.h>
|
| 158 |
+
#include <unistd.h>
|
| 159 |
+
static void sdq_segv(int sig) {
|
| 160 |
+
void * bt[64];
|
| 161 |
+
const int n = backtrace(bt, 64);
|
| 162 |
+
fprintf(stderr, "\n*** sdq: signal %d, backtrace:\n", sig);
|
| 163 |
+
backtrace_symbols_fd(bt, n, 2);
|
| 164 |
+
_exit(9);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// eval callback:把感興趣的張量(用 SDQ_DUMP_NAMES 指定)寫進檔案
|
| 168 |
+
struct dump_state {
|
| 169 |
+
FILE * fp = nullptr;
|
| 170 |
+
uint64_t step = 0;
|
| 171 |
+
std::vector<std::string> names;
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
static dump_state g_dump;
|
| 175 |
+
|
| 176 |
+
static bool sdq_eval_callback(ggml_tensor * t, bool ask, void * user_data) {
|
| 177 |
+
(void) user_data;
|
| 178 |
+
auto & ds = g_dump;
|
| 179 |
+
if (ask) {
|
| 180 |
+
// 除錯模式:每個節點都要資料 → 會強制逐節點同步(慢,但能逐節點比對)
|
| 181 |
+
return ds.fp != nullptr;
|
| 182 |
+
}
|
| 183 |
+
if (!ds.fp) {
|
| 184 |
+
return false;
|
| 185 |
+
}
|
| 186 |
+
const char * nm = ggml_get_name(t);
|
| 187 |
+
const size_t n_len = nm ? strlen(nm) : 0;
|
| 188 |
+
bool want = ds.names.empty();
|
| 189 |
+
for (const auto & n : ds.names) {
|
| 190 |
+
if (nm && strstr(nm, n.c_str())) {
|
| 191 |
+
want = true;
|
| 192 |
+
break;
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
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]};
|
| 196 |
+
const int32_t type = (int32_t) t->type;
|
| 197 |
+
fwrite(&n_len, sizeof(size_t), 1, ds.fp);
|
| 198 |
+
if (n_len) {
|
| 199 |
+
fwrite(nm, 1, n_len, ds.fp);
|
| 200 |
+
}
|
| 201 |
+
fwrite(&ds.step, sizeof(uint64_t), 1, ds.fp);
|
| 202 |
+
fwrite(ne, sizeof(int32_t), 4, ds.fp);
|
| 203 |
+
fwrite(&type, sizeof(int32_t), 1, ds.fp);
|
| 204 |
+
|
| 205 |
+
// 永遠寫「摘要」(前 8 個值 + 平方和 + 最大值),這樣即使沒有名字也能 A/B 找出第一個分歧點
|
| 206 |
+
float summary[10] = {0};
|
| 207 |
+
if (t->type == GGML_TYPE_F32 && t->data) {
|
| 208 |
+
const size_t n = (size_t) ne[0] * ne[1] * ne[2] * ne[3];
|
| 209 |
+
const float * p = (const float *) t->data;
|
| 210 |
+
double ss = 0;
|
| 211 |
+
float mx = 0;
|
| 212 |
+
for (size_t i = 0; i < n; i++) {
|
| 213 |
+
ss += (double) p[i] * p[i];
|
| 214 |
+
if (std::fabs(p[i]) > std::fabs(mx)) {
|
| 215 |
+
mx = p[i];
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
for (int i = 0; i < 8 && (size_t) i < n; i++) {
|
| 219 |
+
summary[i] = p[i];
|
| 220 |
+
}
|
| 221 |
+
summary[8] = (float) std::sqrt(ss);
|
| 222 |
+
summary[9] = mx;
|
| 223 |
+
fwrite(summary, sizeof(float), 10, ds.fp);
|
| 224 |
+
if (want) {
|
| 225 |
+
fwrite(p, sizeof(float), n, ds.fp);
|
| 226 |
+
}
|
| 227 |
+
} else {
|
| 228 |
+
fwrite(summary, sizeof(float), 10, ds.fp);
|
| 229 |
+
}
|
| 230 |
+
ds.step++;
|
| 231 |
+
return true; // 這個節點的資料我們已經讀過了,標記成已完成
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
int main(int argc, char ** argv) {
|
| 235 |
+
signal(SIGSEGV, sdq_segv);
|
| 236 |
+
signal(SIGBUS, sdq_segv);
|
| 237 |
+
signal(SIGABRT, sdq_segv);
|
| 238 |
+
options opt;
|
| 239 |
+
const char * env_model = getenv("SDQ_MODEL_PATH");
|
| 240 |
+
if (env_model) {
|
| 241 |
+
opt.model = env_model;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
for (int i = 1; i < argc; i++) {
|
| 245 |
+
std::string a = argv[i];
|
| 246 |
+
auto next = [&](const char * what) -> std::string {
|
| 247 |
+
if (i + 1 >= argc) {
|
| 248 |
+
fprintf(stderr, "%s 需要參數\n", what);
|
| 249 |
+
exit(1);
|
| 250 |
+
}
|
| 251 |
+
return argv[++i];
|
| 252 |
+
};
|
| 253 |
+
if (a == "-m" || a == "--model") opt.model = next("-m");
|
| 254 |
+
else if (a == "-p" || a == "--prompt") opt.prompt = next("-p");
|
| 255 |
+
else if (a == "--sys") opt.system = next("--sys");
|
| 256 |
+
else if (a == "-c" || a == "--ctx") opt.n_ctx = atoi(next("-c").c_str());
|
| 257 |
+
else if (a == "-n") opt.n_predict = atoi(next("-n").c_str());
|
| 258 |
+
else if (a == "-t") opt.n_threads = atoi(next("-t").c_str());
|
| 259 |
+
else if (a == "-b" || a == "--ubatch") opt.n_ubatch = atoi(next("-b").c_str());
|
| 260 |
+
else if (a == "--temp") opt.temp = atof(next("--temp").c_str());
|
| 261 |
+
else if (a == "--top-p") opt.top_p = atof(next("--top-p").c_str());
|
| 262 |
+
else if (a == "--top-k") opt.top_k = atoi(next("--top-k").c_str());
|
| 263 |
+
else if (a == "--no-chat") opt.chat = false;
|
| 264 |
+
else if (a == "--no-interactive") opt.interactive = false;
|
| 265 |
+
else if (a == "--bench") opt.bench = true;
|
| 266 |
+
else if (a == "--no-prefetch") opt.no_prefetch = true;
|
| 267 |
+
else if (a == "--arena-mb") opt.arena_mb = atoi(next("--arena-mb").c_str());
|
| 268 |
+
else if (a == "--ram-budget-mb") opt.ram_budget_mb = atoi(next("--ram-budget-mb").c_str());
|
| 269 |
+
else if (a == "--no-fadvise") opt.fadvise = false;
|
| 270 |
+
else if (a == "--stats-file") opt.stats_file = next("--stats-file");
|
| 271 |
+
else if (a == "--dump-logits") opt.dump_logits = true;
|
| 272 |
+
else if (a == "--logits-out") opt.logits_out = next("--logits-out");
|
| 273 |
+
else if (a == "--selftest") opt.selftest = next("--selftest");
|
| 274 |
+
else if (a == "--dump-tensors") opt.dump_tensors = next("--dump-tensors");
|
| 275 |
+
else if (a == "--no-pager") opt.no_pager = true;
|
| 276 |
+
else if (a == "--span-test") opt.span_test = next("--span-test");
|
| 277 |
+
else if (a == "-h" || a == "--help") {
|
| 278 |
+
printf(
|
| 279 |
+
"用法:epq-chat -m <model.gguf> [選項]\n"
|
| 280 |
+
" -c, --ctx N 上下文長度(預設 4096;模型原生 262144)\n"
|
| 281 |
+
" -n N 要生成的 token 數\n"
|
| 282 |
+
" -t N CPU 執行緒數\n"
|
| 283 |
+
" -b N ubatch 大小\n"
|
| 284 |
+
" -p, --prompt TEXT 提示詞\n"
|
| 285 |
+
" --sys TEXT system 訊息\n"
|
| 286 |
+
" --no-chat 不要套 chat template(純 completion)\n"
|
| 287 |
+
" --no-interactive 跑完提示詞就結束\n"
|
| 288 |
+
" --bench benchmark 模式(輸出 JSON 到 stats 檔)\n"
|
| 289 |
+
" --arena-mb N expert arena 大小(MB)\n"
|
| 290 |
+
" --ram-budget-mb N RAM 總預算(MB),arena 會配合縮減\n"
|
| 291 |
+
" --no-prefetch 關閉背景預取\n"
|
| 292 |
+
" --no-fadvise 讀完不丟 kernel page cache(會讓 RAM 帳目失真)\n"
|
| 293 |
+
" --stats-file PATH 指標 JSONL 輸出\n"
|
| 294 |
+
" --dump-logits 把每個 token 的 logits 存檔(驗證用)\n"
|
| 295 |
+
" --logits-out PATH logits 輸出路徑\n");
|
| 296 |
+
return 0;
|
| 297 |
+
} else {
|
| 298 |
+
fprintf(stderr, "未知參數:%s(用 --help 看用法)\n", a.c_str());
|
| 299 |
+
return 1;
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
if (opt.model.empty()) {
|
| 304 |
+
fprintf(stderr, "缺少模型檔(-m 或 SDQ_MODEL_PATH)\n");
|
| 305 |
+
return 1;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// ---- 環境變數(pager 讀)
|
| 309 |
+
setenv("SDQ_MODEL_PATH", opt.model.c_str(), 1);
|
| 310 |
+
// I/O 讀取執行緒數:預設 3 × 計算執行緒(上限 48)。
|
| 311 |
+
// 必須在這裡設定 —— pager 的 I/O 池是在「載入模型」時啟動的,那已經比
|
| 312 |
+
// context 建立早了;放到後面設定會讓池子只拿到下限 4 條執行緒。
|
| 313 |
+
// 量測(repeat=3,中位數):序列讀 3.79 → 12 條 5.84 → 24 條 6.68 tok/s。
|
| 314 |
+
if (!getenv("SDQ_IO_THREADS")) {
|
| 315 |
+
const int hw_io = (int) std::max(1u, std::thread::hardware_concurrency());
|
| 316 |
+
const int dflt_io = hw_io < 8 ? hw_io : 8;
|
| 317 |
+
const int n_io = opt.n_threads > 0 ? opt.n_threads : dflt_io;
|
| 318 |
+
setenv("SDQ_IO_THREADS", std::to_string(n_io * 3).c_str(), 1);
|
| 319 |
+
}
|
| 320 |
+
// 只有明確指定時才設定,否則保留呼叫者從環境變數傳入的值
|
| 321 |
+
// (舊版無條件 setenv 會讓 SDQ_PREFETCH=0 被悄悄蓋回 1,除錯時極易誤判)
|
| 322 |
+
if (opt.no_prefetch) {
|
| 323 |
+
setenv("SDQ_PREFETCH", "0", 1);
|
| 324 |
+
}
|
| 325 |
+
setenv("SDQ_FADV_DONTNEED", opt.fadvise ? "1" : "0", 1);
|
| 326 |
+
if (opt.arena_mb > 0) {
|
| 327 |
+
setenv("SDQ_ARENA_MB", std::to_string(opt.arena_mb).c_str(), 1);
|
| 328 |
+
}
|
| 329 |
+
if (opt.ram_budget_mb > 0) {
|
| 330 |
+
setenv("SDQ_RAM_BUDGET_MB", std::to_string(opt.ram_budget_mb).c_str(), 1);
|
| 331 |
+
}
|
| 332 |
+
if (!opt.stats_file.empty()) {
|
| 333 |
+
setenv("SDQ_STATS_FILE", opt.stats_file.c_str(), 1);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
llama_backend_init();
|
| 337 |
+
llama_numa_init(GGML_NUMA_STRATEGY_DISABLED);
|
| 338 |
+
|
| 339 |
+
// ---- 模型載入:expert 權重交給 pager 的 buffer type(0 RAM)
|
| 340 |
+
llama_model_params mparams = llama_model_default_params();
|
| 341 |
+
mparams.n_gpu_layers = 0;
|
| 342 |
+
mparams.load_mode = LLAMA_LOAD_MODE_MMAP;
|
| 343 |
+
if (!opt.no_pager) {
|
| 344 |
+
sdq_set_enabled(true);
|
| 345 |
+
sdq_install_buft_overrides(&mparams);
|
| 346 |
+
} else {
|
| 347 |
+
printf("[sdq] --no-pager:對照模式,專家權重全部留在 RAM(需要 %s RAM)\n", "22GB");
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
printf("[sdq] 載入模型:%s\n", opt.model.c_str());
|
| 351 |
+
const double t_load0 = now_s();
|
| 352 |
+
llama_model * model = llama_model_load_from_file(opt.model.c_str(), mparams);
|
| 353 |
+
if (!model) {
|
| 354 |
+
fprintf(stderr, "[sdq] 模型載入失敗\n");
|
| 355 |
+
return 1;
|
| 356 |
+
}
|
| 357 |
+
const double t_load = now_s() - t_load0;
|
| 358 |
+
|
| 359 |
+
// ---- pager 初始化(讀 GGUF 檔頭、建頁面表、配置 arena)
|
| 360 |
+
auto & P = pager::instance();
|
| 361 |
+
if (!opt.no_pager && !sdq_init(opt.model)) {
|
| 362 |
+
fprintf(stderr, "[sdq] pager 初始化失敗\n");
|
| 363 |
+
return 1;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// ---- 張量 dump(A/B 比對用):在 op 真的算完的那一刻把資料寫出來
|
| 367 |
+
FILE * tensor_fp = nullptr;
|
| 368 |
+
dump_state dump_state_;
|
| 369 |
+
if (!opt.dump_tensors.empty()) {
|
| 370 |
+
tensor_fp = fopen(opt.dump_tensors.c_str(), "wb");
|
| 371 |
+
dump_state_.fp = tensor_fp;
|
| 372 |
+
if (const char * names = getenv("SDQ_DUMP_NAMES")) {
|
| 373 |
+
std::string s = names;
|
| 374 |
+
size_t pos = 0;
|
| 375 |
+
while (pos <= s.size()) {
|
| 376 |
+
size_t c = s.find(',', pos);
|
| 377 |
+
if (c == std::string::npos) c = s.size();
|
| 378 |
+
dump_state_.names.push_back(s.substr(pos, c - pos));
|
| 379 |
+
pos = c + 1;
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
g_dump = dump_state_;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// ---- context
|
| 386 |
+
llama_context_params cparams = llama_context_default_params();
|
| 387 |
+
cparams.n_ctx = (uint32_t) opt.n_ctx;
|
| 388 |
+
cparams.n_batch = (uint32_t) opt.n_ctx;
|
| 389 |
+
// ubatch 預設 128(不是 512)。理由是「8 GB 預算要真的裝得下」:
|
| 390 |
+
// llama.cpp 的 CPU compute buffer 是按 n_ubatch 線性成長的,實測
|
| 391 |
+
// ubatch=512 → 502 MiB、256 → 251 MiB、128 → 125.5 MiB
|
| 392 |
+
// 而 compute buffer 是在 pager 算完 arena 大小之後才配置的,所以它不算在
|
| 393 |
+
// arena 的扣減裡。實測在 RLIMIT_DATA 8 GB 下:
|
| 394 |
+
// ubatch=512 → 提示詞超過 ~256 token 直接 std::bad_alloc(崩潰)
|
| 395 |
+
// ubatch=256 → 同一個提示詞一樣崩潰
|
| 396 |
+
// ubatch=128 → 正常,681 token 的 prefill 跑到 28.8 tok/s
|
| 397 |
+
// 而且 ubatch=128 還更快(docs/04 實測 +7%),因為 prefill 分更多批、
|
| 398 |
+
// 每批的 expert 聯集比較小、命中率比較高。
|
| 399 |
+
cparams.n_ubatch = opt.n_ubatch > 0 ? (uint32_t) opt.n_ubatch : (uint32_t) std::min(opt.n_ctx, 128);
|
| 400 |
+
// 執行緒預設值:不要直接用 hardware_concurrency()。
|
| 401 |
+
// 實測(docs/04,repeat=3):mode C 下 t=8 最快,給更多執行緒反而變慢 ——
|
| 402 |
+
// t=8 4.02 / t=10 3.79 / t=12 3.55 / t=16 3.60 tok/s
|
| 403 |
+
// 原因很直接:每層每 token 只有 n_expert_used = 8 個 expert 可以平行,
|
| 404 |
+
// 執行緒超過 8 之後就只是多開執行緒搶同一批 CPU。
|
| 405 |
+
// 下限取實際核心數,避免在 4 核心的目標機器上開太多。
|
| 406 |
+
const int hw = (int) std::max(1u, std::thread::hardware_concurrency());
|
| 407 |
+
const int dflt_threads = hw < 8 ? hw : 8;
|
| 408 |
+
cparams.n_threads = opt.n_threads > 0 ? opt.n_threads : (int32_t) dflt_threads;
|
| 409 |
+
cparams.n_threads_batch = cparams.n_threads;
|
| 410 |
+
if (!opt.stats_file.empty()) {
|
| 411 |
+
cparams.offload_kqv = false;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
if (tensor_fp) {
|
| 415 |
+
cparams.cb_eval = sdq_eval_callback;
|
| 416 |
+
cparams.cb_eval_user_data = &g_dump;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
llama_context * ctx = llama_init_from_model(model, cparams);
|
| 420 |
+
if (!ctx) {
|
| 421 |
+
fprintf(stderr, "[sdq] context 建立失敗(ctx=%d 太大?)\n", opt.n_ctx);
|
| 422 |
+
return 1;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
printf("[sdq] ctx=%d threads=%d ubatch=%u RAM(RSS)=%zu MB 模型載入 %.1fs\n",
|
| 426 |
+
opt.n_ctx, cparams.n_threads, cparams.n_ubatch, rss_mb(), t_load);
|
| 427 |
+
print_mem_report("載入 + pager 後");
|
| 428 |
+
|
| 429 |
+
// ---- sampler
|
| 430 |
+
auto sparams = llama_sampler_chain_default_params();
|
| 431 |
+
sparams.no_perf = true;
|
| 432 |
+
llama_sampler * smpl = llama_sampler_chain_init(sparams);
|
| 433 |
+
llama_sampler_chain_add(smpl, llama_sampler_init_top_k(opt.top_k));
|
| 434 |
+
llama_sampler_chain_add(smpl, llama_sampler_init_top_p(opt.top_p, 1));
|
| 435 |
+
llama_sampler_chain_add(smpl, llama_sampler_init_temp(opt.temp));
|
| 436 |
+
llama_sampler_chain_add(smpl, llama_sampler_init_dist(LLAMA_DEFAULT_SEED));
|
| 437 |
+
|
| 438 |
+
chat_state st;
|
| 439 |
+
if (!opt.system.empty()) {
|
| 440 |
+
st.msgs.push_back({"system", opt.system.c_str()});
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
std::vector<llama_token> decoded;
|
| 444 |
+
|
| 445 |
+
const llama_vocab * vocab = llama_model_get_vocab(model);
|
| 446 |
+
|
| 447 |
+
auto tokenize = [&](const std::string & text) {
|
| 448 |
+
std::vector<llama_token> toks(text.size() + 16);
|
| 449 |
+
int32_t n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
|
| 450 |
+
toks.data(), (int32_t) toks.size(), true, true);
|
| 451 |
+
if (n < 0) {
|
| 452 |
+
toks.resize((size_t) -n);
|
| 453 |
+
n = llama_tokenize(vocab, text.c_str(), (int32_t) text.size(),
|
| 454 |
+
toks.data(), (int32_t) toks.size(), true, true);
|
| 455 |
+
}
|
| 456 |
+
toks.resize(n);
|
| 457 |
+
return toks;
|
| 458 |
+
};
|
| 459 |
+
|
| 460 |
+
// ---- 送進去一個 ubatch,回傳生成的 token(-1 = 需要繼續生成)
|
| 461 |
+
std::function<int(llama_token, bool)> submit;
|
| 462 |
+
uint64_t n_prefill_tok = 0;
|
| 463 |
+
double t_prefill = 0;
|
| 464 |
+
uint64_t n_decode_tok = 0;
|
| 465 |
+
double t_decode = 0;
|
| 466 |
+
uint64_t io_read0 = read_bytes_io();
|
| 467 |
+
|
| 468 |
+
std::vector<float> logits_store;
|
| 469 |
+
FILE * logits_fp = nullptr;
|
| 470 |
+
if (opt.dump_logits) {
|
| 471 |
+
logits_fp = fopen(opt.logits_out.empty() ? "sdq_logits.bin" : opt.logits_out.c_str(), "wb");
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
submit = [&](llama_token tok, bool last) -> int {
|
| 475 |
+
const uint32_t ctx_size = llama_n_ctx(ctx);
|
| 476 |
+
llama_memory_t mem = llama_get_memory(ctx);
|
| 477 |
+
if (tok == 1) { // BOS:重開記憶體
|
| 478 |
+
llama_memory_clear(mem, true);
|
| 479 |
+
decoded.clear();
|
| 480 |
+
}
|
| 481 |
+
if (decoded.size() >= opt.n_ctx - 4) {
|
| 482 |
+
// 環形丟掉最舊的 token(保留 opt.n_keep)
|
| 483 |
+
const size_t drop = decoded.size() - (size_t) opt.n_keep;
|
| 484 |
+
llama_memory_seq_rm(mem, 0, 0, (int32_t) drop);
|
| 485 |
+
decoded.erase(decoded.begin(), decoded.begin() + (long) drop);
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
llama_batch b = llama_batch_get_one((llama_token *) &tok, 1);
|
| 489 |
+
|
| 490 |
+
const bool is_prefill = decoded.empty();
|
| 491 |
+
const double t0 = now_s();
|
| 492 |
+
const int rc = llama_decode(ctx, b);
|
| 493 |
+
const double dt = now_s() - t0;
|
| 494 |
+
if (rc != 0) {
|
| 495 |
+
fprintf(stderr, "\n[sdq] llama_decode 失敗 rc=%d(記憶體不足?)\n", rc);
|
| 496 |
+
exit(3);
|
| 497 |
+
}
|
| 498 |
+
decoded.push_back(tok);
|
| 499 |
+
|
| 500 |
+
// 每個 ubatch 結束 → 更新路由歷史並觸發下一輪預取
|
| 501 |
+
P.end_token();
|
| 502 |
+
|
| 503 |
+
if (is_prefill) {
|
| 504 |
+
n_prefill_tok++;
|
| 505 |
+
t_prefill += dt;
|
| 506 |
+
} else {
|
| 507 |
+
n_decode_tok++;
|
| 508 |
+
t_decode += dt;
|
| 509 |
+
P.get_stats().tokens.fetch_add(1, std::memory_order_relaxed);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
if (logits_fp) {
|
| 513 |
+
const int32_t n_logits = llama_vocab_n_tokens(vocab);
|
| 514 |
+
const float * lg = llama_get_logits_ith(ctx, -1);
|
| 515 |
+
if (lg) {
|
| 516 |
+
logits_store.assign(lg, lg + n_logits);
|
| 517 |
+
fwrite(&n_logits, sizeof(int32_t), 1, logits_fp);
|
| 518 |
+
fwrite(logits_store.data(), sizeof(float), n_logits, logits_fp);
|
| 519 |
+
fflush(logits_fp);
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
if (last) {
|
| 524 |
+
return -1;
|
| 525 |
+
}
|
| 526 |
+
const llama_token id = llama_sampler_sample(smpl, ctx, -1);
|
| 527 |
+
return (int) id;
|
| 528 |
+
};
|
| 529 |
+
|
| 530 |
+
// ---- 產生 prompt
|
| 531 |
+
auto run_prompt = [&](const std::string & user_text, bool add_to_history) {
|
| 532 |
+
if (opt.chat) {
|
| 533 |
+
st.msgs.push_back({"user", user_text.c_str()});
|
| 534 |
+
}
|
| 535 |
+
std::string text;
|
| 536 |
+
if (opt.chat) {
|
| 537 |
+
const char * tmpl = llama_model_chat_template(model, nullptr);
|
| 538 |
+
size_t n_chars = 0;
|
| 539 |
+
for (const auto & m : st.msgs) {
|
| 540 |
+
n_chars += strlen(m.role) + strlen(m.content) + 32;
|
| 541 |
+
}
|
| 542 |
+
std::vector<char> buf(2 * n_chars + 1024);
|
| 543 |
+
int32_t len = llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(),
|
| 544 |
+
true, buf.data(), (int32_t) buf.size());
|
| 545 |
+
if (len > (int32_t) buf.size()) {
|
| 546 |
+
buf.resize((size_t) len + 1);
|
| 547 |
+
llama_chat_apply_template(tmpl, st.msgs.data(), st.msgs.size(), true,
|
| 548 |
+
buf.data(), (int32_t) buf.size());
|
| 549 |
+
}
|
| 550 |
+
text.assign(buf.data());
|
| 551 |
+
} else {
|
| 552 |
+
text = user_text;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
auto toks = tokenize(text);
|
| 556 |
+
const int n_prompt = (int) toks.size();
|
| 557 |
+
const uint64_t io0 = read_bytes_io();
|
| 558 |
+
|
| 559 |
+
// 分批送進去(每批 = 一個 ubatch,控制記憶體)
|
| 560 |
+
const int32_t ub = (int32_t) cparams.n_ubatch;
|
| 561 |
+
llama_sampler_reset(smpl);
|
| 562 |
+
int32_t pos = 0;
|
| 563 |
+
for (int32_t i = 0; i < n_prompt; i += ub) {
|
| 564 |
+
const int32_t n = std::min(ub, n_prompt - i);
|
| 565 |
+
llama_batch b = llama_batch_init((uint32_t) n, 0, 1);
|
| 566 |
+
for (int32_t j = 0; j < n; j++) {
|
| 567 |
+
b.token[j] = toks[i + j];
|
| 568 |
+
b.pos[j] = pos++;
|
| 569 |
+
b.n_seq_id[j] = 1;
|
| 570 |
+
b.seq_id[j][0] = 0;
|
| 571 |
+
}
|
| 572 |
+
b.n_tokens = (uint32_t) n;
|
| 573 |
+
const double t0 = now_s();
|
| 574 |
+
const int rc = llama_decode(ctx, b);
|
| 575 |
+
t_prefill += now_s() - t0;
|
| 576 |
+
n_prefill_tok += (uint64_t) n;
|
| 577 |
+
if (rc != 0) {
|
| 578 |
+
fprintf(stderr, "\n[sdq] prefill 失敗 rc=%d(%d tokens)\n", rc, n);
|
| 579 |
+
exit(3);
|
| 580 |
+
}
|
| 581 |
+
P.end_token();
|
| 582 |
+
llama_batch_free(b);
|
| 583 |
+
}
|
| 584 |
+
const double t_p = now_s();
|
| 585 |
+
(void) t_p;
|
| 586 |
+
(void) io0;
|
| 587 |
+
|
| 588 |
+
if (add_to_history && opt.chat) {
|
| 589 |
+
st.msgs.push_back({"assistant", ""});
|
| 590 |
+
}
|
| 591 |
+
return n_prompt;
|
| 592 |
+
};
|
| 593 |
+
|
| 594 |
+
// ---- 產生
|
| 595 |
+
auto generate = [&](int32_t n_max) {
|
| 596 |
+
const uint64_t io0 = read_bytes_io();
|
| 597 |
+
const uint64_t io_read_before = io0;
|
| 598 |
+
(void) io_read_before;
|
| 599 |
+
const double t0 = now_s();
|
| 600 |
+
int32_t n_gen = 0;
|
| 601 |
+
llama_token id = llama_sampler_sample(smpl, ctx, -1);
|
| 602 |
+
fputs("assistant: ", stdout);
|
| 603 |
+
fflush(stdout);
|
| 604 |
+
while (n_gen < n_max) {
|
| 605 |
+
const int rc = submit(id, /*last=*/n_gen + 1 >= n_max);
|
| 606 |
+
if (rc < 0) {
|
| 607 |
+
break;
|
| 608 |
+
}
|
| 609 |
+
id = (llama_token) rc;
|
| 610 |
+
char piece_buf[256];
|
| 611 |
+
const int n_piece = llama_token_to_piece(vocab, id, piece_buf, sizeof(piece_buf), 0, true);
|
| 612 |
+
const std::string piece(piece_buf, (size_t) std::max(0, n_piece));
|
| 613 |
+
fputs(piece.c_str(), stdout);
|
| 614 |
+
fflush(stdout);
|
| 615 |
+
n_gen++;
|
| 616 |
+
}
|
| 617 |
+
fputs("\n", stdout);
|
| 618 |
+
const double dt = now_s() - t0;
|
| 619 |
+
const uint64_t io1 = read_bytes_io();
|
| 620 |
+
fprintf(stderr, "[sdq] decode: %d tok / %.2fs = %.2f tok/s | SSD 讀 %.1f MB (%.1f MB/tok) | RSS %zu MB\n",
|
| 621 |
+
n_gen, dt, dt > 0 ? n_gen / dt : 0.0,
|
| 622 |
+
(io1 - io0) / 1048576.0, n_gen ? (io1 - io0) / 1048576.0 / n_gen : 0.0, rss_mb());
|
| 623 |
+
if (opt.chat) {
|
| 624 |
+
for (auto & m : st.msgs) {
|
| 625 |
+
if (strcmp(m.role, "assistant") == 0) {
|
| 626 |
+
m.content = "";
|
| 627 |
+
}
|
| 628 |
+
}
|
| 629 |
+
}
|
| 630 |
+
P.dump_stats("decode");
|
| 631 |
+
return n_gen;
|
| 632 |
+
};
|
| 633 |
+
|
| 634 |
+
// ---- 合併讀取自我測試:--span-test IL:E0:E1:...
|
| 635 |
+
if (!opt.span_test.empty()) {
|
| 636 |
+
std::vector<int> parts;
|
| 637 |
+
std::string cur_s;
|
| 638 |
+
for (char ch : opt.span_test + ":") {
|
| 639 |
+
if (ch == ':') { parts.push_back(atoi(cur_s.c_str())); cur_s.clear(); }
|
| 640 |
+
else cur_s += ch;
|
| 641 |
+
}
|
| 642 |
+
if (parts.size() < 3) {
|
| 643 |
+
fprintf(stderr, "用法:--span-test IL:E0:E1[:E2...](E 要連續)\n");
|
| 644 |
+
return 1;
|
| 645 |
+
}
|
| 646 |
+
const int il = parts[0];
|
| 647 |
+
std::vector<int32_t> ex;
|
| 648 |
+
for (size_t i = 1; i < parts.size(); i++) {
|
| 649 |
+
ex.push_back(parts[i]);
|
| 650 |
+
}
|
| 651 |
+
std::vector<int32_t> slots(ex.size(), -1);
|
| 652 |
+
for (size_t i = 0; i < ex.size(); i++) {
|
| 653 |
+
slots[i] = P.reserve_slot(il, ex[i]);
|
| 654 |
+
}
|
| 655 |
+
const uint64_t got = P.read_expert_span(il, ex.data(), (int) ex.size(), slots.data());
|
| 656 |
+
printf("[sdq-span-test] L%d experts=%zu 讀取 %.2f MB\n", il, ex.size(), got / 1048576.0);
|
| 657 |
+
int bad = 0;
|
| 658 |
+
for (size_t i = 0; i < ex.size(); i++) {
|
| 659 |
+
std::vector<uint8_t> one((size_t) P.layer(il)->experts[ex[i]].bytes, 0);
|
| 660 |
+
if (!P.read_expert(il, ex[i], one.data())) {
|
| 661 |
+
printf(" 單獨讀取失敗\n");
|
| 662 |
+
return 1;
|
| 663 |
+
}
|
| 664 |
+
const uint8_t * span_ptr = P.slot_ptr(slots[i]);
|
| 665 |
+
size_t diff_at = (size_t) -1;
|
| 666 |
+
for (size_t b = 0; b < one.size(); b++) {
|
| 667 |
+
if (one[b] != span_ptr[b]) { diff_at = b; break; }
|
| 668 |
+
}
|
| 669 |
+
printf(" expert %4d: %s", ex[i], diff_at == (size_t) -1 ? "一致 ✅" : "不一致 ❌");
|
| 670 |
+
if (diff_at != (size_t) -1) {
|
| 671 |
+
printf("(第一個不同的 byte 在 %zu / %zu,slot=%d)", diff_at, one.size(), slots[i]);
|
| 672 |
+
bad++;
|
| 673 |
+
}
|
| 674 |
+
printf("\n");
|
| 675 |
+
}
|
| 676 |
+
sdq_shutdown();
|
| 677 |
+
llama_free(ctx);
|
| 678 |
+
llama_model_free(model);
|
| 679 |
+
llama_backend_free();
|
| 680 |
+
return bad ? 1 : 0;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// ---- 自我測試模式:--selftest IL:IE:SEED:OUT.bin
|
| 684 |
+
if (!opt.selftest.empty()) {
|
| 685 |
+
int il = 0, ie = 0, seed = 1234, n_tok = 1;
|
| 686 |
+
std::string outpath;
|
| 687 |
+
{
|
| 688 |
+
std::vector<std::string> parts;
|
| 689 |
+
std::string cur_s;
|
| 690 |
+
for (char c : opt.selftest + ":") {
|
| 691 |
+
if (c == ':') { parts.push_back(cur_s); cur_s.clear(); }
|
| 692 |
+
else cur_s += c;
|
| 693 |
+
}
|
| 694 |
+
if (parts.size() >= 3) {
|
| 695 |
+
il = atoi(parts[0].c_str());
|
| 696 |
+
ie = atoi(parts[1].c_str());
|
| 697 |
+
seed = atoi(parts[2].c_str());
|
| 698 |
+
}
|
| 699 |
+
if (parts.size() >= 4) {
|
| 700 |
+
outpath = parts[3];
|
| 701 |
+
}
|
| 702 |
+
if (parts.size() >= 5) {
|
| 703 |
+
n_tok = atoi(parts[4].c_str());
|
| 704 |
+
}
|
| 705 |
+
}
|
| 706 |
+
const int64_t n_embd = P.info().n_embd;
|
| 707 |
+
const int64_t n_ff = P.info().n_ff_exp;
|
| 708 |
+
std::vector<float> x((size_t) n_embd * n_tok);
|
| 709 |
+
// SDQ_SELFTEST_X: use a real activation vector instead of random noise, so
|
| 710 |
+
// one expert can be checked against NumPy on the exact input the full
|
| 711 |
+
// model produced.
|
| 712 |
+
if (const char * xf = getenv("SDQ_SELFTEST_X")) {
|
| 713 |
+
FILE * fh = fopen(xf, "rb");
|
| 714 |
+
const bool ok = fh && (size_t) fread(x.data(), sizeof(float), x.size(), fh) == x.size();
|
| 715 |
+
if (fh) { fclose(fh); }
|
| 716 |
+
if (!ok) {
|
| 717 |
+
fprintf(stderr, "[sdq] SDQ_SELFTEST_X 讀取失敗:%s\n", xf);
|
| 718 |
+
return 1;
|
| 719 |
+
}
|
| 720 |
+
} else {
|
| 721 |
+
uint32_t rng = (uint32_t) seed * 2654435761u + 1u;
|
| 722 |
+
for (auto & v : x) {
|
| 723 |
+
rng = rng * 1664525u + 1013904223u;
|
| 724 |
+
v = ((float) (rng >> 8) / (float) (1 << 24)) * 2.0f - 1.0f;
|
| 725 |
+
}
|
| 726 |
+
}
|
| 727 |
+
std::vector<float> io((size_t) n_embd * n_tok * 2);
|
| 728 |
+
const int rc = sdq_moe_selftest(il, ie, x.data(), io.data(), n_embd, n_ff, n_tok);
|
| 729 |
+
if (rc == 0 && !outpath.empty()) {
|
| 730 |
+
FILE * f = fopen(outpath.c_str(), "wb");
|
| 731 |
+
fwrite(io.data(), sizeof(float), io.size(), f);
|
| 732 |
+
fclose(f);
|
| 733 |
+
printf("[sdq] selftest 結果已寫到 %s(layer=%d expert=%d seed=%d n_embd=%lld n_ff=%lld)\n",
|
| 734 |
+
outpath.c_str(), il, ie, seed, (long long) n_embd, (long long) n_ff);
|
| 735 |
+
}
|
| 736 |
+
sdq_shutdown();
|
| 737 |
+
llama_free(ctx);
|
| 738 |
+
llama_model_free(model);
|
| 739 |
+
llama_backend_free();
|
| 740 |
+
return rc;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
// ---- 主流程
|
| 744 |
+
if (!opt.prompt.empty()) {
|
| 745 |
+
run_prompt(opt.prompt, true);
|
| 746 |
+
generate(opt.n_predict);
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
if (opt.bench) {
|
| 750 |
+
// benchmark:固定提示詞 ×3,量 prefill / decode / 分頁指標
|
| 751 |
+
const std::vector<std::string> prompts = {
|
| 752 |
+
"請用中文簡短介紹 SSD、RAM、CPU 之間的資料分層是什麼,以及為什麼快取很重要。",
|
| 753 |
+
"Write a short paragraph explaining how a mixture-of-experts model routes tokens to experts.",
|
| 754 |
+
"請列出十種日常會用到電腦的工作,並各用一句話說明。",
|
| 755 |
+
};
|
| 756 |
+
for (int round = 0; round < 3; round++) {
|
| 757 |
+
fprintf(stderr, "[bench] round %d\n", round);
|
| 758 |
+
llama_memory_clear(llama_get_memory(ctx), true);
|
| 759 |
+
decoded.clear();
|
| 760 |
+
P.get_stats().tokens.store(0);
|
| 761 |
+
run_prompt(prompts[round % prompts.size()], false);
|
| 762 |
+
generate(48);
|
| 763 |
+
P.dump_stats(round == 0 ? "bench_warm" : "bench_hot");
|
| 764 |
+
}
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
if (opt.interactive) {
|
| 768 |
+
printf("\n(輸入文字開始對話;/exit 離開、/reset 重設、/stats 顯示指標)\n");
|
| 769 |
+
std::string line;
|
| 770 |
+
while (true) {
|
| 771 |
+
printf("\nuser> ");
|
| 772 |
+
fflush(stdout);
|
| 773 |
+
if (!std::getline(std::cin, line)) {
|
| 774 |
+
break;
|
| 775 |
+
}
|
| 776 |
+
if (line == "/exit" || line == "/quit") {
|
| 777 |
+
break;
|
| 778 |
+
}
|
| 779 |
+
if (line == "/reset") {
|
| 780 |
+
llama_memory_clear(llama_get_memory(ctx), true);
|
| 781 |
+
decoded.clear();
|
| 782 |
+
st.msgs.clear();
|
| 783 |
+
if (!opt.system.empty()) {
|
| 784 |
+
st.msgs.push_back({"system", opt.system.c_str()});
|
| 785 |
+
}
|
| 786 |
+
printf("(已重設)\n");
|
| 787 |
+
continue;
|
| 788 |
+
}
|
| 789 |
+
if (line == "/stats") {
|
| 790 |
+
P.dump_stats("manual");
|
| 791 |
+
fprintf(stderr, "hit_rate=%.1f%% arena=%zu MB RSS=%zu MB\n",
|
| 792 |
+
P.hit_rate() * 100, P.used_bytes() / 1048576, rss_mb());
|
| 793 |
+
continue;
|
| 794 |
+
}
|
| 795 |
+
if (line.empty()) {
|
| 796 |
+
continue;
|
| 797 |
+
}
|
| 798 |
+
run_prompt(line, true);
|
| 799 |
+
generate(opt.n_predict);
|
| 800 |
+
}
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
P.get_stats().moe_calls.fetch_add(0);
|
| 804 |
+
sdq_shutdown();
|
| 805 |
+
|
| 806 |
+
fprintf(stderr, "\n[sdq] ===== 總結 =====\n");
|
| 807 |
+
fprintf(stderr, "[sdq] prefill: %" PRIu64 " tok / %.2fs = %.1f tok/s\n",
|
| 808 |
+
n_prefill_tok, t_prefill, t_prefill > 0 ? n_prefill_tok / t_prefill : 0.0);
|
| 809 |
+
const uint64_t io_end = read_bytes_io();
|
| 810 |
+
fprintf(stderr, "[sdq] 全程 SSD 實際讀取: %.2f GB\n", (io_end - io_read0) / 1073741824.0);
|
| 811 |
+
print_mem_report("結束時");
|
| 812 |
+
|
| 813 |
+
if (logits_fp) {
|
| 814 |
+
fclose(logits_fp);
|
| 815 |
+
}
|
| 816 |
+
llama_sampler_free(smpl);
|
| 817 |
+
llama_free(ctx);
|
| 818 |
+
llama_model_free(model);
|
| 819 |
+
llama_backend_free();
|
| 820 |
+
return 0;
|
| 821 |
+
}
|
cpp/sdq_moe.cpp
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
std::atomic<uint64_t> grouped_batches{0};
|
| 50 |
+
std::atomic<uint64_t> grouped_tokens{0};
|
| 51 |
+
};
|
| 52 |
+
static moe_prof g_prof;
|
| 53 |
+
|
| 54 |
+
static inline bool prof_on() {
|
| 55 |
+
static const bool on = [] { const char * e = getenv("SDQ_PROFILE_TIME"); return e && *e && *e != '0'; }();
|
| 56 |
+
return on;
|
| 57 |
+
}
|
| 58 |
+
static inline uint64_t now_ns() {
|
| 59 |
+
return (uint64_t) std::chrono::duration_cast<std::chrono::nanoseconds>(
|
| 60 |
+
std::chrono::steady_clock::now().time_since_epoch()).count();
|
| 61 |
+
}
|
| 62 |
+
#define PROF_T0_x(v) const uint64_t v = prof_on() ? now_ns() : 0
|
| 63 |
+
#define PROF_ADD(field, tv) do { if (prof_on()) g_prof.field += now_ns() - (tv); } while (0)
|
| 64 |
+
|
| 65 |
+
// ---------------------------------------------------------------- 執行緒柵欄
|
| 66 |
+
|
| 67 |
+
class gen_barrier {
|
| 68 |
+
public:
|
| 69 |
+
void sync(int n) {
|
| 70 |
+
PROF_T0_x(_tbar);
|
| 71 |
+
std::unique_lock<std::mutex> lk(mu_);
|
| 72 |
+
const unsigned long g = gen_;
|
| 73 |
+
if (++count_ == n) {
|
| 74 |
+
count_ = 0;
|
| 75 |
+
gen_++;
|
| 76 |
+
cv_.notify_all();
|
| 77 |
+
PROF_ADD(bar_ns, _tbar);
|
| 78 |
+
return;
|
| 79 |
+
}
|
| 80 |
+
cv_.wait(lk, [this, g] { return gen_ != g; });
|
| 81 |
+
PROF_ADD(bar_ns, _tbar);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
private:
|
| 85 |
+
int count_ = 0;
|
| 86 |
+
unsigned long gen_ = 0;
|
| 87 |
+
std::mutex mu_;
|
| 88 |
+
std::condition_variable cv_;
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
// ---------------------------------------------------------------- 量化點積派送
|
| 92 |
+
|
| 93 |
+
using vec_dot_fn = void (*)(int, float *, size_t, const void *, size_t, const void *, size_t, int);
|
| 94 |
+
|
| 95 |
+
static vec_dot_fn dot_for(ggml_type t) {
|
| 96 |
+
switch (t) {
|
| 97 |
+
case GGML_TYPE_Q4_K: return ggml_vec_dot_q4_K_q8_K;
|
| 98 |
+
case GGML_TYPE_Q5_K: return ggml_vec_dot_q5_K_q8_K;
|
| 99 |
+
case GGML_TYPE_Q6_K: return ggml_vec_dot_q6_K_q8_K;
|
| 100 |
+
case GGML_TYPE_Q3_K: return ggml_vec_dot_q3_K_q8_K;
|
| 101 |
+
case GGML_TYPE_Q2_K: return ggml_vec_dot_q2_K_q8_K;
|
| 102 |
+
case GGML_TYPE_Q8_0: return ggml_vec_dot_q8_0_q8_0;
|
| 103 |
+
case GGML_TYPE_Q4_0: return ggml_vec_dot_q4_0_q8_0;
|
| 104 |
+
case GGML_TYPE_Q5_0: return ggml_vec_dot_q5_0_q8_0;
|
| 105 |
+
case GGML_TYPE_IQ4_NL: return ggml_vec_dot_iq4_nl_q8_0;
|
| 106 |
+
case GGML_TYPE_IQ4_XS: return ggml_vec_dot_iq4_xs_q8_K;
|
| 107 |
+
default: return nullptr;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// K-quant 的權重配 Q8_K activation;其餘配 Q8_0(與 ggml 的 vec_dot_type 一致)
|
| 112 |
+
static ggml_type act_type_for(ggml_type w) {
|
| 113 |
+
switch (w) {
|
| 114 |
+
case GGML_TYPE_Q4_K:
|
| 115 |
+
case GGML_TYPE_Q5_K:
|
| 116 |
+
case GGML_TYPE_Q6_K:
|
| 117 |
+
case GGML_TYPE_Q3_K:
|
| 118 |
+
case GGML_TYPE_Q2_K:
|
| 119 |
+
case GGML_TYPE_IQ4_XS:
|
| 120 |
+
return GGML_TYPE_Q8_K;
|
| 121 |
+
default:
|
| 122 |
+
return GGML_TYPE_Q8_0;
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// 把 activation 量化成對應的 dot 型別
|
| 127 |
+
// 注意:ggml_quantize_chunk() 不支援 Q8_K(Q8_K 只在 mul_mat kernel 內部產生),
|
| 128 |
+
// 所以這裡直接呼叫 ggml-cpu 的 quantize_row_q8_K。
|
| 129 |
+
static void quantize_act(ggml_type t, const float * x, void * dst, int64_t n) {
|
| 130 |
+
switch (t) {
|
| 131 |
+
case GGML_TYPE_Q8_K: quantize_row_q8_K(x, dst, n); break;
|
| 132 |
+
case GGML_TYPE_Q8_0: quantize_row_q8_0(x, dst, n); break;
|
| 133 |
+
default: ggml_quantize_chunk(t, x, dst, 0, 1, n, nullptr); break;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// ⚠️ 不要在這裡「批次化」點積。
|
| 138 |
+
// ggml 的 x86 版 K-quant kernel(ggml/src/ggml-cpu/arch/x86/quants.c)開頭就是
|
| 139 |
+
// assert(nrc == 1); UNUSED(nrc); UNUSED(bx); UNUSED(by); UNUSED(bs);
|
| 140 |
+
// 只有 ARM 的 SVE+MATMUL 版本才支援 nrc==2。而 Release 編譯(-DNDEBUG)會把 assert
|
| 141 |
+
// 編譯掉,於是「傳 nrc=16」不會報錯、只會安靜地算第 1 列,其餘 15 列留在記憶體裡
|
| 142 |
+
// 是舊資料 —— 產出看起來合理但數值全錯。
|
| 143 |
+
// (本專案真的踩過:微基準測量到「nrc=16 快 16 倍」,其實是「只做了 1/16 的工作」,
|
| 144 |
+
// 而且權重填 0x5A 讓 checksum 兩邊都是 0,完美掩蓋了錯誤。教訓見 docs/04。)
|
| 145 |
+
// 想要真的多列 SIMD 效能,只能自己寫 AVX512-VNNI 的 kernel,不是靠改參數。
|
| 146 |
+
|
| 147 |
+
// Debug hook: when set, expert_forward writes its gate/up/SwiGLU intermediates for
|
| 148 |
+
// the first token of the call. Used by tools/ to compare an intermediate against
|
| 149 |
+
// NumPy; unset in normal runs so the hot path is unaffected.
|
| 150 |
+
static float * g_dbg_buf = nullptr;
|
| 151 |
+
static int64_t g_dbg_cap = 0;
|
| 152 |
+
|
| 153 |
+
static inline void dot_row(vec_dot_fn fn, const uint8_t * row, const void * xq,
|
| 154 |
+
size_t stride_row, int n, float & acc) {
|
| 155 |
+
float s = 0.0f;
|
| 156 |
+
fn(n, &s, 0, row, stride_row, xq, 0, 1);
|
| 157 |
+
acc += s;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// 一次算 SDQ_DOT_BATCH 個輸出列。
|
| 161 |
+
// 為什麼需要:ggml 的 K-quant 點積 kernel 是為「多列」設計的,nrc>1 會走完全不同的
|
| 162 |
+
// SIMD 路徑。實測(tools 裡的 microdot):nrc=1 只有 24~28 GMAC/s,nrc=16 有
|
| 163 |
+
// 347~438 GMAC/s(快 14~16 倍),而且 **checksum 完全相同**(逐位元組等價)。
|
| 164 |
+
// 舊程式逐列呼叫 nrc=1,等於把 14 倍的效能丟掉。
|
| 165 |
+
static const int SDQ_DOT_BATCH = 16;
|
| 166 |
+
|
| 167 |
+
static inline void dot_rows(vec_dot_fn fn, const uint8_t * rows, size_t row_stride,
|
| 168 |
+
const void * xq, int n, int64_t n_rows, float * out) {
|
| 169 |
+
int64_t r = 0;
|
| 170 |
+
for (; r + SDQ_DOT_BATCH <= n_rows; r += SDQ_DOT_BATCH) {
|
| 171 |
+
fn(n, out + r, 1, rows + (size_t) r * row_stride, row_stride, xq, 0, SDQ_DOT_BATCH);
|
| 172 |
+
}
|
| 173 |
+
for (; r < n_rows; r++) {
|
| 174 |
+
fn(n, out + r, 0, rows + (size_t) r * row_stride, row_stride, xq, 0, 1);
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// ---------------------------------------------------------------- 路由紀錄(SDQ_ROUTE_LOG)
|
| 179 |
+
// 用途:量測「連續 k 個 token 的 expert 集合重疊多少」。
|
| 180 |
+
// 這決定多 token 批次(投機解碼 / 批次解碼)能不能省下 SSD 讀取:
|
| 181 |
+
// 每層的關鍵路徑是「有沒有任何一個 expert 沒命中」,所以 U(k) < 8k 就有賺。
|
| 182 |
+
// 格式(每行一次 op 呼叫,空白分欄):
|
| 183 |
+
// <layer> <n_tok> <t0:e0,e1,...> <t1:...> ...
|
| 184 |
+
static void sdq_route_log(int il, int64_t n_tok, const ggml_tensor * ids,
|
| 185 |
+
size_t ids_nb0, size_t ids_nb1, int n_used) {
|
| 186 |
+
static const char * path = getenv("SDQ_ROUTE_LOG");
|
| 187 |
+
if (!path || !*path) {
|
| 188 |
+
return;
|
| 189 |
+
}
|
| 190 |
+
static std::mutex mtx;
|
| 191 |
+
static FILE * f = nullptr;
|
| 192 |
+
{
|
| 193 |
+
std::lock_guard<std::mutex> lk(mtx);
|
| 194 |
+
if (!f) {
|
| 195 |
+
f = fopen(path, "w");
|
| 196 |
+
}
|
| 197 |
+
if (!f) {
|
| 198 |
+
return;
|
| 199 |
+
}
|
| 200 |
+
fprintf(f, "%d %lld", il, (long long) n_tok);
|
| 201 |
+
const uint8_t * base = (const uint8_t *) ids->data;
|
| 202 |
+
for (int64_t t = 0; t < n_tok; t++) {
|
| 203 |
+
fprintf(f, " %lld:", (long long) t);
|
| 204 |
+
for (int k = 0; k < n_used; k++) {
|
| 205 |
+
const int32_t e = *(const int32_t *) (base + (size_t) k * ids_nb0 + (size_t) t * ids_nb1);
|
| 206 |
+
fprintf(f, "%s%d", k ? "," : "", e);
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
fputc('\n', f);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// ---------------------------------------------------------------- 每層執行紀錄
|
| 214 |
+
|
| 215 |
+
// 重要:input 張量的指標**不能**在建圖時暫存起來 —— llama.cpp 每次 decode 都會
|
| 216 |
+
// 重新建圖,graph context 的配置器可能把張量搬到不同位址(形狀不同時尤其明顯)。
|
| 217 |
+
// 所以 op 執行時一律從 dst->src[] 取得,這才是唯一可靠的來源。
|
| 218 |
+
struct layer_rec {
|
| 219 |
+
int il = -1;
|
| 220 |
+
int64_t n_expert = 0;
|
| 221 |
+
int64_t n_expert_used = 0;
|
| 222 |
+
float swiglu_limit = 0.0f;
|
| 223 |
+
|
| 224 |
+
// 每個 expert 需要處理的 (token, topk 位置) 配對
|
| 225 |
+
std::vector<std::vector<std::pair<int32_t, int32_t>>> tok_of_expert;
|
| 226 |
+
// 每個 token 用到的 (expert, topk 位置) —— token 切分時用(避免輸出競爭)
|
| 227 |
+
std::vector<std::vector<std::pair<int32_t, int32_t>>> exp_of_tok;
|
| 228 |
+
std::vector<int32_t> need; // 本層要用的 expert 編��
|
| 229 |
+
std::vector<int32_t> need_sorted; // 依編號排序(合併讀取用)
|
| 230 |
+
std::vector<int32_t> need_slot; // 對應的 arena 槽位
|
| 231 |
+
std::vector<std::vector<float>> partial; // mode A:每個執行緒一份累加緩衝
|
| 232 |
+
std::vector<int32_t> slot_of_expert; // [expert] -> arena 槽位(-1 = 沒分頁)
|
| 233 |
+
std::vector<int32_t> need_slot_of_expert; // 合併讀取時暫存槽位
|
| 234 |
+
std::vector<int32_t> miss_sorted; // 需要從 SSD 讀的 expert(已排序)
|
| 235 |
+
gen_barrier bar;
|
| 236 |
+
std::mutex mtx;
|
| 237 |
+
bool warned = false;
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
static std::vector<layer_rec *> g_recs;
|
| 241 |
+
|
| 242 |
+
static layer_rec * rec_for(int il) {
|
| 243 |
+
static std::mutex m;
|
| 244 |
+
std::lock_guard<std::mutex> lk(m);
|
| 245 |
+
if ((int) g_recs.size() <= il) {
|
| 246 |
+
g_recs.resize(il + 1, nullptr);
|
| 247 |
+
}
|
| 248 |
+
if (!g_recs[il]) {
|
| 249 |
+
g_recs[il] = new layer_rec();
|
| 250 |
+
g_recs[il]->il = il;
|
| 251 |
+
}
|
| 252 |
+
return g_recs[il];
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// ---------------------------------------------------------------- 自訂 op 本體
|
| 256 |
+
|
| 257 |
+
// mode C(預設,partial 緩衝 <= 上限):依 expert 流水線,取→算交錯、無中途 barrier。
|
| 258 |
+
// mode B(partial 緩衝太大時的退路):先分頁完再一起算,會有 barrier 等待。
|
| 259 |
+
// 上限給一個很大的值=永遠走 mode C;給 0=強制 mode B(除錯用)。
|
| 260 |
+
static size_t env_partial_cap_mb() {
|
| 261 |
+
const char * e = getenv("SDQ_PARTIAL_CAP_MB");
|
| 262 |
+
return e ? (size_t) atoi(e) : 4096; // MB,預設 4096 MB → 一律 mode C
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// ---------------------------------------------------------------- 單一 expert 的數學
|
| 266 |
+
// 從 arena 的槽位記憶體算出一個 expert 對多個 token 的輸出(加權後直接累加到 out)
|
| 267 |
+
static void expert_forward(const expert_desc & ed, const uint8_t * base,
|
| 268 |
+
const float * cur, int64_t n_embd,
|
| 269 |
+
const std::vector<std::pair<int32_t, int32_t>> & toks,
|
| 270 |
+
const float * wts, size_t wts_nb0, size_t wts_nb1, int /*n_used*/,
|
| 271 |
+
float swiglu_limit,
|
| 272 |
+
float * out, std::vector<uint8_t> * /*scratch*/) {
|
| 273 |
+
int64_t gate_rows = ed.ne1;
|
| 274 |
+
int64_t up_rows = ed.ne1;
|
| 275 |
+
size_t gate_row = ggml_row_size(ed.type[part::GATE], ed.ne0);
|
| 276 |
+
size_t up_row = gate_row;
|
| 277 |
+
if (ed.fused_gate_up) {
|
| 278 |
+
gate_rows = ed.ne1 / 2;
|
| 279 |
+
up_rows = gate_rows;
|
| 280 |
+
} else {
|
| 281 |
+
up_row = ggml_row_size(ed.type[part::UP], ed.ne0);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
const vec_dot_fn f_gate = dot_for(ed.type[part::GATE]);
|
| 285 |
+
const vec_dot_fn f_up = ed.fused_gate_up ? f_gate : dot_for(ed.type[part::UP]);
|
| 286 |
+
const vec_dot_fn f_down = dot_for(ed.type[part::DOWN]);
|
| 287 |
+
if (!f_gate || !f_up || !f_down) {
|
| 288 |
+
fprintf(stderr, "[sdq] 不支援的專家量化型別:gate=%s up=%s down=%s\n",
|
| 289 |
+
ggml_type_name(ed.type[part::GATE]),
|
| 290 |
+
ggml_type_name(ed.type[part::UP]),
|
| 291 |
+
ggml_type_name(ed.type[part::DOWN]));
|
| 292 |
+
return;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
const uint8_t * wg = base + ed.slot_off[part::GATE];
|
| 296 |
+
const uint8_t * wu = base + ed.slot_off[part::UP];
|
| 297 |
+
const uint8_t * wd = base + ed.slot_off[part::DOWN];
|
| 298 |
+
const size_t down_row = ggml_row_size(ed.type[part::DOWN], ed.down_ne0);
|
| 299 |
+
|
| 300 |
+
const ggml_type act_gate = act_type_for(ed.type[part::GATE]);
|
| 301 |
+
const ggml_type act_down = act_type_for(ed.type[part::DOWN]);
|
| 302 |
+
const size_t xq_sz = ggml_row_size(act_gate, ed.ne0);
|
| 303 |
+
const size_t hq_sz = ggml_row_size(act_down, ed.down_ne0);
|
| 304 |
+
std::vector<float> gate_batch(toks.size() * (size_t) gate_rows);
|
| 305 |
+
std::vector<float> up_batch(toks.size() * (size_t) up_rows);
|
| 306 |
+
std::vector<float> h_batch(toks.size() * (size_t) ed.down_ne0);
|
| 307 |
+
|
| 308 |
+
// Pack all activations for this expert contiguously. This keeps the
|
| 309 |
+
// routed token group in one cache-friendly allocation. The current GGML
|
| 310 |
+
// x86 dot ABI is still invoked one activation at a time for correctness.
|
| 311 |
+
std::vector<uint8_t> xq_batch(toks.size() * xq_sz);
|
| 312 |
+
std::vector<uint8_t> hq_batch(toks.size() * hq_sz);
|
| 313 |
+
for (size_t i = 0; i < toks.size(); i++) {
|
| 314 |
+
const int32_t t = toks[i].first;
|
| 315 |
+
PROF_T0_x(_tb);
|
| 316 |
+
quantize_act(act_gate, cur + (size_t) t * n_embd,
|
| 317 |
+
xq_batch.data() + i * xq_sz, ed.ne0);
|
| 318 |
+
PROF_ADD(qact_ns, _tb);
|
| 319 |
+
}
|
| 320 |
+
if (toks.size() > 1) {
|
| 321 |
+
g_prof.grouped_batches.fetch_add(1, std::memory_order_relaxed);
|
| 322 |
+
g_prof.grouped_tokens.fetch_add(toks.size(), std::memory_order_relaxed);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
PROF_T0_x(_ta); // expert_forward 總計時
|
| 326 |
+
PROF_T0_x(_tc); // gate / up dot products
|
| 327 |
+
// A safe grouped kernel for the current GGML ABI: keep one weight row hot
|
| 328 |
+
// while evaluating every routed token. This is intentionally not calling
|
| 329 |
+
// dot_rows with nrc > 1; x86 K-quant dot functions do not expose a stable
|
| 330 |
+
// multi-activation ABI. A future VNNI kernel can replace only this block.
|
| 331 |
+
for (int64_t r = 0; r < gate_rows; r++) {
|
| 332 |
+
for (size_t ti = 0; ti < toks.size(); ti++) {
|
| 333 |
+
float acc = 0.0f;
|
| 334 |
+
dot_row(f_gate, wg + r * gate_row, xq_batch.data() + ti * xq_sz,
|
| 335 |
+
gate_row, (int) ed.ne0, acc);
|
| 336 |
+
gate_batch[ti * (size_t) gate_rows + (size_t) r] = acc;
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
const uint8_t * up_rows_base = ed.fused_gate_up ? wg + gate_rows * gate_row : wu;
|
| 340 |
+
const size_t up_stride = ed.fused_gate_up ? gate_row : up_row;
|
| 341 |
+
for (int64_t r = 0; r < up_rows; r++) {
|
| 342 |
+
for (size_t ti = 0; ti < toks.size(); ti++) {
|
| 343 |
+
float acc = 0.0f;
|
| 344 |
+
dot_row(f_up, up_rows_base + r * up_stride,
|
| 345 |
+
xq_batch.data() + ti * xq_sz, up_stride, (int) ed.ne0, acc);
|
| 346 |
+
up_batch[ti * (size_t) up_rows + (size_t) r] = acc;
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
PROF_ADD(dot_ns, _tc);
|
| 350 |
+
|
| 351 |
+
if (g_dbg_buf != nullptr && g_dbg_cap >= 3 * (int64_t) gate_rows) {
|
| 352 |
+
memcpy(g_dbg_buf, gate_batch.data(), sizeof(float) * (size_t) gate_rows);
|
| 353 |
+
memcpy(g_dbg_buf + gate_rows, up_batch.data(), sizeof(float) * (size_t) up_rows);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
for (size_t ti = 0; ti < toks.size(); ti++) {
|
| 357 |
+
const int32_t t = toks[ti].first;
|
| 358 |
+
const int32_t k = toks[ti].second;
|
| 359 |
+
PROF_T0_x(_td); // SiLU + activation quantization
|
| 360 |
+
for (int64_t r = 0; r < ed.down_ne0; r++) {
|
| 361 |
+
const float g = gate_batch[ti * (size_t) gate_rows + (size_t) r];
|
| 362 |
+
const float u = up_batch[ti * (size_t) up_rows + (size_t) r];
|
| 363 |
+
float act;
|
| 364 |
+
if (swiglu_limit > 1e-6f) {
|
| 365 |
+
const float uc = std::max(-swiglu_limit, std::min(swiglu_limit, u));
|
| 366 |
+
float gc = g / (1.0f + std::exp(-g));
|
| 367 |
+
gc = std::max(-swiglu_limit, std::min(swiglu_limit, gc));
|
| 368 |
+
act = gc * uc;
|
| 369 |
+
} else {
|
| 370 |
+
act = (g / (1.0f + std::exp(-g))) * u;
|
| 371 |
+
}
|
| 372 |
+
h_batch[ti * (size_t) ed.down_ne0 + (size_t) r] = act;
|
| 373 |
+
}
|
| 374 |
+
if (g_dbg_buf != nullptr && ti == 0 && g_dbg_cap >= 3 * (int64_t) gate_rows) {
|
| 375 |
+
memcpy(g_dbg_buf + 2 * gate_rows, h_batch.data(), sizeof(float) * (size_t) ed.down_ne0);
|
| 376 |
+
}
|
| 377 |
+
quantize_act(act_down, h_batch.data() + ti * (size_t) ed.down_ne0,
|
| 378 |
+
hq_batch.data() + ti * hq_sz, ed.down_ne0);
|
| 379 |
+
PROF_ADD(act_ns, _td);
|
| 380 |
+
|
| 381 |
+
const float w = *(const float *) ((const uint8_t *) wts + k * wts_nb0 + (size_t) t * wts_nb1);
|
| 382 |
+
float * o = out + (size_t) t * n_embd;
|
| 383 |
+
for (int64_t r = 0; r < ed.down_ne1; r++) {
|
| 384 |
+
float acc = 0.0f;
|
| 385 |
+
dot_row(f_down, wd + r * down_row, hq_batch.data() + ti * hq_sz,
|
| 386 |
+
down_row, (int) ed.down_ne0, acc);
|
| 387 |
+
o[r] += acc * w;
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
PROF_ADD(fwd_ns, _ta);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
static void moe_custom_op(ggml_tensor * dst, int ith, int nth, void * userdata) {
|
| 395 |
+
layer_rec * R = (layer_rec *) userdata;
|
| 396 |
+
pager & P = pager::instance();
|
| 397 |
+
|
| 398 |
+
// 從 op 的 src 取得輸入(保證是「這一次」真的被計算的張量)
|
| 399 |
+
const ggml_tensor * src_cur = dst->src[0];
|
| 400 |
+
const ggml_tensor * src_ids = dst->src[1];
|
| 401 |
+
const ggml_tensor * src_wts = dst->src[2];
|
| 402 |
+
if (!src_cur || !src_ids || !src_wts) {
|
| 403 |
+
if (ith == 0) {
|
| 404 |
+
fprintf(stderr, "[sdq] op 缺少 src(%d)\n", R->il);
|
| 405 |
+
}
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
const layer_desc * L = P.layer(R->il);
|
| 410 |
+
PROF_T0_x(_tg); // op 總計時
|
| 411 |
+
g_prof.calls++;
|
| 412 |
+
const int64_t n_embd = src_cur->ne[0];
|
| 413 |
+
const int64_t n_tok = src_cur->ne[2];
|
| 414 |
+
const int64_t n_used = R->n_expert_used;
|
| 415 |
+
// ids 是 2D [n_expert_used, n_tokens] → 用 nb[0]/nb[1]
|
| 416 |
+
// wts 是 3D [1, n_expert_used, n_tokens] → 用 nb[1]/nb[2](nb[0] 會是 4 而不是 n_used*4!)
|
| 417 |
+
const size_t ids_nb0 = src_ids->nb[0], ids_nb1 = src_ids->nb[1];
|
| 418 |
+
const size_t wts_nb0 = src_wts->nb[src_wts->ne[0] == 1 ? 1 : 0];
|
| 419 |
+
const size_t wts_nb1 = src_wts->nb[src_wts->ne[0] == 1 ? 2 : 1];
|
| 420 |
+
const float * cur = (const float *) src_cur->data;
|
| 421 |
+
const float * wts = (const float *) src_wts->data;
|
| 422 |
+
|
| 423 |
+
if (!L || L->experts.empty() || dst->type != GGML_TYPE_F32) {
|
| 424 |
+
if (ith == 0 && !R->warned) {
|
| 425 |
+
R->warned = true;
|
| 426 |
+
fprintf(stderr, "[sdq] 層 %d 沒有可用的分頁描述\n", R->il);
|
| 427 |
+
}
|
| 428 |
+
return;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
if (R->il == 0 && getenv("SDQ_DBG_OP")) {
|
| 432 |
+
fprintf(stderr, "[sdq-op] L%d n_tok=%lld nth=%d need=%d ids_nb1=%zu wts_nb1=%zu\n",
|
| 433 |
+
R->il, (long long) n_tok, nth, (int) R->need.size(), ids_nb1, wts_nb1);
|
| 434 |
+
}
|
| 435 |
+
// ---- 1) 解除上一層的 pin(mode B 直接累加輸出,需要先清空)
|
| 436 |
+
PROF_T0_x(_tsetup);
|
| 437 |
+
if (ith == 0) {
|
| 438 |
+
P.unpin_all();
|
| 439 |
+
}
|
| 440 |
+
if (nth > 1) { R->bar.sync(nth); }
|
| 441 |
+
|
| 442 |
+
// ---- 2) 依 expert 分組 token(只有執行緒 0 做,其餘在此等待)
|
| 443 |
+
if (ith == 0) {
|
| 444 |
+
std::lock_guard<std::mutex> lk(R->mtx);
|
| 445 |
+
R->tok_of_expert.assign((size_t) R->n_expert, {});
|
| 446 |
+
R->exp_of_tok.assign((size_t) n_tok, {});
|
| 447 |
+
const uint8_t * ids_base = (const uint8_t *) src_ids->data;
|
| 448 |
+
for (int64_t t = 0; t < n_tok; t++) {
|
| 449 |
+
for (int64_t k = 0; k < n_used; k++) {
|
| 450 |
+
const int32_t e = *(const int32_t *) (ids_base + k * ids_nb0 + (size_t) t * ids_nb1);
|
| 451 |
+
if (e >= 0 && e < R->n_expert) {
|
| 452 |
+
auto & v = R->tok_of_expert[(size_t) e];
|
| 453 |
+
bool seen = false;
|
| 454 |
+
for (const auto & p : v) {
|
| 455 |
+
if (p.first == (int32_t) t) { seen = true; break; }
|
| 456 |
+
}
|
| 457 |
+
if (!seen) {
|
| 458 |
+
v.emplace_back((int32_t) t, (int32_t) k);
|
| 459 |
+
}
|
| 460 |
+
R->exp_of_tok[(size_t) t].emplace_back(e, (int32_t) k);
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
R->need.clear();
|
| 465 |
+
for (int64_t e = 0; e < R->n_expert; e++) {
|
| 466 |
+
if (!R->tok_of_expert[(size_t) e].empty()) {
|
| 467 |
+
R->need.push_back((int32_t) e);
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
R->need_sorted = R->need;
|
| 471 |
+
std::sort(R->need_sorted.begin(), R->need_sorted.end());
|
| 472 |
+
R->need_slot.assign(R->need.size(), -1); // 只能由 ith==0 配置,其餘執行緒唯讀
|
| 473 |
+
R->slot_of_expert.assign((size_t) R->n_expert, -1);
|
| 474 |
+
R->need_slot_of_expert.assign((size_t) R->n_expert, -1);
|
| 475 |
+
R->miss_sorted.clear();
|
| 476 |
+
R->miss_sorted.reserve((size_t) R->n_expert);
|
| 477 |
+
if (R->partial.size() < 64) {
|
| 478 |
+
R->partial.resize(64);
|
| 479 |
+
}
|
| 480 |
+
sdq_route_log(R->il, n_tok, src_ids, ids_nb0, ids_nb1, (int) n_used);
|
| 481 |
+
}
|
| 482 |
+
if (nth > 1) { R->bar.sync(nth); }
|
| 483 |
+
PROF_ADD(setup_ns, _tsetup);
|
| 484 |
+
|
| 485 |
+
const int n_need = (int) R->need.size();
|
| 486 |
+
|
| 487 |
+
// ---- 3+4) 分頁與計算融合成一條 pipeline
|
| 488 |
+
//
|
| 489 |
+
// 關鍵效能點:不要「先把整層的 expert 都讀完再開始算」。做法是每個執行緒
|
| 490 |
+
// 自己依序處理它負責的 expert:讀一個 → 立刻算它 → 再讀下一個。
|
| 491 |
+
// 這樣 SSD 讀取和 CPU 計算自然重疊,而且 8 條執行緒各自發出 pread,
|
| 492 |
+
// SSD 佇列深度也夠(不需要再靠 queue depth)。
|
| 493 |
+
//
|
| 494 |
+
// 平行切分:mode A 依 expert 切(每個執行緒有自己的累加緩衝,最後相加);
|
| 495 |
+
// mode B 依 token 切(同一個 token 的所有 expert 由同一執行緒算,
|
| 496 |
+
// 但必須先把該 token 的 expert 都備齊,所以先分頁再計算)。
|
| 497 |
+
const int64_t out_elems = n_embd * n_tok;
|
| 498 |
+
const size_t partial_bytes = (size_t) out_elems * 4 * (size_t) nth;
|
| 499 |
+
const bool mode_a = partial_bytes <= (size_t) env_partial_cap_mb() << 20;
|
| 500 |
+
float * out = (float *) dst->data;
|
| 501 |
+
|
| 502 |
+
if (!mode_a) {
|
| 503 |
+
memset(dst->data, 0, (size_t) ggml_nbytes(dst));
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
if (mode_a) {
|
| 507 |
+
// ===================== mode C(預設):依 expert 流水線 =====================
|
| 508 |
+
// 三個設計重點,每個都來自實測:
|
| 509 |
+
//
|
| 510 |
+
// (1) **取→算交錯,中間沒有 barrier**。
|
| 511 |
+
// 舊的 mode B 是「先把整層 8 個 expert 全部讀完 → barrier → 才開始算」。
|
| 512 |
+
// 量測顯示 decode 有 66% 的時間卡在 barrier 上等最慢的執行緒
|
| 513 |
+
// (SDQ_PROFILE_TIME 的 barrier 等待 = op 時間的 70%)。改成交錯後
|
| 514 |
+
// 讀取與計算自然重疊,實測 2.79 → 3.49 tok/s。
|
| 515 |
+
//
|
| 516 |
+
// (2) **分頁走 acquire(),不用 reserve_slot + read_expert_span**。
|
| 517 |
+
// reserve_slot() 會在資料還沒讀進來之前就把 (layer,expert) publish 到
|
| 518 |
+
// page_ 表;一旦該層的 pread 失敗或被中斷,這個槽就會留在 page_ 裡
|
| 519 |
+
// 卻是舊資料,之後的 probe() 會把它當成命中 → 靜默算錯。
|
| 520 |
+
// acquire() 是「先讀完再 publish」,成本相同但不會有這個失效模式。
|
| 521 |
+
//
|
| 522 |
+
// (3) **i += nth 交錯分配**,不是切成連續區塊。
|
| 523 |
+
// 每個 expert 的 SSD 延遲差異很大,交錯分配讓快執行緒自然多幫忙。
|
| 524 |
+
//
|
| 525 |
+
// 輸出不直接寫 dst(同一個 token 的不同 expert 會撞在一起),
|
| 526 |
+
// 所以仍然用「每執行緒一份累加緩衝 + 最後歸約」。
|
| 527 |
+
std::vector<float> & mine = R->partial[(size_t) ith];
|
| 528 |
+
mine.assign((size_t) out_elems, 0.0f);
|
| 529 |
+
std::vector<uint8_t> scratch;
|
| 530 |
+
|
| 531 |
+
// 在取本層的 expert **之前**先替下一層排預取。
|
| 532 |
+
//
|
| 533 |
+
// 實測結論(S7):**預設關閉,而且量到開啟反而慢 8%。**
|
| 534 |
+
// 預測來自 prev_used_[il+1](上一個 token 在該層選的專家)。想用它躲在
|
| 535 |
+
// 「本層的讀取+計算」後面,但量到的結果是:
|
| 536 |
+
// 開(5.68 tok/s,預取使用率 0.48 %)vs 關(6.20 tok/s,0.28 %)
|
| 537 |
+
// 命中率幾乎一樣(70.5 % vs 70.7 %),但 SSD 位元組從 201.5 升到 216.0 MB/token
|
| 538 |
+
// → 預測不夠準,多讀的全是白讀,而且還要和本層的 demand 讀搶頻寬。
|
| 539 |
+
// 另外開啟時同一提示詞的輸出不一致 → 預取佔用的槽���(busy)會讓
|
| 540 |
+
// acquire() 偶爾拿不到槽位而**靜默跳過一個 expert**,這是正確性問題。
|
| 541 |
+
//
|
| 542 |
+
// 保留程式碼是為了讓後人不用重新發明;文件見 docs/DESIGN.md §3.3。
|
| 543 |
+
// SDQ_PREFETCH_LAYER_AHEAD=1 可以打開來重現那個負結果。
|
| 544 |
+
static const bool layer_ahead = [] {
|
| 545 |
+
const char * e = getenv("SDQ_PREFETCH_LAYER_AHEAD");
|
| 546 |
+
return e ? atoi(e) != 0 : false;
|
| 547 |
+
}();
|
| 548 |
+
if (layer_ahead && ith == 0) {
|
| 549 |
+
P.prefetch_layer_ahead(R->il + 1);
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
for (int i = ith; i < n_need; i += nth) {
|
| 553 |
+
const int32_t e = R->need[(size_t) i];
|
| 554 |
+
const int32_t sl = P.acquire(R->il, e, true);
|
| 555 |
+
if (sl < 0) {
|
| 556 |
+
continue;
|
| 557 |
+
}
|
| 558 |
+
P.note_used(R->il, e);
|
| 559 |
+
expert_forward(L->experts[(size_t) e], P.slot_ptr(sl), cur, n_embd,
|
| 560 |
+
R->tok_of_expert[(size_t) e], wts, wts_nb0, wts_nb1,
|
| 561 |
+
(int) n_used, R->swiglu_limit, mine.data(), &scratch);
|
| 562 |
+
// expert_forward 回傳後這個槽位就沒人讀了(最後的歸約只讀 mine),
|
| 563 |
+
// 所以立刻放掉 pin,不要讓它被釘到下一層開頭。
|
| 564 |
+
// 不放的話 prefill 一層會把整層用到的 expert 全部釘住(最多 256 個),
|
| 565 |
+
// 大槽位不夠時就會中止(見 pager::release 的說明)。
|
| 566 |
+
P.release(sl);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
if (nth > 1) { R->bar.sync(nth); }
|
| 570 |
+
for (int64_t idx = ith; idx < out_elems; idx += nth) {
|
| 571 |
+
float acc = 0.0f;
|
| 572 |
+
for (int k = 0; k < nth; k++) {
|
| 573 |
+
acc += R->partial[(size_t) k][(size_t) idx];
|
| 574 |
+
}
|
| 575 |
+
out[idx] = acc;
|
| 576 |
+
}
|
| 577 |
+
} else {
|
| 578 |
+
// mode B:token 多到 partial 緩衝太大時,依 token 切分
|
| 579 |
+
// 先把所有需要的 expert 分頁(各執行緒平行 pread)
|
| 580 |
+
{
|
| 581 |
+
PROF_T0_x(_th); // 分頁 / SSD 讀取
|
| 582 |
+
for (int i = ith; i < n_need; i += nth) {
|
| 583 |
+
const int32_t e = R->need[(size_t) i];
|
| 584 |
+
const int s = P.acquire(R->il, e, true);
|
| 585 |
+
g_prof.experts++;
|
| 586 |
+
R->need_slot[(size_t) i] = s;
|
| 587 |
+
if (s >= 0) {
|
| 588 |
+
R->slot_of_expert[(size_t) e] = s;
|
| 589 |
+
P.note_used(R->il, e);
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
PROF_ADD(read_ns, _th);
|
| 593 |
+
}
|
| 594 |
+
if (nth > 1) { R->bar.sync(nth); }
|
| 595 |
+
const float * cur_p = (const float *) src_cur->data;
|
| 596 |
+
const float * wts_p = (const float *) src_wts->data;
|
| 597 |
+
for (int64_t t = ith; t < n_tok; t += nth) {
|
| 598 |
+
std::vector<uint8_t> scratch;
|
| 599 |
+
for (const auto & ek : R->exp_of_tok[(size_t) t]) {
|
| 600 |
+
const int slot = R->slot_of_expert[(size_t) ek.first];
|
| 601 |
+
if (slot < 0) {
|
| 602 |
+
continue;
|
| 603 |
+
}
|
| 604 |
+
std::vector<std::pair<int32_t, int32_t>> one = {{(int32_t) t, ek.second}};
|
| 605 |
+
expert_forward(L->experts[(size_t) ek.first], P.slot_ptr(slot), cur_p, n_embd,
|
| 606 |
+
one, wts_p, wts_nb0, wts_nb1, (int) n_used,
|
| 607 |
+
R->swiglu_limit, out, &scratch);
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
// 除錯:把某一層的實際輸入/輸出 dump 出來,用 NumPy 對照。
|
| 613 |
+
// 必須先 barrier:上面的歸約是由各執行緒分片寫 out 的,沒有這一道會讀到半成品。
|
| 614 |
+
if (nth > 1) { R->bar.sync(nth); }
|
| 615 |
+
if (const char * dump_path = getenv("SDQ_MOE_DUMP")) {
|
| 616 |
+
const int dump_layer = getenv("SDQ_MOE_DUMP_LAYER") ? atoi(getenv("SDQ_MOE_DUMP_LAYER")) : -1;
|
| 617 |
+
if ((dump_layer < 0 || R->il == dump_layer) && ith == 0) {
|
| 618 |
+
std::string path = std::string(dump_path) + "." + std::to_string(R->il);
|
| 619 |
+
FILE * f = fopen(path.c_str(), "wb");
|
| 620 |
+
if (f) {
|
| 621 |
+
const int32_t meta[6] = {(int32_t) n_embd, (int32_t) n_tok, (int32_t) n_used,
|
| 622 |
+
(int32_t) R->n_expert, 1, 2};
|
| 623 |
+
fwrite(meta, sizeof(int32_t), 6, f);
|
| 624 |
+
fwrite(cur, sizeof(float), (size_t) (n_embd * n_tok), f);
|
| 625 |
+
const uint8_t * ids_base = (const uint8_t *) src_ids->data;
|
| 626 |
+
for (int64_t t = 0; t < n_tok; t++) {
|
| 627 |
+
for (int64_t k = 0; k < n_used; k++) {
|
| 628 |
+
const int32_t e = *(const int32_t *) (ids_base + k * ids_nb0 + (size_t) t * ids_nb1);
|
| 629 |
+
fwrite(&e, sizeof(int32_t), 1, f);
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
+
fwrite(wts, sizeof(float), (size_t) (n_used * n_tok), f);
|
| 633 |
+
fwrite(out, sizeof(float), (size_t) (n_embd * n_tok), f);
|
| 634 |
+
fclose(f);
|
| 635 |
+
fprintf(stderr, "[sdq] dumped layer %d -> %s\n", R->il, path.c_str());
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
if (nth > 1) { R->bar.sync(nth); }
|
| 641 |
+
PROF_ADD(op_ns, _tg);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
// 輸出計時結果(sdq_shutdown 呼叫)
|
| 645 |
+
void sdq_print_time_profile() {
|
| 646 |
+
if (!prof_on()) {
|
| 647 |
+
return;
|
| 648 |
+
}
|
| 649 |
+
const double ms = 1e6;
|
| 650 |
+
auto v = [ms](const std::atomic<uint64_t> & a) { return a.load() / ms; };
|
| 651 |
+
const double op = v(g_prof.op_ns);
|
| 652 |
+
const double fwd = v(g_prof.fwd_ns);
|
| 653 |
+
fprintf(stderr, "[sdq-prof] 自訂 MoE op(%llu 次呼叫、%llu 個 expert 分頁)\n",
|
| 654 |
+
(unsigned long long) g_prof.calls.load(), (unsigned long long) g_prof.experts.load());
|
| 655 |
+
fprintf(stderr, "[sdq-prof] op 合計 %9.1f ms\n", op);
|
| 656 |
+
fprintf(stderr, "[sdq-prof] expert_forward %9.1f ms (%5.1f%%) 其中:\n", fwd, op > 0 ? 100.0 * fwd / op : 0.0);
|
| 657 |
+
fprintf(stderr, "[sdq-prof] grouped batches=%llu routed_tokens=%llu\n",
|
| 658 |
+
(unsigned long long) g_prof.grouped_batches.load(),
|
| 659 |
+
(unsigned long long) g_prof.grouped_tokens.load());
|
| 660 |
+
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);
|
| 661 |
+
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);
|
| 662 |
+
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);
|
| 663 |
+
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);
|
| 664 |
+
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);
|
| 665 |
+
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);
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
// ---------------------------------------------------------------- graph 建構
|
| 669 |
+
|
| 670 |
+
ggml_tensor * sdq_build_moe(ggml_cgraph * gf, ggml_context * ctx, ggml_tensor * cur,
|
| 671 |
+
ggml_tensor * ids, ggml_tensor * weights, int il, int64_t n_expert,
|
| 672 |
+
int64_t n_expert_used, float swiglu_limit) {
|
| 673 |
+
layer_rec * R = rec_for(il);
|
| 674 |
+
R->n_expert = n_expert;
|
| 675 |
+
R->n_expert_used = n_expert_used;
|
| 676 |
+
R->swiglu_limit = swiglu_limit;
|
| 677 |
+
|
| 678 |
+
// 呼叫端給的是 2D [n_embd, n_tokens],這裡跟上游一樣先 reshape 成 3D
|
| 679 |
+
// [n_embd, 1, n_tokens](上游 mul_mat_id 路徑也是這樣拿 token 數的)
|
| 680 |
+
ggml_tensor * cur3 = (cur->ne[1] == 1) ? cur : ggml_reshape_3d(ctx, cur, cur->ne[0], 1, cur->ne[1]);
|
| 681 |
+
const int64_t n_tok = cur3->ne[2];
|
| 682 |
+
|
| 683 |
+
ggml_tensor * args[3] = { cur3, ids, weights };
|
| 684 |
+
ggml_tensor * out = ggml_custom_4d(ctx, GGML_TYPE_F32, cur3->ne[0], n_tok, 1, 1,
|
| 685 |
+
args, 3, moe_custom_op, GGML_N_TASKS_MAX, R);
|
| 686 |
+
ggml_set_name(out, "sdq_moe_out");
|
| 687 |
+
// 關鍵:把這個節點註冊進圖,ggml 才會為它配置獨立的 compute buffer。
|
| 688 |
+
// 少了這一行,layer>=1 的輸出會被其他張量覆蓋(layer 0 僥倖正確)。
|
| 689 |
+
ggml_build_forward_expand(gf, out);
|
| 690 |
+
return out;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
} // namespace sdq
|
| 694 |
+
namespace sdq {
|
| 695 |
+
// 自我測試:對指定 (layer, expert) 用 pager 的資料算一次 MoE 輸出,
|
| 696 |
+
// 讓 Python 用純 NumPy 從同一段 GGUF 位元組重算並比對。
|
| 697 |
+
// 輸出:out[0..n_embd) = 輸出,out[n_embd..2*n_embd) = 輸入 activation
|
| 698 |
+
int sdq_moe_selftest(int il, int ie, const float * x, float * out_io,
|
| 699 |
+
int64_t n_embd, int64_t n_ff, int n_tok) {
|
| 700 |
+
pager & P = pager::instance();
|
| 701 |
+
const layer_desc * L = P.layer(il);
|
| 702 |
+
if (!L || ie < 0 || ie >= (int) L->experts.size()) {
|
| 703 |
+
fprintf(stderr, "[sdq-selftest] 層/expert 不存在:%d/%d\n", il, ie);
|
| 704 |
+
return 1;
|
| 705 |
+
}
|
| 706 |
+
const int slot = P.acquire(il, ie, true);
|
| 707 |
+
if (slot < 0) {
|
| 708 |
+
fprintf(stderr, "[sdq-selftest] 分頁失敗\n");
|
| 709 |
+
return 2;
|
| 710 |
+
}
|
| 711 |
+
const expert_desc & ed = L->experts[(size_t) ie];
|
| 712 |
+
if (ed.ne0 != n_embd || ed.ne1 != n_ff) {
|
| 713 |
+
fprintf(stderr, "[sdq-selftest] 維度不符:ne0=%lld ne1=%lld(預期 %lld/%lld)\n",
|
| 714 |
+
(long long) ed.ne0, (long long) ed.ne1, (long long) n_embd, (long long) n_ff);
|
| 715 |
+
return 3;
|
| 716 |
+
}
|
| 717 |
+
std::vector<std::pair<int32_t, int32_t>> toks;
|
| 718 |
+
std::vector<float> wts((size_t) n_tok, 1.0f);
|
| 719 |
+
for (int t = 0; t < n_tok; t++) {
|
| 720 |
+
toks.emplace_back(t, 0);
|
| 721 |
+
}
|
| 722 |
+
std::vector<float> out((size_t) n_embd * n_tok, 0.0f);
|
| 723 |
+
std::vector<uint8_t> scratch;
|
| 724 |
+
std::vector<float> dbg;
|
| 725 |
+
if (getenv("SDQ_SELFTEST_DBG")) {
|
| 726 |
+
dbg.assign((size_t) (3 * n_ff), 0.0f);
|
| 727 |
+
g_dbg_buf = dbg.data();
|
| 728 |
+
g_dbg_cap = 3 * n_ff;
|
| 729 |
+
}
|
| 730 |
+
expert_forward(ed, P.slot_ptr(slot), x, n_embd, toks, wts.data(), 4, 4, 1, 0.0f,
|
| 731 |
+
out.data(), &scratch);
|
| 732 |
+
g_dbg_buf = nullptr;
|
| 733 |
+
if (!dbg.empty()) {
|
| 734 |
+
std::string dp = std::string("sdq-dbg-") + std::to_string(il) + "-" + std::to_string(ie) + ".bin";
|
| 735 |
+
FILE * df = fopen(dp.c_str(), "wb");
|
| 736 |
+
if (df) {
|
| 737 |
+
fwrite(dbg.data(), sizeof(float), dbg.size(), df);
|
| 738 |
+
fclose(df);
|
| 739 |
+
fprintf(stderr, "[sdq-selftest] intermediates -> %s (gate/up/h each %lld)\n",
|
| 740 |
+
dp.c_str(), (long long) n_ff);
|
| 741 |
+
}
|
| 742 |
+
}
|
| 743 |
+
for (int64_t t = 0; t < n_tok; t++) {
|
| 744 |
+
for (int64_t i = 0; i < n_embd; i++) {
|
| 745 |
+
out_io[t * n_embd + i] = out[(size_t) t * n_embd + i];
|
| 746 |
+
out_io[n_tok * n_embd + t * n_embd + i] = x[(size_t) t * n_embd + i];
|
| 747 |
+
}
|
| 748 |
+
}
|
| 749 |
+
P.unpin_all();
|
| 750 |
+
fprintf(stderr, "[sdq-selftest] L%d E%d n_tok=%d 計算完成(gate %s / up %s / down %s)\n",
|
| 751 |
+
il, ie, n_tok, ggml_type_name(ed.type[part::GATE]), ggml_type_name(ed.type[part::UP]),
|
| 752 |
+
ggml_type_name(ed.type[part::DOWN]));
|
| 753 |
+
return 0;
|
| 754 |
+
}
|
| 755 |
+
} // namespace sdq
|
cpp/sdq_mtp.cpp
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// EPQ target with Qwen3.6 MTP draft context.
|
| 2 |
+
#include "llama.h"
|
| 3 |
+
#include "sdq_pager.h"
|
| 4 |
+
#include "common.h"
|
| 5 |
+
#include "sampling.h"
|
| 6 |
+
#include "speculative.h"
|
| 7 |
+
|
| 8 |
+
#include <algorithm>
|
| 9 |
+
#include <chrono>
|
| 10 |
+
#include <cstdio>
|
| 11 |
+
#include <cstdlib>
|
| 12 |
+
#include <string>
|
| 13 |
+
#include <vector>
|
| 14 |
+
|
| 15 |
+
using namespace sdq;
|
| 16 |
+
|
| 17 |
+
namespace {
|
| 18 |
+
struct opt_t { std::string model, prompt; int ctx=4096, threads=8, n=48, draft_n=2, ram=8192, ubatch=128; bool no_pf=false; };
|
| 19 |
+
bool parse(int argc, char ** argv, opt_t & o) {
|
| 20 |
+
auto next = [&](int & i) -> const char * { return ++i < argc ? argv[i] : nullptr; };
|
| 21 |
+
for (int i=1; i<argc; i++) {
|
| 22 |
+
const std::string a=argv[i]; const char * v=nullptr;
|
| 23 |
+
if (a=="-m" || a=="--model") { if (!(v=next(i))) return false; o.model=v; }
|
| 24 |
+
else if (a=="-p" || a=="--prompt") { if (!(v=next(i))) return false; o.prompt=v; }
|
| 25 |
+
else if (a=="-n") { if (!(v=next(i))) return false; o.n=atoi(v); }
|
| 26 |
+
else if (a=="--draft-n") { if (!(v=next(i))) return false; o.draft_n=atoi(v); }
|
| 27 |
+
else if (a=="-c") { if (!(v=next(i))) return false; o.ctx=atoi(v); }
|
| 28 |
+
else if (a=="-t") { if (!(v=next(i))) return false; o.threads=atoi(v); }
|
| 29 |
+
else if (a=="-b") { if (!(v=next(i))) return false; o.ubatch=atoi(v); }
|
| 30 |
+
else if (a=="--ram-budget-mb") { if (!(v=next(i))) return false; o.ram=atoi(v); }
|
| 31 |
+
else if (a=="--no-prefetch") o.no_pf=true;
|
| 32 |
+
else return false;
|
| 33 |
+
}
|
| 34 |
+
return !o.model.empty() && !o.prompt.empty();
|
| 35 |
+
}
|
| 36 |
+
double now_s() { return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
int main(int argc, char ** argv) {
|
| 40 |
+
opt_t o;
|
| 41 |
+
if (!parse(argc, argv, o)) return 1;
|
| 42 |
+
setenv("SDQ_MODEL_PATH", o.model.c_str(), 1);
|
| 43 |
+
setenv("SDQ_IO_THREADS", std::to_string(o.threads*3).c_str(), 1);
|
| 44 |
+
setenv("SDQ_RAM_BUDGET_MB", std::to_string(o.ram).c_str(), 1);
|
| 45 |
+
setenv("SDQ_FADV_DONTNEED", "1", 1);
|
| 46 |
+
if (o.no_pf) setenv("SDQ_PREFETCH", "0", 1);
|
| 47 |
+
llama_backend_init();
|
| 48 |
+
llama_numa_init(GGML_NUMA_STRATEGY_DISABLED);
|
| 49 |
+
llama_model_params mp=llama_model_default_params();
|
| 50 |
+
mp.n_gpu_layers=0; mp.load_mode=LLAMA_LOAD_MODE_MMAP; mp.load_mtp=true;
|
| 51 |
+
sdq_set_enabled(true); sdq_install_buft_overrides(&mp);
|
| 52 |
+
llama_model * model=llama_model_load_from_file(o.model.c_str(), mp);
|
| 53 |
+
if (!model || !sdq_init(o.model)) return 2;
|
| 54 |
+
llama_context_params c=llama_context_default_params();
|
| 55 |
+
c.n_ctx=o.ctx; c.n_seq_max=1; c.n_batch=std::max(o.ubatch,o.draft_n+1); c.n_ubatch=o.ubatch;
|
| 56 |
+
c.n_outputs_max=o.draft_n+1; c.n_outputs_max_per_seq=o.draft_n+1;
|
| 57 |
+
c.n_threads=o.threads; c.n_threads_batch=o.threads; c.ctx_type=LLAMA_CONTEXT_TYPE_DEFAULT;
|
| 58 |
+
llama_context * ctx=llama_init_from_model(model,c);
|
| 59 |
+
if (!ctx) return 3;
|
| 60 |
+
|
| 61 |
+
common_params p;
|
| 62 |
+
p.model.path=o.model; p.n_ctx=o.ctx; p.n_batch=c.n_batch; p.n_ubatch=o.ubatch;
|
| 63 |
+
p.cpuparams.n_threads=o.threads; p.cpuparams_batch.n_threads=o.threads;
|
| 64 |
+
p.speculative.types={COMMON_SPECULATIVE_TYPE_DRAFT_MTP};
|
| 65 |
+
p.speculative.draft.n_max=o.draft_n;
|
| 66 |
+
auto init=common_speculative_init_from_params(p,model,ctx);
|
| 67 |
+
llama_context * dctx=init->context();
|
| 68 |
+
p.speculative.draft.ctx_tgt=ctx; p.speculative.draft.ctx_dft=dctx;
|
| 69 |
+
if (!dctx) { fprintf(stderr,"[sdq-mtp] MTP context init failed\n"); return 4; }
|
| 70 |
+
common_speculative * spec=common_speculative_init(p.speculative,1);
|
| 71 |
+
if (!spec) { fprintf(stderr,"[sdq-mtp] speculative init failed\n"); return 5; }
|
| 72 |
+
common_params_sampling sp; sp.temp=0; sp.top_k=1;
|
| 73 |
+
common_sampler * sampler=common_sampler_init(model,sp);
|
| 74 |
+
auto input=common_tokenize(ctx,o.prompt,true,true);
|
| 75 |
+
if (input.empty()) return 6;
|
| 76 |
+
common_batch pb(ctx);
|
| 77 |
+
for (size_t i=0;i+1<input.size();i++) pb.add(input[i],(llama_pos)i,0,false);
|
| 78 |
+
if (pb.size() && llama_process(ctx,LLAMA_PROCESS_TYPE_DECODE,pb.get())!=0) return 7;
|
| 79 |
+
pager::instance().end_token(); common_speculative_process(spec,pb);
|
| 80 |
+
llama_tokens prompt(input.begin(),input.end()-1); common_speculative_begin(spec,0,prompt);
|
| 81 |
+
llama_token last=input.back(); int past=(int)prompt.size(), gen=0, drafted=0, accepted=0;
|
| 82 |
+
common_prompt_checkpoint ck; llama_tokens draft;
|
| 83 |
+
const double t0=now_s(); printf("assistant: "); fflush(stdout);
|
| 84 |
+
while (gen<o.n) {
|
| 85 |
+
ck.update_pos(prompt.size(),llama_memory_seq_pos_min(llama_get_memory(ctx),0),llama_memory_seq_pos_max(llama_get_memory(ctx),0));
|
| 86 |
+
ck.update_tgt(ctx,0,LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); ck.update_dft(dctx,0,LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
|
| 87 |
+
if (draft.empty()) { auto & dp=common_speculative_get_draft_params(spec,0); dp={}; dp.drafting=true; dp.n_max=std::min(o.draft_n,o.n-gen-1); dp.pos0=past; dp.id_last=last; dp.prompt=&prompt; dp.result=&draft; common_speculative_draft(spec); }
|
| 88 |
+
ck.load_dft(dctx,0,LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY);
|
| 89 |
+
common_batch vb(ctx); vb.add(last,past++,0,true); for(size_t i=0;i<draft.size();i++) vb.add(draft[i],past+(llama_pos)i,0,true);
|
| 90 |
+
if (llama_process(ctx,LLAMA_PROCESS_TYPE_DECODE,vb.get())!=0) break;
|
| 91 |
+
pager::instance().end_token();
|
| 92 |
+
if(!common_speculative_process(spec,vb)) break;
|
| 93 |
+
auto ids=common_sampler_sample_and_accept_n(sampler,ctx,draft); if(ids.empty()) break;
|
| 94 |
+
drafted+=(int)draft.size(); accepted+=(int)ids.size()-1;
|
| 95 |
+
if(ids.size()-1<draft.size()) { ck.load_tgt(ctx,0,LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); ck.load_dft(dctx,0,LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY); prompt.resize(ck.n_tokens); past=ck.n_tokens; draft=std::move(ids); continue; }
|
| 96 |
+
for(auto id:ids) { if(gen>=o.n) break; printf("%s",common_token_to_piece(ctx,id).c_str()); gen++; prompt.push_back(last); last=id; }
|
| 97 |
+
common_speculative_accept(spec,0,(uint16_t)(ids.size()-1)); past+=(int)ids.size()-1;
|
| 98 |
+
}
|
| 99 |
+
printf("\n"); double dt=now_s(); fprintf(stderr,"[sdq-mtp] generated=%d drafted=%d accepted=%d acceptance=%.1f%% speed=%.2f tok/s\n",gen,drafted,accepted,drafted?100.0*accepted/drafted:0,dt-t0?gen/(dt-t0):0);
|
| 100 |
+
common_speculative_print_stats(spec); common_speculative_free(spec); common_sampler_free(sampler); sdq_shutdown(); llama_free(ctx); llama_model_free(model); llama_backend_free(); return 0;
|
| 101 |
+
}
|
cpp/sdq_pager.cpp
ADDED
|
@@ -0,0 +1,2091 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// SDQ expert pager 實作:見 sdq_pager.h 的設計說明
|
| 2 |
+
#include "sdq_pager.h"
|
| 3 |
+
|
| 4 |
+
#include <algorithm>
|
| 5 |
+
#include <cinttypes>
|
| 6 |
+
#include <cmath>
|
| 7 |
+
#include <cstdio>
|
| 8 |
+
#include <cstdlib>
|
| 9 |
+
#include <cstring>
|
| 10 |
+
#include <fcntl.h>
|
| 11 |
+
#include <ctime>
|
| 12 |
+
#include <fstream>
|
| 13 |
+
#include <map>
|
| 14 |
+
#include <sstream>
|
| 15 |
+
#include <stdexcept>
|
| 16 |
+
#include <sys/mman.h>
|
| 17 |
+
#include <sys/resource.h>
|
| 18 |
+
#include <sys/stat.h>
|
| 19 |
+
#include <sys/types.h>
|
| 20 |
+
#include <unistd.h>
|
| 21 |
+
|
| 22 |
+
#if defined(SDQ_HAVE_LIBURING)
|
| 23 |
+
#include <liburing.h>
|
| 24 |
+
#endif
|
| 25 |
+
|
| 26 |
+
namespace sdq {
|
| 27 |
+
|
| 28 |
+
// ============================================================ 小工具
|
| 29 |
+
|
| 30 |
+
uint64_t now_us() {
|
| 31 |
+
struct timespec ts;
|
| 32 |
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
| 33 |
+
return (uint64_t) ts.tv_sec * 1000000ull + (uint64_t) ts.tv_nsec / 1000ull;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
size_t process_rss_bytes() {
|
| 37 |
+
std::ifstream f("/proc/self/statm");
|
| 38 |
+
size_t total = 0, resident = 0;
|
| 39 |
+
if (f >> total >> resident) {
|
| 40 |
+
return resident * (size_t) sysconf(_SC_PAGESIZE);
|
| 41 |
+
}
|
| 42 |
+
return 0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
size_t process_anon_bytes() {
|
| 46 |
+
// RssAnon 是 kernel 維護的精確計數器:不可回收、真正佔住實體 RAM 的部分。
|
| 47 |
+
// arena sizing 必須用這個而不是 statm 的 resident,原因見 alloc_arena()。
|
| 48 |
+
std::ifstream f("/proc/self/status");
|
| 49 |
+
std::string key;
|
| 50 |
+
size_t value = 0;
|
| 51 |
+
while (f >> key) {
|
| 52 |
+
if (key == "RssAnon:") {
|
| 53 |
+
if (f >> value) {
|
| 54 |
+
return value * 1024;
|
| 55 |
+
}
|
| 56 |
+
return 0;
|
| 57 |
+
}
|
| 58 |
+
f.ignore(4096, '\n');
|
| 59 |
+
}
|
| 60 |
+
// 沒有 RssAnon 的舊核心:退回 statm,把 shared 視為可回收的近似值。
|
| 61 |
+
std::ifstream g("/proc/self/statm");
|
| 62 |
+
size_t total = 0, resident = 0, shared = 0;
|
| 63 |
+
if (g >> total >> resident >> shared) {
|
| 64 |
+
const size_t page = (size_t) sysconf(_SC_PAGESIZE);
|
| 65 |
+
return (resident > shared ? resident - shared : 0) * page;
|
| 66 |
+
}
|
| 67 |
+
return 0;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
size_t enforced_memory_ceiling_bytes() {
|
| 71 |
+
// 部署時真正管用的是 kernel 強制的上限,不是 CLI 上的一個數字。
|
| 72 |
+
// RLIMIT_DATA 在 Linux 4.7 之後涵蓋 brk + 私有匿名 mmap,正好等於
|
| 73 |
+
// 「這支程式最多能用多少不可回收的記憶體」;RLIMIT_AS 會把 22 GB 的
|
| 74 |
+
// 檔案對映也算進來,那不是我們要的語意(檔案頁是可回收的)。
|
| 75 |
+
struct rlimit rl;
|
| 76 |
+
for (int res : { RLIMIT_DATA }) {
|
| 77 |
+
if (getrlimit(res, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY) {
|
| 78 |
+
return (size_t) rl.rlim_cur;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
return 0;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
size_t physical_ram_bytes() {
|
| 85 |
+
std::ifstream f("/proc/meminfo");
|
| 86 |
+
std::string key;
|
| 87 |
+
size_t value = 0;
|
| 88 |
+
std::string unit;
|
| 89 |
+
while (f >> key >> value >> unit) {
|
| 90 |
+
if (key == "MemTotal:") {
|
| 91 |
+
return value * 1024;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
return 0;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
static size_t env_size(const char * name, size_t dflt) {
|
| 98 |
+
const char * v = getenv(name);
|
| 99 |
+
if (!v || !*v) {
|
| 100 |
+
return dflt;
|
| 101 |
+
}
|
| 102 |
+
return (size_t) strtoull(v, nullptr, 10);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
static bool env_flag(const char * name, bool dflt) {
|
| 106 |
+
const char * v = getenv(name);
|
| 107 |
+
if (!v || !*v) {
|
| 108 |
+
return dflt;
|
| 109 |
+
}
|
| 110 |
+
return !(v[0] == '0' && v[1] == '\0');
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// ============================================================ GGUF 檔頭解析
|
| 114 |
+
|
| 115 |
+
namespace {
|
| 116 |
+
|
| 117 |
+
struct gguf_reader {
|
| 118 |
+
const uint8_t * p = nullptr;
|
| 119 |
+
size_t size = 0;
|
| 120 |
+
size_t o = 0;
|
| 121 |
+
|
| 122 |
+
uint8_t u8() { return p[o++]; }
|
| 123 |
+
uint32_t u32() { uint32_t v; memcpy(&v, p + o, 4); o += 4; return v; }
|
| 124 |
+
uint64_t u64() { uint64_t v; memcpy(&v, p + o, 8); o += 8; return v; }
|
| 125 |
+
int64_t i64() { int64_t v; memcpy(&v, p + o, 8); o += 8; return v; }
|
| 126 |
+
float f32() { float v; memcpy(&v, p + o, 4); o += 4; return v; }
|
| 127 |
+
std::string str() {
|
| 128 |
+
uint64_t n = u64();
|
| 129 |
+
std::string s((const char *) p + o, (size_t) n);
|
| 130 |
+
o += n;
|
| 131 |
+
return s;
|
| 132 |
+
}
|
| 133 |
+
void skip(uint64_t n) { o += (size_t) n; }
|
| 134 |
+
};
|
| 135 |
+
|
| 136 |
+
struct kv_value {
|
| 137 |
+
enum kind { NONE, INT, REAL, STR, ARR } k = NONE;
|
| 138 |
+
int64_t i = 0;
|
| 139 |
+
double f = 0;
|
| 140 |
+
std::string s;
|
| 141 |
+
std::vector<int64_t> arr;
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
struct tensor_info {
|
| 145 |
+
std::string name;
|
| 146 |
+
std::vector<int64_t> dims;
|
| 147 |
+
ggml_type type = GGML_TYPE_COUNT;
|
| 148 |
+
uint64_t offset = 0;
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
size_t ggml_type_size_of(ggml_type t) {
|
| 152 |
+
return ggml_row_size(t, 1);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
} // namespace
|
| 156 |
+
|
| 157 |
+
// ============================================================ pager
|
| 158 |
+
|
| 159 |
+
pager & pager::instance() {
|
| 160 |
+
static pager p;
|
| 161 |
+
return p;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
void pager::load_gguf_header(const std::string & path) {
|
| 165 |
+
const int fd = open(path.c_str(), O_RDONLY);
|
| 166 |
+
if (fd < 0) {
|
| 167 |
+
throw std::runtime_error("無法開啟 GGUF:" + path);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
struct stat st;
|
| 171 |
+
fstat(fd, &st);
|
| 172 |
+
|
| 173 |
+
// 只讀檔頭:metadata + tensor 資訊大約 11 MB
|
| 174 |
+
const size_t want = (size_t) std::min<int64_t>(st.st_size, 64ull << 20);
|
| 175 |
+
std::vector<uint8_t> buf(want);
|
| 176 |
+
ssize_t got = 0;
|
| 177 |
+
while (got < (ssize_t) want) {
|
| 178 |
+
ssize_t n = pread(fd, buf.data() + got, want - got, got);
|
| 179 |
+
if (n <= 0) {
|
| 180 |
+
break;
|
| 181 |
+
}
|
| 182 |
+
got += n;
|
| 183 |
+
}
|
| 184 |
+
buf.resize((size_t) std::max<ssize_t>(got, 0));
|
| 185 |
+
|
| 186 |
+
gguf_reader r{buf.data(), buf.size(), 0};
|
| 187 |
+
if (memcmp(buf.data(), "GGUF", 4) != 0) {
|
| 188 |
+
close(fd);
|
| 189 |
+
throw std::runtime_error("不是 GGUF 檔:" + path);
|
| 190 |
+
}
|
| 191 |
+
r.o = 4;
|
| 192 |
+
const uint32_t version = r.u32();
|
| 193 |
+
if (version < 2 || version > 3) {
|
| 194 |
+
close(fd);
|
| 195 |
+
throw std::runtime_error("不支援的 GGUF 版本");
|
| 196 |
+
}
|
| 197 |
+
const uint64_t n_tensors = r.u64();
|
| 198 |
+
const uint64_t n_kv = r.u64();
|
| 199 |
+
|
| 200 |
+
std::map<std::string, kv_value> kv;
|
| 201 |
+
for (uint64_t i = 0; i < n_kv; i++) {
|
| 202 |
+
std::string key = r.str();
|
| 203 |
+
const uint32_t t = r.u32();
|
| 204 |
+
kv_value v;
|
| 205 |
+
switch (t) {
|
| 206 |
+
case 0: v.k = kv_value::INT; v.i = (int64_t) r.u32(); v.f = (double) v.i; break;
|
| 207 |
+
case 1: v.k = kv_value::INT; v.i = (int64_t) (int8_t) r.u32(); v.f = (double) v.i; break;
|
| 208 |
+
case 2: v.k = kv_value::INT; v.i = r.u32(); v.f = (double) v.i; break;
|
| 209 |
+
case 3: v.k = kv_value::INT; v.i = (int16_t) r.u32(); v.f = (double) v.i; break;
|
| 210 |
+
case 4: v.k = kv_value::INT; v.i = (int64_t) (uint32_t) r.u32(); v.f = (double) v.i; break;
|
| 211 |
+
case 5: v.k = kv_value::INT; v.i = (int32_t) r.u32(); v.f = (double) v.i; break;
|
| 212 |
+
case 6: v.k = kv_value::REAL; v.f = r.f32(); v.i = (int64_t) v.f; break;
|
| 213 |
+
case 7: v.k = kv_value::INT; v.i = r.u8(); v.f = (double) v.i; break; // BOOL = 1 byte
|
| 214 |
+
case 8: v.k = kv_value::STR; v.s = r.str(); break;
|
| 215 |
+
case 9: {
|
| 216 |
+
v.k = kv_value::ARR;
|
| 217 |
+
const uint32_t et = r.u32();
|
| 218 |
+
const uint64_t n = r.u64();
|
| 219 |
+
for (uint64_t j = 0; j < n; j++) {
|
| 220 |
+
switch (et) {
|
| 221 |
+
case 2: v.arr.push_back(r.u32()); break;
|
| 222 |
+
case 3: v.arr.push_back((int16_t) r.u32()); break;
|
| 223 |
+
case 4: v.arr.push_back((uint32_t) r.u32()); break;
|
| 224 |
+
case 5: v.arr.push_back((int32_t) r.u32()); break;
|
| 225 |
+
case 6: v.arr.push_back((int64_t) r.f32()); break;
|
| 226 |
+
// BOOL is 1 byte. Reading it as u32 desyncs the whole
|
| 227 |
+
// header parser (e.g. qwen35moe.attention.recurrent_layers).
|
| 228 |
+
case 7: v.arr.push_back((int64_t) r.u8()); break;
|
| 229 |
+
case 10: v.arr.push_back((int64_t) r.u64()); break;
|
| 230 |
+
case 11: v.arr.push_back(r.i64()); break;
|
| 231 |
+
case 12: v.arr.push_back((int64_t) (double) 0); break; // 極少見
|
| 232 |
+
case 8: r.skip(r.u64()); v.arr.push_back(0); break;
|
| 233 |
+
// Silently skipping an unknown element type would leave
|
| 234 |
+
// r.o pointing into the middle of the value and corrupt
|
| 235 |
+
// every later key. Fail loudly instead.
|
| 236 |
+
default:
|
| 237 |
+
close(fd);
|
| 238 |
+
throw std::runtime_error("GGUF array element type 不支援: "
|
| 239 |
+
+ std::to_string(et));
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
} break;
|
| 243 |
+
case 10: v.k = kv_value::INT; v.i = (int64_t) r.u64(); v.f = (double) v.i; break;
|
| 244 |
+
case 11: v.k = kv_value::INT; v.i = r.i64(); v.f = (double) v.i; break;
|
| 245 |
+
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;
|
| 246 |
+
default: close(fd); throw std::runtime_error("GGUF kv 型別不支援: " + std::to_string(t));
|
| 247 |
+
}
|
| 248 |
+
kv[key] = v;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
std::vector<tensor_info> tensors;
|
| 252 |
+
tensors.reserve(n_tensors);
|
| 253 |
+
for (uint64_t i = 0; i < n_tensors; i++) {
|
| 254 |
+
tensor_info ti;
|
| 255 |
+
ti.name = r.str();
|
| 256 |
+
const uint32_t nd = r.u32();
|
| 257 |
+
for (uint32_t d = 0; d < nd; d++) {
|
| 258 |
+
ti.dims.push_back((int64_t) r.u64());
|
| 259 |
+
}
|
| 260 |
+
ti.type = (ggml_type) r.u32();
|
| 261 |
+
ti.offset = r.u64();
|
| 262 |
+
tensors.push_back(std::move(ti));
|
| 263 |
+
}
|
| 264 |
+
const uint64_t align = kv.count("general.alignment") ? (uint64_t) kv["general.alignment"].i : 32;
|
| 265 |
+
const uint64_t data_start = (r.o + align - 1) / align * align;
|
| 266 |
+
|
| 267 |
+
close(fd);
|
| 268 |
+
|
| 269 |
+
// ---- hparams
|
| 270 |
+
auto gi = [&](const std::string & k, int64_t dflt) -> int64_t {
|
| 271 |
+
auto it = kv.find(k);
|
| 272 |
+
return it == kv.end() ? dflt : it->second.i;
|
| 273 |
+
};
|
| 274 |
+
info_.arch = kv.count("general.architecture") ? kv["general.architecture"].s : "";
|
| 275 |
+
info_.name = kv.count("general.name") ? kv["general.name"].s : "";
|
| 276 |
+
const std::string pfx = info_.arch + ".";
|
| 277 |
+
info_.n_layer = gi(pfx + "block_count", 0);
|
| 278 |
+
info_.n_expert = gi(pfx + "expert_count", 0);
|
| 279 |
+
info_.n_expert_used= gi(pfx + "expert_used_count", 0);
|
| 280 |
+
info_.n_embd = gi(pfx + "embedding_length", 0);
|
| 281 |
+
info_.n_ff_exp = gi(pfx + "expert_feed_forward_length", 0);
|
| 282 |
+
info_.n_ff_shexp = gi(pfx + "expert_shared_feed_forward_length", 0);
|
| 283 |
+
info_.n_head = gi(pfx + "attention.head_count", 0);
|
| 284 |
+
info_.n_head_kv = gi(pfx + "attention.head_count_kv", 0);
|
| 285 |
+
info_.n_embd_head_k= gi(pfx + "attention.key_length", 0);
|
| 286 |
+
info_.n_embd_head_v= gi(pfx + "attention.value_length", info_.n_embd_head_k);
|
| 287 |
+
info_.context_length = gi(pfx + "context_length", 0);
|
| 288 |
+
info_.full_attention_interval = gi(pfx + "full_attention_interval", 4);
|
| 289 |
+
|
| 290 |
+
if (info_.n_expert <= 0 || info_.n_layer <= 0) {
|
| 291 |
+
throw std::runtime_error("這不是 MoE 模型(expert_count=" + std::to_string(info_.n_expert) + ")");
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// ---- 頁面表:每個 (layer, expert) 的三段
|
| 295 |
+
layers_.assign((size_t) info_.n_layer, {});
|
| 296 |
+
std::map<std::string, const tensor_info *> by_name;
|
| 297 |
+
for (const auto & ti : tensors) {
|
| 298 |
+
by_name[ti.name] = &ti;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
size_t max_expert_bytes = 0;
|
| 302 |
+
for (int64_t il = 0; il < info_.n_layer; il++) {
|
| 303 |
+
const std::string b = "blk." + std::to_string(il) + ".";
|
| 304 |
+
layer_desc & L = layers_[(size_t) il];
|
| 305 |
+
L.experts.assign((size_t) info_.n_expert, {});
|
| 306 |
+
|
| 307 |
+
auto get = [&](const std::string & suffix) -> const tensor_info * {
|
| 308 |
+
auto it = by_name.find(b + suffix);
|
| 309 |
+
return it == by_name.end() ? nullptr : it->second;
|
| 310 |
+
};
|
| 311 |
+
|
| 312 |
+
const tensor_info * t_gate = get("ffn_gate_exps.weight");
|
| 313 |
+
const tensor_info * t_up = get("ffn_up_exps.weight");
|
| 314 |
+
const tensor_info * t_gu = get("ffn_gate_up_exps.weight");
|
| 315 |
+
const tensor_info * t_down = get("ffn_down_exps.weight");
|
| 316 |
+
|
| 317 |
+
if (!t_down || (t_gate && !t_up && !t_gu)) {
|
| 318 |
+
continue; // 這層不是 MoE(理論上不會發生)
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
for (int64_t e = 0; e < info_.n_expert; e++) {
|
| 322 |
+
expert_desc & ed = L.experts[(size_t) e];
|
| 323 |
+
if (t_gate || t_gu) {
|
| 324 |
+
const tensor_info * t = t_gate ? t_gate : t_gu;
|
| 325 |
+
const uint64_t nb = (uint64_t) ggml_row_size(t->type, t->dims[0]) * (uint64_t) t->dims[1];
|
| 326 |
+
ed.seg[part::GATE].off = data_start + t->offset + (uint64_t) e * nb;
|
| 327 |
+
ed.seg[part::GATE].size = nb;
|
| 328 |
+
ed.ne0 = t->dims[0];
|
| 329 |
+
ed.ne1 = t->dims[1];
|
| 330 |
+
ed.type[part::GATE] = t->type;
|
| 331 |
+
}
|
| 332 |
+
if (t_up) {
|
| 333 |
+
const uint64_t nb = (uint64_t) ggml_row_size(t_up->type, t_up->dims[0]) * (uint64_t) t_up->dims[1];
|
| 334 |
+
ed.seg[part::UP].off = data_start + t_up->offset + (uint64_t) e * nb;
|
| 335 |
+
ed.seg[part::UP].size = nb;
|
| 336 |
+
ed.type[part::UP] = t_up->type; // gate/up 都是 Q4_K,型別一定要各自填
|
| 337 |
+
if (!t_gate) {
|
| 338 |
+
ed.ne0 = t_up->dims[0];
|
| 339 |
+
ed.ne1 = t_up->dims[1];
|
| 340 |
+
ed.type[part::GATE] = t_up->type;
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
{
|
| 344 |
+
const uint64_t nb = (uint64_t) ggml_row_size(t_down->type, t_down->dims[0]) * (uint64_t) t_down->dims[1];
|
| 345 |
+
ed.seg[part::DOWN].off = data_start + t_down->offset + (uint64_t) e * nb;
|
| 346 |
+
ed.seg[part::DOWN].size = nb;
|
| 347 |
+
ed.type[part::DOWN] = t_down->type;
|
| 348 |
+
ed.down_ne0 = t_down->dims[0];
|
| 349 |
+
ed.down_ne1 = t_down->dims[1];
|
| 350 |
+
}
|
| 351 |
+
ed.fused_gate_up = (t_gate == nullptr && t_gu != nullptr);
|
| 352 |
+
ed.bytes = 0;
|
| 353 |
+
int64_t acc = 0;
|
| 354 |
+
for (int p = 0; p < 3; p++) {
|
| 355 |
+
if (ed.seg[p].size == 0) {
|
| 356 |
+
continue;
|
| 357 |
+
}
|
| 358 |
+
acc = (acc + 63) & ~(int64_t) 63; // 每段 64B 對齊
|
| 359 |
+
ed.slot_off[p] = acc;
|
| 360 |
+
acc += (int64_t) ed.seg[p].size;
|
| 361 |
+
}
|
| 362 |
+
ed.bytes = acc;
|
| 363 |
+
max_expert_bytes = std::max(max_expert_bytes, (size_t) ed.bytes);
|
| 364 |
+
}
|
| 365 |
+
L.bytes_per_expert = L.experts.empty() ? 0 : L.experts[0].bytes;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
slot_bytes_ = (max_expert_bytes + 4095) & ~(size_t) 4095;
|
| 369 |
+
if (slot_bytes_ < (256ull << 10)) {
|
| 370 |
+
slot_bytes_ = 256ull << 10;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
// ---- 大小兩種槽位 ----------------------------------------------------
|
| 374 |
+
// 這個模型有 37 層的 expert 是 1900544 B(Q4_K + Q5_K),但層 34/38/39 是
|
| 375 |
+
// 2039808 B(down 換成 Q6_K)。若所有槽位都照最大的配,arena 有 6.8% 被浪費
|
| 376 |
+
// ——而 arena 就是 8 GB 預算裡最寶貴的資源(實測 +5% 槽位 ≈ +9% tok/s)。
|
| 377 |
+
//
|
| 378 |
+
// 解法:槽位分成兩區。「小槽位」給 37 層用,「大槽位」只給那 3 層用,
|
| 379 |
+
// 大槽位的數量按「那 3 層佔請求的比例」分配(3/40 = 7.5%)。
|
| 380 |
+
// 選「大多數層」當小的:用眾數而不是最大值,這樣即使某個模型只有一種大小,
|
| 381 |
+
// 也不會誤判。
|
| 382 |
+
{
|
| 383 |
+
std::map<size_t, int> hist;
|
| 384 |
+
for (const auto & L : layers_) {
|
| 385 |
+
hist[(size_t) L.bytes_per_expert]++;
|
| 386 |
+
}
|
| 387 |
+
size_t small = 0;
|
| 388 |
+
int best_n = -1;
|
| 389 |
+
for (const auto & kv : hist) {
|
| 390 |
+
if (kv.second > best_n) {
|
| 391 |
+
best_n = kv.second;
|
| 392 |
+
small = kv.first;
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
size_t big = 0;
|
| 396 |
+
for (const auto & L : layers_) {
|
| 397 |
+
if ((size_t) L.bytes_per_expert > small) {
|
| 398 |
+
big = std::max(big, (size_t) L.bytes_per_expert);
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
small_bytes_ = (small + 4095) & ~(size_t) 4095;
|
| 402 |
+
big_bytes_ = big ? ((big + 4095) & ~(size_t) 4095) : 0;
|
| 403 |
+
if (big_bytes_ && big_bytes_ <= small_bytes_) {
|
| 404 |
+
big_bytes_ = 0; // 其實一樣大,別分兩區
|
| 405 |
+
}
|
| 406 |
+
slot_bytes_ = big_bytes_ ? big_bytes_ : small_bytes_;
|
| 407 |
+
int n_big_layers = 0;
|
| 408 |
+
for (auto & L : layers_) {
|
| 409 |
+
L.needs_big_slot = big_bytes_ && (size_t) L.bytes_per_expert > small_bytes_;
|
| 410 |
+
if (L.needs_big_slot) {
|
| 411 |
+
n_big_layers++;
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
fprintf(stderr,
|
| 415 |
+
"[sdq] 槽位大小:小的 %zu B(%zu 層)/大的 %zu B(%d 層)\n",
|
| 416 |
+
small_bytes_, (int) layers_.size() - n_big_layers, big_bytes_, n_big_layers);
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
bool pager::need_big_slot(int il, int ie) const {
|
| 421 |
+
if (!big_bytes_) {
|
| 422 |
+
return false;
|
| 423 |
+
}
|
| 424 |
+
const layer_desc * L = layer(il);
|
| 425 |
+
if (!L) {
|
| 426 |
+
return false;
|
| 427 |
+
}
|
| 428 |
+
if (L->needs_big_slot) {
|
| 429 |
+
return true;
|
| 430 |
+
}
|
| 431 |
+
return ie >= 0 && ie < (int) L->experts.size() &&
|
| 432 |
+
(size_t) L->experts[(size_t) ie].bytes > small_bytes_;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
void pager::alloc_arena() {
|
| 436 |
+
const size_t slot = cfg_.slot_bytes ? cfg_.slot_bytes : slot_bytes_;
|
| 437 |
+
slot_bytes_ = slot;
|
| 438 |
+
|
| 439 |
+
// ---- arena 大小怎麼決定
|
| 440 |
+
//
|
| 441 |
+
// 預算 = 這個行程「最多可以用多少不可回收的記憶體」,也就是部署機器的實體
|
| 442 |
+
// RAM 預算。來源只有 --ram-budget-mb / SDQ_RAM_BUDGET_MB,沒有就退回實體
|
| 443 |
+
// RAM 的 70%。
|
| 444 |
+
//
|
| 445 |
+
// 這裡「不」把 RLIMIT_DATA 當成預算來源,是刻意的:RLIMIT_DATA 算的是匿名
|
| 446 |
+
// **虛擬**記憶體,而 llama.cpp 會配置大量「映射了但永遠不會 touch」的圖與
|
| 447 |
+
// compute buffer。實測(ctx=4096、ubatch=128)那些沒碰過的映射約 1.5 GiB,
|
| 448 |
+
// 拿它當預算會讓 arena 無謂地少 1.5 GiB。
|
| 449 |
+
// RLIMIT_DATA 只用來當**上界**:使用者給的預算比 kernel 的上限還大時,
|
| 450 |
+
// 以 kernel 的為準,因為那才是真的會配置失敗的界線。
|
| 451 |
+
size_t budget = cfg_.ram_budget_bytes;
|
| 452 |
+
const size_t ceiling = enforced_memory_ceiling_bytes();
|
| 453 |
+
if (ceiling) {
|
| 454 |
+
budget = budget ? std::min(budget, ceiling) : 0;
|
| 455 |
+
if (budget) {
|
| 456 |
+
fprintf(stderr, "[sdq] RAM 預算取 min(--ram-budget-mb, RLIMIT_DATA)=%.2f GiB\n",
|
| 457 |
+
budget / 1073741824.0);
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
// ---- 已用量要用「匿名 RSS」,不是「總 RSS」
|
| 462 |
+
//
|
| 463 |
+
// statm 的 resident 把 mmap 進來的模型權重頁也算進去,但那些是乾淨的
|
| 464 |
+
// file-backed 頁:記憶體吃緊時核心會直接回收它們,既不佔用寶貴的實體 RAM,
|
| 465 |
+
// 也不會產生 swap I/O(cgroup memory.max 的行為也是回收而不是換出)。
|
| 466 |
+
// 把它們算進「已用」會讓 arena 被無謂地縮水 —— 實測在 8 GiB 上多算了
|
| 467 |
+
// 約 1.8 GiB,白白少掉近 1000 個槽位。
|
| 468 |
+
// 真正會跟 arena 搶實體 RAM 的,是 arena / KV cache / compute buffer / 執行堆疊,
|
| 469 |
+
// 也就是匿名記憶體。
|
| 470 |
+
const size_t rss_now = process_rss_bytes();
|
| 471 |
+
const size_t anon_now = process_anon_bytes();
|
| 472 |
+
const size_t used = anon_now ? anon_now : rss_now;
|
| 473 |
+
|
| 474 |
+
// 預留量要涵蓋 arena 之外、但在 pager sizing 之後才長出來的記憶體:
|
| 475 |
+
// * 非專家權重:sizing 的當下只有一部分被 touch 進來,其餘在第一次
|
| 476 |
+
// forward 時才 fault in(Qwen3.6-35B-A3B 約 1 GiB 的 VA)
|
| 477 |
+
// * llama.cpp 的 CPU compute buffer(125 MiB @ubatch=128、502 MiB @512)
|
| 478 |
+
// * KV cache(ctx=4096 約 80 MiB)與圖、執行緒堆疊
|
| 479 |
+
// 舊版固定抓 900 MB,而且扣的是「總 RSS」——兩個問題疊在一起,讓 8 GiB
|
| 480 |
+
// 預算下的 arena 只有 3.68 GiB。
|
| 481 |
+
//
|
| 482 |
+
// 640 MB 是用強制 8 GiB 實測校正出來的(tools/strict8g.py,見 docs/09):
|
| 483 |
+
// 預留 380 MB → 峰值匿名 RSS 8151 MB(離 8192 只剩 41 MB,太緊)
|
| 484 |
+
// 預留 560 MB → 峰值匿名 RSS 7979 MB(餘 213 MB)
|
| 485 |
+
// 預留 640 MB → 峰值匿名 RSS 約 7900 MB(餘約 290 MB)
|
| 486 |
+
// 預留 660 MB → 峰值匿名 RSS 7873 MB(餘 319 MB)
|
| 487 |
+
// 仍然可用 SDQ_RESERVE_MB 覆寫;換 ctx 或 ubatch 時必須重新校正,
|
| 488 |
+
// 因為 KV cache 與 compute buffer 會跟著長大。
|
| 489 |
+
const size_t reserve = (size_t) env_size("SDQ_RESERVE_MB", 640) << 20;
|
| 490 |
+
|
| 491 |
+
size_t arena;
|
| 492 |
+
if (budget) {
|
| 493 |
+
size_t avail = budget > used + reserve ? budget - used - reserve : 512ull << 20;
|
| 494 |
+
arena = avail;
|
| 495 |
+
if (cfg_.arena_bytes) {
|
| 496 |
+
arena = std::min(arena, cfg_.arena_bytes);
|
| 497 |
+
}
|
| 498 |
+
fprintf(stderr,
|
| 499 |
+
"[sdq] RAM 預算 %.2f GiB:已用(匿名 RSS) %.2f GiB / 總 RSS %.2f GiB、"
|
| 500 |
+
"預留 %.2f GiB → arena %.2f GiB\n",
|
| 501 |
+
budget / 1073741824.0,
|
| 502 |
+
used / 1073741824.0, rss_now / 1073741824.0,
|
| 503 |
+
reserve / 1073741824.0, arena / 1073741824.0);
|
| 504 |
+
} else {
|
| 505 |
+
const size_t phys = physical_ram_bytes();
|
| 506 |
+
const size_t phys_budget = (size_t) (phys * 0.70);
|
| 507 |
+
arena = cfg_.arena_bytes ? cfg_.arena_bytes : std::min(phys_budget, (size_t) (4ull << 30));
|
| 508 |
+
}
|
| 509 |
+
if (arena < slot_bytes_ * 16) {
|
| 510 |
+
throw std::runtime_error("expert arena 太小");
|
| 511 |
+
}
|
| 512 |
+
// 切成兩區:大槽位只分給「需要大槽位的層」,比例 = 那些層佔全部層的比例。
|
| 513 |
+
const bool force_uniform = env_flag("SDQ_FORCE_UNIFORM_SLOT", false);
|
| 514 |
+
if (big_bytes_ && small_bytes_ && !layers_.empty() && !force_uniform) {
|
| 515 |
+
int n_big_layers = 0;
|
| 516 |
+
for (const auto & L : layers_) {
|
| 517 |
+
if (L.needs_big_slot) {
|
| 518 |
+
n_big_layers++;
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
const double f = (double) n_big_layers / (double) layers_.size();
|
| 522 |
+
const double avg = f * (double) big_bytes_ + (1.0 - f) * (double) small_bytes_;
|
| 523 |
+
size_t n_total = (size_t)((double) arena / avg);
|
| 524 |
+
size_t nb = (size_t)(n_total * f);
|
| 525 |
+
// 大槽位的下限不是「有點就好」,而是**工作集的下限**:
|
| 526 |
+
// 一個 token 在每個大槽位層要 8 個大 expert,這個模型有 3 個大槽位層
|
| 527 |
+
// → 至少要 3 × 8 = 24 個大槽位,而且還要留給預取的空間。
|
| 528 |
+
// 只給 16 個時量到過「找不到能放 il=34 ie=114 的槽位」而中止
|
| 529 |
+
// (這是在 verify 新增的預取壓力測試 [2b] 抓到的)。
|
| 530 |
+
const size_t big_need = (size_t) (n_big_layers * info_.n_expert_used);
|
| 531 |
+
if (nb < big_need) {
|
| 532 |
+
nb = big_need;
|
| 533 |
+
}
|
| 534 |
+
if (nb > n_total / 4) {
|
| 535 |
+
nb = n_total / 4;
|
| 536 |
+
}
|
| 537 |
+
// 連工作集下限都放不進去時,寧可讓小槽位少一點(n_total 保持不變),
|
| 538 |
+
// 否則會算出一個「必定會中止」的配置。
|
| 539 |
+
n_big_ = nb;
|
| 540 |
+
n_small_ = n_total > nb ? n_total - nb : 0;
|
| 541 |
+
arena = n_small_ * small_bytes_ + n_big_ * big_bytes_;
|
| 542 |
+
} else {
|
| 543 |
+
// 單一尺寸:全部槽位都是「大的那一種」(slot_bytes_)。
|
| 544 |
+
// 這是 SDQ_FORCE_UNIFORM_SLOT=1 與「模型只有一種 expert 大小」時的路徑。
|
| 545 |
+
//
|
| 546 |
+
// 這裡必須記成 n_small_=0 / n_big_=全部,不能記成「n_small_=全部、單位用大的」:
|
| 547 |
+
// slot_ptr()/slot_size_of() 是用「索引 < n_small_ → 小槽位」來分支的,
|
| 548 |
+
// 把它記成全部都是小槽位的話,大 expert 會拿到小槽位的 stride
|
| 549 |
+
// (寫出槽位邊界、算錯),而且大 expert 永遠配不到槽位 → 推論卡死。
|
| 550 |
+
// (第一版就是這樣,實驗跑到一半 hang 才找出來。)
|
| 551 |
+
const size_t unit = slot_bytes_ ? slot_bytes_ : small_bytes_;
|
| 552 |
+
n_small_ = 0;
|
| 553 |
+
n_big_ = arena / unit;
|
| 554 |
+
arena = n_big_ * unit;
|
| 555 |
+
}
|
| 556 |
+
if (n_small_ == 0 && n_big_ == 0) {
|
| 557 |
+
throw std::runtime_error("expert arena 太小(算不出槽位)");
|
| 558 |
+
}
|
| 559 |
+
n_slots_ = n_small_ + n_big_;
|
| 560 |
+
if (n_slots_ < 16) {
|
| 561 |
+
throw std::runtime_error("expert arena 太小(算不出 16 個槽位)");
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
void * p = nullptr;
|
| 565 |
+
const size_t alloc = arena + (2ull << 20);
|
| 566 |
+
if (posix_memalign(&p, 2ull << 20, alloc) != 0 || !p) {
|
| 567 |
+
throw std::runtime_error("無法配置 expert arena(" + std::to_string(arena >> 20) + " MB)");
|
| 568 |
+
}
|
| 569 |
+
madvise(p, alloc, MADV_HUGEPAGE);
|
| 570 |
+
|
| 571 |
+
// 只「預先碰」有可能真的被用到的部分,不要碰整個 arena。
|
| 572 |
+
//
|
| 573 |
+
// arena 是快取,不是工作集:它的上限由 RAM 預算決定,但它的**使用量**被
|
| 574 |
+
// 路由決定 —— 一個 expert 只會在它被選中時被寫入,而且全部路由專家權重
|
| 575 |
+
// 加起來只有 expert_total(Qwen3.6-35B-A3B 約 18.22 GiB)。
|
| 576 |
+
// 舊版無條件 memset 整個 arena,等於要 core 為「永遠不會被用到」的配置
|
| 577 |
+
// 也實體化一份 RAM。實測:--ram-budget-mb 65536 會讓行程在啟動時就吃掉
|
| 578 |
+
// 64.8 GiB RSS(雖然它最多只會用到約 18 GiB),在 64 GiB 單一行程上限的
|
| 579 |
+
// 主機上直接被殺,而且光是 memset 就要花掉 20 秒以上的啟動時間。
|
| 580 |
+
//
|
| 581 |
+
// 預先碰觸的好處是真的(decode 期間不會卡在 page fault 與 THP 配置上),
|
| 582 |
+
// 所以保留 —— 但上限就是 expert_total,多出來的不碰。
|
| 583 |
+
size_t expert_total = 0;
|
| 584 |
+
for (const auto & L : layers_) {
|
| 585 |
+
expert_total += (size_t) L.experts.size() * (size_t) L.bytes_per_expert;
|
| 586 |
+
}
|
| 587 |
+
size_t prefault = alloc;
|
| 588 |
+
if (expert_total && prefault > expert_total + (2ull << 20)) {
|
| 589 |
+
prefault = expert_total + (2ull << 20);
|
| 590 |
+
}
|
| 591 |
+
// SDQ_PREFAULT_MB 可強制指定;給 0 = 完全不預先碰(純 lazy)。
|
| 592 |
+
const size_t pf_mb = env_size("SDQ_PREFAULT_MB", (size_t) -1);
|
| 593 |
+
if (pf_mb != (size_t) -1) {
|
| 594 |
+
prefault = std::min(alloc, pf_mb << 20);
|
| 595 |
+
}
|
| 596 |
+
memset(p, 0, prefault);
|
| 597 |
+
arena_ = (uint8_t *) p;
|
| 598 |
+
arena_bytes_ = arena;
|
| 599 |
+
fprintf(stderr, "[sdq] arena %.2f GiB(%zu 槽):預先碰 %.2f GiB%s\n",
|
| 600 |
+
arena / 1073741824.0, n_slots_, prefault / 1073741824.0,
|
| 601 |
+
prefault < alloc ? "(其餘用到才碰)" : "");
|
| 602 |
+
|
| 603 |
+
// OPT-1:剖面計數表(n_layer × n_expert,40×256 = 10240 筆 ≈ 40 KB,可忽略)
|
| 604 |
+
prof_.assign((size_t) info_.n_layer * (size_t) info_.n_expert, 0);
|
| 605 |
+
if (!cfg_.profile_in.empty()) {
|
| 606 |
+
load_profile(cfg_.profile_in);
|
| 607 |
+
}
|
| 608 |
+
profile_out_ = cfg_.profile_out;
|
| 609 |
+
|
| 610 |
+
slots_.assign(n_slots_, {});
|
| 611 |
+
free_slots_.reserve(n_small_);
|
| 612 |
+
for (size_t i = n_small_; i-- > 0;) {
|
| 613 |
+
free_slots_.push_back((int32_t) i);
|
| 614 |
+
}
|
| 615 |
+
free_big_slots_.reserve(n_big_);
|
| 616 |
+
for (size_t i = n_slots_; i-- > n_small_;) {
|
| 617 |
+
free_big_slots_.push_back((int32_t) i);
|
| 618 |
+
}
|
| 619 |
+
used_.store(0);
|
| 620 |
+
|
| 621 |
+
hist_.assign((size_t) info_.n_layer, {});
|
| 622 |
+
for (auto & per_layer : hist_) {
|
| 623 |
+
per_layer.assign((size_t) info_.n_expert, {});
|
| 624 |
+
}
|
| 625 |
+
prev_used_.assign((size_t) info_.n_layer, {});
|
| 626 |
+
cur_used_.assign((size_t) info_.n_layer, {});
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
void pager::init(const config & cfg, ggml_context * ctx) {
|
| 630 |
+
(void) ctx;
|
| 631 |
+
if (ready_) {
|
| 632 |
+
return;
|
| 633 |
+
}
|
| 634 |
+
cfg_ = cfg;
|
| 635 |
+
if (cfg_.model_path.empty()) {
|
| 636 |
+
const char * v = getenv("SDQ_MODEL_PATH");
|
| 637 |
+
cfg_.model_path = v ? v : "";
|
| 638 |
+
}
|
| 639 |
+
if (cfg_.model_path.empty()) {
|
| 640 |
+
throw std::runtime_error("SDQ 缺少模型路徑(設定 SDQ_MODEL_PATH)");
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
open_model_fd();
|
| 644 |
+
if (fd_ < 0) {
|
| 645 |
+
throw std::runtime_error("無法開啟模型檔:" + cfg_.model_path);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
load_gguf_header(cfg_.model_path);
|
| 649 |
+
// 順序有意義:I/O 池的對齊緩衝必須在 alloc_arena() 之前配置,
|
| 650 |
+
// 否則 arena 會把它當成「不存在的 RAM」,實際上就偷用了 8 GB 預算。
|
| 651 |
+
io_pool_start();
|
| 652 |
+
alloc_arena();
|
| 653 |
+
|
| 654 |
+
if (cfg_.prefetch) {
|
| 655 |
+
pf_ = std::thread([this] { pf_worker(); });
|
| 656 |
+
}
|
| 657 |
+
ready_ = true;
|
| 658 |
+
print_summary();
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
const layer_desc * pager::layer(int il) const {
|
| 662 |
+
if (il < 0 || il >= (int) layers_.size()) {
|
| 663 |
+
return nullptr;
|
| 664 |
+
}
|
| 665 |
+
const layer_desc & L = layers_[(size_t) il];
|
| 666 |
+
if (L.experts.empty()) {
|
| 667 |
+
return nullptr;
|
| 668 |
+
}
|
| 669 |
+
return &L;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
void pager::open_model_fd() {
|
| 673 |
+
fd_ = open(cfg_.model_path.c_str(), O_RDONLY);
|
| 674 |
+
if (fd_ < 0) {
|
| 675 |
+
return;
|
| 676 |
+
}
|
| 677 |
+
// O_DIRECT:讀資料完全不進 kernel page cache。
|
| 678 |
+
// 這是「RAM 帳目誠實」的關鍵 —— 否則核心預讀會讓 mmap 檔案的頁算進 RSS
|
| 679 |
+
// (實測 4K prefill 時檔案對映 RSS 可以多到 6 GB 以上),而且量到的
|
| 680 |
+
// 「SSD 讀取量」會被上一輪的 page cache 灌水。
|
| 681 |
+
const char * no_direct = getenv("SDQ_NO_DIRECT_IO");
|
| 682 |
+
if (no_direct && *no_direct && *no_direct != '0') {
|
| 683 |
+
return;
|
| 684 |
+
}
|
| 685 |
+
#ifdef O_DIRECT
|
| 686 |
+
fd_direct_ = open(cfg_.model_path.c_str(), O_RDONLY | O_DIRECT);
|
| 687 |
+
if (fd_direct_ >= 0) {
|
| 688 |
+
use_direct_ = true;
|
| 689 |
+
fprintf(stderr, "[sdq] 使用 O_DIRECT 讀取模型檔(不佔用 kernel page cache)\n");
|
| 690 |
+
} else {
|
| 691 |
+
fprintf(stderr, "[sdq] O_DIRECT 不可用,改用 buffered + fadvise(DONTNEED)\n");
|
| 692 |
+
}
|
| 693 |
+
#endif
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
// 有對齊的緩衝區(O_DIRECT 要求),thread_local 避免競爭
|
| 697 |
+
static thread_local uint8_t * g_direct_buf = nullptr;
|
| 698 |
+
static thread_local size_t g_direct_sz = 0;
|
| 699 |
+
|
| 700 |
+
// 與 read_range_direct 相同,但用呼叫端提供的緩衝區(已配置好、已算進 RSS)。
|
| 701 |
+
// I/O 執行緒池走這條:每條 worker 在啟動時就配置好對齊緩衝,於是 arena sizing 時
|
| 702 |
+
// 這筆記憶體已經在 RSS 裡,不會變成「額外偷用的 RAM」。
|
| 703 |
+
bool pager::read_range_staged(uint64_t off, uint8_t * dst, size_t size,
|
| 704 |
+
uint8_t * stage, size_t stage_sz) {
|
| 705 |
+
if (!use_direct_) {
|
| 706 |
+
return read_range(off, dst, size);
|
| 707 |
+
}
|
| 708 |
+
const size_t blk = 4096;
|
| 709 |
+
const uint64_t start = off & ~(uint64_t)(blk - 1);
|
| 710 |
+
const uint64_t end = (off + size + blk - 1) & ~(uint64_t)(blk - 1);
|
| 711 |
+
const size_t bytes = (size_t)(end - start);
|
| 712 |
+
if (stage_sz < bytes) {
|
| 713 |
+
// 預先配置不足(不該發生):退回 thread_local 的通用路徑
|
| 714 |
+
return read_range_direct(off, dst, size);
|
| 715 |
+
}
|
| 716 |
+
size_t done = 0;
|
| 717 |
+
while (done < bytes) {
|
| 718 |
+
const ssize_t n = pread(fd_direct_, stage + done, bytes - done,
|
| 719 |
+
(off_t)(start + done));
|
| 720 |
+
if (n <= 0) {
|
| 721 |
+
return false;
|
| 722 |
+
}
|
| 723 |
+
done += (size_t) n;
|
| 724 |
+
}
|
| 725 |
+
memcpy(dst, stage + (off - start), size);
|
| 726 |
+
return true;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
bool pager::read_range_direct(uint64_t off, uint8_t * dst, size_t size) {
|
| 730 |
+
if (!use_direct_) {
|
| 731 |
+
return read_range(off, dst, size);
|
| 732 |
+
}
|
| 733 |
+
const size_t blk = 4096;
|
| 734 |
+
const uint64_t start = off & ~(uint64_t)(blk - 1);
|
| 735 |
+
const uint64_t end = (off + size + blk - 1) & ~(uint64_t)(blk - 1);
|
| 736 |
+
const size_t bytes = (size_t)(end - start);
|
| 737 |
+
if (g_direct_sz < bytes) {
|
| 738 |
+
if (g_direct_buf) {
|
| 739 |
+
free(g_direct_buf);
|
| 740 |
+
}
|
| 741 |
+
if (posix_memalign((void **) &g_direct_buf, blk, bytes) != 0) {
|
| 742 |
+
g_direct_buf = nullptr;
|
| 743 |
+
g_direct_sz = 0;
|
| 744 |
+
use_direct_ = false;
|
| 745 |
+
return read_range(off, dst, size);
|
| 746 |
+
}
|
| 747 |
+
g_direct_sz = bytes;
|
| 748 |
+
}
|
| 749 |
+
size_t done = 0;
|
| 750 |
+
while (done < bytes) {
|
| 751 |
+
const ssize_t n = pread(fd_direct_, g_direct_buf + done, bytes - done,
|
| 752 |
+
(off_t)(start + done));
|
| 753 |
+
if (n <= 0) {
|
| 754 |
+
return false;
|
| 755 |
+
}
|
| 756 |
+
done += (size_t) n;
|
| 757 |
+
}
|
| 758 |
+
memcpy(dst, g_direct_buf + (off - start), size);
|
| 759 |
+
return true;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
bool pager::read_range(uint64_t off, uint8_t * dst, size_t size) {
|
| 763 |
+
size_t done = 0;
|
| 764 |
+
while (done < size) {
|
| 765 |
+
const ssize_t n = pread(fd_, dst + done, size - done, (off_t) (off + done));
|
| 766 |
+
if (n <= 0) {
|
| 767 |
+
return false;
|
| 768 |
+
}
|
| 769 |
+
done += (size_t) n;
|
| 770 |
+
}
|
| 771 |
+
// 讓 RAM 帳目誠實:把 kernel 對這個檔案的 page cache 丟掉,避免「藏在核心裡的 RAM」
|
| 772 |
+
if (cfg_.fadvise_dontneed) {
|
| 773 |
+
posix_fadvise(fd_, (off_t) off, (off_t) size, POSIX_FADV_DONTNEED);
|
| 774 |
+
}
|
| 775 |
+
return true;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
bool pager::read_expert(int il, int ie, uint8_t * dst) {
|
| 779 |
+
const layer_desc * L = layer(il);
|
| 780 |
+
if (!L || ie < 0 || ie >= (int) L->experts.size()) {
|
| 781 |
+
return false;
|
| 782 |
+
}
|
| 783 |
+
const expert_desc & ed = L->experts[(size_t) ie];
|
| 784 |
+
for (int p = 0; p < 3; p++) {
|
| 785 |
+
if (ed.seg[p].size == 0) {
|
| 786 |
+
continue;
|
| 787 |
+
}
|
| 788 |
+
if (!read_range_direct(ed.seg[p].off, dst + ed.slot_off[p], (size_t) ed.seg[p].size)) {
|
| 789 |
+
return false;
|
| 790 |
+
}
|
| 791 |
+
}
|
| 792 |
+
return true;
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
// ---------------------------------------------------------------- I/O 執行緒池
|
| 796 |
+
//
|
| 797 |
+
// 問題:一個 expert = gate / up / down 三段,檔案上是三段不相鄰的資料。
|
| 798 |
+
// 循序讀就是 3 次 pread 排在同一條執行緒上,而單次 1.9 MB 的 pread 在本機量到
|
| 799 |
+
// ~7 ms(≈275 MB/s 的單流頻寬)。因此「一個 expert 要多久」= 3 × 單次延遲,
|
| 800 |
+
// 這條路徑的長度跟「有多少條執行緒」無關 —— mode C 再怎麼排程都救不了。
|
| 801 |
+
//
|
| 802 |
+
// 做法:把三段交給三條執行緒同時 pread,臨界路徑降到約「單段」的時間。
|
| 803 |
+
// 量測(8 個 expert 全部 miss 的一層,各 9 次取中位數):
|
| 804 |
+
// 8 執行緒 × 三段序列讀 : 12.60 ms
|
| 805 |
+
// 16 執行緒 × 三段平行讀 : 7.57 ms
|
| 806 |
+
// 24 執行緒 × 三段平行讀 : 7.58 ms ← 16 就飽和,所以預設 3 × n_threads
|
| 807 |
+
//
|
| 808 |
+
// 這個池子對 prefill 更有用:一層要 256 個 expert = 768 個讀取工作,
|
| 809 |
+
// 序列讀時 16 執行緒只能拿到 16 路的頻寬 utilization。
|
| 810 |
+
void pager::io_pool_start() {
|
| 811 |
+
if (!cfg_.io_parallel) {
|
| 812 |
+
fprintf(stderr, "[sdq] I/O 段平行:關(每個 expert 的 gate/up/down 循序讀)\n");
|
| 813 |
+
return;
|
| 814 |
+
}
|
| 815 |
+
int n = cfg_.io_threads > 0 ? cfg_.io_threads : cfg_.n_threads * 3;
|
| 816 |
+
if (n < 4) {
|
| 817 |
+
n = 4;
|
| 818 |
+
}
|
| 819 |
+
if (n > 48) {
|
| 820 |
+
n = 48;
|
| 821 |
+
}
|
| 822 |
+
// 先算「一段權重最大有多少位元組」,每條 worker 配一塊對齊緩衝。
|
| 823 |
+
// 這必须在 alloc_arena() 之前做完,否則這筆 RAM 會變成帳外支出。
|
| 824 |
+
size_t max_seg = 0;
|
| 825 |
+
for (const auto & L : layers_) {
|
| 826 |
+
for (const auto & ed : L.experts) {
|
| 827 |
+
for (int p = 0; p < 3; p++) {
|
| 828 |
+
max_seg = std::max(max_seg, (size_t) ed.seg[p].size);
|
| 829 |
+
}
|
| 830 |
+
}
|
| 831 |
+
}
|
| 832 |
+
const size_t stage_bytes = ((max_seg + 2 * 4096) + 4095) & ~(size_t) 4095;
|
| 833 |
+
io_stage_.assign((size_t) n, io_stage{});
|
| 834 |
+
for (int i = 0; i < n; i++) {
|
| 835 |
+
void * p = nullptr;
|
| 836 |
+
if (posix_memalign(&p, 4096, stage_bytes) == 0 && p) {
|
| 837 |
+
memset(p, 0, stage_bytes); // 實際碰頁,讓它出現在 RSS 裡
|
| 838 |
+
io_stage_[(size_t) i].buf = (uint8_t *) p;
|
| 839 |
+
io_stage_[(size_t) i].sz = stage_bytes;
|
| 840 |
+
}
|
| 841 |
+
}
|
| 842 |
+
io_split_ = cfg_.io_split > 0 ? cfg_.io_split : 1;
|
| 843 |
+
io_stop_ = false;
|
| 844 |
+
io_pool_.reserve((size_t) n);
|
| 845 |
+
for (int i = 0; i < n; i++) {
|
| 846 |
+
const size_t wid = (size_t) i;
|
| 847 |
+
try {
|
| 848 |
+
io_pool_.emplace_back([this, wid] { io_worker(wid); });
|
| 849 |
+
} catch (...) {
|
| 850 |
+
break;
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
io_threads_ = (int) io_pool_.size();
|
| 854 |
+
io_pool_on_ = io_threads_ > 0;
|
| 855 |
+
if (io_pool_on_) {
|
| 856 |
+
fprintf(stderr, "[sdq] I/O 段平行:%d 條讀取執行緒 × %zu KB 對齊緩衝"
|
| 857 |
+
"(gate/up/down 同時讀,每段再切 %d 塊)\n",
|
| 858 |
+
io_threads_, stage_bytes >> 10, io_split_);
|
| 859 |
+
}
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
void pager::io_pool_stop() {
|
| 863 |
+
if (io_pool_.empty()) {
|
| 864 |
+
for (auto & s : io_stage_) {
|
| 865 |
+
free(s.buf);
|
| 866 |
+
s.buf = nullptr;
|
| 867 |
+
}
|
| 868 |
+
io_stage_.clear();
|
| 869 |
+
return;
|
| 870 |
+
}
|
| 871 |
+
{
|
| 872 |
+
std::lock_guard<std::mutex> lk(io_mu_);
|
| 873 |
+
io_stop_ = true;
|
| 874 |
+
}
|
| 875 |
+
io_cv_.notify_all();
|
| 876 |
+
for (auto & t : io_pool_) {
|
| 877 |
+
if (t.joinable()) {
|
| 878 |
+
t.join();
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
io_pool_.clear();
|
| 882 |
+
io_pool_on_ = false;
|
| 883 |
+
for (auto & s : io_stage_) {
|
| 884 |
+
free(s.buf);
|
| 885 |
+
s.buf = nullptr;
|
| 886 |
+
s.sz = 0;
|
| 887 |
+
}
|
| 888 |
+
io_stage_.clear();
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
void pager::io_worker(size_t wid) {
|
| 892 |
+
for (;;) {
|
| 893 |
+
io_job job{};
|
| 894 |
+
{
|
| 895 |
+
std::unique_lock<std::mutex> lk(io_mu_);
|
| 896 |
+
io_cv_.wait(lk, [this] { return io_stop_ || !io_q_.empty(); });
|
| 897 |
+
if (io_q_.empty()) {
|
| 898 |
+
if (io_stop_) {
|
| 899 |
+
return;
|
| 900 |
+
}
|
| 901 |
+
continue;
|
| 902 |
+
}
|
| 903 |
+
job = io_q_.front();
|
| 904 |
+
io_q_.pop_front();
|
| 905 |
+
}
|
| 906 |
+
io_jobs_.fetch_add(1, std::memory_order_relaxed);
|
| 907 |
+
// 讀取本身不持鎖:io_mu_ 只保護佇列與完成計數
|
| 908 |
+
const bool ok = (wid < io_stage_.size() && io_stage_[wid].buf)
|
| 909 |
+
? read_range_staged(job.off, job.dst, job.size,
|
| 910 |
+
io_stage_[wid].buf, io_stage_[wid].sz)
|
| 911 |
+
: read_range_direct(job.off, job.dst, job.size);
|
| 912 |
+
{
|
| 913 |
+
std::lock_guard<std::mutex> lk(io_mu_);
|
| 914 |
+
if (!ok) {
|
| 915 |
+
job.ctx->ok = false;
|
| 916 |
+
}
|
| 917 |
+
if (job.ctx->remaining.fetch_sub(1) == 1) {
|
| 918 |
+
io_done_cv_.notify_all();
|
| 919 |
+
}
|
| 920 |
+
}
|
| 921 |
+
}
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
// 把 (il,ie) 的三段交給執行緒池並行讀,呼叫端等到整批完成才返回。
|
| 925 |
+
// ctx 放在呼叫端 stack:worker 只會在 remaining 歸零之後才再碰它,
|
| 926 |
+
// 而我們正是等到 remaining==0 才離開函式,所以沒有 use-after-return。
|
| 927 |
+
bool pager::read_expert_par(int il, int ie, uint8_t * dst) {
|
| 928 |
+
const layer_desc * L = layer(il);
|
| 929 |
+
if (!L || ie < 0 || ie >= (int) L->experts.size()) {
|
| 930 |
+
return false;
|
| 931 |
+
}
|
| 932 |
+
if (!io_pool_on_) {
|
| 933 |
+
return read_expert(il, ie, dst);
|
| 934 |
+
}
|
| 935 |
+
const expert_desc & ed = L->experts[(size_t) ie];
|
| 936 |
+
io_ctx ctx;
|
| 937 |
+
int n = 0;
|
| 938 |
+
// 把每一段再切成一樣大的好幾塊,交給不同的執行緒同時讀。
|
| 939 |
+
//
|
| 940 |
+
// 為什麼:單一 pread 的時間 ≈ 固定延遲 + 位元組數 / 單流頻寬。
|
| 941 |
+
// 本機實測單流約 275 MB/s、4K 隨機讀延遲約 0.57 ms,所以
|
| 942 |
+
// 590 KB 讀一次 ≈ 0.57 + 2.1 = 2.7 ms
|
| 943 |
+
// 590 KB 切成 2 塊 ≈ 0.57 + 1.05 = 1.6 ms
|
| 944 |
+
// 而「讀完一個 expert」的時間正是每層的關鍵路徑,切塊就是直接砍關鍵路徑。
|
| 945 |
+
// 設定檔 SDQ_IO_SPLIT。**實測預設是 1(不切塊)**:切塊在 24 條讀取執行緒
|
| 946 |
+
// 之下沒有可重現的改善(split=1..6 的中位數在 4.9~5.9 之間跳動,純噪音),
|
| 947 |
+
// 因為 8 個 expert × 3 段 = 24 個並行請求已經把裝置的佇列灌飽,
|
| 948 |
+
// 這時瓶頸是裝置總頻寬而不是單一請求的延遲。保留這個開關是為了其他裝置。
|
| 949 |
+
const int split = io_split_;
|
| 950 |
+
const size_t blk = 4096;
|
| 951 |
+
{
|
| 952 |
+
std::lock_guard<std::mutex> lk(io_mu_);
|
| 953 |
+
for (int p = 0; p < 3; p++) {
|
| 954 |
+
const size_t sz = (size_t) ed.seg[p].size;
|
| 955 |
+
if (sz == 0) {
|
| 956 |
+
continue;
|
| 957 |
+
}
|
| 958 |
+
uint8_t * base = dst + ed.slot_off[p];
|
| 959 |
+
if (split <= 1 || sz <= 2 * blk) {
|
| 960 |
+
io_q_.push_back(io_job{ed.seg[p].off, base, sz, &ctx});
|
| 961 |
+
n++;
|
| 962 |
+
continue;
|
| 963 |
+
}
|
| 964 |
+
size_t chunk = ((sz + (size_t) split - 1) / (size_t) split + blk - 1) & ~(blk - 1);
|
| 965 |
+
for (size_t off = 0; off < sz; off += chunk) {
|
| 966 |
+
const size_t take = sz - off < chunk ? sz - off : chunk;
|
| 967 |
+
io_q_.push_back(io_job{ed.seg[p].off + off, base + off, take, &ctx});
|
| 968 |
+
n++;
|
| 969 |
+
}
|
| 970 |
+
}
|
| 971 |
+
ctx.remaining.store(n);
|
| 972 |
+
}
|
| 973 |
+
if (n == 0) {
|
| 974 |
+
return true;
|
| 975 |
+
}
|
| 976 |
+
io_cv_.notify_all();
|
| 977 |
+
const uint64_t t0 = now_us();
|
| 978 |
+
{
|
| 979 |
+
std::unique_lock<std::mutex> lk(io_mu_);
|
| 980 |
+
io_done_cv_.wait(lk, [&ctx] { return ctx.remaining.load() == 0; });
|
| 981 |
+
}
|
| 982 |
+
io_wait_ns_.fetch_add((now_us() - t0) * 1000ull, std::memory_order_relaxed);
|
| 983 |
+
return ctx.ok;
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
bool pager::read_expert_uring(int il, int ie, uint8_t * dst) {
|
| 987 |
+
#if !defined(SDQ_HAVE_LIBURING)
|
| 988 |
+
(void) il;
|
| 989 |
+
(void) ie;
|
| 990 |
+
// Keep SDQ_IO_URING safe on builds without liburing. The normal threaded
|
| 991 |
+
// pread implementation remains the correctness-preserving fallback.
|
| 992 |
+
return read_expert_par(il, ie, dst);
|
| 993 |
+
#else
|
| 994 |
+
const layer_desc * L = layer(il);
|
| 995 |
+
if (!L || ie < 0 || ie >= (int) L->experts.size() || use_direct_) {
|
| 996 |
+
return false;
|
| 997 |
+
}
|
| 998 |
+
const expert_desc & ed = L->experts[(size_t) ie];
|
| 999 |
+
io_uring ring;
|
| 1000 |
+
if (io_uring_queue_init(3, &ring, 0) < 0) {
|
| 1001 |
+
return false;
|
| 1002 |
+
}
|
| 1003 |
+
int submitted = 0;
|
| 1004 |
+
for (int p = 0; p < 3; p++) {
|
| 1005 |
+
if (ed.seg[p].size == 0) {
|
| 1006 |
+
continue;
|
| 1007 |
+
}
|
| 1008 |
+
io_uring_sqe * sqe = io_uring_get_sqe(&ring);
|
| 1009 |
+
if (!sqe) {
|
| 1010 |
+
io_uring_queue_exit(&ring);
|
| 1011 |
+
return false;
|
| 1012 |
+
}
|
| 1013 |
+
io_uring_prep_read(sqe, fd_, dst + ed.slot_off[p],
|
| 1014 |
+
(unsigned) ed.seg[p].size, (off_t) ed.seg[p].off);
|
| 1015 |
+
submitted++;
|
| 1016 |
+
}
|
| 1017 |
+
if (io_uring_submit_and_wait(&ring, submitted) < 0) {
|
| 1018 |
+
io_uring_queue_exit(&ring);
|
| 1019 |
+
return false;
|
| 1020 |
+
}
|
| 1021 |
+
bool ok = true;
|
| 1022 |
+
for (int completed = 0; completed < submitted; completed++) {
|
| 1023 |
+
io_uring_cqe * cqe = nullptr;
|
| 1024 |
+
if (io_uring_wait_cqe(&ring, &cqe) != 0 || !cqe) {
|
| 1025 |
+
ok = false;
|
| 1026 |
+
break;
|
| 1027 |
+
}
|
| 1028 |
+
if (cqe->res < 0) {
|
| 1029 |
+
ok = false;
|
| 1030 |
+
}
|
| 1031 |
+
io_uring_cqe_seen(&ring, cqe);
|
| 1032 |
+
}
|
| 1033 |
+
io_uring_queue_exit(&ring);
|
| 1034 |
+
if (ok && cfg_.fadvise_dontneed) {
|
| 1035 |
+
for (int p = 0; p < 3; p++) {
|
| 1036 |
+
if (ed.seg[p].size) {
|
| 1037 |
+
posix_fadvise(fd_, (off_t) ed.seg[p].off,
|
| 1038 |
+
(off_t) ed.seg[p].size, POSIX_FADV_DONTNEED);
|
| 1039 |
+
}
|
| 1040 |
+
}
|
| 1041 |
+
}
|
| 1042 |
+
return ok;
|
| 1043 |
+
#endif
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
uint64_t pager::read_expert_span(int il, const int32_t * experts, int n, const int32_t * slots) {
|
| 1047 |
+
const layer_desc * L = layer(il);
|
| 1048 |
+
if (!L || n <= 0) {
|
| 1049 |
+
return 0;
|
| 1050 |
+
}
|
| 1051 |
+
const uint64_t t_start = now_us();
|
| 1052 |
+
// 每個執行緒一份暫存區(多執行緒會同時呼叫,不能共用一個 buffer)
|
| 1053 |
+
static thread_local std::vector<uint8_t> scratch_buf;
|
| 1054 |
+
uint64_t total = 0;
|
| 1055 |
+
|
| 1056 |
+
// 把 experts 切成「檔案編號連續」的區段
|
| 1057 |
+
int i = 0;
|
| 1058 |
+
while (i < n) {
|
| 1059 |
+
int j = i + 1;
|
| 1060 |
+
while (j < n && experts[j] == experts[j - 1] + 1) {
|
| 1061 |
+
j++;
|
| 1062 |
+
}
|
| 1063 |
+
const int cnt = j - i;
|
| 1064 |
+
const expert_desc & first = L->experts[(size_t) experts[i]];
|
| 1065 |
+
const expert_desc & last = L->experts[(size_t) experts[j - 1]];
|
| 1066 |
+
|
| 1067 |
+
for (int p = 0; p < 3; p++) {
|
| 1068 |
+
if (first.seg[p].size == 0 || last.seg[p].size == 0) {
|
| 1069 |
+
continue;
|
| 1070 |
+
}
|
| 1071 |
+
const uint64_t off = first.seg[p].off;
|
| 1072 |
+
const uint64_t end = last.seg[p].off + last.seg[p].size;
|
| 1073 |
+
const size_t bytes = (size_t) (end - off);
|
| 1074 |
+
if (scratch_buf.size() < bytes) {
|
| 1075 |
+
scratch_buf.assign(bytes, 0);
|
| 1076 |
+
}
|
| 1077 |
+
if (!read_range_direct(off, scratch_buf.data(), bytes)) {
|
| 1078 |
+
return total;
|
| 1079 |
+
}
|
| 1080 |
+
total += bytes;
|
| 1081 |
+
// 散到各自的槽位:scratch 從 off 開始,所以偏移是「相對於 off」
|
| 1082 |
+
for (int k = i; k < j; k++) {
|
| 1083 |
+
const expert_desc & ed = L->experts[(size_t) experts[k]];
|
| 1084 |
+
const size_t seg_bytes = (size_t) ed.seg[p].size;
|
| 1085 |
+
const size_t delta = (size_t) (ed.seg[p].off - off);
|
| 1086 |
+
if (slots[k] < 0) {
|
| 1087 |
+
continue;
|
| 1088 |
+
}
|
| 1089 |
+
if ((size_t) slots[k] >= n_slots_ || (uint64_t) ed.slot_off[p] + seg_bytes > slot_bytes_
|
| 1090 |
+
|| delta + seg_bytes > scratch_buf.size()) {
|
| 1091 |
+
fprintf(stderr, "[sdq-pager] span 越界:il=%d slot=%d/%zu slot_off=%lld seg=%zu delta=%zu scratch=%zu p=%d e=%d\n",
|
| 1092 |
+
il, slots[k], n_slots_, (long long) ed.slot_off[p], seg_bytes, delta, scratch_buf.size(), p, experts[k]);
|
| 1093 |
+
return total;
|
| 1094 |
+
}
|
| 1095 |
+
memcpy(slot_ptr(slots[k]) + ed.slot_off[p],
|
| 1096 |
+
scratch_buf.data() + delta, seg_bytes);
|
| 1097 |
+
}
|
| 1098 |
+
}
|
| 1099 |
+
i = j;
|
| 1100 |
+
}
|
| 1101 |
+
stats_.demand_wait_us.fetch_add(now_us() - t_start, std::memory_order_relaxed);
|
| 1102 |
+
return total;
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
int pager::reserve_slot(int il, int ie) {
|
| 1106 |
+
if (!ready_) {
|
| 1107 |
+
return -1;
|
| 1108 |
+
}
|
| 1109 |
+
count_request(il, ie);
|
| 1110 |
+
std::lock_guard<std::mutex> lk(mu_);
|
| 1111 |
+
const int32_t exist = find_slot_locked(il, ie);
|
| 1112 |
+
if (exist >= 0) {
|
| 1113 |
+
slots_[(size_t) exist].last_use = ++clock_;
|
| 1114 |
+
slots_[(size_t) exist].pin++;
|
| 1115 |
+
touch_freq_locked(slots_[(size_t) exist]);
|
| 1116 |
+
return exist;
|
| 1117 |
+
}
|
| 1118 |
+
const int32_t s = victim_locked();
|
| 1119 |
+
if (s < 0) {
|
| 1120 |
+
return -1;
|
| 1121 |
+
}
|
| 1122 |
+
slot_state & st = slots_[(size_t) s];
|
| 1123 |
+
if (st.layer >= 0) {
|
| 1124 |
+
page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
|
| 1125 |
+
stats_.evictions.fetch_add(1, std::memory_order_relaxed);
|
| 1126 |
+
used_.fetch_sub(1, std::memory_order_relaxed);
|
| 1127 |
+
}
|
| 1128 |
+
st.layer = il;
|
| 1129 |
+
st.expert = ie;
|
| 1130 |
+
st.last_use = ++clock_;
|
| 1131 |
+
st.freq = 1;
|
| 1132 |
+
st.pin = 1;
|
| 1133 |
+
st.is_hot = hot_lookup(il, ie);
|
| 1134 |
+
page_[((int64_t) il << 20) | (int64_t) ie] = s;
|
| 1135 |
+
used_.fetch_add(1, std::memory_order_relaxed);
|
| 1136 |
+
stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
|
| 1137 |
+
return s;
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
int pager::probe(int il, int ie) {
|
| 1141 |
+
if (!ready_) {
|
| 1142 |
+
return -1;
|
| 1143 |
+
}
|
| 1144 |
+
stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
|
| 1145 |
+
count_request(il, ie);
|
| 1146 |
+
std::lock_guard<std::mutex> lk(mu_);
|
| 1147 |
+
const int32_t s = find_slot_locked(il, ie);
|
| 1148 |
+
if (s < 0) {
|
| 1149 |
+
stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
|
| 1150 |
+
return -1;
|
| 1151 |
+
}
|
| 1152 |
+
slots_[(size_t) s].last_use = ++clock_;
|
| 1153 |
+
slots_[(size_t) s].pin++;
|
| 1154 |
+
touch_freq_locked(slots_[(size_t) s]);
|
| 1155 |
+
stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
|
| 1156 |
+
return s;
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
void pager::note_span_read(uint64_t bytes) {
|
| 1160 |
+
stats_.ssb_read_bytes_demand.fetch_add(bytes, std::memory_order_relaxed);
|
| 1161 |
+
stats_.demand_wait_us.fetch_add(0, std::memory_order_relaxed);
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
// 衰減式頻率:每 8192 次使用就把所有計數減半,讓「過去熱門、現在變冷」的 expert 能被淘汰
|
| 1165 |
+
void pager::touch_freq_locked(slot_state & st) {
|
| 1166 |
+
if (++freq_tick_ >= 8192) {
|
| 1167 |
+
freq_tick_ = 0;
|
| 1168 |
+
for (auto & s : slots_) {
|
| 1169 |
+
s.freq /= 2;
|
| 1170 |
+
}
|
| 1171 |
+
}
|
| 1172 |
+
if (st.freq < 0xFFFFFFFFu) {
|
| 1173 |
+
st.freq++;
|
| 1174 |
+
}
|
| 1175 |
+
}
|
| 1176 |
+
|
| 1177 |
+
// ============================== OPT-1:熱門 expert 剖面 ==============================
|
| 1178 |
+
// 問題實測(docs/03):arena 3.88 GiB 只放得下 2044 個 expert,總共卻有 10240 個,
|
| 1179 |
+
// prefill 的幾百個「只用一次」的專家會把熱區整個洗掉,命中率卡在 ~70%。
|
| 1180 |
+
// 解法借用 Strata 的做法(src/core/expert_cache.cpp):**離線剖面 + 靜態排行榜 +
|
| 1181 |
+
// 熱頁永不淘汰**。這裡不動檔案格式,用純文字存排行榜。
|
| 1182 |
+
|
| 1183 |
+
void pager::count_request(int il, int ie) {
|
| 1184 |
+
if (prof_.empty() || il < 0 || ie < 0) {
|
| 1185 |
+
return;
|
| 1186 |
+
}
|
| 1187 |
+
const size_t idx = (size_t) il * (size_t) info_.n_expert + (size_t) ie;
|
| 1188 |
+
if (idx < prof_.size()) {
|
| 1189 |
+
// 單執行緒 ++ 即可:請求次數是統計用途,不參與任何正確性判斷
|
| 1190 |
+
if (prof_[idx] < 0xFFFFFFFFu) {
|
| 1191 |
+
prof_[idx]++;
|
| 1192 |
+
}
|
| 1193 |
+
}
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
bool pager::hot_lookup(int il, int ie) const {
|
| 1197 |
+
if (hot_.empty() || il < 0 || ie < 0) {
|
| 1198 |
+
return false;
|
| 1199 |
+
}
|
| 1200 |
+
const size_t idx = (size_t) il * (size_t) info_.n_expert + (size_t) ie;
|
| 1201 |
+
return idx < hot_.size() && hot_[idx] != 0;
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
// 挑一個容得下 (il,ie) 的槽。與 victim_locked 的差別:**先只在冷頁裡找**,
|
| 1205 |
+
// 找不到才退讓去動熱頁。這就是「熱區不會被 prefill 洗掉」的機制。
|
| 1206 |
+
// 這個槽位放得下「需要 big 的 expert」嗎?
|
| 1207 |
+
// 小槽位:只能放小 expert
|
| 1208 |
+
// 大槽位:小expert 也放得下(只是浪費一點空間)
|
| 1209 |
+
static inline bool slot_accepts(bool need_big, bool slot_is_big) {
|
| 1210 |
+
return !need_big || slot_is_big;
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
int pager::admit_slot_locked(int il, int ie) {
|
| 1214 |
+
// il/ie 都 < 0 代表「不指定大小」→ 兩種槽位都可以(舊的呼叫路徑)。
|
| 1215 |
+
const bool need_big = (il >= 0 && ie >= 0) ? need_big_slot(il, ie) : false;
|
| 1216 |
+
// 完全沒用過的空槽永遠優先(它不是熱頁,佔用等於浪費)
|
| 1217 |
+
// 大 expert 先看大槽位;小 expert 先看小槽位。
|
| 1218 |
+
std::vector<int32_t> & first = need_big ? free_big_slots_ : free_slots_;
|
| 1219 |
+
std::vector<int32_t> & second = need_big ? free_slots_ : free_big_slots_;
|
| 1220 |
+
while (!first.empty()) {
|
| 1221 |
+
const int32_t s = first.back();
|
| 1222 |
+
if (!slots_[(size_t) s].busy) {
|
| 1223 |
+
first.pop_back();
|
| 1224 |
+
return s;
|
| 1225 |
+
}
|
| 1226 |
+
}
|
| 1227 |
+
// 第一輪:只在「冷頁」裡挑最久未用的。
|
| 1228 |
+
// want_hot=true → 熱門 expert 進來時,把冷頁換掉(不動其他熱頁)
|
| 1229 |
+
// want_hot=false → 冷門 expert 進來時,優先吃冷頁
|
| 1230 |
+
// 兩種情況的規則相同,所以 hot_lookup 的結果不影響選擇順序,只影響標記。
|
| 1231 |
+
for (int pass = 0; pass < 2; pass++) {
|
| 1232 |
+
int64_t best = -1;
|
| 1233 |
+
uint64_t best_clock = UINT64_MAX;
|
| 1234 |
+
for (size_t s = 0; s < n_slots_; s++) {
|
| 1235 |
+
const slot_state & st = slots_[s];
|
| 1236 |
+
if (st.pin > 0 || st.busy) {
|
| 1237 |
+
continue;
|
| 1238 |
+
}
|
| 1239 |
+
if (pass == 0 && st.is_hot) {
|
| 1240 |
+
continue; // 第一輪不動熱頁
|
| 1241 |
+
}
|
| 1242 |
+
if (!slot_accepts(need_big, is_big_slot(s))) {
|
| 1243 |
+
continue;
|
| 1244 |
+
}
|
| 1245 |
+
if (st.last_use < best_clock) {
|
| 1246 |
+
best_clock = st.last_use;
|
| 1247 |
+
best = (int64_t) s;
|
| 1248 |
+
}
|
| 1249 |
+
}
|
| 1250 |
+
if (best >= 0) {
|
| 1251 |
+
return (int) best;
|
| 1252 |
+
}
|
| 1253 |
+
}
|
| 1254 |
+
// 退讓:另一種大小的空槽。**一定要檢查放不放得下** —— 小槽位放不下大 expert,
|
| 1255 |
+
// 硬塞會讓 read_expert 寫出槽位邊界(靜默把別的 expert 寫壞)。
|
| 1256 |
+
while (!second.empty()) {
|
| 1257 |
+
const int32_t s = second.back();
|
| 1258 |
+
if (!slots_[(size_t) s].busy &&
|
| 1259 |
+
slot_accepts(need_big, is_big_slot(s))) {
|
| 1260 |
+
second.pop_back();
|
| 1261 |
+
return s;
|
| 1262 |
+
}
|
| 1263 |
+
if (!slots_[(size_t) s].busy) {
|
| 1264 |
+
second.pop_back();
|
| 1265 |
+
} else {
|
| 1266 |
+
break;
|
| 1267 |
+
}
|
| 1268 |
+
}
|
| 1269 |
+
return -1;
|
| 1270 |
+
}
|
| 1271 |
+
|
| 1272 |
+
bool pager::load_profile(const std::string & path) {
|
| 1273 |
+
std::FILE * f = std::fopen(path.c_str(), "rb");
|
| 1274 |
+
if (!f) {
|
| 1275 |
+
fprintf(stderr, "[sdq] 找不到 expert 剖面 %s(將以動態 LRU/LFU 運作)\n", path.c_str());
|
| 1276 |
+
return false;
|
| 1277 |
+
}
|
| 1278 |
+
char magic[8] = {0};
|
| 1279 |
+
if (std::fread(magic, 1, 4, f) != 4 || std::memcmp(magic, "SDQP", 4) != 0) {
|
| 1280 |
+
std::fclose(f);
|
| 1281 |
+
fprintf(stderr, "[sdq] %s 不是 SDQ expert 剖面(缺少 SDQP 標頭)\n", path.c_str());
|
| 1282 |
+
return false;
|
| 1283 |
+
}
|
| 1284 |
+
uint32_t hdr[3] = {0, 0, 0};
|
| 1285 |
+
if (std::fread(hdr, sizeof(uint32_t), 3, f) != 3) {
|
| 1286 |
+
std::fclose(f);
|
| 1287 |
+
return false;
|
| 1288 |
+
}
|
| 1289 |
+
const uint32_t n = hdr[2];
|
| 1290 |
+
std::vector<std::pair<uint32_t, std::pair<int32_t, int32_t>>> ranked;
|
| 1291 |
+
ranked.reserve(n);
|
| 1292 |
+
for (uint32_t i = 0; i < n; i++) {
|
| 1293 |
+
int32_t l = 0, e = 0;
|
| 1294 |
+
uint32_t c = 0;
|
| 1295 |
+
if (std::fread(&l, sizeof(int32_t), 1, f) != 1) break;
|
| 1296 |
+
if (std::fread(&e, sizeof(int32_t), 1, f) != 1) break;
|
| 1297 |
+
if (std::fread(&c, sizeof(uint32_t), 1, f) != 1) break;
|
| 1298 |
+
ranked.push_back({c, {l, e}});
|
| 1299 |
+
}
|
| 1300 |
+
std::fclose(f);
|
| 1301 |
+
std::sort(ranked.begin(), ranked.end(),
|
| 1302 |
+
[](const auto & a, const auto & b) { return a.first > b.first; });
|
| 1303 |
+
|
| 1304 |
+
hot_.assign((size_t) info_.n_layer * (size_t) info_.n_expert, 0);
|
| 1305 |
+
const int want = cfg_.hot_pin > 0 ? cfg_.hot_pin : (int) ranked.size();
|
| 1306 |
+
n_hot_ = 0;
|
| 1307 |
+
for (const auto & r : ranked) {
|
| 1308 |
+
if (n_hot_ >= want) {
|
| 1309 |
+
break;
|
| 1310 |
+
}
|
| 1311 |
+
const int l = r.second.first, e = r.second.second;
|
| 1312 |
+
if (l < 0 || l >= info_.n_layer || e < 0 || e >= info_.n_expert) {
|
| 1313 |
+
continue;
|
| 1314 |
+
}
|
| 1315 |
+
const size_t idx = (size_t) l * (size_t) info_.n_expert + (size_t) e;
|
| 1316 |
+
if (!hot_[idx]) {
|
| 1317 |
+
hot_[idx] = 1;
|
| 1318 |
+
n_hot_++;
|
| 1319 |
+
}
|
| 1320 |
+
}
|
| 1321 |
+
fprintf(stderr, "[sdq] expert 剖面:%s → 釘選 %d / %zu 個熱門 expert(永不淘汰)\n",
|
| 1322 |
+
path.c_str(), n_hot_, ranked.size());
|
| 1323 |
+
return n_hot_ > 0;
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
void pager::dump_profile(const std::string & path) {
|
| 1327 |
+
if (prof_.empty()) {
|
| 1328 |
+
return;
|
| 1329 |
+
}
|
| 1330 |
+
std::vector<std::pair<uint32_t, std::pair<int32_t, int32_t>>> ranked;
|
| 1331 |
+
ranked.reserve(prof_.size());
|
| 1332 |
+
uint64_t total = 0;
|
| 1333 |
+
for (int l = 0; l < info_.n_layer; l++) {
|
| 1334 |
+
for (int e = 0; e < info_.n_expert; e++) {
|
| 1335 |
+
const uint32_t c = prof_[(size_t) l * (size_t) info_.n_expert + (size_t) e];
|
| 1336 |
+
if (c > 0) {
|
| 1337 |
+
ranked.push_back({c, {l, e}});
|
| 1338 |
+
total += c;
|
| 1339 |
+
}
|
| 1340 |
+
}
|
| 1341 |
+
}
|
| 1342 |
+
std::sort(ranked.begin(), ranked.end(),
|
| 1343 |
+
[](const auto & a, const auto & b) { return a.first > b.first; });
|
| 1344 |
+
std::FILE * f = std::fopen(path.c_str(), "wb");
|
| 1345 |
+
if (!f) {
|
| 1346 |
+
fprintf(stderr, "[sdq] 無法寫出 expert 剖面 %s\n", path.c_str());
|
| 1347 |
+
return;
|
| 1348 |
+
}
|
| 1349 |
+
std::fwrite("SDQP", 1, 4, f);
|
| 1350 |
+
const uint32_t hdr[3] = {1, (uint32_t) info_.n_layer, (uint32_t) ranked.size()};
|
| 1351 |
+
std::fwrite(hdr, sizeof(uint32_t), 3, f);
|
| 1352 |
+
for (const auto & r : ranked) {
|
| 1353 |
+
const int32_t l = r.second.first, e = r.second.second;
|
| 1354 |
+
const uint32_t c = r.first;
|
| 1355 |
+
std::fwrite(&l, sizeof(int32_t), 1, f);
|
| 1356 |
+
std::fwrite(&e, sizeof(int32_t), 1, f);
|
| 1357 |
+
std::fwrite(&c, sizeof(uint32_t), 1, f);
|
| 1358 |
+
}
|
| 1359 |
+
std::fclose(f);
|
| 1360 |
+
// 覆盖率報告:前 K 名可以蓋掉多少請求 → 直接決定 hot_pin 該設多少
|
| 1361 |
+
uint64_t acc = 0;
|
| 1362 |
+
const size_t ks[6] = {512, 1024, 1536, 2048, 3072, 4096};
|
| 1363 |
+
fprintf(stderr, "[sdq] expert 剖面已寫到 %s(%zu 個有被用到,總請求 %llu)\n",
|
| 1364 |
+
path.c_str(), ranked.size(), (unsigned long long) total);
|
| 1365 |
+
size_t ki = 0;
|
| 1366 |
+
for (size_t i = 0; i < ranked.size(); i++) {
|
| 1367 |
+
acc += ranked[i].first;
|
| 1368 |
+
if (ki < 6 && i + 1 == ks[ki]) {
|
| 1369 |
+
fprintf(stderr, "[sdq] 前 %5zu 名涵蓋 %6.2f%% 的請求\n",
|
| 1370 |
+
ks[ki], 100.0 * (double) acc / (double) (total ? total : 1));
|
| 1371 |
+
ki++;
|
| 1372 |
+
}
|
| 1373 |
+
}
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
int pager::find_slot_locked(int il, int ie) {
|
| 1377 |
+
const int64_t key = ((int64_t) il << 20) | (int64_t) ie;
|
| 1378 |
+
auto it = page_.find(key);
|
| 1379 |
+
return it == page_.end() ? -1 : it->second;
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
// 找一個「可以放 (il,ie) 而且 last_use <= stale_before」的槽位。
|
| 1383 |
+
//
|
| 1384 |
+
// 為什麼不能直接用 victim_stale_locked():大小兩種槽位之後,那個函式會掃過**所有**
|
| 1385 |
+
// 槽位(包含小槽位),於是一個需要大槽位的 expert 可能拿到小槽位,
|
| 1386 |
+
// 預取時 read_expert 會寫出槽位邊界 → 靜默把鄰居槽位的資料寫壞。
|
| 1387 |
+
// 這是引進大小兩種槽位時真正會咬人的地方。
|
| 1388 |
+
//
|
| 1389 |
+
// 順序:先看對應尺寸的空槽(從沒用過,最該拿來放預取),
|
| 1390 |
+
// 再掃全部槽位挑最久未用的(放得下的那種尺寸)。
|
| 1391 |
+
int pager::victim_stale_for(int il, int ie, uint64_t stale_before) {
|
| 1392 |
+
const bool need_big = need_big_slot(il, ie);
|
| 1393 |
+
std::vector<int32_t> & first = need_big ? free_big_slots_ : free_slots_;
|
| 1394 |
+
std::vector<int32_t> & second = need_big ? free_slots_ : free_big_slots_;
|
| 1395 |
+
for (std::vector<int32_t> * lst : { &first, &second }) {
|
| 1396 |
+
while (!lst->empty()) {
|
| 1397 |
+
const int32_t s = lst->back();
|
| 1398 |
+
if (slots_[(size_t) s].busy) {
|
| 1399 |
+
break;
|
| 1400 |
+
}
|
| 1401 |
+
if (slots_[(size_t) s].last_use <= stale_before &&
|
| 1402 |
+
slot_accepts(need_big, is_big_slot(s))) {
|
| 1403 |
+
lst->pop_back();
|
| 1404 |
+
return s;
|
| 1405 |
+
}
|
| 1406 |
+
lst->pop_back();
|
| 1407 |
+
}
|
| 1408 |
+
}
|
| 1409 |
+
int64_t best = -1;
|
| 1410 |
+
uint64_t best_clock = UINT64_MAX;
|
| 1411 |
+
for (size_t i = 0; i < n_slots_; i++) {
|
| 1412 |
+
const slot_state & st = slots_[i];
|
| 1413 |
+
if (st.pin > 0 || st.busy || st.last_use > stale_before) {
|
| 1414 |
+
continue;
|
| 1415 |
+
}
|
| 1416 |
+
if (!slot_accepts(need_big, is_big_slot(i))) {
|
| 1417 |
+
continue;
|
| 1418 |
+
}
|
| 1419 |
+
if (st.last_use < best_clock) {
|
| 1420 |
+
best_clock = st.last_use;
|
| 1421 |
+
best = (int64_t) i;
|
| 1422 |
+
}
|
| 1423 |
+
}
|
| 1424 |
+
return (int) best;
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
int pager::victim_stale_locked(uint64_t stale_before) {
|
| 1428 |
+
// 只挑「last_use 很舊」而且沒被 pin 的槽:保護最近的工作集
|
| 1429 |
+
while (!free_slots_.empty()) {
|
| 1430 |
+
const int32_t s = free_slots_.back();
|
| 1431 |
+
if (!slots_[(size_t) s].busy && slots_[(size_t) s].last_use <= stale_before) {
|
| 1432 |
+
free_slots_.pop_back();
|
| 1433 |
+
return s;
|
| 1434 |
+
}
|
| 1435 |
+
if (free_slots_.size() * 4 < n_slots_) {
|
| 1436 |
+
break;
|
| 1437 |
+
}
|
| 1438 |
+
free_slots_.pop_back();
|
| 1439 |
+
}
|
| 1440 |
+
int64_t best = -1;
|
| 1441 |
+
uint64_t best_clock = UINT64_MAX;
|
| 1442 |
+
for (size_t i = 0; i < n_slots_; i++) {
|
| 1443 |
+
const slot_state & st = slots_[i];
|
| 1444 |
+
if (st.pin > 0 || st.busy || st.last_use > stale_before) {
|
| 1445 |
+
continue;
|
| 1446 |
+
}
|
| 1447 |
+
if (st.last_use < best_clock) {
|
| 1448 |
+
best_clock = st.last_use;
|
| 1449 |
+
best = (int64_t) i;
|
| 1450 |
+
}
|
| 1451 |
+
}
|
| 1452 |
+
return (int) best;
|
| 1453 |
+
}
|
| 1454 |
+
|
| 1455 |
+
int pager::acquire(int il, int ie, bool blocking) {
|
| 1456 |
+
if (!ready_) {
|
| 1457 |
+
return -1;
|
| 1458 |
+
}
|
| 1459 |
+
std::unique_lock<std::mutex> lk(mu_);
|
| 1460 |
+
int32_t s = find_slot_locked(il, ie);
|
| 1461 |
+
if (s >= 0 && slots_[s].busy) {
|
| 1462 |
+
// 這個槽位**正在被填入**:另一條執行緒剛 admit 它、資料還沒讀完。
|
| 1463 |
+
//
|
| 1464 |
+
// 這裡一定要等,不能當成命中直接用。否則讀到的是「寫到一半的權重」,
|
| 1465 |
+
// 而且那個錯誤是**非決定性**的 —— 每次執行結果可能不同(實測在
|
| 1466 |
+
// prefill 時會讓同一個提示詞跑出不同的文字)。
|
| 1467 |
+
// busy 的槽位在 admit/victim 那邊本來就會被跳過,所以這裡是唯一
|
| 1468 |
+
// 還能拿到它的路徑,也因此必須在這裡等。
|
| 1469 |
+
const uint64_t t0 = now_us();
|
| 1470 |
+
while (slots_[s].busy) {
|
| 1471 |
+
lk.unlock();
|
| 1472 |
+
std::this_thread::sleep_for(std::chrono::microseconds(200));
|
| 1473 |
+
lk.lock();
|
| 1474 |
+
if (now_us() - t0 > (uint64_t) env_size("SDQ_BUSY_WAIT_MS", 30000) * 1000) {
|
| 1475 |
+
fprintf(stderr,
|
| 1476 |
+
"[sdq] 致命:等待槽位 il=%d ie=%d 讀入超過 %u ms(busy 一直沒解除)\n",
|
| 1477 |
+
il, ie, (unsigned) env_size("SDQ_BUSY_WAIT_MS", 30000));
|
| 1478 |
+
std::fflush(stderr);
|
| 1479 |
+
abort();
|
| 1480 |
+
}
|
| 1481 |
+
}
|
| 1482 |
+
// 讀取可能失敗(那時槽位已被清空)→ 重新走一次正常的 miss 路徑
|
| 1483 |
+
if (find_slot_locked(il, ie) != s) {
|
| 1484 |
+
s = -1;
|
| 1485 |
+
}
|
| 1486 |
+
}
|
| 1487 |
+
if (s >= 0) {
|
| 1488 |
+
slots_[s].last_use = ++clock_;
|
| 1489 |
+
slots_[s].pin++;
|
| 1490 |
+
touch_freq_locked(slots_[(size_t) s]);
|
| 1491 |
+
stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
|
| 1492 |
+
stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
|
| 1493 |
+
return s;
|
| 1494 |
+
}
|
| 1495 |
+
|
| 1496 |
+
stats_.expert_requests.fetch_add(1, std::memory_order_relaxed);
|
| 1497 |
+
count_request(il, ie);
|
| 1498 |
+
stats_.expert_misses.fetch_add(1, std::memory_order_relaxed);
|
| 1499 |
+
|
| 1500 |
+
// 先找一個已經在背景讀入這頁的槽
|
| 1501 |
+
for (size_t i = 0; i < n_slots_; i++) {
|
| 1502 |
+
slot_state & st = slots_[i];
|
| 1503 |
+
if (st.busy && st.layer == il && st.expert == ie) {
|
| 1504 |
+
// 等背景執行緒讀完
|
| 1505 |
+
lk.unlock();
|
| 1506 |
+
const uint64_t t0 = now_us();
|
| 1507 |
+
while (true) {
|
| 1508 |
+
lk.lock();
|
| 1509 |
+
if (!slots_[i].busy) {
|
| 1510 |
+
break;
|
| 1511 |
+
}
|
| 1512 |
+
lk.unlock();
|
| 1513 |
+
std::this_thread::sleep_for(std::chrono::microseconds(200));
|
| 1514 |
+
}
|
| 1515 |
+
s = (int32_t) i;
|
| 1516 |
+
lk.unlock();
|
| 1517 |
+
stats_.prefetch_wait_us.fetch_add(now_us() - t0, std::memory_order_relaxed);
|
| 1518 |
+
std::unique_lock<std::mutex> lk2(mu_);
|
| 1519 |
+
slots_[s].last_use = ++clock_;
|
| 1520 |
+
slots_[s].pin++;
|
| 1521 |
+
stats_.expert_hits.fetch_add(1, std::memory_order_relaxed);
|
| 1522 |
+
stats_.prefetch_used.fetch_add(1, std::memory_order_relaxed);
|
| 1523 |
+
return s;
|
| 1524 |
+
}
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
if (!blocking) {
|
| 1528 |
+
return -1;
|
| 1529 |
+
}
|
| 1530 |
+
|
| 1531 |
+
// 選槽位時必須知道「這個 expert 需要哪一種大小」,否則大 expert 可能被塞進小槽位
|
| 1532 |
+
// (寫出邊界 → 靜默算錯),或者小 expert 佔掉大槽位(浪費 6.8% 的 arena)。
|
| 1533 |
+
s = admit_slot_locked(il, ie);
|
| 1534 |
+
if (s < 0) {
|
| 1535 |
+
// 拿不到槽位:短暫等待別的執行緒解除 pin(每層開頭會 unpin_all)。
|
| 1536 |
+
// 這裡「重試」而不是直接回 -1,因為回 -1 會讓呼叫端**跳過這個 expert**,
|
| 1537 |
+
// 結果是靜默少算一條專家分支 —— 那是最嚴重的錯誤。
|
| 1538 |
+
// 2000 次 × 250 µs = 最多等 0.5 秒;真的撐不到那麼久,代表 arena 被
|
| 1539 |
+
// 預取的 busy 槽位佔住了(實測:跨層預取開啟時會發生)。
|
| 1540 |
+
for (int attempt = 0; attempt < 2000 && s < 0; attempt++) {
|
| 1541 |
+
lk.unlock();
|
| 1542 |
+
std::this_thread::sleep_for(std::chrono::microseconds(250));
|
| 1543 |
+
lk.lock();
|
| 1544 |
+
s = admit_slot_locked(il, ie);
|
| 1545 |
+
}
|
| 1546 |
+
if (s < 0) {
|
| 1547 |
+
fprintf(stderr,
|
| 1548 |
+
"[sdq] 嚴重:找不到能放 il=%d ie=%d(need_big=%d)的槽位,"
|
| 1549 |
+
"大槽 %zu/小槽 %zu\n",
|
| 1550 |
+
il, ie, (int) need_big_slot(il, ie), n_big_, n_small_);
|
| 1551 |
+
// 這裡「回 -1」會讓呼叫端**跳過這個 expert**,結果是靜默少算一條專家分支
|
| 1552 |
+
// —— 那是整個引擎最嚴重的錯誤型態(模型會安靜地給出錯誤答案,而且
|
| 1553 |
+
// 每次執行還不一定一樣)。寧可整個行程死掉,也不要產出「看起來正常」
|
| 1554 |
+
// 的錯誤結果。
|
| 1555 |
+
fprintf(stderr,
|
| 1556 |
+
"[sdq] 致命:無法取得 expert 槽位。為了不給出靜默錯誤的結果,"
|
| 1557 |
+
"直接中止。請回報這個情況(多半是預取把槽位佔成 busy 太久)。\n");
|
| 1558 |
+
std::fflush(stderr);
|
| 1559 |
+
abort();
|
| 1560 |
+
}
|
| 1561 |
+
}
|
| 1562 |
+
slot_state & st = slots_[s];
|
| 1563 |
+
if (st.layer >= 0) {
|
| 1564 |
+
page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
|
| 1565 |
+
stats_.evictions.fetch_add(1, std::memory_order_relaxed);
|
| 1566 |
+
used_.fetch_sub(1, std::memory_order_relaxed);
|
| 1567 |
+
}
|
| 1568 |
+
st.layer = il;
|
| 1569 |
+
st.expert = ie;
|
| 1570 |
+
st.last_use = ++clock_;
|
| 1571 |
+
st.pin = 1;
|
| 1572 |
+
st.is_hot = hot_lookup(il, ie);
|
| 1573 |
+
// **先標 busy,再 publish 到 page_。**
|
| 1574 |
+
// 順序反過來的話(先 publish 再標 busy 中間有一個窗口)另一條執行緒會把它
|
| 1575 |
+
// 當成命中,然後在資料還沒讀完時就拿去做矩陣乘法 → 非決定性的錯誤輸出。
|
| 1576 |
+
// acquire() 看到 busy 會等;admit/victim 本來就會跳過 busy 的槽位。
|
| 1577 |
+
st.busy = true;
|
| 1578 |
+
page_[((int64_t) il << 20) | (int64_t) ie] = s;
|
| 1579 |
+
|
| 1580 |
+
lk.unlock();
|
| 1581 |
+
|
| 1582 |
+
const uint64_t t0 = now_us();
|
| 1583 |
+
const bool ok = cfg_.io_uring ? read_expert_uring(il, ie, slot_ptr(s))
|
| 1584 |
+
: read_expert_par(il, ie, slot_ptr(s));
|
| 1585 |
+
const uint64_t dt = now_us() - t0;
|
| 1586 |
+
|
| 1587 |
+
std::unique_lock<std::mutex> lk2(mu_);
|
| 1588 |
+
st.busy = false;
|
| 1589 |
+
if (!ok) {
|
| 1590 |
+
st.layer = -1;
|
| 1591 |
+
st.expert = -1;
|
| 1592 |
+
st.pin = 0;
|
| 1593 |
+
page_.erase(((int64_t) il << 20) | (int64_t) ie);
|
| 1594 |
+
return -1;
|
| 1595 |
+
}
|
| 1596 |
+
used_.fetch_add(1, std::memory_order_relaxed);
|
| 1597 |
+
stats_.demand_wait_us.fetch_add(dt, std::memory_order_relaxed);
|
| 1598 |
+
const layer_desc * L = layer(il);
|
| 1599 |
+
if (L) {
|
| 1600 |
+
stats_.ssb_read_bytes_demand.fetch_add((uint64_t) L->experts[(size_t) ie].bytes, std::memory_order_relaxed);
|
| 1601 |
+
}
|
| 1602 |
+
return s;
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
void pager::prefetch_expert(int il, int ie) {
|
| 1606 |
+
if (!ready_ || !cfg_.prefetch) {
|
| 1607 |
+
return;
|
| 1608 |
+
}
|
| 1609 |
+
{
|
| 1610 |
+
std::unique_lock<std::mutex> lk(mu_);
|
| 1611 |
+
if (find_slot_locked(il, ie) >= 0) {
|
| 1612 |
+
stats_.prefetch_duplicate.fetch_add(1, std::memory_order_relaxed);
|
| 1613 |
+
return;
|
| 1614 |
+
}
|
| 1615 |
+
// 預取只能占用「空槽」或「已經很久沒被用到的頁」。
|
| 1616 |
+
// (實測:無差別淘汰會讓命中率從 90% 掉到 27%,而預取使用率是 0%,純粹有害。)
|
| 1617 |
+
//
|
| 1618 |
+
// **槽位一定要選對尺寸**:need_big_slot() 為真的 expert 拿到小槽位會寫出邊界。
|
| 1619 |
+
const uint64_t stale_before = clock_ - (uint64_t) env_size("SDQ_PREFETCH_STALE_TOKENS", 2) * 320;
|
| 1620 |
+
// 預取佇列也不能太深:SSD 讀不夠快時,排在後面的預取早就過期了
|
| 1621 |
+
if (pf_q_.size() > (size_t) env_size("SDQ_PREFETCH_QUEUE", 64)) {
|
| 1622 |
+
return;
|
| 1623 |
+
}
|
| 1624 |
+
// **必須留夠空的槽位給 demand。**
|
| 1625 |
+
// 預取會把槽位標成 busy 並佔著它讀取;大槽位本來就少(工作集下限是
|
| 1626 |
+
// n_big_layers × n_expert_used),如果預取把最後幾個空槽都吃掉,
|
| 1627 |
+
// 下一層的 demand 就會「找不到能放 il=34 ie=114 的槽位」而中止。
|
| 1628 |
+
// (這是 verify 新增的預取壓力測試 [2b] 抓到的實際情況。)
|
| 1629 |
+
// 所以:對應尺寸的空槽少於 n_expert_used 個時,這一筆預取直接不做。
|
| 1630 |
+
const size_t reserve = (size_t) info_.n_expert_used;
|
| 1631 |
+
const size_t n_free_right = need_big_slot(il, ie) ? free_big_slots_.size() : free_slots_.size();
|
| 1632 |
+
if (n_free_right < reserve) {
|
| 1633 |
+
stats_.prefetch_duplicate.fetch_add(1, std::memory_order_relaxed);
|
| 1634 |
+
return;
|
| 1635 |
+
}
|
| 1636 |
+
const int32_t s = victim_stale_for(il, ie, stale_before);
|
| 1637 |
+
if (s < 0) {
|
| 1638 |
+
return;
|
| 1639 |
+
}
|
| 1640 |
+
slot_state & st = slots_[s];
|
| 1641 |
+
if (st.layer >= 0) {
|
| 1642 |
+
page_.erase(((int64_t) st.layer << 20) | (int64_t) st.expert);
|
| 1643 |
+
stats_.evictions.fetch_add(1, std::memory_order_relaxed);
|
| 1644 |
+
used_.fetch_sub(1, std::memory_order_relaxed);
|
| 1645 |
+
}
|
| 1646 |
+
st.layer = il;
|
| 1647 |
+
st.expert = ie;
|
| 1648 |
+
st.busy = true;
|
| 1649 |
+
st.pin = 0;
|
| 1650 |
+
pf_q_.push_back({s, il, ie, now_us()});
|
| 1651 |
+
stats_.prefetch_issued.fetch_add(1, std::memory_order_relaxed);
|
| 1652 |
+
}
|
| 1653 |
+
pf_cv_.notify_one();
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
void pager::pf_worker() {
|
| 1657 |
+
std::unique_lock<std::mutex> lk(pf_mu_);
|
| 1658 |
+
for (;;) {
|
| 1659 |
+
pf_cv_.wait(lk, [this] { return pf_stop_ || !pf_q_.empty(); });
|
| 1660 |
+
if (pf_stop_ && pf_q_.empty()) {
|
| 1661 |
+
return;
|
| 1662 |
+
}
|
| 1663 |
+
const pf_item item = pf_q_.front();
|
| 1664 |
+
pf_q_.pop_front();
|
| 1665 |
+
lk.unlock();
|
| 1666 |
+
|
| 1667 |
+
// 這個槽可能已經被淘汰/重用 → 檢查 generation
|
| 1668 |
+
{
|
| 1669 |
+
std::unique_lock<std::mutex> g(mu_);
|
| 1670 |
+
const slot_state & st = slots_[item.slot];
|
| 1671 |
+
const bool still = st.busy && st.layer == item.il && st.expert == item.ie;
|
| 1672 |
+
if (!still) {
|
| 1673 |
+
lk.lock();
|
| 1674 |
+
continue;
|
| 1675 |
+
}
|
| 1676 |
+
}
|
| 1677 |
+
|
| 1678 |
+
// 預取也走 I/O 段平行:單獨一條執行緒循序讀三段,是這條路徑唯一的瓶頸。
|
| 1679 |
+
const bool ok = cfg_.io_uring ? read_expert_uring(item.il, item.ie, slot_ptr(item.slot))
|
| 1680 |
+
: (cfg_.io_parallel ? read_expert_par(item.il, item.ie, slot_ptr(item.slot))
|
| 1681 |
+
: read_expert(item.il, item.ie, slot_ptr(item.slot)));
|
| 1682 |
+
const layer_desc * L = layer(item.il);
|
| 1683 |
+
{
|
| 1684 |
+
std::unique_lock<std::mutex> g(mu_);
|
| 1685 |
+
slot_state & st = slots_[item.slot];
|
| 1686 |
+
if (st.busy && st.layer == item.il && st.expert == item.ie) {
|
| 1687 |
+
st.busy = false;
|
| 1688 |
+
st.last_use = ++clock_;
|
| 1689 |
+
if (ok) {
|
| 1690 |
+
page_[((int64_t) item.il << 20) | (int64_t) item.ie] = item.slot;
|
| 1691 |
+
used_.fetch_add(1, std::memory_order_relaxed);
|
| 1692 |
+
stats_.prefetch_completed.fetch_add(1, std::memory_order_relaxed);
|
| 1693 |
+
if (L) {
|
| 1694 |
+
stats_.ssb_read_bytes_prefetch.fetch_add(
|
| 1695 |
+
(uint64_t) L->experts[(size_t) item.ie].bytes, std::memory_order_relaxed);
|
| 1696 |
+
}
|
| 1697 |
+
} else {
|
| 1698 |
+
st.layer = -1;
|
| 1699 |
+
st.expert = -1;
|
| 1700 |
+
}
|
| 1701 |
+
}
|
| 1702 |
+
}
|
| 1703 |
+
lk.lock();
|
| 1704 |
+
}
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
void pager::release(int s) {
|
| 1708 |
+
if (s < 0 || (size_t) s >= n_slots_) {
|
| 1709 |
+
return;
|
| 1710 |
+
}
|
| 1711 |
+
std::lock_guard<std::mutex> lk(mu_);
|
| 1712 |
+
slot_state & st = slots_[(size_t) s];
|
| 1713 |
+
if (st.pin > 0) {
|
| 1714 |
+
st.pin--;
|
| 1715 |
+
}
|
| 1716 |
+
}
|
| 1717 |
+
|
| 1718 |
+
void pager::unpin_all() {
|
| 1719 |
+
std::unique_lock<std::mutex> lk(mu_);
|
| 1720 |
+
for (auto & st : slots_) {
|
| 1721 |
+
st.pin = 0;
|
| 1722 |
+
}
|
| 1723 |
+
}
|
| 1724 |
+
|
| 1725 |
+
void pager::note_used(int il, int ie) {
|
| 1726 |
+
if (il < 0 || ie < 0) {
|
| 1727 |
+
return;
|
| 1728 |
+
}
|
| 1729 |
+
std::lock_guard<std::mutex> lk(hist_mu_);
|
| 1730 |
+
if (il >= (int) cur_used_.size()) {
|
| 1731 |
+
return;
|
| 1732 |
+
}
|
| 1733 |
+
auto & v = cur_used_[(size_t) il];
|
| 1734 |
+
if (std::find(v.begin(), v.end(), ie) == v.end()) {
|
| 1735 |
+
v.push_back(ie);
|
| 1736 |
+
}
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
// 「下一層」預取:只在 layer L 的 op 開頭呼叫一次。
|
| 1740 |
+
//
|
| 1741 |
+
// 為什麼需要這個函式:end_token() 是**整個 token 的 40 層都算完之後**才被呼叫的
|
| 1742 |
+
// (sdq_cli.cpp 在 llama_decode 之後呼叫),所以它排的預取完全沒有和任何計算重疊
|
| 1743 |
+
// —— 量測上 prefetch 使用率只有 0~8 %,等於整條預取路徑是關的。
|
| 1744 |
+
// 在 layer L 開始時就替 layer L+1 排預取,才有一整層的時間(讀+算,約 4 ms)
|
| 1745 |
+
// 可以讓 SSD 讀取躲在後面。
|
| 1746 |
+
//
|
| 1747 |
+
// 預測用的是 prev_used_[il+1](上一個 token 在該層選的專家)—— MoE 的路由在
|
| 1748 |
+
// 鄰近 token 之間非常穩定,這是最便宜且不用訓練的預測器。
|
| 1749 |
+
void pager::prefetch_layer_ahead(int il) {
|
| 1750 |
+
if (!ready_ || !cfg_.prefetch || il < 0 || il >= (int) layers_.size()) {
|
| 1751 |
+
return;
|
| 1752 |
+
}
|
| 1753 |
+
int32_t ids[64];
|
| 1754 |
+
const int k = (int) info_.n_expert_used;
|
| 1755 |
+
if (k <= 0 || k > 64) {
|
| 1756 |
+
return;
|
| 1757 |
+
}
|
| 1758 |
+
predict(il, ids, k);
|
| 1759 |
+
for (int i = 0; i < k; i++) {
|
| 1760 |
+
prefetch_expert(il, ids[i]);
|
| 1761 |
+
}
|
| 1762 |
+
}
|
| 1763 |
+
|
| 1764 |
+
void pager::end_token() {
|
| 1765 |
+
if (!ready_) {
|
| 1766 |
+
return;
|
| 1767 |
+
}
|
| 1768 |
+
// 用「上個 token 的選擇」更新一階 Markov 轉移表
|
| 1769 |
+
{
|
| 1770 |
+
std::lock_guard<std::mutex> lk(hist_mu_);
|
| 1771 |
+
for (size_t il = 0; il < cur_used_.size(); il++) {
|
| 1772 |
+
auto & cur = cur_used_[il];
|
| 1773 |
+
if (cur.empty()) {
|
| 1774 |
+
continue;
|
| 1775 |
+
}
|
| 1776 |
+
auto & layer_hist = hist_[il];
|
| 1777 |
+
// 以每層最近一次(或平均)的前一次選擇為前驅
|
| 1778 |
+
if (!prev_used_.empty() && il < prev_used_.size() && !prev_used_[il].empty()) {
|
| 1779 |
+
for (int32_t from : prev_used_[il]) {
|
| 1780 |
+
trans & t = layer_hist[(size_t) from];
|
| 1781 |
+
for (int32_t to : cur) {
|
| 1782 |
+
int found = -1;
|
| 1783 |
+
for (int32_t i = 0; i < t.n; i++) {
|
| 1784 |
+
if (t.to[i] == to) {
|
| 1785 |
+
t.cnt[i]++;
|
| 1786 |
+
found = i;
|
| 1787 |
+
break;
|
| 1788 |
+
}
|
| 1789 |
+
}
|
| 1790 |
+
if (found < 0 && t.n < 64) {
|
| 1791 |
+
t.to[t.n] = to;
|
| 1792 |
+
t.cnt[t.n] = 1;
|
| 1793 |
+
t.n++;
|
| 1794 |
+
}
|
| 1795 |
+
}
|
| 1796 |
+
}
|
| 1797 |
+
}
|
| 1798 |
+
prev_used_[il] = cur;
|
| 1799 |
+
cur.clear();
|
| 1800 |
+
}
|
| 1801 |
+
}
|
| 1802 |
+
|
| 1803 |
+
// 依預測排預取
|
| 1804 |
+
if (!cfg_.prefetch) {
|
| 1805 |
+
return;
|
| 1806 |
+
}
|
| 1807 |
+
int32_t ids[64];
|
| 1808 |
+
for (size_t il = 0; il < layers_.size(); il++) {
|
| 1809 |
+
const int k = (int) info_.n_expert_used;
|
| 1810 |
+
predict((int) il, ids, k);
|
| 1811 |
+
for (int i = 0; i < k; i++) {
|
| 1812 |
+
prefetch_expert((int) il, ids[i]);
|
| 1813 |
+
}
|
| 1814 |
+
}
|
| 1815 |
+
}
|
| 1816 |
+
|
| 1817 |
+
void pager::predict(int il, int32_t * out, int k) {
|
| 1818 |
+
if (il < 0 || il >= (int) prev_used_.size()) {
|
| 1819 |
+
for (int i = 0; i < k; i++) {
|
| 1820 |
+
out[i] = i;
|
| 1821 |
+
}
|
| 1822 |
+
return;
|
| 1823 |
+
}
|
| 1824 |
+
// 以最近一次選擇為前驅;沒有歷史就直接沿用上一次的選擇
|
| 1825 |
+
const std::vector<int32_t> & prev = prev_used_[(size_t) il];
|
| 1826 |
+
std::vector<std::pair<uint32_t, int32_t>> scored;
|
| 1827 |
+
if (prev.empty()) {
|
| 1828 |
+
for (int i = 0; i < k; i++) {
|
| 1829 |
+
out[i] = i;
|
| 1830 |
+
}
|
| 1831 |
+
return;
|
| 1832 |
+
}
|
| 1833 |
+
for (int32_t from : prev) {
|
| 1834 |
+
const trans & t = hist_[(size_t) il][(size_t) from];
|
| 1835 |
+
for (int32_t i = 0; i < t.n; i++) {
|
| 1836 |
+
scored.push_back({t.cnt[i], t.to[i]});
|
| 1837 |
+
}
|
| 1838 |
+
}
|
| 1839 |
+
if (scored.empty()) {
|
| 1840 |
+
for (size_t i = 0; i < prev.size() && i < (size_t) k; i++) {
|
| 1841 |
+
out[i] = prev[i];
|
| 1842 |
+
}
|
| 1843 |
+
for (int i = (int) prev.size(); i < k; i++) {
|
| 1844 |
+
out[i] = i;
|
| 1845 |
+
}
|
| 1846 |
+
return;
|
| 1847 |
+
}
|
| 1848 |
+
std::sort(scored.begin(), scored.end(), [](auto & a, auto & b) {
|
| 1849 |
+
return a.first > b.first;
|
| 1850 |
+
});
|
| 1851 |
+
for (int i = 0; i < k && i < (int) scored.size(); i++) {
|
| 1852 |
+
out[i] = scored[(size_t) i].second;
|
| 1853 |
+
}
|
| 1854 |
+
}
|
| 1855 |
+
|
| 1856 |
+
double pager::hit_rate() const {
|
| 1857 |
+
const uint64_t req = stats_.expert_requests.load();
|
| 1858 |
+
const uint64_t hit = stats_.expert_hits.load();
|
| 1859 |
+
return req ? (double) hit / (double) req : 0.0;
|
| 1860 |
+
}
|
| 1861 |
+
|
| 1862 |
+
void pager::shutdown() {
|
| 1863 |
+
if (!ready_) {
|
| 1864 |
+
return;
|
| 1865 |
+
}
|
| 1866 |
+
if (!profile_out_.empty()) {
|
| 1867 |
+
std::lock_guard<std::mutex> lk(mu_);
|
| 1868 |
+
dump_profile(profile_out_);
|
| 1869 |
+
}
|
| 1870 |
+
if (pf_.joinable()) {
|
| 1871 |
+
{
|
| 1872 |
+
std::unique_lock<std::mutex> lk(pf_mu_);
|
| 1873 |
+
pf_stop_ = true;
|
| 1874 |
+
}
|
| 1875 |
+
pf_cv_.notify_all();
|
| 1876 |
+
pf_.join();
|
| 1877 |
+
}
|
| 1878 |
+
io_pool_stop();
|
| 1879 |
+
if (fd_ >= 0) {
|
| 1880 |
+
close(fd_);
|
| 1881 |
+
fd_ = -1;
|
| 1882 |
+
}
|
| 1883 |
+
if (fd_direct_ >= 0) {
|
| 1884 |
+
close(fd_direct_);
|
| 1885 |
+
fd_direct_ = -1;
|
| 1886 |
+
}
|
| 1887 |
+
ready_ = false;
|
| 1888 |
+
}
|
| 1889 |
+
|
| 1890 |
+
pager::~pager() {
|
| 1891 |
+
shutdown();
|
| 1892 |
+
if (arena_) {
|
| 1893 |
+
// 注意:arena 是 posix_memalign 配置的,必須用 free() 釋放,
|
| 1894 |
+
// 用 munmap() 會破壞 glibc 的 heap(之後任何 free 都會報 double free)。
|
| 1895 |
+
free(arena_);
|
| 1896 |
+
arena_ = nullptr;
|
| 1897 |
+
}
|
| 1898 |
+
}
|
| 1899 |
+
|
| 1900 |
+
static double pct(uint64_t a, uint64_t b) {
|
| 1901 |
+
return b ? 100.0 * (double) a / (double) b : 0.0;
|
| 1902 |
+
}
|
| 1903 |
+
|
| 1904 |
+
void pager::print_summary() const {
|
| 1905 |
+
if (!ready_) {
|
| 1906 |
+
return;
|
| 1907 |
+
}
|
| 1908 |
+
const layer_desc * L0 = layer(0);
|
| 1909 |
+
if (L0 && !L0->experts.empty()) {
|
| 1910 |
+
const expert_desc & e = L0->experts[0];
|
| 1911 |
+
fprintf(stderr, "[sdq] page-map: n_layer=%lld n_expert=%lld n_expert_used=%lld n_embd=%lld n_ff_exp=%lld"
|
| 1912 |
+
" | L0E0 gate=%llu/%llu up=%llu/%llu down=%llu/%llu (off/size) ne0=%lld ne1=%lld d_ne0=%lld d_ne1=%lld\n",
|
| 1913 |
+
(long long) info_.n_layer, (long long) info_.n_expert, (long long) info_.n_expert_used,
|
| 1914 |
+
(long long) info_.n_embd, (long long) info_.n_ff_exp,
|
| 1915 |
+
(unsigned long long) e.seg[part::GATE].off, (unsigned long long) e.seg[part::GATE].size,
|
| 1916 |
+
(unsigned long long) e.seg[part::UP].off, (unsigned long long) e.seg[part::UP].size,
|
| 1917 |
+
(unsigned long long) e.seg[part::DOWN].off, (unsigned long long) e.seg[part::DOWN].size,
|
| 1918 |
+
(long long) e.ne0, (long long) e.ne1, (long long) e.down_ne0, (long long) e.down_ne1);
|
| 1919 |
+
}
|
| 1920 |
+
fprintf(stderr,
|
| 1921 |
+
"[sdq] expert pager ready: %s\n"
|
| 1922 |
+
"[sdq] 熱門 expert 釘選:%d 個%s\n"
|
| 1923 |
+
"[sdq] slot=%.2f MiB (%zu B) slots=%zu arena=%.2f GiB (RAM 實測 RSS=%.2f GiB)\n",
|
| 1924 |
+
cfg_.model_path.c_str(),
|
| 1925 |
+
n_hot_, n_hot_ > 0 ? "(永不淘汰)" : "(無剖面,用動態 LRU/LFU)",
|
| 1926 |
+
slot_bytes_ / 1048576.0, slot_bytes_, n_slots_, arena_bytes_ / 1073741824.0,
|
| 1927 |
+
process_rss_bytes() / 1073741824.0);
|
| 1928 |
+
if (n_big_) {
|
| 1929 |
+
// 大小兩種槽位:把配置結果講清楚,否則只看「slots=N」會誤以為每個槽一樣大。
|
| 1930 |
+
fprintf(stderr,
|
| 1931 |
+
"[sdq] 槽位分兩種:小 %.2f MiB × %zu + 大 %.2f MiB × %zu(平均 %.2f MiB)\n",
|
| 1932 |
+
small_bytes_ / 1048576.0, n_small_,
|
| 1933 |
+
big_bytes_ / 1048576.0, n_big_,
|
| 1934 |
+
(double) arena_bytes_ / (double) (n_slots_ ? n_slots_ : 1) / 1048576.0);
|
| 1935 |
+
}
|
| 1936 |
+
fprintf(stderr,
|
| 1937 |
+
"[sdq] prefetch=%s fadvise_dontneed=%d\n",
|
| 1938 |
+
cfg_.prefetch ? "on" : "off", (int) cfg_.fadvise_dontneed);
|
| 1939 |
+
}
|
| 1940 |
+
|
| 1941 |
+
void pager::dump_stats(const char * phase) {
|
| 1942 |
+
if (cfg_.stats_path.empty()) {
|
| 1943 |
+
return;
|
| 1944 |
+
}
|
| 1945 |
+
const uint64_t tokens = stats_.tokens.load();
|
| 1946 |
+
const uint64_t req = stats_.expert_requests.load();
|
| 1947 |
+
const uint64_t hit = stats_.expert_hits.load();
|
| 1948 |
+
const uint64_t issued = stats_.prefetch_issued.load();
|
| 1949 |
+
const uint64_t used = stats_.prefetch_used.load();
|
| 1950 |
+
const uint64_t b_dem = stats_.ssb_read_bytes_demand.load();
|
| 1951 |
+
const uint64_t b_pre = stats_.ssb_read_bytes_prefetch.load();
|
| 1952 |
+
|
| 1953 |
+
std::ostringstream o;
|
| 1954 |
+
o << "{\"phase\":\"" << phase << "\""
|
| 1955 |
+
<< ",\"tokens\":" << tokens
|
| 1956 |
+
<< ",\"expert_requests\":" << req
|
| 1957 |
+
<< ",\"expert_hits\":" << hit
|
| 1958 |
+
<< ",\"expert_misses\":" << (req - hit)
|
| 1959 |
+
<< ",\"hit_rate\":" << pct(hit, req)
|
| 1960 |
+
<< ",\"ssb_demand_bytes\":" << b_dem
|
| 1961 |
+
<< ",\"ssb_prefetch_bytes\":" << b_pre
|
| 1962 |
+
<< ",\"ssb_total_bytes\":" << (b_dem + b_pre)
|
| 1963 |
+
<< ",\"ssd_bytes_per_token\":" << (tokens ? (double) (b_dem + b_pre) / (double) tokens : 0.0)
|
| 1964 |
+
<< ",\"prefetch_issued\":" << issued
|
| 1965 |
+
<< ",\"prefetch_used\":" << used
|
| 1966 |
+
<< ",\"prefetch_used_rate\":" << pct(used, issued)
|
| 1967 |
+
<< ",\"prefetch_duplicate\":" << stats_.prefetch_duplicate.load()
|
| 1968 |
+
<< ",\"evictions\":" << stats_.evictions.load()
|
| 1969 |
+
<< ",\"demand_wait_us\":" << stats_.demand_wait_us.load()
|
| 1970 |
+
<< ",\"prefetch_wait_us\":" << stats_.prefetch_wait_us.load()
|
| 1971 |
+
<< ",\"arena_bytes\":" << arena_bytes_
|
| 1972 |
+
// 大小兩種槽位之後,「用掉幾個槽位」不能直接乘 slot_bytes_(那是大的那種)。
|
| 1973 |
+
// dump_stats 只在階段轉換時呼叫一次,遍歷兩千幾個槽位的成本可以忽略。
|
| 1974 |
+
<< ",\"arena_used_bytes\":" << used_bytes_snapshot()
|
| 1975 |
+
<< ",\"rss_bytes\":" << process_rss_bytes()
|
| 1976 |
+
<< ",\"n_slots\":" << n_slots_
|
| 1977 |
+
<< ",\"n_small_slots\":" << n_small_
|
| 1978 |
+
<< ",\"n_big_slots\":" << n_big_
|
| 1979 |
+
<< ",\"small_slot_bytes\":" << small_bytes_
|
| 1980 |
+
<< ",\"big_slot_bytes\":" << big_bytes_
|
| 1981 |
+
<< ",\"slot_bytes\":" << slot_bytes_
|
| 1982 |
+
<< "}";
|
| 1983 |
+
std::ofstream f(cfg_.stats_path, std::ios::app);
|
| 1984 |
+
if (f) {
|
| 1985 |
+
f << o.str() << "\n";
|
| 1986 |
+
}
|
| 1987 |
+
}
|
| 1988 |
+
|
| 1989 |
+
} // namespace sdq
|
| 1990 |
+
namespace sdq {
|
| 1991 |
+
// 由 CLI 呼叫:依環境變數/參數初始化 pager
|
| 1992 |
+
bool sdq_pager_active_for_layer(int il) {
|
| 1993 |
+
pager & P = pager::instance();
|
| 1994 |
+
if (!P.ready() || P.layer(il) == nullptr) {
|
| 1995 |
+
return false;
|
| 1996 |
+
}
|
| 1997 |
+
// 除錯用:SDQ_MOE_LO / SDQ_MOE_HI 可以只讓部分層走分頁路徑,
|
| 1998 |
+
// 用來二分搜尋第一個發生數值差異的層。
|
| 1999 |
+
static const int lo = [] { const char * e = getenv("SDQ_MOE_LO"); return e ? atoi(e) : 0; }();
|
| 2000 |
+
static const int hi = [] { const char * e = getenv("SDQ_MOE_HI"); return e ? atoi(e) : 1 << 30; }();
|
| 2001 |
+
return il >= lo && il < hi;
|
| 2002 |
+
}
|
| 2003 |
+
|
| 2004 |
+
bool sdq_init(const std::string & model_path) {
|
| 2005 |
+
config c;
|
| 2006 |
+
c.model_path = model_path;
|
| 2007 |
+
c.arena_bytes = env_size("SDQ_ARENA_MB", 0) << 20;
|
| 2008 |
+
c.ram_budget_bytes = env_size("SDQ_RAM_BUDGET_MB", 0) << 20;
|
| 2009 |
+
c.slot_bytes = env_size("SDQ_SLOT_KB", 0) << 10;
|
| 2010 |
+
c.prefetch = env_flag("SDQ_PREFETCH", true);
|
| 2011 |
+
// ---- OPT-1:熱門 expert 剖面(Strata 式)----
|
| 2012 |
+
if (const char * pi = getenv("SDQ_PROFILE_IN")) { c.profile_in = pi; }
|
| 2013 |
+
if (const char * po = getenv("SDQ_PROFILE_OUT")) { c.profile_out = po; }
|
| 2014 |
+
c.hot_pin = (int) env_size("SDQ_HOT_PIN", 0);
|
| 2015 |
+
c.admit_min_hits = (int) env_size("SDQ_ADMIT_MIN_HITS", 1);
|
| 2016 |
+
c.fadvise_dontneed = env_flag("SDQ_FADV_DONTNEED", true);
|
| 2017 |
+
c.io_parallel = env_flag("SDQ_IO_PARALLEL", true);
|
| 2018 |
+
c.io_uring = env_flag("SDQ_IO_URING", false);
|
| 2019 |
+
c.io_threads = (int) env_size("SDQ_IO_THREADS", 0);
|
| 2020 |
+
c.io_split = (int) env_size("SDQ_IO_SPLIT", 1);
|
| 2021 |
+
c.verbose = env_flag("SDQ_VERBOSE", false);
|
| 2022 |
+
const char * sp = getenv("SDQ_STATS_FILE");
|
| 2023 |
+
if (sp) {
|
| 2024 |
+
c.stats_path = sp;
|
| 2025 |
+
}
|
| 2026 |
+
sdq_set_config(c);
|
| 2027 |
+
try {
|
| 2028 |
+
pager::instance().init(c, nullptr);
|
| 2029 |
+
} catch (const std::exception & e) {
|
| 2030 |
+
fprintf(stderr, "[sdq] pager 初始化失敗:%s\n", e.what());
|
| 2031 |
+
return false;
|
| 2032 |
+
}
|
| 2033 |
+
return pager::instance().ready();
|
| 2034 |
+
}
|
| 2035 |
+
|
| 2036 |
+
|
| 2037 |
+
// ---------------------------------------------------------------- 除錯:dump 任意 F32 張量
|
| 2038 |
+
// 目的:讓「原生 mul_mat_id 路徑」與「SDQ 分頁路徑」輸出同一層的 MoE 結果,
|
| 2039 |
+
// 兩邊各自 dump 一次就能直接 A/B 對照,判定到底是哪一邊算錯。
|
| 2040 |
+
namespace {
|
| 2041 |
+
struct dump_arg { std::string path; };
|
| 2042 |
+
|
| 2043 |
+
void dump_op(ggml_tensor * dst, int ith, int /*nth*/, void * userdata) {
|
| 2044 |
+
if (ith != 0) {
|
| 2045 |
+
return;
|
| 2046 |
+
}
|
| 2047 |
+
const ggml_tensor * src = dst->src[0];
|
| 2048 |
+
dump_arg * a = (dump_arg *) userdata;
|
| 2049 |
+
FILE * f = fopen(a->path.c_str(), "wb");
|
| 2050 |
+
if (!f) {
|
| 2051 |
+
fprintf(stderr, "[sdq-dump] 無法開啟 %s\n", a->path.c_str());
|
| 2052 |
+
return;
|
| 2053 |
+
}
|
| 2054 |
+
const int64_t meta[6] = { src->ne[0], src->ne[1], src->ne[2], src->ne[3],
|
| 2055 |
+
(int64_t) ggml_nbytes(src), 0 };
|
| 2056 |
+
fwrite(meta, sizeof(int64_t), 6, f);
|
| 2057 |
+
// 只支援 F32 contiguous;其他情況就明確報錯,不要假設
|
| 2058 |
+
if (src->type != GGML_TYPE_F32 || !ggml_is_contiguous(src)) {
|
| 2059 |
+
fprintf(stderr, "[sdq-dump] %s 不是 contiguous F32(type=%d)\n",
|
| 2060 |
+
a->path.c_str(), (int) src->type);
|
| 2061 |
+
} else {
|
| 2062 |
+
fwrite(src->data, sizeof(float), (size_t) (ggml_nelements(src)), f);
|
| 2063 |
+
}
|
| 2064 |
+
fclose(f);
|
| 2065 |
+
fprintf(stderr, "[sdq-dump] %s (%lld 個元素)\n", a->path.c_str(),
|
| 2066 |
+
(long long) ggml_nelements(src));
|
| 2067 |
+
}
|
| 2068 |
+
} // namespace
|
| 2069 |
+
|
| 2070 |
+
ggml_tensor * sdq_maybe_dump_tensor(ggml_context * ctx, ggml_tensor * t, int il) {
|
| 2071 |
+
const char * prefix = getenv("SDQ_MOE_OUT_DUMP");
|
| 2072 |
+
if (!prefix || !t) {
|
| 2073 |
+
return t;
|
| 2074 |
+
}
|
| 2075 |
+
int layer = getenv("SDQ_MOE_OUT_DUMP_LAYER") ? atoi(getenv("SDQ_MOE_OUT_DUMP_LAYER")) : -1;
|
| 2076 |
+
if (layer >= 0 && il != layer) {
|
| 2077 |
+
return t;
|
| 2078 |
+
}
|
| 2079 |
+
dump_arg * a = new dump_arg{ std::string(prefix) + "." + std::to_string(il) };
|
| 2080 |
+
return ggml_custom_4d(ctx, GGML_TYPE_F32, t->ne[0], t->ne[1], t->ne[2], t->ne[3],
|
| 2081 |
+
&t, 1, dump_op, 1, a);
|
| 2082 |
+
}
|
| 2083 |
+
|
| 2084 |
+
void sdq_shutdown() {
|
| 2085 |
+
pager & P = pager::instance();
|
| 2086 |
+
sdq_print_time_profile();
|
| 2087 |
+
P.dump_stats("shutdown");
|
| 2088 |
+
P.print_summary();
|
| 2089 |
+
P.shutdown();
|
| 2090 |
+
}
|
| 2091 |
+
} // namespace sdq
|
cpp/sdq_pager.h
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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_head = 0;
|
| 43 |
+
int64_t n_head_kv = 0;
|
| 44 |
+
int64_t n_embd_head_k = 0;
|
| 45 |
+
int64_t n_embd_head_v = 0;
|
| 46 |
+
int64_t context_length = 0;
|
| 47 |
+
int64_t full_attention_interval = 4;
|
| 48 |
+
int64_t is_recr[512] = {0};
|
| 49 |
+
std::string arch;
|
| 50 |
+
std::string name;
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
// 一個 expert 槽位:gate/up/down 三段(有些模型把 gate+up 合成 gate_up)
|
| 54 |
+
struct seg_desc {
|
| 55 |
+
uint64_t off = 0; // 檔案內位元組偏移
|
| 56 |
+
uint64_t size = 0; // 位元組數
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
enum part : int { GATE = 0, UP = 1, DOWN = 2, COUNT = 3 };
|
| 60 |
+
|
| 61 |
+
struct expert_desc {
|
| 62 |
+
seg_desc seg[3];
|
| 63 |
+
int64_t slot_off[3] = {0, 0, 0}; // 三段在 arena 槽位內的相對偏移
|
| 64 |
+
int64_t ne0 = 0; // gate/up row 長度(= n_embd)
|
| 65 |
+
int64_t ne1 = 0; // gate/up row 數(= n_ff_exp)
|
| 66 |
+
int64_t down_ne0 = 0; // down row 長度(= n_ff_exp)
|
| 67 |
+
int64_t down_ne1 = 0; // down row 數(= n_embd)
|
| 68 |
+
ggml_type type[3] = {GGML_TYPE_COUNT, GGML_TYPE_COUNT, GGML_TYPE_COUNT};
|
| 69 |
+
int64_t bytes = 0; // 三段總大小
|
| 70 |
+
bool fused_gate_up = false;
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
// 一層的所有 expert 描述(40 層 × 256 個)
|
| 74 |
+
struct layer_desc {
|
| 75 |
+
std::vector<expert_desc> experts;
|
| 76 |
+
int64_t bytes_per_expert = 0;
|
| 77 |
+
// 這個層的 expert 比「大多數層」大(Q4_K/Q5_K vs Q6_K)→ 需要大槽位。
|
| 78 |
+
// 不然的話所有槽位都得按最大的那種配,白白浪費 6.8% 的 arena。
|
| 79 |
+
bool needs_big_slot = false;
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
// ---------------------------------------------------------------- 指標
|
| 83 |
+
struct stats {
|
| 84 |
+
std::atomic<uint64_t> expert_requests{0}; // 每個 (layer, token) 需要幾個 expert
|
| 85 |
+
std::atomic<uint64_t> expert_hits{0};
|
| 86 |
+
std::atomic<uint64_t> expert_misses{0};
|
| 87 |
+
std::atomic<uint64_t> ssb_read_bytes_demand{0};
|
| 88 |
+
std::atomic<uint64_t> ssb_read_bytes_prefetch{0};
|
| 89 |
+
std::atomic<uint64_t> prefetch_issued{0};
|
| 90 |
+
std::atomic<uint64_t> prefetch_completed{0};
|
| 91 |
+
std::atomic<uint64_t> prefetch_used{0}; // 預取後真的被用到
|
| 92 |
+
std::atomic<uint64_t> prefetch_duplicate{0};// 預取時已經在 RAM 裡
|
| 93 |
+
std::atomic<uint64_t> evictions{0};
|
| 94 |
+
std::atomic<uint64_t> demand_wait_us{0}; // 因為缺頁而停下來等 SSD 的時間
|
| 95 |
+
std::atomic<uint64_t> prefetch_wait_us{0};
|
| 96 |
+
std::atomic<uint64_t> tokens{0};
|
| 97 |
+
std::atomic<uint64_t> moe_calls{0};
|
| 98 |
+
std::atomic<uint64_t> flops{0};
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
// ---------------------------------------------------------------- pager
|
| 102 |
+
struct config {
|
| 103 |
+
size_t arena_bytes = 0; // 0 = 自動(依 RAM 預算)
|
| 104 |
+
size_t slot_bytes = 0; // 0 = 自動(依模型推導)
|
| 105 |
+
int n_threads = 0;
|
| 106 |
+
bool prefetch = true;
|
| 107 |
+
int prefetch_ahead_layers = 0; // >0 時限制同時在飛的預取數量
|
| 108 |
+
bool fadvise_dontneed = true; // 讀完丟掉 kernel page cache,讓 RAM 帳目誠實
|
| 109 |
+
bool verbose = false;
|
| 110 |
+
uint64_t ram_budget_bytes = 0; // 0 = 自動(實體 RAM 的一小部分)
|
| 111 |
+
std::string model_path;
|
| 112 |
+
std::string stats_path;
|
| 113 |
+
int slot_bias = 0;
|
| 114 |
+
|
| 115 |
+
// ---- I/O 執行緒池 ----
|
| 116 |
+
// 為什麼需要:一個 expert 在檔案上是 gate / up / down 三段「不相鄰」的資料,
|
| 117 |
+
// 循序讀要 3 次 pread,而單一 pread 在本機量到約 7 ms(1.9 MB / 275 MB/s)。
|
| 118 |
+
// 也就是說「讀一個 expert」的延遲完全由這三段循序決定,執行緒再多也沒用。
|
| 119 |
+
// 把三段交給三條執行緒同時讀,單一 expert 的延遵掉到約 1/2~1/3。
|
| 120 |
+
// (實測:8 個 expert 全部 miss 的一層,序列讀 12.6 ms → 段平行 7.6 ms。)
|
| 121 |
+
bool io_parallel = true; // SDQ_IO_PARALLEL=0 可關(回到序列讀)
|
| 122 |
+
bool io_uring = false; // SDQ_IO_URING=1:以 liburing 提交同批非同步讀取
|
| 123 |
+
int io_threads = 0; // 0 = 自動(3 × n_threads,上限 48)
|
| 124 |
+
int io_split = 1; // SDQ_IO_SPLIT:每一段再切成幾塊並行讀(實測 1 最好)
|
| 125 |
+
|
| 126 |
+
// ---- Strata 式「熱門 expert 剖面」(OPT-1)----
|
| 127 |
+
// 離線用代表性文字跑一次,把每個 (layer, expert) 的請求次數記成剖面;
|
| 128 |
+
// 執行時把排行榜前 hot_pin 個釘成「熱頁」:熱頁可以被讀取,但**永遠不被淘汰**,
|
| 129 |
+
// 淘汰時一律先從冷頁挑。這解決的是「prefill 的一次性專家把熱區洗掉」。
|
| 130 |
+
std::string profile_in; // 讀取剖面(SDQ_PROFILE_IN)
|
| 131 |
+
std::string profile_out; // 結束時輸出剖面(SDQ_PROFILE_OUT)
|
| 132 |
+
int hot_pin = 0; // 釘選前幾名(SDQ_HOT_PIN)
|
| 133 |
+
int admit_min_hits = 1; // 請求次數低於此值就不收進快取(SDQ_ADMIT_MIN_HITS)
|
| 134 |
+
};
|
| 135 |
+
|
| 136 |
+
class pager {
|
| 137 |
+
public:
|
| 138 |
+
static pager & instance();
|
| 139 |
+
|
| 140 |
+
// 從 GGUF 檔頭建立 expert 頁面表,並配置 RAM arena
|
| 141 |
+
void init(const config & cfg, ggml_context * ctx);
|
| 142 |
+
|
| 143 |
+
bool ready() const { return ready_; }
|
| 144 |
+
size_t arena_bytes() const { return arena_bytes_; }
|
| 145 |
+
size_t slot_bytes() const { return slot_bytes_; }
|
| 146 |
+
size_t n_slots() const { return n_slots_; }
|
| 147 |
+
size_t small_bytes() const { return small_bytes_; }
|
| 148 |
+
size_t big_bytes() const { return big_bytes_; }
|
| 149 |
+
size_t n_small_slots() const { return n_small_; }
|
| 150 |
+
size_t n_big_slots() const { return n_big_; }
|
| 151 |
+
const gguf_info & info() const { return info_; }
|
| 152 |
+
|
| 153 |
+
// 取得 layer 描述(idx 越界或沒資料回 nullptr)
|
| 154 |
+
const layer_desc * layer(int il) const;
|
| 155 |
+
|
| 156 |
+
// 一次把「連續編號的一串 expert」讀進 RAM:同一層的三個矩陣(gate/up/down)本來
|
| 157 |
+
// 就各自連續存放,所以 k 個連續 expert 只需要 3 次大讀取(而不是 3k 次)。
|
| 158 |
+
// 對高延遲的儲存裝置(每個 request 都貴)這是最大的單項加速。
|
| 159 |
+
// experts 必須已依編號遞增排序;slots[i] 是 experts[i] 對應的槽位(由呼叫端配置)
|
| 160 |
+
// 回傳實際讀取的位元組數。
|
| 161 |
+
uint64_t read_expert_span(int il, const int32_t * experts, int n, const int32_t * slots);
|
| 162 |
+
|
| 163 |
+
// 只配置槽位(不讀資料):回傳槽位索引,-1 表示沒有空槽
|
| 164 |
+
int reserve_slot(int il, int ie);
|
| 165 |
+
|
| 166 |
+
// 查這個 expert 是否已經在 RAM:命中則 pin 並回傳槽位,未命中回傳 -1(不配置)
|
| 167 |
+
int probe(int il, int ie);
|
| 168 |
+
|
| 169 |
+
// 記錄一次合併讀取的位元組數(給指標用)
|
| 170 |
+
void note_span_read(uint64_t bytes);
|
| 171 |
+
|
| 172 |
+
// 確保 (layer, expert) 在 RAM,回傳槽位索引;-1 表示失敗
|
| 173 |
+
// pinned: 本 token 使用中,不可被淘汰;blocking: 缺頁時是否同步讀 SSD
|
| 174 |
+
int acquire(int il, int ie, bool blocking);
|
| 175 |
+
|
| 176 |
+
// 背景預取:把 page 讀進 arena(非同步),依歷史預測下一個 token 會用到的 expert
|
| 177 |
+
void prefetch_expert(int il, int ie);
|
| 178 |
+
// 「下一層」預取:在 layer L 的 op 開頭呼叫,替 layer L+1 排預取。
|
| 179 |
+
// 與 end_token() 的差別在時機 —— 後者在一個 token 的 40 層全算完才跑,
|
| 180 |
+
// 那時排的預取沒有任何計算可以重疊,實測使用率只有 0~8 %。
|
| 181 |
+
void prefetch_layer_ahead(int il);
|
| 182 |
+
|
| 183 |
+
// 每層計算開始時呼叫:解除上一層的 pin(本層用到的槽位才受保護)
|
| 184 |
+
void unpin_all();
|
| 185 |
+
// 用完一個槽位後立刻放掉 pin(成對呼叫 acquire())。
|
| 186 |
+
//
|
| 187 |
+
// 為什麼需要:原本 pin 是「整層釘住、下一層開頭才 unpin_all」。mode C 的
|
| 188 |
+
// expert_forward 回傳之後就再也不讀那個槽位(最後的歸約只讀各執行緒自己的
|
| 189 |
+
// partial 累加緩衝),所以繼續釘著只是白白把工作集撐大。
|
| 190 |
+
// 撐大的後果很具體:prefill 一個 ubatch 有 128 個 token,一層最多需要
|
| 191 |
+
// 256 個不同的 expert → 整層釘住 = 256 個槽位不能回收。大小兩種槽位之下,
|
| 192 |
+
// 大槽位只有約 n_big_layers × n_expert_used 個,於是**必定**在 prefill 時
|
| 193 |
+
// 「找不到能放 il=34 ie=… 的槽位」而中止。
|
| 194 |
+
void release(int s);
|
| 195 |
+
void note_used(int il, int ie);
|
| 196 |
+
void end_token();
|
| 197 |
+
|
| 198 |
+
// 依歷史預測某層下一個 token 的 top-k expert(回傳信心 0..1)
|
| 199 |
+
void predict(int il, int32_t * out, int k);
|
| 200 |
+
|
| 201 |
+
void shutdown();
|
| 202 |
+
stats & get_stats() { return stats_; }
|
| 203 |
+
|
| 204 |
+
// 把當前指標寫成一行 JSON(append 到 stats_path)
|
| 205 |
+
void dump_stats(const char * phase);
|
| 206 |
+
void print_summary() const;
|
| 207 |
+
|
| 208 |
+
// arena 內的槽位指標
|
| 209 |
+
// 槽位索引空間是統一的:[0, n_small_) 是小槽位(連續),
|
| 210 |
+
// [n_small_, n_slots_) 是大槽位(接在小槽位區之後)。
|
| 211 |
+
uint8_t * slot_ptr(int s) {
|
| 212 |
+
return (size_t) s < n_small_
|
| 213 |
+
? arena_ + (size_t) s * small_bytes_
|
| 214 |
+
: arena_ + small_bytes_ * n_small_ + (size_t)(s - (int) n_small_) * big_bytes_;
|
| 215 |
+
}
|
| 216 |
+
size_t slot_size_of(int s) const {
|
| 217 |
+
return (size_t) s < n_small_ ? small_bytes_ : big_bytes_;
|
| 218 |
+
}
|
| 219 |
+
bool need_big_slot(int il, int ie) const;
|
| 220 |
+
|
| 221 |
+
// 這個槽位算不算「大槽位」。
|
| 222 |
+
//
|
| 223 |
+
// 判斷依據是**槽位的實際大小**,不是索引 >= n_small_:
|
| 224 |
+
// 統一尺寸(SDQ_FORCE_UNIFORM_SLOT=1,或模型只有一種 expert 大小)時,
|
| 225 |
+
// 所有槽位的索引都 < n_small_ 但大小都是大的 —— 用索引判斷會讓大 expert
|
| 226 |
+
// 永遠配不到槽位,整個推論卡在 admit 的重試迴圈裡。
|
| 227 |
+
bool is_big_slot(int s) const {
|
| 228 |
+
return big_bytes_ && slot_size_of(s) >= big_bytes_;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
size_t used_bytes() const { return used_bytes_snapshot(); }
|
| 232 |
+
double hit_rate() const;
|
| 233 |
+
|
| 234 |
+
// arena 裡實際被占用的位元組數。大小兩種槽位之後不能用「槽位數 × slot_bytes_」
|
| 235 |
+
// 計算(slot_bytes_ 是大的那一種),所以真的把槽位走一遍。
|
| 236 |
+
// 呼叫端:dump_stats(每次解碼階段結束)與 print_summary,不是熱路徑。
|
| 237 |
+
size_t used_bytes_snapshot() const {
|
| 238 |
+
size_t b = 0;
|
| 239 |
+
for (size_t s = 0; s < slots_.size(); s++) {
|
| 240 |
+
if (slots_[s].layer >= 0) {
|
| 241 |
+
b += slot_size_of((int) s);
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
return b;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// 讀 SSD(同步,給 acquire 用)
|
| 248 |
+
bool read_expert(int il, int ie, uint8_t * dst);
|
| 249 |
+
|
| 250 |
+
// ---- I/O 執行緒池(見 config::io_parallel 的說明)----
|
| 251 |
+
// 一個「讀取批次」的完成狀態;放在呼叫端的 stack 上,worker 只在 remaining==0
|
| 252 |
+
// 之後才會再碰它,所以離開 read_expert_par() 之後不會有 dangling pointer。
|
| 253 |
+
struct io_ctx {
|
| 254 |
+
std::atomic<int> remaining{0};
|
| 255 |
+
bool ok = true;
|
| 256 |
+
};
|
| 257 |
+
struct io_job {
|
| 258 |
+
uint64_t off;
|
| 259 |
+
uint8_t * dst;
|
| 260 |
+
size_t size;
|
| 261 |
+
io_ctx * ctx;
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
void io_pool_start();
|
| 265 |
+
void io_pool_stop();
|
| 266 |
+
void io_worker(size_t wid);
|
| 267 |
+
bool read_expert_par(int il, int ie, uint8_t * dst);
|
| 268 |
+
bool read_expert_uring(int il, int ie, uint8_t * dst);
|
| 269 |
+
bool read_range_staged(uint64_t off, uint8_t * dst, size_t size,
|
| 270 |
+
uint8_t * stage, size_t stage_sz);
|
| 271 |
+
|
| 272 |
+
// I/O 執行緒池每條 worker 自帶的 O_DIRECT 對齊緩衝(啟動時配置,已計入 RSS)
|
| 273 |
+
struct io_stage { uint8_t * buf = nullptr; size_t sz = 0; };
|
| 274 |
+
std::vector<io_stage> io_stage_;
|
| 275 |
+
|
| 276 |
+
bool io_pool_on_ = false;
|
| 277 |
+
int io_threads_ = 0;
|
| 278 |
+
int io_split_ = 1;
|
| 279 |
+
std::vector<std::thread> io_pool_;
|
| 280 |
+
std::mutex io_mu_;
|
| 281 |
+
std::condition_variable io_cv_; // 有工作 / 要停
|
| 282 |
+
std::condition_variable io_done_cv_; // 某個批次讀完了
|
| 283 |
+
std::deque<io_job> io_q_;
|
| 284 |
+
bool io_stop_ = false;
|
| 285 |
+
std::atomic<uint64_t> io_jobs_{0};
|
| 286 |
+
std::atomic<uint64_t> io_wait_ns_{0};
|
| 287 |
+
|
| 288 |
+
private:
|
| 289 |
+
pager() = default;
|
| 290 |
+
~pager();
|
| 291 |
+
|
| 292 |
+
struct slot_state {
|
| 293 |
+
int32_t layer = -1;
|
| 294 |
+
int32_t expert = -1;
|
| 295 |
+
uint64_t last_use = 0; // LRU 記時
|
| 296 |
+
uint32_t freq = 0; // 這個 expert 被用到幾次(衰減式;用來做 LFU 混合策略)
|
| 297 |
+
uint32_t pin = 0; // >0 時不可淘汰
|
| 298 |
+
bool busy = false; // 背景執行緒正在寫入
|
| 299 |
+
bool is_hot = false; // 屬於剖面排行榜:淘汰時最後才會被選中
|
| 300 |
+
};
|
| 301 |
+
|
| 302 |
+
struct pf_item {
|
| 303 |
+
int slot;
|
| 304 |
+
int il;
|
| 305 |
+
int ie;
|
| 306 |
+
uint64_t issue_time_us;
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
void touch_freq_locked(slot_state & st);
|
| 310 |
+
// ---- OPT-1:剖面 / 熱頁 ----
|
| 311 |
+
void count_request(int il, int ie); // 記一次請求(給剖面用)
|
| 312 |
+
bool load_profile(const std::string & path); // 讀剖面並標記前 hot_pin 名
|
| 313 |
+
void dump_profile(const std::string & path); // 輸出排行榜
|
| 314 |
+
bool hot_lookup(int il, int ie) const;
|
| 315 |
+
void load_gguf_header(const std::string & path);
|
| 316 |
+
void build_page_map(const std::string & path);
|
| 317 |
+
void alloc_arena();
|
| 318 |
+
int find_slot_locked(int il, int ie);
|
| 319 |
+
int admit_slot_locked(int il, int ie);
|
| 320 |
+
int victim_locked() { return admit_slot_locked(-1, -1); }
|
| 321 |
+
int victim_stale_locked(uint64_t stale_before);
|
| 322 |
+
int victim_stale_for(int il, int ie, uint64_t stale_before);
|
| 323 |
+
void pf_worker();
|
| 324 |
+
bool read_range(uint64_t off, uint8_t * dst, size_t size);
|
| 325 |
+
bool read_range_direct(uint64_t off, uint8_t * dst, size_t size);
|
| 326 |
+
void open_model_fd();
|
| 327 |
+
|
| 328 |
+
bool ready_ = false;
|
| 329 |
+
config cfg_;
|
| 330 |
+
gguf_info info_;
|
| 331 |
+
std::vector<layer_desc> layers_;
|
| 332 |
+
|
| 333 |
+
uint8_t * arena_ = nullptr;
|
| 334 |
+
size_t arena_bytes_ = 0;
|
| 335 |
+
size_t slot_bytes_ = 0; // = max(small_bytes_, big_bytes_),保留給舊的訊息輸出
|
| 336 |
+
size_t small_bytes_ = 0; // 小槽位大小(大多數層用)
|
| 337 |
+
size_t big_bytes_ = 0; // 大槽位大小;0 = 這個模型沒有大層
|
| 338 |
+
size_t n_small_ = 0;
|
| 339 |
+
size_t n_big_ = 0;
|
| 340 |
+
size_t n_slots_ = 0;
|
| 341 |
+
int fd_ = -1;
|
| 342 |
+
int fd_direct_ = -1; // O_DIRECT:完全繞過 kernel page cache
|
| 343 |
+
bool use_direct_ = false;
|
| 344 |
+
|
| 345 |
+
mutable std::mutex mu_;
|
| 346 |
+
std::vector<slot_state> slots_;
|
| 347 |
+
std::unordered_map<int64_t, int32_t> page_; // (layer<<16|expert) -> slot
|
| 348 |
+
std::vector<int32_t> free_slots_; // 空的小槽位
|
| 349 |
+
std::vector<int32_t> free_big_slots_;
|
| 350 |
+
|
| 351 |
+
// ---- OPT-1:剖面 / 熱頁(Strata 式:靜態排行榜 + 熱頁不淘汰)----
|
| 352 |
+
std::vector<uint32_t> prof_; // 每個 (layer, expert) 的累計請求次數
|
| 353 |
+
std::vector<uint8_t> hot_; // 是否在排行榜前 hot_pin 名
|
| 354 |
+
int n_hot_ = 0;
|
| 355 |
+
std::string profile_out_;
|
| 356 |
+
uint32_t freq_tick_ = 0;
|
| 357 |
+
std::atomic<size_t> used_{0};
|
| 358 |
+
uint64_t clock_ = 0;
|
| 359 |
+
|
| 360 |
+
// 背景預取
|
| 361 |
+
std::thread pf_;
|
| 362 |
+
std::mutex pf_mu_;
|
| 363 |
+
std::condition_variable pf_cv_;
|
| 364 |
+
std::deque<pf_item> pf_q_;
|
| 365 |
+
bool pf_stop_ = false;
|
| 366 |
+
std::atomic<uint64_t> pf_inflight_{0};
|
| 367 |
+
|
| 368 |
+
// 路由歷史(一階 Markov:上個 token 的 expert -> 這次被選到的次數)
|
| 369 |
+
struct trans { int32_t to[64]; uint32_t cnt[64]; int32_t n = 0; };
|
| 370 |
+
std::vector<std::vector<trans>> hist_; // [layer][prev_expert]
|
| 371 |
+
std::vector<std::vector<int32_t>> prev_used_; // [layer] 上一個 token 用到的 expert(預測用)
|
| 372 |
+
std::vector<std::vector<int32_t>> cur_used_; // [layer] 當前 token 用到的 expert
|
| 373 |
+
std::mutex hist_mu_; // 保護 cur_used_ / prev_used_ / hist_(多執行緒會同時 note_used)
|
| 374 |
+
std::atomic<uint64_t> hist_lock_{0};
|
| 375 |
+
|
| 376 |
+
stats stats_;
|
| 377 |
+
};
|
| 378 |
+
|
| 379 |
+
// ---------------------------------------------------------------- 工具
|
| 380 |
+
uint64_t now_us();
|
| 381 |
+
size_t process_rss_bytes();
|
| 382 |
+
size_t process_anon_bytes();
|
| 383 |
+
size_t enforced_memory_ceiling_bytes();
|
| 384 |
+
size_t physical_ram_bytes();
|
| 385 |
+
|
| 386 |
+
// ---------------------------------------------------------------- ggml 介接
|
| 387 |
+
// 自訂 buffer type:expert 權重佔 0 個 RAM 位元組,資料留在檔案裡
|
| 388 |
+
ggml_backend_buffer_type_t sdq_expert_buft();
|
| 389 |
+
|
| 390 |
+
// 註冊 model params 的 tensor buffer override(expert 權重 → 我們的 buffer)
|
| 391 |
+
void sdq_install_buft_overrides(llama_model_params * params);
|
| 392 |
+
|
| 393 |
+
// 建立被分頁的 MoE FFN(替代 build_moe_ffn 裡的三次 mul_mat_id)
|
| 394 |
+
ggml_tensor * sdq_build_moe(ggml_context * ctx, ggml_tensor * cur,
|
| 395 |
+
ggml_tensor * ids, ggml_tensor * weights, int il,
|
| 396 |
+
int64_t n_expert, int64_t n_expert_used,
|
| 397 |
+
float swiglu_limit);
|
| 398 |
+
|
| 399 |
+
// 設定 arena 大小 / 預取開關等(CLI 在載入模型前呼叫)
|
| 400 |
+
bool sdq_init(const std::string & model_path);
|
| 401 |
+
void sdq_set_config(const config & c);
|
| 402 |
+
const config & sdq_get_config();
|
| 403 |
+
void sdq_set_enabled(bool on);
|
| 404 |
+
void sdq_shutdown();
|
| 405 |
+
|
| 406 |
+
bool sdq_enabled();
|
| 407 |
+
|
| 408 |
+
// 效能計時探針的結果(SDQ_PROFILE_TIME=1)
|
| 409 |
+
void sdq_print_time_profile();
|
| 410 |
+
|
| 411 |
+
// 自我測試:算一次指定 expert 的 MoE 輸出(給 NumPy 對照用)
|
| 412 |
+
int sdq_moe_selftest(int il, int ie, const float * x, float * out_io,
|
| 413 |
+
int64_t n_embd, int64_t n_ff, int n_tok);
|
| 414 |
+
|
| 415 |
+
// 建立我們的 MoE 自訂節點。
|
| 416 |
+
// **必須把 gf 傳進來並呼叫 ggml_build_forward_expand(gf, out)**:
|
| 417 |
+
// ggml 的排程器只會為「進了圖的節點」配置 compute buffer,沒註冊的節點其輸出
|
| 418 |
+
// 緩衝區會被 liveness 分析判成可重用,導致 layer>=1 的結果被其他張量覆蓋
|
| 419 |
+
// (實測症狀:layer 0 正確、layer 1 起整個錯掉)。
|
| 420 |
+
ggml_tensor * sdq_build_moe(ggml_cgraph * gf, ggml_context * ctx, ggml_tensor * cur,
|
| 421 |
+
ggml_tensor * ids, ggml_tensor * weights, int il,
|
| 422 |
+
int64_t n_expert, int64_t n_expert_used, float swiglu_limit);
|
| 423 |
+
|
| 424 |
+
// 這一層的分頁是否可用(graph build 時呼叫)
|
| 425 |
+
bool sdq_pager_active_for_layer(int il);
|
| 426 |
+
|
| 427 |
+
// 除錯/驗證用:把某個 F32 張量 dump 到檔案。
|
| 428 |
+
// 必須在「原生 mul_mat_id 路徑」與「SDQ 分頁路徑」兩邊都呼叫,才能做真正的 A/B 對照
|
| 429 |
+
// (env: SDQ_MOE_OUT_DUMP=prefix、SDQ_MOE_OUT_DUMP_LAYER=層編號,-1 = 全部)
|
| 430 |
+
ggml_tensor * sdq_maybe_dump_tensor(ggml_context * ctx, ggml_tensor * t, int il);
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
} // namespace sdq
|
demo.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""EPM 單一入口 demo:從「什麼都沒有」到「可以聊天」。
|
| 3 |
+
|
| 4 |
+
python demo.py
|
| 5 |
+
|
| 6 |
+
會依序做這些事(每一步都可以重跑而不會壞掉:續傳、增量編譯、已存在就跳過):
|
| 7 |
+
|
| 8 |
+
1. 下載本專案的 code(從 Hugging Face model repo)
|
| 9 |
+
—— 如果你已經在本專案的原始碼目錄裡執行,就直接用現有的,不重下。
|
| 10 |
+
2. 下載模型 `openbmb/MiniCPM-V-4.7-35B-A3B-gguf/MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf`
|
| 11 |
+
(19.71 GB,支援續傳;中斷再跑會從斷點繼續)
|
| 12 |
+
3. 取得 llama.cpp(**固定 commit**)+ 套用 EPM patch + 編譯
|
| 13 |
+
4. 以 8 GiB 實體 RAM 預算啟動 `epq-chat`,進入互動式聊天
|
| 14 |
+
|
| 15 |
+
EPM pager budget 是「實體 RAM 預算」,不是虛擬記憶體上限,也不會停用系統 swap。
|
| 16 |
+
|
| 17 |
+
注意:SDQ_RESERVE_MB 是逐模型、逐 ctx/ubatch 校正出來的預留量。本模型在
|
| 18 |
+
ctx=4096 / ubatch=128 / 8 GiB 預算下是 512 MiB;換 RAM 預算必須重新校正
|
| 19 |
+
(見 docs/DESIGN.md §3)。
|
| 20 |
+
要取得嚴格 8 GiB / no-swap 的證據,請用 `tools/strict8g.py`(或
|
| 21 |
+
`python -m sdq.strict_bench`)把整個行程管治起來;方法與限制見
|
| 22 |
+
docs/STRICT_8GIB.md。
|
| 23 |
+
|
| 24 |
+
設計原則:任何一步都可以單獨重跑;失敗時不會留下壞掉的半成品。
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import os
|
| 31 |
+
import shutil
|
| 32 |
+
import subprocess
|
| 33 |
+
import sys
|
| 34 |
+
import time
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
|
| 37 |
+
HERE = Path(__file__).resolve().parent
|
| 38 |
+
sys.path.insert(0, str(HERE))
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from sdq import config as C
|
| 42 |
+
except Exception: # 允許在 code 尚未下載完時也能顯示說明
|
| 43 |
+
C = None
|
| 44 |
+
|
| 45 |
+
DEFAULT_ROOT = Path(os.environ.get(
|
| 46 |
+
"EPM_HOME", os.environ.get("SDQ_HOME", str(Path.home() / "minicpm47-epm"))
|
| 47 |
+
))
|
| 48 |
+
|
| 49 |
+
# 從 HF 拉回來的檔案(給 fetch_code 用)
|
| 50 |
+
# 從 HF 拉回來的檔案(給 fetch_code 用)。
|
| 51 |
+
# tools/*.c 也要拉:tools/strict8g.py 會在執行時把它編譯成 LD_PRELOAD helper。
|
| 52 |
+
CODE_PATTERNS = [
|
| 53 |
+
"*.py", "*.md", "*.c",
|
| 54 |
+
"patches/*", "cpp/*", "docs/*", "tools/*", "sdq/*.py",
|
| 55 |
+
"bench/expert_index.json",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def log(msg: str) -> None:
|
| 60 |
+
print(f"[demo] {msg}", flush=True)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def run(cmd: list, **kw) -> subprocess.CompletedProcess:
|
| 64 |
+
log("$ " + " ".join(str(c) for c in cmd))
|
| 65 |
+
return subprocess.run([str(c) for c in cmd], **kw)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def human(n: float) -> str:
|
| 69 |
+
for u in ("B", "KiB", "MiB", "GiB", "TiB"):
|
| 70 |
+
if abs(n) < 1024:
|
| 71 |
+
return f"{n:.2f} {u}"
|
| 72 |
+
n /= 1024
|
| 73 |
+
return f"{n:.1f} PiB"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def cfg(name: str, fallback):
|
| 77 |
+
return getattr(C, name, fallback) if C else fallback
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def reload_config(code_dir: Path) -> None:
|
| 81 |
+
"""code 是從 HF 下載到別的目錄時,改用那份 code 裡的 sdq/config.py。"""
|
| 82 |
+
global C
|
| 83 |
+
if str(code_dir) not in sys.path:
|
| 84 |
+
sys.path.insert(0, str(code_dir))
|
| 85 |
+
for mod in [m for m in list(sys.modules) if m == "sdq" or m.startswith("sdq.")]:
|
| 86 |
+
del sys.modules[mod]
|
| 87 |
+
try:
|
| 88 |
+
from sdq import config as new_c
|
| 89 |
+
C = new_c
|
| 90 |
+
except Exception:
|
| 91 |
+
pass
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# ---------------------------------------------------------------- 1) 取得 code
|
| 95 |
+
def fetch_code(dest: Path, force: bool = False) -> Path:
|
| 96 |
+
"""如果目前就在本專案的原始碼裡,直接回傳;否則從 HF 下載。"""
|
| 97 |
+
if (dest / "cpp" / "sdq_pager.cpp").exists() and (dest / cfg("PATCH_FILE", "")).exists() \
|
| 98 |
+
and not force:
|
| 99 |
+
log(f"已經在 EPM 專案原始碼中:{dest}")
|
| 100 |
+
return dest
|
| 101 |
+
log(f"從 Hugging Face 下載專案 code:{cfg('HF_REPO', 'HelloSun/minicpm47woek')}")
|
| 102 |
+
from huggingface_hub import snapshot_download
|
| 103 |
+
|
| 104 |
+
dest.mkdir(parents=True, exist_ok=True)
|
| 105 |
+
snapshot_download(
|
| 106 |
+
repo_id=cfg("HF_REPO", "HelloSun/minicpm47woek"),
|
| 107 |
+
repo_type="model",
|
| 108 |
+
local_dir=dest,
|
| 109 |
+
allow_patterns=CODE_PATTERNS,
|
| 110 |
+
token=os.environ.get("HF_TOKEN"),
|
| 111 |
+
)
|
| 112 |
+
if not (dest / cfg("PATCH_FILE", "")).exists():
|
| 113 |
+
raise SystemExit(
|
| 114 |
+
f"下載完成但缺少 {cfg('PATCH_FILE', '')};請確認 HF 上的 code 完整,"
|
| 115 |
+
"或直接在專案原始碼目錄執行 demo.py"
|
| 116 |
+
)
|
| 117 |
+
log(f"code 已下載到 {dest}")
|
| 118 |
+
return dest
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------------------------------------------------------------- 2) 模型
|
| 122 |
+
def fetch_model(models_dir: Path, force: bool = False) -> Path:
|
| 123 |
+
repo = cfg("MODEL_REPO", "openbmb/MiniCPM-V-4.7-35B-A3B-gguf")
|
| 124 |
+
name = cfg("MODEL_FILE", "MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf")
|
| 125 |
+
size = cfg("MODEL_SIZE", 21_166_255_008)
|
| 126 |
+
path = models_dir / name
|
| 127 |
+
if path.exists() and not force and path.stat().st_size == size:
|
| 128 |
+
log(f"模型已存在:{path}({human(path.stat().st_size)})")
|
| 129 |
+
return path
|
| 130 |
+
|
| 131 |
+
log(f"下載模型 {repo}/{name}({human(size)},可續傳)")
|
| 132 |
+
log(" 需要約 22 GB 磁碟空間;下載到一半再執行本程式會從斷點繼續")
|
| 133 |
+
from huggingface_hub import hf_hub_download
|
| 134 |
+
|
| 135 |
+
models_dir.mkdir(parents=True, exist_ok=True)
|
| 136 |
+
p = hf_hub_download(repo, name, local_dir=str(models_dir),
|
| 137 |
+
token=os.environ.get("HF_TOKEN"))
|
| 138 |
+
got = Path(p)
|
| 139 |
+
if got.stat().st_size != size:
|
| 140 |
+
log(f"⚠️ 檔案大小 {got.stat().st_size} 與預期 {size} 不同,請再跑一次本程式續傳")
|
| 141 |
+
log(f"模型完成:{got}")
|
| 142 |
+
return got
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# ---------------------------------------------------------------- 3) 原始碼 + 編譯
|
| 146 |
+
def fetch_source(src_dir: Path, force: bool = False) -> Path:
|
| 147 |
+
"""取得固定 commit 的 llama.cpp。已存在且 commit 正確就跳過。"""
|
| 148 |
+
commit = cfg("LLAMA_COMMIT", "836d57176dc699a726c55418e4f96b8ca628e1bf")
|
| 149 |
+
repo = cfg("LLAMA_REPO", "https://github.com/ggml-org/llama.cpp.git")
|
| 150 |
+
src_dir.mkdir(parents=True, exist_ok=True)
|
| 151 |
+
|
| 152 |
+
if (src_dir / ".git").exists() and not force:
|
| 153 |
+
cur = subprocess.run(["git", "rev-parse", "HEAD"], cwd=src_dir,
|
| 154 |
+
capture_output=True, text=True).stdout.strip()
|
| 155 |
+
if cur == commit:
|
| 156 |
+
log(f"llama.cpp 原始碼已存在且版本正確:{src_dir}")
|
| 157 |
+
return src_dir
|
| 158 |
+
log(f"版本不符(目前 {cur[:10]}),重新取得 {commit[:10]}")
|
| 159 |
+
shutil.rmtree(src_dir / ".git")
|
| 160 |
+
shutil.rmtree(src_dir / "src", ignore_errors=True)
|
| 161 |
+
|
| 162 |
+
if not (src_dir / ".git").exists():
|
| 163 |
+
log(f"clone llama.cpp(固定 commit {commit[:10]})")
|
| 164 |
+
tmp = src_dir.parent / (src_dir.name + ".tmp")
|
| 165 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 166 |
+
run(["git", "clone", "--filter=blob:none", "--no-checkout", repo, tmp],
|
| 167 |
+
check=True)
|
| 168 |
+
# GitHub 允許依 commit 直接 fetch,失敗就退回淺層 clone 再 checkout
|
| 169 |
+
r = subprocess.run(["git", "fetch", "-q", "--depth", "1", "origin", commit],
|
| 170 |
+
cwd=tmp, capture_output=True, text=True)
|
| 171 |
+
if r.returncode != 0:
|
| 172 |
+
log("依 commit 直接 fetch 失敗,改用完整 clone")
|
| 173 |
+
run(["git", "fetch", "-q", "--tags", "origin"], cwd=tmp, check=True)
|
| 174 |
+
run(["git", "fetch", "-q", "origin"], cwd=tmp, check=True)
|
| 175 |
+
run(["git", "checkout", "-q", commit], cwd=tmp, check=True)
|
| 176 |
+
shutil.rmtree(src_dir, ignore_errors=True)
|
| 177 |
+
tmp.rename(src_dir)
|
| 178 |
+
return src_dir
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def apply_patch(src_dir: Path, code_dir: Path = None) -> None:
|
| 182 |
+
"""把 SDQ 的 expert pager 引擎套進 llama.cpp 的 src/。
|
| 183 |
+
|
| 184 |
+
`code_dir` 是本專案 code 所在的位置(可能不是 demo.py 旁邊,例如 code 是從 HF
|
| 185 |
+
下載到 <home>/code 的情況)。
|
| 186 |
+
"""
|
| 187 |
+
rel = cfg("PATCH_FILE", "patches/0001-sdq-expert-pager.patch")
|
| 188 |
+
patch = (code_dir or HERE) / rel
|
| 189 |
+
target = src_dir / cfg("PATCH_TARGET_DIR", "src")
|
| 190 |
+
marker = src_dir / ".sdq_patched"
|
| 191 |
+
|
| 192 |
+
if marker.exists() and all((target / f).exists()
|
| 193 |
+
for f in cfg("SDQ_SOURCE_FILES", [])):
|
| 194 |
+
log("SDQ patch 已套用過")
|
| 195 |
+
return
|
| 196 |
+
if not patch.exists():
|
| 197 |
+
raise SystemExit(f"找不到 patch:{patch}")
|
| 198 |
+
if not all((target / f).exists() for f in cfg("SDQ_SOURCE_FILES", [])):
|
| 199 |
+
log("套用 SDQ expert pager patch(引擎檔案會進到 llama.cpp/src/)")
|
| 200 |
+
r = subprocess.run(["git", "apply", "--check", str(patch)], cwd=src_dir,
|
| 201 |
+
capture_output=True, text=True)
|
| 202 |
+
if r.returncode != 0:
|
| 203 |
+
# patch 已經在樹裡(常見於換機/換目錄重建)
|
| 204 |
+
if all((target / f).exists() for f in cfg("SDQ_SOURCE_FILES", [])):
|
| 205 |
+
log("patch 已在樹中(略過重複套用)")
|
| 206 |
+
marker.touch()
|
| 207 |
+
return
|
| 208 |
+
raise SystemExit(f"patch 套用失敗:{r.stderr.strip()}")
|
| 209 |
+
subprocess.run(["git", "apply", str(patch)], cwd=src_dir, check=True)
|
| 210 |
+
# Keep the checked-in pager implementation in sync with the patch's
|
| 211 |
+
# original source payload. This also lets incremental rebuilds consume
|
| 212 |
+
# optional backends added after the historical patch was generated.
|
| 213 |
+
for name in cfg("SDQ_SOURCE_FILES", []):
|
| 214 |
+
source = (code_dir or HERE) / "cpp" / name
|
| 215 |
+
target_file = target / name
|
| 216 |
+
if source.exists() and target_file.exists():
|
| 217 |
+
shutil.copy2(source, target_file)
|
| 218 |
+
marker.touch()
|
| 219 |
+
log("patch 套用完成")
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def build(src_dir: Path, jobs: int, force: bool = False) -> Path:
|
| 223 |
+
target = cfg("BUILD_TARGET", "epq-chat")
|
| 224 |
+
binary = src_dir / "build" / "bin" / target
|
| 225 |
+
if binary.exists() and not force:
|
| 226 |
+
log(f"已經編譯過:{binary}")
|
| 227 |
+
return binary
|
| 228 |
+
|
| 229 |
+
build_dir = src_dir / "build"
|
| 230 |
+
log("設定 cmake(CPU 最佳化:AVX2/AVX512 由 GGML_NATIVE 自動開啟)")
|
| 231 |
+
run(["cmake", "-B", str(build_dir), "-S", str(src_dir),
|
| 232 |
+
"-DCMAKE_BUILD_TYPE=Release", "-DGGML_NATIVE=ON",
|
| 233 |
+
"-DLLAMA_CURL=OFF", "-DLLAMA_BUILD_TESTS=OFF",
|
| 234 |
+
"-DLLAMA_BUILD_EXAMPLES=OFF", "-DLLAMA_BUILD_SERVER=OFF"],
|
| 235 |
+
check=True)
|
| 236 |
+
log(f"編譯中(-j{jobs}),參考實測約 2-5 分鐘")
|
| 237 |
+
t0 = time.time()
|
| 238 |
+
run(["cmake", "--build", str(build_dir), "-j", str(jobs), "--target", target],
|
| 239 |
+
check=True)
|
| 240 |
+
log(f"編譯完成({time.time() - t0:.0f}s):{binary}")
|
| 241 |
+
return binary
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def reserve_mb(ram_mb) -> int:
|
| 245 |
+
"""arena 之外要預留多少。
|
| 246 |
+
|
| 247 |
+
預留量必須涵蓋 arena sizing 之後才長出來的記憶體:非專家權重第一次 forward
|
| 248 |
+
才 fault in、CPU compute buffer、KV cache、圖與執行緒堆疊。這是逐模型、
|
| 249 |
+
逐 ctx/ubatch 校正出來的量,這裡用一條比例規則讓不同 RAM 預算都有合理餘量。
|
| 250 |
+
"""
|
| 251 |
+
ram = int(ram_mb)
|
| 252 |
+
if C is not None and getattr(C, "RESERVE_MB", None) and ram == int(C.RAM_BUDGET_MB):
|
| 253 |
+
return int(C.RESERVE_MB)
|
| 254 |
+
return max(128, min(1024, ram // 16))
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# ---------------------------------------------------------------- 4) 執行
|
| 258 |
+
def chat(binary: Path, model: Path, args) -> int:
|
| 259 |
+
cmd = [
|
| 260 |
+
binary, "-m", model,
|
| 261 |
+
"-c", args.ctx, "-t", args.threads, "-n", args.n_predict,
|
| 262 |
+
"--ram-budget-mb", args.ram_mb,
|
| 263 |
+
]
|
| 264 |
+
if args.arena_mb:
|
| 265 |
+
cmd += ["--arena-mb", args.arena_mb]
|
| 266 |
+
if args.no_prefetch:
|
| 267 |
+
cmd += ["--no-prefetch"]
|
| 268 |
+
if args.stats:
|
| 269 |
+
cmd += ["--stats-file", args.stats]
|
| 270 |
+
# SDQ_RESERVE_MB 是逐模型、逐 ctx/ubatch 校正出來的預留量,預設沒有設定。
|
| 271 |
+
# 這裡依 config 的比例規則帶入,否則使用者拿到的是另一個模型的校正值。
|
| 272 |
+
reserve = reserve_mb(args.ram_mb)
|
| 273 |
+
env = dict(os.environ, SDQ_RESERVE_MB=str(reserve))
|
| 274 |
+
log(f"啟動 epq-chat(RAM 預算 {args.ram_mb} MiB,reserve {reserve} MiB;"
|
| 275 |
+
f"改 ctx 或 ubatch 必須重新校正,見 docs/DESIGN.md §3)")
|
| 276 |
+
return subprocess.run([str(c) for c in cmd], env=env).returncode
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def main() -> int:
|
| 280 |
+
ap = argparse.ArgumentParser(
|
| 281 |
+
description="MiniCPM-V-4.7-35B-A3B EPM CPU inference: download → build → chat",
|
| 282 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 283 |
+
epilog="""常用範例:
|
| 284 |
+
python demo.py # 完整流程,然後進入聊天
|
| 285 |
+
python demo.py --bench # 不聊天,直接跑基準測量(量 RSS / SSD I/O / 命中率 / tok/s)
|
| 286 |
+
python demo.py --verify # 驗證分頁路徑的數值正確性(A/B + NumPy)
|
| 287 |
+
python demo.py --verify --fast # 只跑 8 GiB 內做得完的層(略過端到端 A/B)
|
| 288 |
+
python demo.py --once # 不聊天,單跑一句示範(適合 CI / 腳本)
|
| 289 |
+
python demo.py --model-path /x.gguf # 模型已經在自己電腦上
|
| 290 |
+
python demo.py --rebuild # 強制重新編譯 llama.cpp
|
| 291 |
+
""")
|
| 292 |
+
ap.add_argument("--home", default=str(DEFAULT_ROOT), help="工作目錄(預設 ~/minicpm47-epm)")
|
| 293 |
+
ap.add_argument("--src", help="llama.cpp 原始碼目錄(預設 <home>/llama.cpp)")
|
| 294 |
+
ap.add_argument("--model-path", help="已下載好的模型路徑")
|
| 295 |
+
ap.add_argument("--ctx", default=str(cfg("CTX_VERIFY", 4096)),
|
| 296 |
+
help="上下文長度(模型原生 262144,這裡實測 4K)")
|
| 297 |
+
ap.add_argument("--threads", type=int, default=0, help="CPU 執行緒(0 = 自動)")
|
| 298 |
+
ap.add_argument("--n-predict", type=int, default=cfg("N_PREDICT", 256),
|
| 299 |
+
help="每次回答產生多少 token")
|
| 300 |
+
ap.add_argument("--ram-mb", default=str(cfg("RAM_BUDGET_MB", 8192)),
|
| 301 |
+
help="SDQ pager sizing budget in MiB (not a total RSS/swap limit)")
|
| 302 |
+
ap.add_argument("--arena-mb", type=int, default=cfg("ARENA_MB", 0),
|
| 303 |
+
help="固定 arena 大小(0 = 由 RAM 預算推算)")
|
| 304 |
+
ap.add_argument("--jobs", type=int, default=max(1, (os.cpu_count() or 4)), help="編譯並行度")
|
| 305 |
+
ap.add_argument("--no-prefetch", action="store_true", help="關閉背景預取")
|
| 306 |
+
ap.add_argument("--skip-code", action="store_true", help="不要抓 HF 上的 code")
|
| 307 |
+
ap.add_argument("--skip-model", action="store_true", help="不要下載模型")
|
| 308 |
+
ap.add_argument("--skip-build", action="store_true", help="不要編譯")
|
| 309 |
+
ap.add_argument("--rebuild", action="store_true", help="強制重新編譯")
|
| 310 |
+
ap.add_argument("--bench", action="store_true", help="跑完就做基準測量,不進聊天")
|
| 311 |
+
ap.add_argument("--verify", action="store_true", help="做正確性驗證(A/B 對照 + NumPy 逐層比對)")
|
| 312 |
+
ap.add_argument("--fast", action="store_true",
|
| 313 |
+
help="--verify 時只跑 8 GiB 內做得完的層:略過端到端 A/B"
|
| 314 |
+
"(那一層需要整個 22 GiB 模型常駐記憶體)")
|
| 315 |
+
ap.add_argument("--once", action="store_true", help="不聊天,單跑一句示範就結束")
|
| 316 |
+
ap.add_argument("--stats", default="", help="把指標寫到指定 JSONL")
|
| 317 |
+
args = ap.parse_args()
|
| 318 |
+
|
| 319 |
+
home = Path(args.home).expanduser().resolve()
|
| 320 |
+
home.mkdir(parents=True, exist_ok=True)
|
| 321 |
+
|
| 322 |
+
# --- 1) code:已在專案原始碼裡就直接用,否則從 HF 抓
|
| 323 |
+
in_source_tree = (HERE / "cpp" / "sdq_pager.cpp").exists()
|
| 324 |
+
if in_source_tree and not args.skip_code:
|
| 325 |
+
code_dir = HERE
|
| 326 |
+
log(f"code:使用目前的專案原始碼 {code_dir}")
|
| 327 |
+
elif args.skip_code:
|
| 328 |
+
code_dir = HERE
|
| 329 |
+
else:
|
| 330 |
+
code_dir = fetch_code(home / "code")
|
| 331 |
+
|
| 332 |
+
log(f"工作目錄:{home}")
|
| 333 |
+
log(f"專案 code:{code_dir}")
|
| 334 |
+
reload_config(code_dir)
|
| 335 |
+
|
| 336 |
+
# --- 2) 模型
|
| 337 |
+
model = Path(args.model_path) if args.model_path else (home / "models" / cfg("MODEL_FILE", ""))
|
| 338 |
+
if not args.skip_model and not args.model_path:
|
| 339 |
+
model = fetch_model(model.parent)
|
| 340 |
+
|
| 341 |
+
# --- 3) 原始碼 + patch + 編譯
|
| 342 |
+
src_dir = Path(args.src) if args.src else (home / "llama.cpp")
|
| 343 |
+
target = cfg("BUILD_TARGET", "epq-chat")
|
| 344 |
+
if args.skip_build:
|
| 345 |
+
binary = src_dir / "build" / "bin" / target
|
| 346 |
+
else:
|
| 347 |
+
fetch_source(src_dir, force=args.rebuild)
|
| 348 |
+
apply_patch(src_dir, code_dir)
|
| 349 |
+
binary = build(src_dir, args.jobs, force=args.rebuild)
|
| 350 |
+
|
| 351 |
+
if not binary.exists():
|
| 352 |
+
raise SystemExit(f"找不到執行檔:{binary}")
|
| 353 |
+
|
| 354 |
+
sys.path.insert(0, str(code_dir))
|
| 355 |
+
|
| 356 |
+
# --- 4a) 正確性驗證
|
| 357 |
+
if args.verify:
|
| 358 |
+
from sdq.verify import run_verification
|
| 359 |
+
if args.fast:
|
| 360 |
+
log("--fast:只跑 kernel / 整層 / 預取壓力;跳過端到端 A/B"
|
| 361 |
+
"(那一層需要整個 22 GiB 模型常駐 RAM,8 GiB 機器上不可能)")
|
| 362 |
+
return run_verification(binary, model, code_dir, threads=args.threads or 8,
|
| 363 |
+
fast=args.fast)
|
| 364 |
+
|
| 365 |
+
# --- 4b) 基準測量
|
| 366 |
+
if args.bench:
|
| 367 |
+
from sdq.bench import main as bench_main
|
| 368 |
+
sys.argv = ["sdq.bench", "--model", str(model), "--bin", str(binary),
|
| 369 |
+
"--ram-mb", args.ram_mb, "--ctx", args.ctx,
|
| 370 |
+
"--threads", str(args.threads or 8), "--label", "demo"]
|
| 371 |
+
return bench_main()
|
| 372 |
+
|
| 373 |
+
# --- 4c) 非互動模式(CI / 腳本)
|
| 374 |
+
if args.once or not sys.stdin.isatty():
|
| 375 |
+
if not args.once:
|
| 376 |
+
log("沒有互動式終端,改用預設提示詞跑一次示範(加 --once 可明確指定)")
|
| 377 |
+
cmd = [binary, "-m", model, "-c", args.ctx, "-t", args.threads,
|
| 378 |
+
"--ram-budget-mb", args.ram_mb, "--no-chat", "--no-interactive",
|
| 379 |
+
"--temp", "0", "-p", "請用一句話介紹你自己。", "-n", "64"]
|
| 380 |
+
reserve = reserve_mb(args.ram_mb)
|
| 381 |
+
log(f"單次示範:RAM 預算 {args.ram_mb} MiB,reserve {reserve} MiB"
|
| 382 |
+
f"(改 ctx 或 ubatch 必須重新校正,見 docs/DESIGN.md §3)")
|
| 383 |
+
log("注意:這個示範不受 strict8g 管治,實際 RSS 可能略高於 RAM 預算;"
|
| 384 |
+
"受管治的量測請用 tools/strict8g.py")
|
| 385 |
+
return subprocess.run([str(c) for c in cmd],
|
| 386 |
+
env=dict(os.environ, SDQ_RESERVE_MB=str(reserve))).returncode
|
| 387 |
+
|
| 388 |
+
# --- 4d) 互動式聊天
|
| 389 |
+
log("")
|
| 390 |
+
log("=" * 68)
|
| 391 |
+
log("開始聊天!模型 19.71 GB 只住在 SSD,RAM 只放有限容量的 expert 快取。")
|
| 392 |
+
log("指令:/exit 離開 /reset 重設對話 /stats 看分頁指標(命中率 / SSD 讀取量)")
|
| 393 |
+
log("=" * 68)
|
| 394 |
+
return chat(binary, model, args)
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
if __name__ == "__main__":
|
| 398 |
+
sys.exit(main())
|
docs/CORRECTNESS.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 正確性驗證
|
| 2 |
+
|
| 3 |
+
分頁路徑的每一層都要能被獨立證明是對的。這份文件記錄三層驗證、決定性證據,
|
| 4 |
+
以及一個在這個模型上才會出現的數值現象(§4)。
|
| 5 |
+
|
| 6 |
+
三層的分工:
|
| 7 |
+
|
| 8 |
+
| 層 | 證明什麼 | 對照組 |
|
| 9 |
+
| --- | --- | --- |
|
| 10 |
+
| 1 kernel | 單一 expert 的前向(Q8_K 量化 → 三次量化點積 → SiLU → 加權) | NumPy 純重算,照抄 ggml 的 dequantize 與 quantize 定義 |
|
| 11 |
+
| 2 整層 | 多 token、真實路由、權重縮放、槽位配置、預取路徑 | NumPy 用模型自己產生的 `cur / selected_experts / weights` 重算 |
|
| 12 |
+
| 3 端到端 | 圖層面接線、跨層一致性 | llama.cpp 原生 `mul_mat_id`(**在 8 GiB 上不可能執行**,見 §3.3) |
|
| 13 |
+
|
| 14 |
+
執行方式:
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
python3 -m sdq.verify --model <gguf> --bin <epq-chat> --fast
|
| 18 |
+
python3 tools/determinism_check.py --model <gguf> --bin <epq-chat> --runs 5 -n 48
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1. kernel 層級(單一 expert)
|
| 24 |
+
|
| 25 |
+
四組 (layer, expert),用 `--selftest` 讓 pager 從 arena 算出結果,Python 從
|
| 26 |
+
**同一段 GGUF 位元組**獨立重算:
|
| 27 |
+
|
| 28 |
+
| 層 | Expert | 結果 | max abs diff |
|
| 29 |
+
| --- | ---: | --- | ---: |
|
| 30 |
+
| 0 | 3 | PASS | 3.7e-09 |
|
| 31 |
+
| 17 | 200 | PASS | — |
|
| 32 |
+
| 33 | 7 | PASS | — |
|
| 33 |
+
| 39 | 100 | PASS | — |
|
| 34 |
+
|
| 35 |
+
**為什麼是這四組**:這個模型有**兩種 expert 大小**。
|
| 36 |
+
|
| 37 |
+
* 20 層的 `ffn_down_exps` 是 Q6_K → 每個 expert 2,039,808 B
|
| 38 |
+
* 20 層的 `ffn_down_exps` 是 Q4_K → 每個 expert 1,769,472 B
|
| 39 |
+
|
| 40 |
+
L39 屬於前者,是唯一會走到「大槽位」配置路徑的測試;少了它,槽位配置器把
|
| 41 |
+
小槽位發給大 expert 的錯誤不會被發現。L17 / L33 屬於後者。
|
| 42 |
+
|
| 43 |
+
`tools/moe_ref.py` 對 Q4_K / Q5_K / Q6_K 三種 block 格式各自實作反量化,
|
| 44 |
+
其中 Q6_K 的實作另外與 ggml 的 `dequantize_row_q6_K` **逐位元組比對過**
|
| 45 |
+
(4096 個 block,最大差 0)。
|
| 46 |
+
|
| 47 |
+
## 2. 整層層級(真實路由輸入)
|
| 48 |
+
|
| 49 |
+
用 `SDQ_MOE_DUMP` 導出某一層的真實輸入(21 個 token,中文提示詞)與輸出:
|
| 50 |
+
|
| 51 |
+
| 層 | max abs diff | max rel | 1-LSB 翻轉上界 | 判定 |
|
| 52 |
+
| --- | ---: | ---: | ---: | --- |
|
| 53 |
+
| 0 | 1.12e-08 | 5.1e-07 | 1.73e-03 | PASS |
|
| 54 |
+
| 39 | 8.82e-05 | 7.7e-03 | 3.22e-02 | PASS(量化邊界,見 §4) |
|
| 55 |
+
|
| 56 |
+
預取壓力測試(預取開著 + 極小 arena 強制回收,兩種槽位配置各跑一次)4 組全 PASS。
|
| 57 |
+
|
| 58 |
+
## 3. 端到端與決定性
|
| 59 |
+
|
| 60 |
+
### 3.3 為什麼沒有端到端 A/B
|
| 61 |
+
|
| 62 |
+
原生對照組需要把整個 21.17 GB 模型常駐 RAM。在 8 GiB 上這**在定義上不可能**,
|
| 63 |
+
而這正是本專案存在的理由。實測未修改的 llama.cpp 在 8 GiB 上連模型都載不進去:
|
| 64 |
+
|
| 65 |
+
```
|
| 66 |
+
ggml_backend_buft_alloc_buffer_n_default: failed to allocate CPU_REPACK buffer of size 15099494400
|
| 67 |
+
llama_model_load: error loading model: unable to allocate CPU_REPACK buffer
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
所以等價性改由 kernel / 整層 / 預取壓力三層,加上下面的決定性證據支撐。
|
| 71 |
+
|
| 72 |
+
### 3.1 決定性(決定性 = 同條件重複執行,逐位元組一致)
|
| 73 |
+
|
| 74 |
+
`tools/determinism_check.py`,5 次執行 × 48 token × 248,144 vocab:
|
| 75 |
+
|
| 76 |
+
```
|
| 77 |
+
run0: 14.1s run1: 14.1s run2: 14.4s run3: 14.3s run4: 15.6s
|
| 78 |
+
✅ run1..run4 與 run0 全部逐位元組完全相同
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
分頁路徑在同一台機器、同一組條件下是**決定性**的。快取命中與否都包含在這個
|
| 82 |
+
證據裡:5 次執行的 arena 內容不同(命中模式隨存取順序變動),輸出仍然逐位元組相同。
|
| 83 |
+
|
| 84 |
+
### 3.2 過程中修掉的問題
|
| 85 |
+
|
| 86 |
+
1. **GGUF 標頭解析在 BOOL 陣列上錯位。** 這個模型有
|
| 87 |
+
`qwen35moe.attention.recurrent_layers`(40 個元素的 **BOOL 陣列**)。參考實作
|
| 88 |
+
把陣列元素型別 7 讀成 4 bytes,標頭解析從那裡起全部錯位,最後以
|
| 89 |
+
`basic_string::_M_create`(讀到垃圾長度)失敗。修正:BOOL 陣列元素讀 1 byte,
|
| 90 |
+
並且讓未知的元素型別**丟出例外**而不是靜默跳過 —— 靜默跳過會讓 `r.o` 停在
|
| 91 |
+
值的中間,之後每一個 key 都是錯的。
|
| 92 |
+
這個 key 是混合架構(SSM 層)才有的,參考的 Qwen3.6-35B-A3B 沒有。
|
| 93 |
+
|
| 94 |
+
## 4. 一個 Q8_K 量化邊界造成的 7.9e-4 差異
|
| 95 |
+
|
| 96 |
+
L39 / 某個 token 上,整層比對差 8.82e-05,超過原本 3e-3 的相對容差門檻
|
| 97 |
+
(該元素 |out| 只有 0.0186,所以相對誤差 7.7e-3)。逐層追查的結果:
|
| 98 |
+
|
| 99 |
+
**這不是 pager 的錯誤,是驗證工具的浮點參考不夠準。** 完整證據鏈:
|
| 100 |
+
|
| 101 |
+
1. **Q8_K 輸入量化逐位元組一致。** 取出該 token 的 activation,用 ggml 自己的
|
| 102 |
+
`quantize_row_q8_K_ref` 量化,與 Python 端的 481 個 block 總和
|
| 103 |
+
(`bsums`)**全部一致**。
|
| 104 |
+
2. **權重反量化逐位元組一致。** Q6_K 的 `dequantize_row_q6_K` 在 4096 個 block 上
|
| 105 |
+
與 NumPy 實作最大差 0。
|
| 106 |
+
3. **差異只來自 Q8_K 的 1 個 LSB。** 把 C++ 端算出的 SiLU 中間值 `h` 讀回來餵給
|
| 107 |
+
NumPy 參考:481 個非零 Q8_K code 中**剛好 1 個差 1**,輸出立刻從 7.86e-04
|
| 108 |
+
掉到 **6.7e-08**。
|
| 109 |
+
4. **上游 `go` 的��異來自 ggml 的 float32 累加。** ggml 的 `vec_dot` 在 float32
|
| 110 |
+
裡累加,本專案的 NumPy 參考用 float64。C++ 端 `gate` / `up` 與 NumPy 最多差
|
| 111 |
+
9.5e-07 —— 這是 ggml 側的捨入,不是 NumPy 的。
|
| 112 |
+
5. **把 `go` 人為擾動 1e-7(相對)就能重現整個差異**,而且只有那一個 token 受影響,
|
| 113 |
+
同一層其他 5 個 token 仍在 1e-07。
|
| 114 |
+
|
| 115 |
+
也就是說:`h` 上 1e-6 的差 → Q8_K 四捨五入跨界 1 格 → 輸出跳 7.9e-4。
|
| 116 |
+
**單一 LSB 翻轉的理論上界是 3.22e-02,實測 8.82e-05,差 365 倍。**
|
| 117 |
+
|
| 118 |
+
因此 `tools/moe_layer_check.py` 的判定改成兩段式:先照舊用固定容差比對,
|
| 119 |
+
若不通過則檢查實測差異是否落在「單一 Q8_K LSB 翻轉」的理論上界之內,
|
| 120 |
+
並在輸出裡明確標成 `PASS (quantization-boundary)` —— 而不是把容差默默放寬。
|
| 121 |
+
|
| 122 |
+
**這個現象是模型性質,不是這個 repo 的性質。** 參考的 Qwen3.6-35B-A3B 在同樣的
|
| 123 |
+
檢查下是 8.11e-06(PASS),因為它沒有碰到這種條件數的輸入。
|
docs/DESIGN.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 設計:記憶體帳目、SSD→RAM→CPU 資料流
|
| 2 |
+
|
| 3 |
+
這個模型在 8 GiB 裡能跑,不是因為它小,而是因為**19.71 GiB 的權重裡有 18.16 GiB
|
| 4 |
+
從來不需要同時在 RAM 裡**。
|
| 5 |
+
|
| 6 |
+
## 1. 權重帳目
|
| 7 |
+
|
| 8 |
+
`MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf`,21,166,255,008 B = **19.71 GiB**,
|
| 9 |
+
架構 `qwen35moe`,40 個 block。
|
| 10 |
+
|
| 11 |
+
| 項目 | 大小 |
|
| 12 |
+
| --- | ---: |
|
| 13 |
+
| 路由專家權重(40 層 × 256 個 × 3 個投影) | **18.16 GiB** |
|
| 14 |
+
| 非專家常駐權重(embedding / lm_head / 注意力 / SSM / shared expert) | 1.54 GiB |
|
| 15 |
+
| 單一 token 的專家工作集(40 層 × top-8) | 14.53 MiB 平均 / 15.56 MiB 最大 |
|
| 16 |
+
|
| 17 |
+
單一 token 在解碼時需要 320 個 layer-expert,但因為槽位分兩種尺寸,正確的說法是
|
| 18 |
+
**160 個大槽位 + 160 個小槽位**。8 GiB 的 arena 塞得下 3714 個槽位,其中約四成是
|
| 19 |
+
大槽位(20/40 的層用大槽位),所以命中率可以到 70% 以上;2 GiB 的 arena 只有
|
| 20 |
+
332 個槽位湊不出這個組合,命中率崩到 22.7%(見 README 的 budget 曲線)。
|
| 21 |
+
|
| 22 |
+
### 兩種 expert 大小
|
| 23 |
+
|
| 24 |
+
這個模型的 `ffn_down_exps` 量化型別**逐層不同**:
|
| 25 |
+
|
| 26 |
+
| 層 | `ffn_down_exps` | 每個 expert 位元組 |
|
| 27 |
+
| --- | --- | ---: |
|
| 28 |
+
| 20 層 | Q6_K | **2,039,808 B** |
|
| 29 |
+
| 20 層 | Q4_K | **1,769,472 B** |
|
| 30 |
+
|
| 31 |
+
`ffn_gate_exps` 與 `ffn_up_exps` 全部是 Q4_K,每個 589,824 B。
|
| 32 |
+
|
| 33 |
+
平均下來每個 layer-expert 是 1.95 MiB,因此 arena 的槽位分成兩種尺寸:
|
| 34 |
+
**小 1.69 MiB × 2420 + 大 1.95 MiB × 806**(8 GiB 預算下)。
|
| 35 |
+
|
| 36 |
+
這一點是這個模型相對於參考實作最重要的結構差異:參考的 Qwen3.6-35B-A3B 是
|
| 37 |
+
37 層 1,900,544 B + 3 層 2,039,808 B,槽位比例完全不同。**槽位尺寸必須從
|
| 38 |
+
模型 metadata 推導,不能寫死。**
|
| 39 |
+
|
| 40 |
+
### 混合架構
|
| 41 |
+
|
| 42 |
+
`qwen35moe.block_count = 40`,但只有 10 層是全注意力
|
| 43 |
+
(`full_attention_interval = 4`),另外 30 層是 SSM / Mamba 層
|
| 44 |
+
(`ssm.*` 張量,`recurrent_layers` 40 個元素全為 1)。
|
| 45 |
+
|
| 46 |
+
這影響兩件事:
|
| 47 |
+
|
| 48 |
+
1. **正確性**:模型 metadata 裡因此多了一個 BOOL 陣列,參考實作的 GGUF 標頭
|
| 49 |
+
解析器會在這裡錯位(見 `CORRECTNESS.md` §3.2)。
|
| 50 |
+
2. **效能**:SSM 層的計算可以平行,不像注意力那樣受限於序列維度,所以
|
| 51 |
+
**執行緒數的結論與參考實作相反**(見 `NEGATIVE_RESULTS.md`)。
|
| 52 |
+
|
| 53 |
+
## 2. 資料流
|
| 54 |
+
|
| 55 |
+
```
|
| 56 |
+
GGUF 檔(19.71 GiB)
|
| 57 |
+
│
|
| 58 |
+
│ 非專家權重:mmap,乾淨的 file-backed 頁
|
| 59 |
+
│ 路由專家權重:自訂 buffer type,只回報 4 KB,
|
| 60 |
+
│ 真正的內容留在檔案裡(pager 用 pread 管理)
|
| 61 |
+
▼
|
| 62 |
+
SSD → RAM:pager 在需求觸發時把選中的 expert 讀進 arena 槽位
|
| 63 |
+
│
|
| 64 |
+
▼
|
| 65 |
+
RAM → CPU:ggml 自己的量化點積(ggml_vec_dot_q4_K_q8_K / q6_K_q8_K)
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
路由本身(logits / softmax / top-k / 權重正規化)仍然由 ggml 算完,
|
| 69 |
+
`build_moe_ffn` 只在「把被選到的 8 個 expert 算出來並加權求和」這一段
|
| 70 |
+
交給 `sdq_build_moe`。
|
| 71 |
+
|
| 72 |
+
## 3. 實作的關鍵決定
|
| 73 |
+
|
| 74 |
+
### 三段並行讀取
|
| 75 |
+
|
| 76 |
+
一個 expert 的 gate / up / down 在檔案上互不相鄰。循序讀是 3 × 延遲的鏈,
|
| 77 |
+
改成三段同時讀之後,一層 8 個全 miss 的 expert 明顯變快。
|
| 78 |
+
|
| 79 |
+
### 需求觸發的 LRU 淘汰 + pin
|
| 80 |
+
|
| 81 |
+
`acquire()` 是「先讀完再 publish」。`reserve_slot()` 會在資料還沒讀進來之前
|
| 82 |
+
就把 (layer, expert) 記進 page 表,一旦 pread 失敗,中斷槽就會留在表裡卻是
|
| 83 |
+
舊資料,之後的 `probe()` 會把它當成命中 → **靜默算錯**。成本相同,但不能這麼做。
|
| 84 |
+
|
| 85 |
+
### arena sizing 扣的是「匿名 RSS」
|
| 86 |
+
|
| 87 |
+
`statm` 的 resident 把 mmap 進來的模型權重頁也算成已用,但那些是乾淨的
|
| 88 |
+
file-backed 頁,記憶體吃緊時核心會直接回收,既不佔用寶貴的實體 RAM 也不會產生
|
| 89 |
+
swap I/O。把它算進「已用」會讓 arena 被無謂地縮水。
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
arena = 預算 − 匿名 RSS − SDQ_RESERVE_MB
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
`RLIMIT_DATA` 只當**上界**,不當預算來源:它算的是匿名**虛擬**記憶體,而
|
| 96 |
+
llama.cpp 會配置大量「映射了但永遠不會 touch」的圖與 compute buffer。
|
| 97 |
+
|
| 98 |
+
### 8 GiB 下的 reserve 校正
|
| 99 |
+
|
| 100 |
+
`SDQ_RESERVE_MB` 必須**逐模型、逐 ctx/ubatch 校正**。本模型在
|
| 101 |
+
ctx=4096 / ubatch=128 下量到的:
|
| 102 |
+
|
| 103 |
+
| reserve | arena | 槽位 | 峰值匿名 RSS | 8 GiB 內? |
|
| 104 |
+
| ---: | ---: | ---: | ---: | --- |
|
| 105 |
+
| 128 MiB | — | — | 4042.6 MiB | 是(餘 53 MiB,太緊) |
|
| 106 |
+
| 256 MiB | — | — | 3918.1 MiB | 是(餘 178 MiB) |
|
| 107 |
+
| 384 MiB | — | — | 3795.7 MiB | 是(餘 300 MiB) |
|
| 108 |
+
| **512 MiB** | **6.35 GiB** | **3714** | **7799 MiB** | **是(餘 446 MiB)** |
|
| 109 |
+
| 640 MiB | 6.23 GiB | 3644 | 7623.0 MiB | 是(餘 292 MiB) |
|
| 110 |
+
| 768 MiB | 6.11 GiB | 3573 | 7498.5 MiB | 是 |
|
| 111 |
+
| 1024 MiB | 5.87 GiB | 3432 | 7251.6 MiB | 是 |
|
| 112 |
+
|
| 113 |
+
峰值匿名 RSS 幾乎與 reserve 成 1:1 反比(每減 128 MiB,RSS 多約 123 MiB),
|
| 114 |
+
因為省下來的預留全部變成 arena。**512 MiB 是這個模型在 8 GiB 下的選擇。**
|
| 115 |
+
|
| 116 |
+
換 RAM 預算時 reserve 也要跟著換。本 repo 的量測腳本用一條比例規則:
|
| 117 |
+
|
| 118 |
+
```
|
| 119 |
+
reserve = clamp(RAM 預算 / 16, 128 MiB, 1024 MiB)
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
在 8 GiB 重現 512 MiB,在 4 GiB 給出 256 MiB(餘 178 MiB)。
|
| 123 |
+
**用固定的 640 MiB 跑 4 GiB 會直接超出上限。**
|
| 124 |
+
|
| 125 |
+
## 4. 輸出不直接寫 dst
|
| 126 |
+
|
| 127 |
+
同一個 token 的 8 個 expert 會撞在一起,所以每個執行緒一份累加緩衝,
|
| 128 |
+
最後一道 barrier 之後歸約。同時每個 expert 算完立刻 `release()` 它的槽位 ——
|
| 129 |
+
不放的話 prefill 一層會把整層用到的 expert 全部釘住(最多 256 個),
|
| 130 |
+
大槽位不夠時會中止。
|
| 131 |
+
|
| 132 |
+
## 5. 這個設計沒有做的事
|
| 133 |
+
|
| 134 |
+
* **不碰路由。** 路由必須完整算完才知道要哪些 expert,而路由本身很小。
|
| 135 |
+
* **不用 swap。** swap 的延遲不可預測,而且本專案的 no-swap 是核心保證。
|
| 136 |
+
* **不做壓縮。** 權重已經是 Q4_K_M,再壓就是品質換速度,不在本專案範圍。
|
| 137 |
+
* **不處理視覺。** 這個 GGUF 的 vision projector 在另一個檔案
|
| 138 |
+
(`mmproj-model-f16.gguf`),本路徑只做文字推論。
|
docs/NEGATIVE_RESULTS.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 負結果:試過但沒留下的最佳化
|
| 2 |
+
|
| 3 |
+
記錄在 8 GiB 上**量過、放棄**的選項。每一項都有原始資料在
|
| 4 |
+
`bench/results/strict_prefetch.json` 與 `bench/results/strict_threads.json`。
|
| 5 |
+
|
| 6 |
+
## 1. 預取(關閉)
|
| 7 |
+
|
| 8 |
+
pager 可以從上一個 token 的路由預測下一層會用到哪些 expert,在本層計算的時候
|
| 9 |
+
先把資料讀好。三種設定各 6 輪,交錯執行:
|
| 10 |
+
|
| 11 |
+
| 設定 | 平均 tok/s | 單輪標準差 | 單輪全距 |
|
| 12 |
+
| --- | ---: | ---: | --- |
|
| 13 |
+
| **關閉(預設)** | **6.03 ± 0.44** | 0.44 | 5.55 – 6.68 |
|
| 14 |
+
| 跨層預取 `SDQ_PREFETCH_LAYER_AHEAD=1` | 5.95 ± 0.37 | 0.37 | 5.38 – 6.29 |
|
| 15 |
+
| 僅 end_token `SDQ_PREFETCH=1` | 5.78 ± 0.33 | 0.33 | 5.45 – 6.27 |
|
| 16 |
+
|
| 17 |
+
**三者差異遠小於單輪抖動,統計上無法分辨。** 而且預取開著時 SSD 讀取量
|
| 18 |
+
在多數輪次是上升的(預測不夠準,多讀的全是白讀)。
|
| 19 |
+
|
| 20 |
+
更嚴重的是正確性問題:預取佔用的槽位(busy)會讓 `acquire()` 偶爾拿不到槽位,
|
| 21 |
+
而舊版的失敗處理是**靜默跳過一個 expert**。預測率不到 1% 的最佳化不應該
|
| 22 |
+
承擔這個風險。
|
| 23 |
+
|
| 24 |
+
**預設關閉。** 程式碼保留(`SDQ_PREFETCH_LAYER_AHEAD=1` 可以重現),
|
| 25 |
+
免得後人重新發明。
|
| 26 |
+
|
| 27 |
+
## 2. 執行緒數(預設 8,但 12 更好)
|
| 28 |
+
|
| 29 |
+
| threads | 平均 tok/s | 單輪標準差 |
|
| 30 |
+
| ---: | ---: | ---: |
|
| 31 |
+
| 4 | 4.65 ± 0.22 | 0.22 |
|
| 32 |
+
| 8 | 5.96 ± 0.45 | 0.45 |
|
| 33 |
+
| 10 | 6.16 ± 0.48 | 0.48 |
|
| 34 |
+
| **12** | **6.35 ± 0.51** | 0.51 |
|
| 35 |
+
| 16 | 6.27 ± 0.35 | 0.35 |
|
| 36 |
+
|
| 37 |
+
**這個結論與參考實作相反。** 參考的 Qwen3.6-35B-A3B 是純 transformer,
|
| 38 |
+
每層每 token 只有 8 個 expert 可以平行,加執行緒只是在搶同一批 CPU,
|
| 39 |
+
實測 8 → 12 → 16 是下降的。
|
| 40 |
+
|
| 41 |
+
MiniCPM-V-4.7-35B-A3B 是**混合架構**:40 層裡只有 10 層是全注意力,
|
| 42 |
+
其餘 30 層是 SSM 層(見 `DESIGN.md` §1)。SSM 層的計算不受序列維度限制,
|
| 43 |
+
可以吃滿更多執行緒,所以 12 執行緒比 8 快約 6.5%。
|
| 44 |
+
|
| 45 |
+
**這是本模型與參考實作最實際的一個行為差異,也說明「執行緒數」這種設定
|
| 46 |
+
不能從上一個模型照抄。** 8 保留為預設(與標配量測一致),12 是建議值。
|
| 47 |
+
|
| 48 |
+
> 4 → 8 是真實且顯著的提升(4.65 → 5.96,+28%)。8 之後的差異(8/10/12/16)
|
| 49 |
+
> 都在單輪抖動之內,不足以支撐更細的宣稱。
|
| 50 |
+
|
| 51 |
+
## 3. 單一尺寸槽位(放棄)
|
| 52 |
+
|
| 53 |
+
把所有槽位都配成最大的那種(`SDQ_FORCE_UNIFORM_SLOT=1`)最簡單,代價是
|
| 54 |
+
同樣的 arena 少掉約 18% 的槽位。本模型兩種 expert 大小相差 15%
|
| 55 |
+
(2,039,808 vs 1,769,472 B),所以分級槽位的收益比參考實作小,但仍然是免費的。
|
| 56 |
+
|
| 57 |
+
驗證用的 `prefetch_stress_check` 兩種配置都跑,確保分級槽位的配置器正確。
|
| 58 |
+
|
| 59 |
+
## 4. 端到端 A/B 對照(做不到)
|
| 60 |
+
|
| 61 |
+
需要把整個 21.17 GB 模型常駐 RAM 作為對照組。在 8 GiB 上不可能,
|
| 62 |
+
這正是本專案存在的理由。見 `STRICT_8GIB.md` §3。
|
docs/STRICT_8GIB.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 8 GiB / no-swap 的方法、證據與限制
|
| 2 |
+
|
| 3 |
+
本專案的核心主張是「21.17 GB 的模型在 8 GiB RAM、沒有 swap 的條件下解碼」。
|
| 4 |
+
**每一次執行都由 `tools/strict8g.py` 在行程執行期間管治**,不是事後推論。
|
| 5 |
+
`strict_ok=False` 的樣本一律不採用。
|
| 6 |
+
|
| 7 |
+
## 1. 兩條腿
|
| 8 |
+
|
| 9 |
+
### no-swap 由核心保證,不是讀一次數值
|
| 10 |
+
|
| 11 |
+
行程所有**匿名**映射都被 `tools/epq_lock.so` 以 `mlock()` 鎖住。
|
| 12 |
+
被鎖定的頁永遠不會被寫到 swap 裝置,所以 no-swap 是核心在整段執行期間強制
|
| 13 |
+
的,而不是執行完之後從某一次讀取推論出來的。
|
| 14 |
+
|
| 15 |
+
標配執行下 `VmLck` ≈ 7.6 GiB,這個數字可以直接驗證。
|
| 16 |
+
|
| 17 |
+
### 實體常駐量有硬上限
|
| 18 |
+
|
| 19 |
+
取樣器(預設每 5 ms 讀一次 `/proc/<pid>/status`)一旦發現匿名 RSS 超過上限
|
| 20 |
+
立刻 `SIGKILL`;另外用 `RLIMIT_DATA` 當第二道防失控的閘門。
|
| 21 |
+
|
| 22 |
+
每次執行都記錄 `VmHWM`、`RssAnon`、`VmSwap`、`VmLck`。
|
| 23 |
+
|
| 24 |
+
## 2. 標配結果
|
| 25 |
+
|
| 26 |
+
8 GiB 上限、ctx=4096、ubatch=128、8 threads、greedy、48 requested tokens、
|
| 27 |
+
提示詞中英交替、**N=9 配對交錯**:
|
| 28 |
+
|
| 29 |
+
| 指標 | 值 |
|
| 30 |
+
| --- | ---: |
|
| 31 |
+
| **Decode tok/s(中位數)** | **5.83** |
|
| 32 |
+
| 單輪全距 | 5.19 – 6.63(抖動 25%) |
|
| 33 |
+
| Expert cache hit rate | 68.68% |
|
| 34 |
+
| SSD 讀取 / decode token | 214.1 MB |
|
| 35 |
+
| Arena | 6.35 GiB(3714 slots) |
|
| 36 |
+
| **峰值匿名 RSS** | **7799 MiB** / 8192 MiB 上限 |
|
| 37 |
+
| **峰值 VmSwap** | **0.000 MiB** |
|
| 38 |
+
| strict_ok | 9/9 |
|
| 39 |
+
|
| 40 |
+
條件:2× Intel Xeon Platinum 8559C(AVX-512 / AMX)。
|
| 41 |
+
|
| 42 |
+
> 這是**共享主機**,同一組設定的單輪抖動可達 25%。數字足以支撐「能不能跑」
|
| 43 |
+
> 與「大概多快」,不足以支撐 5% 等級的宣稱。
|
| 44 |
+
|
| 45 |
+
## 3. 同一個 8 GiB 上限下的傳統 llama.cpp
|
| 46 |
+
|
| 47 |
+
未修改的 llama.cpp(commit `9d7d5049`,獨立 worktree 編譯的 `llama-completion`)
|
| 48 |
+
在**同一個 8192 MiB 上限**下:
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
[strict8g] FAIL strict: rc=1 wall=27.3s | anon=928.8 file=20199.0 total=21127.2
|
| 52 |
+
ggml_aligned_malloc: insufficient memory (attempted to allocate 14400.00 MB)
|
| 53 |
+
ggml_backend_buft_alloc_buffer_n_default: failed to allocate CPU_REPACK buffer of size 15099494400
|
| 54 |
+
llama_model_load: error loading model: unable to allocate CPU_REPACK buffer
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
**模型都載不進去**,在載入階段就失敗。
|
| 58 |
+
|
| 59 |
+
| 路徑 | 結果 |
|
| 60 |
+
| --- | --- |
|
| 61 |
+
| 傳統 llama.cpp(未套 patch) | **無法載入模型** |
|
| 62 |
+
| EPM `epq-chat` | **5.83 tok/s**,峰值匿名 RSS 7799 MiB,VmSwap 0 |
|
| 63 |
+
|
| 64 |
+
對照必須在**同一個上限**下建立:先量「兩邊都沒限制」再比較,是把 RAM 這個
|
| 65 |
+
變數漏掉了。
|
| 66 |
+
|
| 67 |
+
## 3b. RAM 擴張曲線怎麼讀
|
| 68 |
+
|
| 69 |
+
完整表格在 README。方法學上兩件事必須先講清楚:
|
| 70 |
+
|
| 71 |
+
1. 這條曲線**不用 mlock**(理由見上),no-swap 的保證只保留給 8 GiB 這個部署目標。
|
| 72 |
+
2. **arena 不預先碰滿**。全部路由專家權重只有 18.16 GiB,所以 64 GiB 預算下
|
| 73 |
+
預先碰觸的上限就是 18.17 GiB;峰值 RSS 停在約 24.0 GiB 才是真實的記憶體佔用。
|
| 74 |
+
|
| 75 |
+
結論(每個點 N=9,配對交錯,Welch t 檢定,36/36 輪 strict_ok):
|
| 76 |
+
|
| 77 |
+
* **4 GiB → 8 GiB:+16.6%,t = −5.44,p = 0.0001(顯著)。**
|
| 78 |
+
* **8 GiB → 16 GiB:−0.8%,t = −0.30,p = 0.76(無法分辨)。**
|
| 79 |
+
* **8 GiB → 64 GiB:−0.2%,t = −0.05,p = 0.96(無法分辨)。**
|
| 80 |
+
* 命中率在 16 GiB 飽和於 77.54%,SSD/token 飽和於 ~158 MB。
|
| 81 |
+
* 記憶體需求飽和於約 24.0 GiB 峰值。
|
| 82 |
+
|
| 83 |
+
## 4. 儲存裝置會改變結論
|
| 84 |
+
|
| 85 |
+
本機是本地 NVMe / overlayfs,不是網路檔案系統。`tools/io_probe.py` 量到的儲存
|
| 86 |
+
特性決定了瓶頸在哪一邊:
|
| 87 |
+
|
| 88 |
+
* **慢速儲存上** I/O 排程是瓶頸,預取與並行讀取才有意義。
|
| 89 |
+
* **快速本機 NVMe 上** 命中率(arena 大小)才是瓶頸,I/O 最佳化的邊際收益很低。
|
| 90 |
+
|
| 91 |
+
本 repo 的 tok/s **不能直接套用到慢速儲存的機器上**。先跑 `tools/io_probe.py`
|
| 92 |
+
再決定要最佳化哪一邊。
|
| 93 |
+
|
| 94 |
+
## 5. 這個量測**沒有**證明的事
|
| 95 |
+
|
| 96 |
+
1. **不是 cgroup 驗證。** 這個環境的 cgroup2 掛載唯讀、沒有 `CAP_SYS_ADMIN`,
|
| 97 |
+
所以做不到 `memory.max=8G` + `memory.swap.max=0`。目前的保證是
|
| 98 |
+
「kernel `RLIMIT_DATA` + 取樣殺行程」與「mlock 匿名頁」兩條腿。
|
| 99 |
+
要做到與 cgroup 完全等價,需要在有 `CAP_SYS_ADMIN` 的主機上跑
|
| 100 |
+
(`docker run --memory=8g --memory-swap=8g`,進容器後確認
|
| 101 |
+
`memory.max=8589934592`、`memory.swap.max=0`)。
|
| 102 |
+
2. **主機 swap 是開啟的。** no-swap 由 `mlock` 讓它不可能發生,不是把主機的
|
| 103 |
+
swap 關掉。主機層級的 swap 行為沒有被測到。
|
| 104 |
+
3. **`brk()` 主堆疊未被鎖**(glibc 內部直接下 syscall)。大型配置都走 `mmap`。
|
| 105 |
+
4. **共享主機**,單輪抖動可達 25%。
|
| 106 |
+
5. **只驗到 4K context**;模型原生支援 262 K。
|
| 107 |
+
6. **只做文字推論。** vision projector 是另一個檔案,沒有測。
|
| 108 |
+
7. **儲存裝置比歷史量測的機器快一到兩個數量級。**
|
| 109 |
+
|
| 110 |
+
完整方法與原始資料�� `bench/results/`。
|
| 111 |
+
|
| 112 |
+
## 6. 重現
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
# 受管治的單次執行
|
| 116 |
+
python3 tools/strict8g.py --limit-mb 8192 --record /tmp/one.json -- \
|
| 117 |
+
epq-chat -m model.gguf -p "..." -n 48 -c 4096 -t 8 -b 128 \
|
| 118 |
+
--temp 0 --no-chat --no-interactive --ram-budget-mb 8192
|
| 119 |
+
|
| 120 |
+
# 標配基準(N=9,配對交錯)
|
| 121 |
+
python3 -m sdq.strict_bench --model model.gguf \
|
| 122 |
+
--bins base=./build/bin/epq-chat --rounds 9 --strict-mb 8192 --label headline
|
| 123 |
+
|
| 124 |
+
# 曲線與負結果
|
| 125 |
+
python3 bench/run_budget_curve.py --model model.gguf --bin ./build/bin/epq-chat --rounds 5
|
| 126 |
+
python3 bench/run_ramsweep.py --model model.gguf --bin ./build/bin/epq-chat --rounds 3
|
| 127 |
+
python3 bench/run_n9.py --model model.gguf --bin ./build/bin/epq-chat --rounds 9
|
| 128 |
+
python3 bench/run_negative.py --which prefetch --model model.gguf --bin ./build/bin/epq-chat
|
| 129 |
+
python3 bench/run_negative.py --which threads --model model.gguf --bin ./build/bin/epq-chat
|
| 130 |
+
```
|
patches/0001-sdq-expert-pager.patch
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
sdq/__init__.py
ADDED
|
File without changes
|
sdq/bench.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""SDQ benchmark: measure pager/I/O/cache/tok/s and report RSS/VmSwap.
|
| 3 |
+
|
| 4 |
+
這個模組是**舊的**基準工具,它給的數字不是 8 GiB / no-swap 證據:
|
| 5 |
+
|
| 6 |
+
1. 預設的 `RLIMIT_DATA` 只限制 data/heap 配置,不計 file-backed 常駐頁,
|
| 7 |
+
也不是 swap 開關。
|
| 8 |
+
2. 報告的 `VmSwap` 只是「那一次執行沒有被換出」,不是「核心不可能換出」。
|
| 9 |
+
|
| 10 |
+
**要 8 GiB / no-swap 的證據請用 `tools/strict8g.py` 或 `sdq.strict_bench.py`。**
|
| 11 |
+
本模組保留給快速的非受管治量測,以及 `demo.py --bench` 呼叫。
|
| 12 |
+
|
| 13 |
+
做法
|
| 14 |
+
----
|
| 15 |
+
1. 每次量測前用 tools/drop_model_cache.py 把模型檔的 page cache 丟掉,
|
| 16 |
+
避免「上一輪留在核心 RAM 的頁」讓 SSD 讀取量看起來比實際少。
|
| 17 |
+
|
| 18 |
+
用法
|
| 19 |
+
----
|
| 20 |
+
python -m sdq.bench --model /path/model.gguf --bin build/bin/epq-chat
|
| 21 |
+
python -m sdq.bench --ram-mb 8192 --arena-mb 3800 --threads 4
|
| 22 |
+
|
| 23 |
+
受管治的版本(會輸出 strict_ok)::
|
| 24 |
+
|
| 25 |
+
python -m sdq.strict_bench --model /path/model.gguf --bin build/bin/epq-chat \
|
| 26 |
+
--rounds 9 --strict-mb 8192 --label headline
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import argparse
|
| 32 |
+
import json
|
| 33 |
+
import os
|
| 34 |
+
import resource
|
| 35 |
+
import subprocess
|
| 36 |
+
import sys
|
| 37 |
+
import time
|
| 38 |
+
from pathlib import Path
|
| 39 |
+
|
| 40 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 41 |
+
PROMPTS = [
|
| 42 |
+
"請用三句話說明什麼是 SSD、RAM、CPU 之間的資料分層。",
|
| 43 |
+
"Write a short paragraph about how mixture-of-experts models route tokens.",
|
| 44 |
+
"請列出五種日常會用到電腦的工作,並各用一句話說明。",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def drop_cache(model: str) -> None:
|
| 49 |
+
subprocess.run([sys.executable, str(REPO / "tools" / "drop_model_cache.py"), model],
|
| 50 |
+
check=False, capture_output=True)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def run_once(binary: str, model: str, args: argparse.Namespace, prompt: str,
|
| 54 |
+
n_predict: int, tag: str) -> dict:
|
| 55 |
+
stats_file = Path(args.workdir) / f"stats_{tag}.jsonl"
|
| 56 |
+
if stats_file.exists():
|
| 57 |
+
stats_file.unlink()
|
| 58 |
+
|
| 59 |
+
cmd = [
|
| 60 |
+
binary, "-m", model, "-p", prompt, "-n", str(n_predict),
|
| 61 |
+
"-c", str(args.ctx), "-t", str(args.threads), "-b", str(args.ubatch),
|
| 62 |
+
"--temp", "0", "--no-chat", "--no-interactive",
|
| 63 |
+
"--stats-file", str(stats_file),
|
| 64 |
+
]
|
| 65 |
+
if args.ram_mb:
|
| 66 |
+
cmd += ["--ram-budget-mb", str(args.ram_mb)]
|
| 67 |
+
if args.arena_mb:
|
| 68 |
+
cmd += ["--arena-mb", str(args.arena_mb)]
|
| 69 |
+
if args.no_prefetch:
|
| 70 |
+
cmd += ["--no-prefetch"]
|
| 71 |
+
|
| 72 |
+
env = dict(os.environ)
|
| 73 |
+
env.setdefault("SDQ_FADV_DONTNEED", "1")
|
| 74 |
+
|
| 75 |
+
if args.ram_mb and not args.no_ulimit:
|
| 76 |
+
# RLIMIT_DATA limits data/heap allocations, not total RSS or swap.
|
| 77 |
+
def _limit():
|
| 78 |
+
limit = args.ram_mb * 1024 * 1024
|
| 79 |
+
resource.setrlimit(resource.RLIMIT_DATA, (limit, limit))
|
| 80 |
+
preexec = _limit
|
| 81 |
+
else:
|
| 82 |
+
preexec = None
|
| 83 |
+
|
| 84 |
+
if args.drop_cache:
|
| 85 |
+
drop_cache(model)
|
| 86 |
+
|
| 87 |
+
t0 = time.time()
|
| 88 |
+
proc = subprocess.run(cmd, capture_output=True, text=True, env=env, preexec_fn=preexec)
|
| 89 |
+
wall = time.time() - t0
|
| 90 |
+
if proc.returncode != 0:
|
| 91 |
+
print(f"[bench] {tag} 執行失敗(rc={proc.returncode})")
|
| 92 |
+
print(proc.stderr[-2000:])
|
| 93 |
+
return {"tag": tag, "error": proc.returncode}
|
| 94 |
+
|
| 95 |
+
# 解析指標
|
| 96 |
+
stats = {}
|
| 97 |
+
if stats_file.exists():
|
| 98 |
+
for line in stats_file.read_text().splitlines():
|
| 99 |
+
rec = json.loads(line)
|
| 100 |
+
stats[rec["phase"]] = rec
|
| 101 |
+
|
| 102 |
+
mem = {}
|
| 103 |
+
for line in proc.stderr.splitlines():
|
| 104 |
+
if "記憶體(載入後)" in line or "記憶體:" in line:
|
| 105 |
+
mem["line"] = line
|
| 106 |
+
for key, val in re_findall(line):
|
| 107 |
+
mem[key] = val
|
| 108 |
+
|
| 109 |
+
tok = {}
|
| 110 |
+
for line in proc.stderr.splitlines():
|
| 111 |
+
if "decode:" in line:
|
| 112 |
+
tok["decode"] = line
|
| 113 |
+
if "prefill:" in line:
|
| 114 |
+
tok["prefill"] = line
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
"tag": tag,
|
| 118 |
+
"wall_s": round(wall, 2),
|
| 119 |
+
"stdout": proc.stdout.strip()[-400:],
|
| 120 |
+
"tok": tok,
|
| 121 |
+
"mem": mem,
|
| 122 |
+
"stats": stats,
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def re_findall(line: str):
|
| 127 |
+
import re
|
| 128 |
+
return re.findall(r"(RSS|VmSwap|峰值 RSS\(VmHWM\))\)?[:=]?\s*(\d+)\s*MB", line)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def summarize(result: dict) -> str:
|
| 132 |
+
s = result["stats"].get("decode") or result["stats"].get("bench_hot") or {}
|
| 133 |
+
if not s:
|
| 134 |
+
return f"[{result['tag']}] 無指標"
|
| 135 |
+
return (
|
| 136 |
+
f"[{result['tag']}] hit={s['hit_rate']:.1f}% SSD/token={s['ssd_bytes_per_token']/2**20:.1f} MB "
|
| 137 |
+
f"預取使用率={s['prefetch_used_rate']:.1f}% arena={s['arena_used_bytes']/2**30:.2f}/{s['arena_bytes']/2**30:.2f} GiB "
|
| 138 |
+
f"RSS={s['rss_bytes']/2**20:.0f} MB"
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def main() -> None:
|
| 143 |
+
ap = argparse.ArgumentParser(description="SDQ 8GB RAM 基準測量")
|
| 144 |
+
ap.add_argument("--model", required=True)
|
| 145 |
+
ap.add_argument("--bin", default=str(REPO / "build" / "bin" / "epq-chat"))
|
| 146 |
+
ap.add_argument("--ram-mb", type=int, default=8192,
|
| 147 |
+
help="SDQ pager sizing budget (also RLIMIT_DATA unless --no-ulimit; not an RSS/swap cap)")
|
| 148 |
+
ap.add_argument("--arena-mb", type=int, default=0, help="固定 arena 大小(0 = 由預算推算)")
|
| 149 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 150 |
+
ap.add_argument("--ubatch", type=int, default=128)
|
| 151 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 152 |
+
ap.add_argument("--n-predict", type=int, default=48)
|
| 153 |
+
ap.add_argument("--rounds", type=int, default=2)
|
| 154 |
+
ap.add_argument("--workdir", default=str(REPO / "bench" / "results"))
|
| 155 |
+
ap.add_argument("--no-prefetch", action="store_true")
|
| 156 |
+
ap.add_argument("--no-ulimit", action="store_true",
|
| 157 |
+
help="Do not set RLIMIT_DATA; still not an OS memory/swap cap")
|
| 158 |
+
ap.add_argument("--no-drop-cache", dest="drop_cache", action="store_false", default=True)
|
| 159 |
+
ap.add_argument("--label", default="run")
|
| 160 |
+
args = ap.parse_args()
|
| 161 |
+
|
| 162 |
+
Path(args.workdir).mkdir(parents=True, exist_ok=True)
|
| 163 |
+
|
| 164 |
+
results = []
|
| 165 |
+
# 冷啟動:整個模型第一次被讀(cache 是空的)
|
| 166 |
+
results.append(run_once(args.bin, args.model, args, PROMPTS[0], args.n_predict, "cold"))
|
| 167 |
+
print(summarize(results[-1]))
|
| 168 |
+
# 熱身後:專家 cache 已經有內容
|
| 169 |
+
for r in range(args.rounds):
|
| 170 |
+
res = run_once(args.bin, args.model, args, PROMPTS[(r + 1) % len(PROMPTS)],
|
| 171 |
+
args.n_predict, f"hot{r}")
|
| 172 |
+
results.append(res)
|
| 173 |
+
print(summarize(res))
|
| 174 |
+
|
| 175 |
+
out = Path(args.workdir) / f"bench_{args.label}.json"
|
| 176 |
+
out.write_text(json.dumps({
|
| 177 |
+
"config": vars(args),
|
| 178 |
+
"results": results,
|
| 179 |
+
}, ensure_ascii=False, indent=1), encoding="utf-8")
|
| 180 |
+
print(f"\n[bench] 完整結果已寫到 {out}")
|
| 181 |
+
|
| 182 |
+
# 印出對照用的解譯文字(確認模型真的有在回答,不是亂碼)
|
| 183 |
+
for res in results:
|
| 184 |
+
print(f"\n--- {res['tag']} 生成內容 ---\n{res.get('stdout','')}")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
main()
|
sdq/config.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""EPM 全專案唯一的設定來源(其他檔案一律 import 這裡,不要各自硬寫常數)。
|
| 2 |
+
|
| 3 |
+
改版號 → 改這裡 → 重新執行 `python demo.py` 就會自動重抓、重新編譯。
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------------- 專案
|
| 12 |
+
HF_REPO = os.environ.get(
|
| 13 |
+
"EPM_HF_REPO",
|
| 14 |
+
os.environ.get("SDQ_HF_REPO", "HelloSun/minicpm47woek"),
|
| 15 |
+
) # 存程式碼、文件與量測結果
|
| 16 |
+
DEFAULT_HOME = Path(os.environ.get(
|
| 17 |
+
"EPM_HOME", os.environ.get("SDQ_HOME", str(Path.home() / "minicpm47-epm"))
|
| 18 |
+
))
|
| 19 |
+
|
| 20 |
+
# ---------------------------------------------------------------- llama.cpp
|
| 21 |
+
LLAMA_REPO = "https://github.com/ggml-org/llama.cpp.git"
|
| 22 |
+
# EPM patch 就是針對這個 commit 生成的;換 commit 一定要重新產生 patch
|
| 23 |
+
# (見 docs/01 的「續作指引」一節與 patches/ 的產生方式)。
|
| 24 |
+
LLAMA_COMMIT = "9d7d5049804be03fce6575cc9f49c058b3c48788"
|
| 25 |
+
PATCH_FILE = "patches/0001-sdq-expert-pager.patch"
|
| 26 |
+
# patch 會把引擎檔案放進 llama.cpp 的 src/ 目錄
|
| 27 |
+
PATCH_TARGET_DIR = "src"
|
| 28 |
+
SDQ_SOURCE_FILES = ["sdq_pager.h", "sdq_pager.cpp", "sdq_buft.cpp", "sdq_moe.cpp", "sdq_cli.cpp", "sdq_mtp.cpp"]
|
| 29 |
+
BUILD_TARGET = "epq-chat"
|
| 30 |
+
|
| 31 |
+
# ---------------------------------------------------------------- 模型
|
| 32 |
+
MODEL_REPO = "openbmb/MiniCPM-V-4.7-35B-A3B-gguf"
|
| 33 |
+
MODEL_FILE = "MiniCPM-V-4_7-35B-A3B-Q4_K_M.gguf"
|
| 34 |
+
MODEL_SIZE = 21_166_255_008 # bytes
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------------- 執行參數
|
| 37 |
+
# 目標部署:8 GiB RAM 的 Intel CPU(無 GPU)。
|
| 38 |
+
# RAM_BUDGET_MB 是**實體 RAM 預算**,不是虛擬記憶體上限,也不是 swap 開關;
|
| 39 |
+
# 嚴格的 8 GiB / no-swap 驗證由 tools/strict8g.py 管治(見 docs/STRICT_8GIB.md)。
|
| 40 |
+
RAM_BUDGET_MB = 8192
|
| 41 |
+
ARENA_MB = 0 # 0 = 由 RAM 預算自動推算(見 docs/02 第 6 節)
|
| 42 |
+
# arena sizing 時要預留多少記憶體(見 docs/DESIGN.md §3 的校正表)。
|
| 43 |
+
# 逐模型、逐 ctx/ubatch 校正:換 RAM 預算要跟著換,否則 4 GiB 會超出上限。
|
| 44 |
+
# 比例規則 RAM_BUDGET_MB/16 在 8 GiB 重現校正出來的 512 MiB。
|
| 45 |
+
RESERVE_MB = RAM_BUDGET_MB // 16
|
| 46 |
+
CTX_VERIFY = 4096 # 本專案實際驗證的 context 長度
|
| 47 |
+
UBATCH = 128 # llama.cpp 的 n_ubatch。預設 512 會讓長提示詞在 8 GB 預算下
|
| 48 |
+
# 直接 std::bad_alloc(compute buffer 502 MiB 是在 arena sizing
|
| 49 |
+
# 之後才配置的);128 修好崩潰而且 prefill 快 4 倍(見 docs/05 §5.1)
|
| 50 |
+
CTX_THEORETICAL = 262_144 # 模型原生 context(本專案只驗證到 4K)
|
| 51 |
+
N_PREDICT = 256
|
| 52 |
+
N_EXPERT = 256 # 每層專家數
|
| 53 |
+
N_EXPERT_USED = 8 # 每 token 啟用專家數
|
| 54 |
+
N_LAYER = 40
|
sdq/experiments.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""SDQ 最佳化實驗 harness:跑一組設定矩陣,產出可重現、可比較的結果。
|
| 3 |
+
|
| 4 |
+
設計原則
|
| 5 |
+
--------
|
| 6 |
+
1. **每次量測前丟掉模型檔的 page cache**(POSIX_FADV_DONTNEED),否則上一輪留在
|
| 7 |
+
核心記憶體的頁會讓 SSD 讀取量虛高、命中率虛高。
|
| 8 |
+
2. **每個設定跑 N 次取中位數**:單次結果抖動 ±10% 很常見,中位數才有意義。
|
| 9 |
+
3. **同時回報 KPI**:decode tok/s、cache 命中率、SSD 讀取量/token、峰值 RSS 與
|
| 10 |
+
本行程 VmSwap。VmSwap=0 僅表示該行程取樣為 0,不代表主機停用了 swap。
|
| 11 |
+
4. **自動檢查正確性指紋**:把每個實驗產生的第一個 token 記下來,
|
| 12 |
+
如果某個最佳化讓第一個 token 變了,代表它改變了模型行為 → 必須標記為失敗。
|
| 13 |
+
(速度不能拿正確性換,這是本專案的硬規則。)
|
| 14 |
+
5. 結果寫成 JSON + Markdown 表格,方便直接貼進 docs/。
|
| 15 |
+
|
| 16 |
+
用法
|
| 17 |
+
----
|
| 18 |
+
python -m sdq.experiments --model <gguf> --bin <epq-chat> --suite quick
|
| 19 |
+
python -m sdq.experiments --model <gguf> --bin <epq-chat> --suite full --repeat 3
|
| 20 |
+
python -m sdq.experiments ... --only thread,direct # 只跑某些實驗
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import argparse
|
| 26 |
+
import json
|
| 27 |
+
import os
|
| 28 |
+
import re
|
| 29 |
+
import resource
|
| 30 |
+
import statistics
|
| 31 |
+
import subprocess
|
| 32 |
+
import sys
|
| 33 |
+
import time
|
| 34 |
+
from pathlib import Path
|
| 35 |
+
|
| 36 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 37 |
+
|
| 38 |
+
# 固定的量測條件:改這些會讓不同實驗之間無法比較
|
| 39 |
+
FIXED = {
|
| 40 |
+
"ctx": "4096",
|
| 41 |
+
# S5 起引擎的 n_ubatch 預設是 128(512 在 8 GB 預算下會讓長提示詞 bad_alloc,
|
| 42 |
+
# 見 docs/05 §5.1)。這裡必須跟著預設走,否則量到的不是我們要交付的那條路徑。
|
| 43 |
+
"ubatch": "128",
|
| 44 |
+
"temp": "0",
|
| 45 |
+
"n_predict": "48",
|
| 46 |
+
"ram_mb": "8192",
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
PROMPTS = [
|
| 50 |
+
"請用三句話說明什麼是 SSD、RAM、CPU 之間的資料分層。",
|
| 51 |
+
"Write a short paragraph about how mixture-of-experts models route tokens.",
|
| 52 |
+
"請列出五種日常會用到電腦的工作,並各用一句話說明。",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ------------------------------------------------------------------ 實驗定義
|
| 57 |
+
# 每個實驗 = 一組環境變數 / CLI 參數的覆寫。None 代表沿用預設。
|
| 58 |
+
def _suite_quick() -> list[dict]:
|
| 59 |
+
"""先摸底:threads、I/O 模式、預取、arena、ubatch。成本低、風險低。"""
|
| 60 |
+
out = []
|
| 61 |
+
for t in (4, 6, 8, 10, 12, 16):
|
| 62 |
+
out.append({"name": f"threads={t}", "args": {"threads": str(t)}})
|
| 63 |
+
out += [
|
| 64 |
+
{"name": "prefetch=off", "args": {"no_prefetch": True}},
|
| 65 |
+
{"name": "fadvise=off", "env": {"SDQ_FADV_DONTNEED": "0"}},
|
| 66 |
+
{"name": "arena=2048MB", "args": {"arena_mb": "2048"}},
|
| 67 |
+
{"name": "arena=3072MB", "args": {"arena_mb": "3072"}},
|
| 68 |
+
{"name": "arena=4608MB", "args": {"arena_mb": "4608"}},
|
| 69 |
+
{"name": "ubatch=128", "args": {"ubatch": "128"}},
|
| 70 |
+
# OPT-1:Strata 式熱門 expert 靜態釘選(實測對本模型無效,保留作為對照)
|
| 71 |
+
# Needs a profile produced by tools/route_overlap.py; point SDQ_PROFILE_IN
|
| 72 |
+
# at your own file. Kept as a control even though it measured as no-op.
|
| 73 |
+
{"name": "hot-pin=2048", "args": {},
|
| 74 |
+
"env": {"SDQ_PROFILE_IN": str(REPO / "bench" / "expert_profile.txt"),
|
| 75 |
+
"SDQ_HOT_PIN": "2048"}},
|
| 76 |
+
# 排程模式對照
|
| 77 |
+
{"name": "modeB(partial=0)", "args": {}, "env": {"SDQ_PARTIAL_CAP_MB": "0"}},
|
| 78 |
+
]
|
| 79 |
+
return out
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _suite_io() -> list[dict]:
|
| 83 |
+
"""I/O 排程:一個 expert 的 gate/up/down 三段要不要同時讀。
|
| 84 |
+
|
| 85 |
+
這是 docs/05 的主線實驗。單一 pread 在本機約 7 ms(1.9 MB / 275 MB/s),
|
| 86 |
+
序列讀三段就是 ~21 ms 的延遲鏈,而且這條鏈的長度與執行緒數無關。
|
| 87 |
+
"""
|
| 88 |
+
out = [{"name": "baseline", "args": {}}]
|
| 89 |
+
for n in (4, 8, 12, 16, 24, 32, 48):
|
| 90 |
+
out.append({"name": f"io-threads={n}", "args": {},
|
| 91 |
+
"env": {"SDQ_IO_THREADS": str(n)}})
|
| 92 |
+
out.append({"name": "io-parallel=off", "args": {},
|
| 93 |
+
"env": {"SDQ_IO_PARALLEL": "0"}})
|
| 94 |
+
return out
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _suite_cache() -> list[dict]:
|
| 98 |
+
"""快取策略:這是命中率與 I/O 量的主戰場。"""
|
| 99 |
+
return [
|
| 100 |
+
{"name": "baseline", "args": {}},
|
| 101 |
+
{"name": "hot-pin=1024", "args": {}, "env": {"SDQ_HOT_PIN": "1024"}},
|
| 102 |
+
{"name": "hot-pin=3072", "args": {}, "env": {"SDQ_HOT_PIN": "3072"}},
|
| 103 |
+
{"name": "admit=2nd", "args": {}, "env": {"SDQ_ADMIT_MIN_HITS": "2"}},
|
| 104 |
+
{"name": "modeA(cap=256)", "args": {}, "env": {"SDQ_PARTIAL_CAP_MB": "256"}},
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _suite_slot() -> list[dict]:
|
| 109 |
+
"""槽位配置:大小兩種槽位 vs 全部照最大的配。
|
| 110 |
+
|
| 111 |
+
背景:這個模型 37 層的 expert 是 1900544 B,層 34/38/39 是 2039808 B(down 用 Q6_K)。
|
| 112 |
+
舊做法所有槽位都照最大的配 → arena 有 6.8% 是浪費掉的,而 arena 在 8 GB 預算裡
|
| 113 |
+
是最貴的資源(實測 +5% 槽位 ≈ +8~9% tok/s)。
|
| 114 |
+
SDQ_FORCE_UNIFORM_SLOT=1 回到舊行為當對照。
|
| 115 |
+
"""
|
| 116 |
+
return [
|
| 117 |
+
{"name": "baseline(大小兩種槽位)", "args": {}},
|
| 118 |
+
{"name": "uniform(全部照大槽位)", "args": {}, "env": {"SDQ_FORCE_UNIFORM_SLOT": "1"}},
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _suite_arena() -> list[dict]:
|
| 123 |
+
"""arena 大小掃描:驗證「槽位數 → tok/s」的斜率,並確認 8 GB 預算內的上限。"""
|
| 124 |
+
out = [{"name": "baseline", "args": {}}]
|
| 125 |
+
for mb in (3072, 3584, 4096):
|
| 126 |
+
out.append({"name": f"arena={mb}MB", "args": {"arena_mb": str(mb)}})
|
| 127 |
+
return out
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _suite_pf() -> list[dict]:
|
| 131 |
+
"""跨層預取:layer L 開頭就替 layer L+1 排預取。
|
| 132 |
+
|
| 133 |
+
背景:end_token() 是在一個 token 的 40 層「全部算完」之後才排預取的,
|
| 134 |
+
那時沒有任何計算可以重疊 → 實測 prefetch 使用率只有 0~8 %,等於整條路徑是關的。
|
| 135 |
+
這一組量的是「把預取搬到每層開頭」有沒有價值。
|
| 136 |
+
"""
|
| 137 |
+
return [
|
| 138 |
+
{"name": "baseline(層開頭預取)", "args": {}, "env": {"SDQ_PREFETCH_LAYER_AHEAD": "1"}},
|
| 139 |
+
{"name": "layer-ahead=off", "args": {}, "env": {"SDQ_PREFETCH_LAYER_AHEAD": "0"}},
|
| 140 |
+
{"name": "prefetch=off", "args": {}, "env": {"SDQ_PREFETCH": "0"}},
|
| 141 |
+
{"name": "pf(queue=256,stale=1)", "args": {},
|
| 142 |
+
"env": {"SDQ_PREFETCH_LAYER_AHEAD": "1", "SDQ_PREFETCH_QUEUE": "256",
|
| 143 |
+
"SDQ_PREFETCH_STALE_TOKENS": "1"}},
|
| 144 |
+
]
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# The former "spec" suite drove speculative decoding through SDQ_NGRAM_DRAFT, an
|
| 148 |
+
# env var that no longer exists anywhere in cpp/. Prompt-lookup n-gram drafting was
|
| 149 |
+
# measured at zero accepted tokens on this model (see docs/NEGATIVE_RESULTS.md §4)
|
| 150 |
+
# and MTP has its own entry point (cpp/sdq_mtp.cpp -> epq-mtp, see docs/MTP.md),
|
| 151 |
+
# so the suite was dead code that would silently no-op rather than measure anything.
|
| 152 |
+
SUITES = {"quick": _suite_quick, "io": _suite_io, "cache": _suite_cache,
|
| 153 |
+
"slot": _suite_slot, "arena": _suite_arena, "pf": _suite_pf}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
# ------------------------------------------------------------------ 執行
|
| 157 |
+
def drop_cache(model: str) -> None:
|
| 158 |
+
subprocess.run([sys.executable, str(REPO / "tools" / "drop_model_cache.py"), model],
|
| 159 |
+
check=False, capture_output=True)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _limit(ram_mb: str):
|
| 163 |
+
def f():
|
| 164 |
+
n = int(ram_mb) * 1024 * 1024
|
| 165 |
+
resource.setrlimit(resource.RLIMIT_DATA, (n, n))
|
| 166 |
+
return f
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def first_generated_token(stdout: str) -> str:
|
| 170 |
+
"""取出「第一個生成 token」的**空白正規化**指紋,用來偵測輸出有沒有改變。
|
| 171 |
+
|
| 172 |
+
為什麼要把所有空白都拿掉再取前 24 個字元:
|
| 173 |
+
模型的輸出可能以換行開頭,而 stdout 是被分塊寫出的,所以同一次生成在不同
|
| 174 |
+
執行裡「assistant: 後面第一個非空行」的位置不一定相同(例如一次是
|
| 175 |
+
`assistant: <think>`、另一次是 `assistant:` 換行後才出現 `<think>`)。
|
| 176 |
+
如果指紋取「換行前的原始片段」,**完全相同的輸出**會被判成不一致。
|
| 177 |
+
|
| 178 |
+
量測(2026-10-04,本機):同一提示詞連跑 4 次(含 SDQ_FADV_DONTNEED=1
|
| 179 |
+
並丟掉 20.6 GiB page cache),stdout 長度都是 294 bytes、指紋都是 `<think>`;
|
| 180 |
+
`tools/determinism_check.py` 另外證明 48 token × 248320 vocab 的 logits
|
| 181 |
+
**逐位元組一致** → 分頁路徑是決定性的,過去那些 ⚠️ 都是假警報。
|
| 182 |
+
|
| 183 |
+
抓不到內容時回空字串,呼叫端不去比對空指紋(寧可少檢查也不要假警報)。
|
| 184 |
+
"""
|
| 185 |
+
lines = stdout.splitlines()
|
| 186 |
+
started = False
|
| 187 |
+
chunks: list[str] = []
|
| 188 |
+
for line in lines:
|
| 189 |
+
if not started:
|
| 190 |
+
if line.startswith("assistant:"):
|
| 191 |
+
started = True
|
| 192 |
+
chunks.append(line[len("assistant:"):])
|
| 193 |
+
continue
|
| 194 |
+
if line.startswith("[sdq]"):
|
| 195 |
+
continue
|
| 196 |
+
chunks.append(line)
|
| 197 |
+
text = re.sub(r"\s+", "", "".join(chunks))
|
| 198 |
+
return text[:24]
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def run_once(binary: str, model: str, exp: dict, prompt: str, idx: int) -> dict:
|
| 202 |
+
stats_file = Path("/tmp") / f"sdqexp_{os.getpid()}_{idx}.jsonl"
|
| 203 |
+
if stats_file.exists():
|
| 204 |
+
stats_file.unlink()
|
| 205 |
+
|
| 206 |
+
cmd = [binary, "-m", model, "-p", prompt, "-n", FIXED["n_predict"],
|
| 207 |
+
"-c", FIXED["ctx"], "-t", str(exp.get("args", {}).get("threads", 8)),
|
| 208 |
+
"-b", str(exp.get("args", {}).get("ubatch", FIXED["ubatch"])), "--temp", FIXED["temp"],
|
| 209 |
+
"--no-chat", "--no-interactive", "--stats-file", str(stats_file),
|
| 210 |
+
"--ram-budget-mb", FIXED["ram_mb"]]
|
| 211 |
+
if exp.get("args", {}).get("arena_mb"):
|
| 212 |
+
cmd += ["--arena-mb", str(exp["args"]["arena_mb"])]
|
| 213 |
+
if exp.get("args", {}).get("no_prefetch"):
|
| 214 |
+
cmd += ["--no-prefetch"]
|
| 215 |
+
|
| 216 |
+
env = dict(os.environ)
|
| 217 |
+
env.update(exp.get("env", {}))
|
| 218 |
+
env.setdefault("SDQ_FADV_DONTNEED", "1")
|
| 219 |
+
|
| 220 |
+
drop_cache(model)
|
| 221 |
+
t0 = time.time()
|
| 222 |
+
try:
|
| 223 |
+
proc = subprocess.run(cmd, capture_output=True, text=True, env=env,
|
| 224 |
+
preexec_fn=_limit(FIXED["ram_mb"]), timeout=900)
|
| 225 |
+
except subprocess.TimeoutExpired:
|
| 226 |
+
return {"error": "timeout"}
|
| 227 |
+
wall = time.time() - t0
|
| 228 |
+
if proc.returncode != 0:
|
| 229 |
+
return {"error": f"rc={proc.returncode}", "stderr": proc.stderr[-800:]}
|
| 230 |
+
|
| 231 |
+
stats = {}
|
| 232 |
+
if stats_file.exists():
|
| 233 |
+
for line in stats_file.read_text().splitlines():
|
| 234 |
+
rec = json.loads(line)
|
| 235 |
+
stats[rec["phase"]] = rec
|
| 236 |
+
stats_file.unlink(missing_ok=True)
|
| 237 |
+
|
| 238 |
+
dec = stats.get("decode") or {}
|
| 239 |
+
tok_s = 0.0
|
| 240 |
+
m = re.search(r"decode: (\d+) tok / ([\d.]+)s = ([\d.]+) tok/s", proc.stderr)
|
| 241 |
+
if m:
|
| 242 |
+
tok_s = float(m.group(3))
|
| 243 |
+
peak_rss = 0
|
| 244 |
+
vmswap = 0
|
| 245 |
+
m = re.search(r"峰值 (\d+) MB / (?:swap|本行程 VmSwap) (\d+) MB", proc.stderr)
|
| 246 |
+
if m:
|
| 247 |
+
peak_rss, vmswap = int(m.group(1)), int(m.group(2))
|
| 248 |
+
|
| 249 |
+
# 正確性指紋:第一個生成 token(用來偵測「最佳化偷偷改變了模型行為」)
|
| 250 |
+
first_tok = first_generated_token(proc.stdout)
|
| 251 |
+
|
| 252 |
+
return {
|
| 253 |
+
"tok_s": tok_s,
|
| 254 |
+
"wall_s": round(wall, 2),
|
| 255 |
+
"hit_rate": dec.get("hit_rate"),
|
| 256 |
+
"ssd_mb_per_tok": (dec.get("ssd_bytes_per_token") or 0) / 2 ** 20,
|
| 257 |
+
"peak_rss_mb": peak_rss,
|
| 258 |
+
"vmswap_mb": vmswap,
|
| 259 |
+
"n_slots": dec.get("n_slots"),
|
| 260 |
+
"evictions": dec.get("evictions"),
|
| 261 |
+
"prefetch_used_rate": dec.get("prefetch_used_rate"),
|
| 262 |
+
"first_tok": first_tok,
|
| 263 |
+
"text": proc.stdout.strip()[-260:],
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def run_exp(binary: str, model: str, exp: dict, repeat: int, prompts: list[str]) -> dict:
|
| 268 |
+
runs = []
|
| 269 |
+
for r in range(repeat):
|
| 270 |
+
res = run_once(binary, model, exp, prompts[r % len(prompts)], r)
|
| 271 |
+
if res.get("error"):
|
| 272 |
+
return {"name": exp["name"], "error": res["error"], "stderr": res.get("stderr", "")}
|
| 273 |
+
runs.append(res)
|
| 274 |
+
ok = [r for r in runs if r["tok_s"] > 0]
|
| 275 |
+
if not ok:
|
| 276 |
+
return {"name": exp["name"], "error": "沒有任何一次量到 tok/s"}
|
| 277 |
+
med = lambda k: statistics.median([r[k] for r in ok if r.get(k) is not None]) if any(r.get(k) is not None for r in ok) else None
|
| 278 |
+
# 只比較「同一個提示詞」的重複執行:實驗會輪替提示詞,跨提示詞比字串沒有意義。
|
| 279 |
+
# **空指紋不參與比較**:抓不到第一個 token 時回 "",那是 stdout 分塊造成的,
|
| 280 |
+
# 不是輸出不一致(見 first_generated_token 的說明)。寧可少檢查也不要假警報。
|
| 281 |
+
by_prompt: dict[str, set] = {}
|
| 282 |
+
for i, r in enumerate(ok):
|
| 283 |
+
if not r["first_tok"]:
|
| 284 |
+
continue
|
| 285 |
+
by_prompt.setdefault(PROMPTS[i % len(PROMPTS)], set()).add(r["first_tok"])
|
| 286 |
+
unstable = [p[:20] for p, v in by_prompt.items() if len(v) > 1]
|
| 287 |
+
# 有沒有真的比到東西(沒比到就不能宣稱「輸出穩定」)
|
| 288 |
+
checked = sum(len(v) for v in by_prompt.values()) - len(by_prompt)
|
| 289 |
+
return {
|
| 290 |
+
"name": exp["name"],
|
| 291 |
+
"args": exp.get("args", {}), "env": exp.get("env", {}),
|
| 292 |
+
"tok_s": round(med("tok_s"), 3),
|
| 293 |
+
"tok_s_all": [r["tok_s"] for r in ok],
|
| 294 |
+
"hit_rate": round(med("hit_rate"), 2) if med("hit_rate") is not None else None,
|
| 295 |
+
"ssd_mb_per_tok": round(med("ssd_mb_per_tok"), 1) if med("ssd_mb_per_tok") is not None else None,
|
| 296 |
+
"peak_rss_mb": med("peak_rss_mb"),
|
| 297 |
+
"vmswap_mb": med("vmswap_mb"),
|
| 298 |
+
"n_slots": med("n_slots"),
|
| 299 |
+
"evictions": med("evictions"),
|
| 300 |
+
"prefetch_used_rate": med("prefetch_used_rate"),
|
| 301 |
+
"stable_output": not unstable,
|
| 302 |
+
"stable_checked": checked, # 實際有幾組「同提示詞重複」可比
|
| 303 |
+
"unstable_prompts": unstable,
|
| 304 |
+
"first_tok": ok[0]["first_tok"],
|
| 305 |
+
"runs": len(ok),
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# ------------------------------------------------------------------ 報告
|
| 310 |
+
def to_markdown(results: list[dict]) -> str:
|
| 311 |
+
head = ("| 實驗 | decode tok/s | 命中率 | SSD MB/token | 峰值 RSS | swap | 槽數 | 淘汰 | 預取使用率 | 輸出穩定 |")
|
| 312 |
+
sep = "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | :---: |"
|
| 313 |
+
lines = [head, sep]
|
| 314 |
+
base = next((r for r in results if r["name"] in ("baseline", "threads=8")), None)
|
| 315 |
+
bt = base["tok_s"] if base else None
|
| 316 |
+
for r in results:
|
| 317 |
+
if r.get("error"):
|
| 318 |
+
lines.append(f"| {r['name']} | ❌ {r['error']} | | | | | | | | |")
|
| 319 |
+
continue
|
| 320 |
+
delta = ""
|
| 321 |
+
if bt:
|
| 322 |
+
d = (r["tok_s"] - bt) / bt * 100
|
| 323 |
+
delta = f" ({d:+.1f}%)"
|
| 324 |
+
lines.append(
|
| 325 |
+
f"| {r['name']} | {r['tok_s']}{delta} | {r['hit_rate']}% | {r['ssd_mb_per_tok']} | "
|
| 326 |
+
f"{r['peak_rss_mb']} MB | {r['vmswap_mb']} MB | {r['n_slots']} | {r['evictions']} | "
|
| 327 |
+
f"{r['prefetch_used_rate']}% | "
|
| 328 |
+
f"{'✅' if r['stable_output'] else ('⚠️不一致' if r.get('stable_checked') else '—未比到')} |")
|
| 329 |
+
return "\n".join(lines)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def main() -> int:
|
| 333 |
+
ap = argparse.ArgumentParser(description="SDQ 最佳化實驗")
|
| 334 |
+
ap.add_argument("--model", required=True)
|
| 335 |
+
ap.add_argument("--bin", default=str(REPO.parent / "llama.cpp" / "build" / "bin" / "epq-chat"),
|
| 336 |
+
help="epq-chat 路徑(預設 <repo>/../llama.cpp/build/bin/epq-chat)")
|
| 337 |
+
ap.add_argument("--suite", default="quick", choices=sorted(SUITES) + ["all"])
|
| 338 |
+
ap.add_argument("--only", default="", help="只跑名稱包含這些關鍵字的實驗(逗號分隔)")
|
| 339 |
+
ap.add_argument("--repeat", type=int, default=2)
|
| 340 |
+
ap.add_argument("--out", default=str(REPO / "bench" / "results"))
|
| 341 |
+
ap.add_argument("--label", default="")
|
| 342 |
+
args = ap.parse_args()
|
| 343 |
+
|
| 344 |
+
exps: list[dict] = []
|
| 345 |
+
names = sorted(SUITES) if args.suite == "all" else [args.suite]
|
| 346 |
+
for n in names:
|
| 347 |
+
exps += SUITES[n]()
|
| 348 |
+
# 去掉重複名稱(不同 suite 可能有 baseline)
|
| 349 |
+
seen, uniq = set(), []
|
| 350 |
+
for e in exps:
|
| 351 |
+
if e["name"] in seen:
|
| 352 |
+
continue
|
| 353 |
+
seen.add(e["name"])
|
| 354 |
+
uniq.append(e)
|
| 355 |
+
if args.only:
|
| 356 |
+
keys = [k.strip() for k in args.only.split(",") if k.strip()]
|
| 357 |
+
uniq = [e for e in uniq if any(k in e["name"] for k in keys)]
|
| 358 |
+
|
| 359 |
+
print(f"[exp] 模型:{args.model}")
|
| 360 |
+
print(f"[exp] 執行檔:{args.bin}")
|
| 361 |
+
print(f"[exp] 實驗數:{len(uniq)},每個跑 {args.repeat} 次取中位數")
|
| 362 |
+
print(f"[exp] 固定條件:{FIXED}")
|
| 363 |
+
print()
|
| 364 |
+
|
| 365 |
+
results = []
|
| 366 |
+
for e in uniq:
|
| 367 |
+
print(f"[exp] → {e['name']} ...", end=" ", flush=True)
|
| 368 |
+
r = run_exp(args.bin, args.model, e, args.repeat, PROMPTS)
|
| 369 |
+
results.append(r)
|
| 370 |
+
if r.get("error"):
|
| 371 |
+
print(f"❌ {r['error']}")
|
| 372 |
+
else:
|
| 373 |
+
print(f"{r['tok_s']} tok/s 命中率 {r['hit_rate']}% SSD {r['ssd_mb_per_tok']} MB/tok"
|
| 374 |
+
+ ("" if r["stable_output"] or not r.get("stable_checked")
|
| 375 |
+
else " ⚠️ 輸出不一致"))
|
| 376 |
+
|
| 377 |
+
Path(args.out).mkdir(parents=True, exist_ok=True)
|
| 378 |
+
label = args.label or args.suite
|
| 379 |
+
jf = Path(args.out) / f"exp_{label}.json"
|
| 380 |
+
jf.write_text(json.dumps({"fixed": FIXED, "repeat": args.repeat,
|
| 381 |
+
"results": results}, ensure_ascii=False, indent=1), encoding="utf-8")
|
| 382 |
+
md = to_markdown(results)
|
| 383 |
+
mf = Path(args.out) / f"exp_{label}.md"
|
| 384 |
+
mf.write_text(md + "\n", encoding="utf-8")
|
| 385 |
+
|
| 386 |
+
print()
|
| 387 |
+
print(md)
|
| 388 |
+
print()
|
| 389 |
+
print(f"[exp] JSON → {jf}")
|
| 390 |
+
print(f"[exp] Markdown → {mf}")
|
| 391 |
+
return 0
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
if __name__ == "__main__":
|
| 395 |
+
sys.exit(main())
|
sdq/hf_sync.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face 儲存庫同步:commit 並上傳程式碼與量測結果到 EPM model repo。
|
| 2 |
+
|
| 3 |
+
用法:
|
| 4 |
+
|
| 5 |
+
# commit + 上傳(需要 HF_TOKEN,或 ~/.cache/huggingface/token)
|
| 6 |
+
python -m sdq.hf_sync push -m "說明這次改了什麼"
|
| 7 |
+
|
| 8 |
+
# 只看本地狀態(給接手的人 / AI)
|
| 9 |
+
python -m sdq.hf_sync status
|
| 10 |
+
|
| 11 |
+
# 只下載 HF 上的 code(給 demo.py 用)
|
| 12 |
+
python -m sdq.hf_sync pull --dest /path/to/dir
|
| 13 |
+
|
| 14 |
+
# 從 patch 重新產生 patches/0001-sdq-expert-pager.patch
|
| 15 |
+
python -m sdq.hf_sync regen-patch --src /path/to/llama.cpp
|
| 16 |
+
|
| 17 |
+
`push` 會尊重 `.gitignore`:模型權重、建置產物與 `tools/epq_lock.so`
|
| 18 |
+
(由 `tools/strict8g.py` 在執行時編譯)不會被上傳。
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import os
|
| 25 |
+
import subprocess
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
REPO_DIR = Path(__file__).resolve().parent.parent
|
| 29 |
+
|
| 30 |
+
DEFAULT_HF_REPO = os.environ.get(
|
| 31 |
+
"EPM_HF_REPO",
|
| 32 |
+
os.environ.get("SDQ_HF_REPO", "HelloSun/minicpm47woek"),
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
# demo.py fetch_code() 下載的檔案類型。新增檔案時這裡也要跟著改,否則
|
| 36 |
+
# 使用者從 HF 拉下來的 code 會缺東西。
|
| 37 |
+
CODE_PATTERNS = [
|
| 38 |
+
"*.py", "*.md", "*.c",
|
| 39 |
+
"cpp/*", "docs/*", "patches/*", "tools/*", "sdq/*.py",
|
| 40 |
+
"bench/expert_index.json",
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def git(*args: str) -> str:
|
| 45 |
+
return subprocess.run(
|
| 46 |
+
["git", *args], cwd=REPO_DIR, check=True, capture_output=True, text=True
|
| 47 |
+
).stdout.strip()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _git_optional(*args: str) -> str:
|
| 51 |
+
r = subprocess.run(["git", *args], cwd=REPO_DIR, capture_output=True, text=True)
|
| 52 |
+
return r.stdout.strip() if r.returncode == 0 else ""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _token() -> str | None:
|
| 56 |
+
token = os.environ.get("HF_TOKEN")
|
| 57 |
+
if token:
|
| 58 |
+
return token
|
| 59 |
+
p = Path.home() / ".cache" / "huggingface" / "token"
|
| 60 |
+
return p.read_text().strip() if p.exists() else None
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def push(message: str, hf_repo: str = DEFAULT_HF_REPO) -> None:
|
| 64 |
+
git("add", "-A")
|
| 65 |
+
if not git("diff", "--cached", "--name-only"):
|
| 66 |
+
print("[sync] 沒有變更,略過 commit")
|
| 67 |
+
else:
|
| 68 |
+
git("commit", "-m", message)
|
| 69 |
+
print(f"[sync] 已 commit:{message}")
|
| 70 |
+
|
| 71 |
+
from huggingface_hub import HfApi
|
| 72 |
+
|
| 73 |
+
api = HfApi(token=_token())
|
| 74 |
+
try:
|
| 75 |
+
api.create_repo(hf_repo, repo_type="model", exist_ok=True)
|
| 76 |
+
except Exception as exc:
|
| 77 |
+
print(f"[sync] create_repo: {exc}")
|
| 78 |
+
ignore = REPO_DIR / ".gitignore"
|
| 79 |
+
patterns = ([ln.strip() for ln in ignore.read_text().splitlines() if ln.strip()]
|
| 80 |
+
if ignore.exists() else [])
|
| 81 |
+
api.upload_folder(
|
| 82 |
+
folder_path=str(REPO_DIR),
|
| 83 |
+
repo_id=hf_repo,
|
| 84 |
+
repo_type="model",
|
| 85 |
+
ignore_patterns=patterns or None,
|
| 86 |
+
commit_message=message,
|
| 87 |
+
)
|
| 88 |
+
print(f"[sync] 已上傳到 https://huggingface.co/{hf_repo}")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def status() -> None:
|
| 92 |
+
print(f"[sync] repo:{REPO_DIR}")
|
| 93 |
+
print(f"[sync] 遠端:{_git_optional('remote', '-v').splitlines()[:1]}")
|
| 94 |
+
log = _git_optional("log", "--oneline", "-10")
|
| 95 |
+
print(f"\n最近提交:\n{log or '(無)'}")
|
| 96 |
+
print(f"\n工作區狀態:\n{_git_optional('status', '--short') or '(乾淨)'}")
|
| 97 |
+
docs = sorted(p.name for p in (REPO_DIR / "docs").glob("*.md"))
|
| 98 |
+
print(f"\ndocs/:{', '.join(docs) if docs else '(空)'}")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def pull(dest: str) -> None:
|
| 102 |
+
from huggingface_hub import snapshot_download
|
| 103 |
+
|
| 104 |
+
path = snapshot_download(
|
| 105 |
+
repo_id=DEFAULT_HF_REPO, repo_type="model", local_dir=dest,
|
| 106 |
+
token=_token(), allow_patterns=CODE_PATTERNS,
|
| 107 |
+
)
|
| 108 |
+
print(f"[sync] 已下載 code 到 {path}")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def regen_patch(src_dir: str) -> None:
|
| 112 |
+
"""把 cpp/ 的引擎檔重新產生成 llama.cpp 可套用的 patch。
|
| 113 |
+
|
| 114 |
+
`patches/` 裡的 patch 是給 `demo.py` 從零重建 llama.cpp 用的。改了
|
| 115 |
+
`cpp/*.cpp` 之後一定要重跑這個指令,否則從 HF 下載的使用者會拿到舊引擎。
|
| 116 |
+
"""
|
| 117 |
+
from sdq.config import PATCH_FILE, SDQ_SOURCE_FILES
|
| 118 |
+
|
| 119 |
+
src = Path(src_dir).resolve()
|
| 120 |
+
if not (src / ".git").exists():
|
| 121 |
+
raise SystemExit(f"{src} 不是 git 工作目錄")
|
| 122 |
+
if not (src / "src").exists():
|
| 123 |
+
raise SystemExit(f"{src}/src 不存在")
|
| 124 |
+
for f in SDQ_SOURCE_FILES:
|
| 125 |
+
cur = REPO_DIR / "cpp" / f
|
| 126 |
+
dst = src / "src" / f
|
| 127 |
+
if cur.exists() and dst.exists() and cur.read_bytes() != dst.read_bytes():
|
| 128 |
+
dst.write_bytes(cur.read_bytes())
|
| 129 |
+
print(f"[sync] 已同步 cpp/{f} → {src}/src/{f}")
|
| 130 |
+
|
| 131 |
+
out = REPO_DIR / PATCH_FILE
|
| 132 |
+
# `git diff` does not see untracked files. The engine sources are new files in
|
| 133 |
+
# the llama.cpp tree, so mark them intent-to-add first -- without this the
|
| 134 |
+
# patch would carry only the small edits to CMakeLists/llama-graph/llama-model
|
| 135 |
+
# and a fresh clone would build without any of the pager code.
|
| 136 |
+
subprocess.run(["git", "add", "-N", *[f"src/{f}" for f in SDQ_SOURCE_FILES]],
|
| 137 |
+
cwd=src, check=True, capture_output=True)
|
| 138 |
+
diff = subprocess.run(
|
| 139 |
+
["git", "diff", "--binary", "--", "src/CMakeLists.txt", "src/llama-graph.cpp",
|
| 140 |
+
"src/llama-model.cpp", *[f"src/{f}" for f in SDQ_SOURCE_FILES]],
|
| 141 |
+
cwd=src, capture_output=True, text=True, check=True,
|
| 142 |
+
).stdout
|
| 143 |
+
if not diff.strip():
|
| 144 |
+
raise SystemExit("diff 是空的:llama.cpp 樹裡已經沒有 EPM 的改動?")
|
| 145 |
+
out.write_text(diff, encoding="utf-8")
|
| 146 |
+
print(f"[sync] 已重新產生 {out}({len(diff)} bytes)")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def main() -> None:
|
| 150 |
+
ap = argparse.ArgumentParser(description="Hugging Face 同步")
|
| 151 |
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 152 |
+
|
| 153 |
+
p_push = sub.add_parser("push", help="commit 並上傳 HF")
|
| 154 |
+
p_push.add_argument("-m", "--message", default=None)
|
| 155 |
+
p_push.add_argument("--repo", default=DEFAULT_HF_REPO)
|
| 156 |
+
|
| 157 |
+
sub.add_parser("status", help="印出目前狀態")
|
| 158 |
+
p_pull = sub.add_parser("pull", help="下載 HF 上的 code")
|
| 159 |
+
p_pull.add_argument("--dest", required=True)
|
| 160 |
+
p_patch = sub.add_parser("regen-patch", help="從 cpp/ 重新產生 llama.cpp patch")
|
| 161 |
+
p_patch.add_argument("--src", required=True, help="已套用 patch 的 llama.cpp 原始碼目錄")
|
| 162 |
+
|
| 163 |
+
args = ap.parse_args()
|
| 164 |
+
if args.cmd == "push":
|
| 165 |
+
push(args.message or "update", args.repo)
|
| 166 |
+
elif args.cmd == "status":
|
| 167 |
+
status()
|
| 168 |
+
elif args.cmd == "pull":
|
| 169 |
+
pull(args.dest)
|
| 170 |
+
elif args.cmd == "regen-patch":
|
| 171 |
+
regen_patch(args.src)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
main()
|
sdq/strict_bench.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""EPM 嚴格 8 GiB / no-swap 基準測量:配對、交錯、每次都受上限管治。
|
| 3 |
+
|
| 4 |
+
為什麼要自己寫一個 driver,而不是直接呼叫 tools/strict8g.py
|
| 5 |
+
--------------------------------------------------------
|
| 6 |
+
EPM 的 decode 速度在共享主機上跑一次就有 ±25% 的抖動(同一組設定、三次執行
|
| 7 |
+
可以差 30%)。任何「A 比 B 快」的分別,在這種狀況下如果只是照順序各跑三次,
|
| 8 |
+
量到的多半是主機負載的漂移,不是程式差異。
|
| 9 |
+
|
| 10 |
+
所以這個 driver 做兩件事:
|
| 11 |
+
|
| 12 |
+
1. **交錯**:每一輪把各個 arm 依序跑完,下一輪旋轉順序(round-robin)。讓任何
|
| 13 |
+
慢性的主機負載趨勢平均掉,而不是全部落在某一個 arm 上。
|
| 14 |
+
2. **配對**:以「同一輪內的 arm」為單位比較,並回報每個 arm 的中位數與
|
| 15 |
+
全距(min–max),讓讀者自己看抖動有多大,而不是只看一個漂亮的平均值。
|
| 16 |
+
|
| 17 |
+
每次執行都被 tools/strict8g.py 包起來,所以每個樣本都同時帶著
|
| 18 |
+
峰值匿名 RSS、VmSwap 與 mlock 覆蓋量 —— 結論不會建立在未受管治的執行上。
|
| 19 |
+
|
| 20 |
+
用法
|
| 21 |
+
----
|
| 22 |
+
# 兩個 binary 配對比較
|
| 23 |
+
python3 -m sdq.strict_bench --model /models/m.gguf \
|
| 24 |
+
--bins baseline=/path/epq-chat-baseline current=/path/epq-chat \
|
| 25 |
+
--rounds 7 --strict-mb 8192
|
| 26 |
+
|
| 27 |
+
# 同一個 binary 比較不同 pager 預算
|
| 28 |
+
python3 -m sdq.strict_bench --model /models/m.gguf \
|
| 29 |
+
--bins old="--ram-budget-mb 8192" new="--ram-budget-mb 8192" \
|
| 30 |
+
--rounds 5
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import argparse
|
| 36 |
+
import json
|
| 37 |
+
import re
|
| 38 |
+
import statistics
|
| 39 |
+
import subprocess
|
| 40 |
+
import sys
|
| 41 |
+
import time
|
| 42 |
+
from pathlib import Path
|
| 43 |
+
|
| 44 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 45 |
+
STRICT = REPO / "tools" / "strict8g.py"
|
| 46 |
+
|
| 47 |
+
DEFAULT_PROMPTS = [
|
| 48 |
+
"Write a short paragraph about how mixture-of-experts models route tokens.",
|
| 49 |
+
"請用三句話說明什麼是 SSD、RAM、CPU 之間的資料分層。",
|
| 50 |
+
"請列出五種日常會用到電腦的工作,並各用一句話說明。",
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
# EPM writes these counters already scaled: hit_rate and prefetch_used_rate are
|
| 54 |
+
# percentages in the JSONL, only ssd_bytes_per_token needs converting.
|
| 55 |
+
_STATS_KEYS = {
|
| 56 |
+
"hit_rate": "hit_rate_pct",
|
| 57 |
+
"ssd_bytes_per_token": "ssd_mb_per_token",
|
| 58 |
+
"prefetch_used_rate": "prefetch_used_pct",
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _decode_re(txt: str):
|
| 63 |
+
"""epq-chat reports `decode: N tok / T s = V tok/s`; epq-mtp reports
|
| 64 |
+
`generated=N drafted=D accepted=A acceptance=X% speed=V tok/s`. Accept both so
|
| 65 |
+
the MTP target can be benchmarked through the same driver."""
|
| 66 |
+
m = re.search(r"decode:\s*(\d+) tok / ([\d.]+)s = ([\d.]+) tok/s", txt)
|
| 67 |
+
if m:
|
| 68 |
+
return int(m.group(1)), float(m.group(2)), float(m.group(3))
|
| 69 |
+
m = re.search(r"generated=(\d+).*?speed=([\d.]+) tok/s", txt)
|
| 70 |
+
if m:
|
| 71 |
+
return int(m.group(1)), float("nan"), float(m.group(2))
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _slots_re(txt: str):
|
| 76 |
+
m = re.search(r"slots=(\d+)\s+arena=([\d.]+) GiB", txt)
|
| 77 |
+
return (int(m.group(1)), float(m.group(2))) if m else (None, None)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def run_once(arm: dict, args, stats_file: Path, prompt: str, round_i: int) -> dict:
|
| 81 |
+
if stats_file.exists():
|
| 82 |
+
stats_file.unlink()
|
| 83 |
+
out_dir = args.workdir / "runs"
|
| 84 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 85 |
+
out_txt = out_dir / f"{arm['name']}_r{round_i}.out"
|
| 86 |
+
|
| 87 |
+
# epq-chat and epq-mtp have completely separate argument parsers, and
|
| 88 |
+
# epq-mtp's returns false on the first flag it does not recognise -- so the
|
| 89 |
+
# flag set has to be built per entry point rather than shared.
|
| 90 |
+
common = [arm["bin"], "-m", args.model, "-p", prompt, "-n", str(args.n_predict),
|
| 91 |
+
"-c", str(args.ctx), "-t", str(args.threads), "-b", str(args.ubatch)]
|
| 92 |
+
if args.cli == "mtp":
|
| 93 |
+
cmd = common + list(arm["opts"]) + args.extra
|
| 94 |
+
# epq-mtp has no --stats-file, so hit rate / SSD bytes are unavailable for
|
| 95 |
+
# this entry point; arena and slot count still come from the pager's own
|
| 96 |
+
# startup line and are parsed below.
|
| 97 |
+
if "--no-prefetch" not in cmd:
|
| 98 |
+
cmd.append("--no-prefetch")
|
| 99 |
+
else:
|
| 100 |
+
cmd = common + ["--temp", "0", "--no-chat", "--no-interactive",
|
| 101 |
+
"--stats-file", str(stats_file)] + arm["opts"] + args.extra
|
| 102 |
+
if args.no_prefetch and "--no-prefetch" not in cmd:
|
| 103 |
+
cmd.append("--no-prefetch")
|
| 104 |
+
|
| 105 |
+
# Default the pager budget to the enforced ceiling so the arena is sized for
|
| 106 |
+
# the memory we actually cap at -- unless the arm is deliberately varying it.
|
| 107 |
+
if not args.ram_budget_mb and "--ram-budget-mb" not in arm["opts"]:
|
| 108 |
+
cmd += ["--ram-budget-mb", str(args.strict_mb)]
|
| 109 |
+
cmd = [str(c) for c in cmd]
|
| 110 |
+
rec_path = args.workdir / "strict_records.jsonl"
|
| 111 |
+
strict_cmd = [sys.executable, str(STRICT),
|
| 112 |
+
"--limit-mb", str(args.strict_mb), "--metric", args.metric,
|
| 113 |
+
"--label", f"{args.label}:{arm['name']}:r{round_i}",
|
| 114 |
+
"--record", str(rec_path), "--quiet", "--"] + cmd
|
| 115 |
+
if args.strict_data_mb:
|
| 116 |
+
strict_cmd[3:3] = ["--data-mb", str(args.strict_data_mb)]
|
| 117 |
+
if args.no_lock:
|
| 118 |
+
# must go BEFORE the "--" separator, or it lands in the child argv
|
| 119 |
+
strict_cmd.insert(strict_cmd.index("--"), "--no-lock")
|
| 120 |
+
|
| 121 |
+
t0 = time.time()
|
| 122 |
+
proc = subprocess.run(strict_cmd, capture_output=True, text=True)
|
| 123 |
+
wall = time.time() - t0
|
| 124 |
+
out_txt.write_text(proc.stderr, encoding="utf-8")
|
| 125 |
+
|
| 126 |
+
strict = {}
|
| 127 |
+
try:
|
| 128 |
+
lines = rec_path.read_text(encoding="utf-8").splitlines()
|
| 129 |
+
for line in reversed(lines):
|
| 130 |
+
d = json.loads(line)
|
| 131 |
+
if d["label"] == f"{args.label}:{arm['name']}:r{round_i}":
|
| 132 |
+
strict = d
|
| 133 |
+
break
|
| 134 |
+
except OSError:
|
| 135 |
+
pass
|
| 136 |
+
|
| 137 |
+
res = {"arm": arm["name"], "round": round_i, "wall_s": round(wall, 2),
|
| 138 |
+
"returncode": strict.get("returncode", proc.returncode),
|
| 139 |
+
"strict_ok": strict.get("strict_ok", False),
|
| 140 |
+
"peak_anon_mb": strict.get("peak_anon_mb"),
|
| 141 |
+
"peak_swap_mb": strict.get("peak_swap_mb"),
|
| 142 |
+
"vm_lck_mb": strict.get("vm_lck_mb"),
|
| 143 |
+
"swap_samples_over_zero": strict.get("swap_samples_over_zero"),
|
| 144 |
+
"peak_file_mb": strict.get("peak_file_mb")}
|
| 145 |
+
|
| 146 |
+
dec = _decode_re(proc.stderr)
|
| 147 |
+
if dec:
|
| 148 |
+
res["decode_tokens"], res["decode_s"], res["decode_tok_s"] = dec
|
| 149 |
+
acc = re.search(r"generated=(\d+) drafted=(\d+) accepted=(\d+) "
|
| 150 |
+
r"acceptance=([\d.]+)%", proc.stderr)
|
| 151 |
+
if acc:
|
| 152 |
+
res["drafted"] = int(acc.group(2))
|
| 153 |
+
res["accepted"] = int(acc.group(3))
|
| 154 |
+
res["acceptance_pct"] = float(acc.group(4))
|
| 155 |
+
slots, arena = _slots_re(proc.stderr)
|
| 156 |
+
res["slots"], res["arena_gib"] = slots, arena
|
| 157 |
+
|
| 158 |
+
if stats_file.exists():
|
| 159 |
+
for line in stats_file.read_text().splitlines():
|
| 160 |
+
rec = json.loads(line)
|
| 161 |
+
if rec.get("phase") == "decode":
|
| 162 |
+
for k, out in _STATS_KEYS.items():
|
| 163 |
+
if k in rec:
|
| 164 |
+
res[out] = rec[k] / 2 ** 20 if k.endswith("_bytes_per_token") else rec[k]
|
| 165 |
+
if not args.keep_text:
|
| 166 |
+
res["stdout_tail"] = proc.stdout.strip()[-160:]
|
| 167 |
+
return res
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def stats_table(arms: list, rounds: int, results: list) -> str:
|
| 171 |
+
by = {a: [] for a in arms}
|
| 172 |
+
for r in results:
|
| 173 |
+
by[r["arm"]].append(r)
|
| 174 |
+
|
| 175 |
+
def med(vals):
|
| 176 |
+
vals = [v for v in vals if v is not None]
|
| 177 |
+
return statistics.median(vals) if vals else None
|
| 178 |
+
|
| 179 |
+
def rng(vals):
|
| 180 |
+
vals = [v for v in vals if v is not None]
|
| 181 |
+
return (min(vals), max(vals)) if vals else (None, None)
|
| 182 |
+
|
| 183 |
+
keys = [("decode_tok_s", "Decode tok/s (median)", "{:.2f}")]
|
| 184 |
+
has_stats = any(r.get("hit_rate_pct") for r in results)
|
| 185 |
+
if has_stats:
|
| 186 |
+
# epq-mtp has no --stats-file, so these columns are absent for that entry
|
| 187 |
+
# point rather than zero; better to omit the row than to print "0.00".
|
| 188 |
+
keys += [("hit_rate_pct", "expert cache hit %", "{:.2f}"),
|
| 189 |
+
("ssd_mb_per_token", "SSD MB / decode token", "{:.1f}")]
|
| 190 |
+
keys += [("slots", "slots", "{:.0f}"),
|
| 191 |
+
("arena_gib", "arena GiB", "{:.2f}"),
|
| 192 |
+
("peak_anon_mb", "peak anon RSS MiB", "{:.0f}"),
|
| 193 |
+
("peak_swap_mb", "peak VmSwap MiB", "{:.3f}"),
|
| 194 |
+
("acceptance_pct", "draft acceptance % (min-max)", None)]
|
| 195 |
+
|
| 196 |
+
lines = ["| 指標 | " + " | ".join(arms) + " |",
|
| 197 |
+
"| --- | " + " | ".join("---:" for _ in arms) + " |"]
|
| 198 |
+
for key, label, fmt in keys:
|
| 199 |
+
cells = []
|
| 200 |
+
for a in arms:
|
| 201 |
+
vals = [r.get(key) for r in by[a] if r.get(key) is not None]
|
| 202 |
+
if not vals:
|
| 203 |
+
cells.append("—")
|
| 204 |
+
elif fmt is None:
|
| 205 |
+
# Acceptance varies by prompt, not by the setting under test, so a
|
| 206 |
+
# bare median reads as a constant and hides the spread. Show the
|
| 207 |
+
# range, and "all equal" only when every round truly agreed.
|
| 208 |
+
lo, hi = min(vals), max(vals)
|
| 209 |
+
cells.append(f"{lo:.1f}" if lo == hi else f"{lo:.1f} – {hi:.1f}")
|
| 210 |
+
else:
|
| 211 |
+
cells.append(fmt.format(statistics.median(vals)))
|
| 212 |
+
lines.append(f"| {label} | " + " | ".join(cells) + " |")
|
| 213 |
+
|
| 214 |
+
lo, hi = rng([r.get("decode_tok_s") for r in by[arms[0]]])
|
| 215 |
+
lines.append("")
|
| 216 |
+
if lo is None:
|
| 217 |
+
lines.append(f"每個 arm {rounds} 輪都沒有量到 decode tok/s,請看逐輪紀錄確認失敗原因。")
|
| 218 |
+
return "\n".join(lines)
|
| 219 |
+
base = statistics.median([r["decode_tok_s"] for r in by[arms[0]]
|
| 220 |
+
if r.get("decode_tok_s")])
|
| 221 |
+
lines.append(f"每個 arm {rounds} 輪;第一個 arm 的單輪 decode tok/s 全距 {lo}–{hi},"
|
| 222 |
+
f"抖動幅度 {(hi - lo) / base * 100:.0f}%。"
|
| 223 |
+
" 請以中位數比較,不要用單輪數字。")
|
| 224 |
+
return "\n".join(lines)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def main() -> int:
|
| 228 |
+
ap = argparse.ArgumentParser(description="EPM strict 8 GiB / no-swap benchmark")
|
| 229 |
+
ap.add_argument("--model", required=True)
|
| 230 |
+
ap.add_argument("--bins", nargs="+", required=True,
|
| 231 |
+
help="arms: name=/path/to/bin, or name=\"--opt --opt\" to vary "
|
| 232 |
+
"flags on the binary given by --bin")
|
| 233 |
+
ap.add_argument("--bin", default="", help="binary used for every arm that only sets opts")
|
| 234 |
+
ap.add_argument("--rounds", type=int, default=5)
|
| 235 |
+
ap.add_argument("--strict-mb", type=int, default=8192)
|
| 236 |
+
ap.add_argument("--strict-data-mb", type=int, default=0,
|
| 237 |
+
help="RLIMIT_DATA override; 0 = --strict-mb plus VA slack")
|
| 238 |
+
ap.add_argument("--metric", choices=["anon", "total"], default="anon")
|
| 239 |
+
ap.add_argument("--cli", choices=["chat", "mtp"], default="chat",
|
| 240 |
+
help="which entry point to drive; their argument parsers are "
|
| 241 |
+
"separate and epq-mtp rejects any flag it does not know")
|
| 242 |
+
ap.add_argument("--ram-budget-mb", type=int, default=0,
|
| 243 |
+
help="EPM pager budget; 0 = same as --strict-mb so the arena is "
|
| 244 |
+
"sized for the enforced ceiling rather than for the host's RAM")
|
| 245 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 246 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 247 |
+
ap.add_argument("--ubatch", type=int, default=128)
|
| 248 |
+
ap.add_argument("--n-predict", type=int, default=48)
|
| 249 |
+
ap.add_argument("--no-prefetch", action="store_true")
|
| 250 |
+
ap.add_argument("--no-lock", action="store_true",
|
| 251 |
+
help="run the governed child without the mlock preload; required "
|
| 252 |
+
"for arenas larger than the weight set, because mlock() "
|
| 253 |
+
"makes every page resident and would report the configured "
|
| 254 |
+
"arena size as RSS instead of real usage")
|
| 255 |
+
ap.add_argument("--extra", nargs="*", default=[], help="extra flags for every run")
|
| 256 |
+
ap.add_argument("--label", default="run")
|
| 257 |
+
ap.add_argument("--workdir", default=str(REPO / "bench" / "results"))
|
| 258 |
+
ap.add_argument("--keep-text", action="store_true", help="keep full stdout in the record")
|
| 259 |
+
args = ap.parse_args()
|
| 260 |
+
|
| 261 |
+
arms = []
|
| 262 |
+
for spec in args.bins:
|
| 263 |
+
name, rhs = spec.split("=", 1)
|
| 264 |
+
rhs = rhs.strip().strip("\"'")
|
| 265 |
+
# An arm is a binary when the right-hand side is a path, and a set of
|
| 266 |
+
# flags when it is empty or starts with '-'. Deciding on the '-' prefix
|
| 267 |
+
# instead of on quoting keeps `--bins b="--a --b"` working even after the
|
| 268 |
+
# shell has already removed the quotes.
|
| 269 |
+
if rhs == "" or rhs.startswith("-"):
|
| 270 |
+
arms.append({"name": name, "bin": args.bin, "opts": rhs.split()})
|
| 271 |
+
else:
|
| 272 |
+
arms.append({"name": name, "bin": rhs, "opts": []})
|
| 273 |
+
if not all(a["bin"] for a in arms):
|
| 274 |
+
raise SystemExit("--bin is required for arms that only carry options")
|
| 275 |
+
|
| 276 |
+
args.workdir = Path(args.workdir)
|
| 277 |
+
args.workdir.mkdir(parents=True, exist_ok=True)
|
| 278 |
+
stats_file = args.workdir / "strict_stats.jsonl"
|
| 279 |
+
|
| 280 |
+
results = []
|
| 281 |
+
for r in range(args.rounds):
|
| 282 |
+
order = list(arms[r % len(arms):]) + list(arms[:r % len(arms)])
|
| 283 |
+
prompt = DEFAULT_PROMPTS[r % len(DEFAULT_PROMPTS)]
|
| 284 |
+
for arm in order:
|
| 285 |
+
res = run_once(arm, args, stats_file, prompt, r)
|
| 286 |
+
results.append(res)
|
| 287 |
+
tps = res.get("decode_tok_s")
|
| 288 |
+
print(f"[{r+1}/{args.rounds}] {arm['name']:<10} "
|
| 289 |
+
f"tok/s={'—' if tps is None else f'{tps:.2f}'} "
|
| 290 |
+
f"anon={res['peak_anon_mb']} MiB swap={res['peak_swap_mb']} "
|
| 291 |
+
f"strict_ok={res['strict_ok']}", flush=True)
|
| 292 |
+
|
| 293 |
+
names = [a["name"] for a in arms]
|
| 294 |
+
out_json = args.workdir / f"strict_{args.label}.json"
|
| 295 |
+
out_json.write_text(json.dumps({
|
| 296 |
+
"config": {k: str(v) for k, v in vars(args).items()},
|
| 297 |
+
"results": results,
|
| 298 |
+
}, ensure_ascii=False, indent=1), encoding="utf-8")
|
| 299 |
+
|
| 300 |
+
table = stats_table(names, args.rounds, results)
|
| 301 |
+
out_md = args.workdir / f"strict_{args.label}.md"
|
| 302 |
+
out_md.write_text(
|
| 303 |
+
f"# strict {args.strict_mb} MiB / no-swap — {args.label}\n\n"
|
| 304 |
+
f"- 模型:`{args.model}`\n"
|
| 305 |
+
f"- ctx={args.ctx} ubatch={args.ubatch} threads={args.threads} "
|
| 306 |
+
f"n_predict={args.n_predict} temp=0\n"
|
| 307 |
+
f"- 上限:{args.strict_mb} MiB({args.metric}),每次執行都由 "
|
| 308 |
+
f"`tools/strict8g.py` 管治;VmSwap 必須全程為 0,匿名記憶體全程 mlock\n\n"
|
| 309 |
+
f"{table}\n\n完整逐輪資料:`{out_json.name}`\n", encoding="utf-8")
|
| 310 |
+
|
| 311 |
+
print("\n" + table)
|
| 312 |
+
print(f"\n[{args.label}] → {out_md} / {out_json}")
|
| 313 |
+
return 0 if all(r["strict_ok"] for r in results) else 1
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
if __name__ == "__main__":
|
| 317 |
+
sys.exit(main())
|
sdq/verify.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""SDQ 正確性驗證:三層證據,全部要過才算數。
|
| 3 |
+
|
| 4 |
+
1. **kernel 層級**:單一 expert 的 MoE 前向 vs NumPy 純重算(照抄 ggml 的
|
| 5 |
+
dequantize_row_q4_K / q5_K / q6_K 與 quantize_row_q8_K 定義)。
|
| 6 |
+
→ 證明分頁讀到的位元組、量化點積、SiLU、加權都正確。
|
| 7 |
+
2. **整層層級**:用模型自己算出來的 cur / selected_experts / weights 丟進 NumPy 重算
|
| 8 |
+
整層 MoE 輸出 → 證明多 token、路由串接、權重縮放都正確。
|
| 9 |
+
3. **端到端 A/B**:同一支 binary、同一組 tokenizers/sampler,一邊走我們的分頁路徑、
|
| 10 |
+
一邊走 llama.cpp 原生的 mul_mat_id(整個模型在 RAM),比對生成文字與每個 token 的
|
| 11 |
+
logits → 證明圖層面接線正確、沒有漏算或重算。
|
| 12 |
+
|
| 13 |
+
用法:
|
| 14 |
+
python -m sdq.verify --model <gguf> --bin <epq-chat> [--fast]
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import argparse
|
| 20 |
+
import os
|
| 21 |
+
import struct
|
| 22 |
+
import subprocess
|
| 23 |
+
import sys
|
| 24 |
+
import tempfile
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 30 |
+
sys.path.insert(0, str(REPO / "tools"))
|
| 31 |
+
|
| 32 |
+
# 單 token 的提示詞:用來要求「逐位元組一致」(logits 必須完全相同)
|
| 33 |
+
STRICT_PROMPTS = ["Paris", "Tokyo"]
|
| 34 |
+
# 多 token 的提示詞:分頁路徑與原生路徑在「多 token prefill 的累加順序」上不同,
|
| 35 |
+
# 數學等價但浮點誤差會累積,因此這裡比較前 1 個 token 是否一致 + logits 相對誤差
|
| 36 |
+
LENIENT_PROMPTS = [
|
| 37 |
+
"The capital of France is",
|
| 38 |
+
"請用一句話介紹你自己。",
|
| 39 |
+
"Write one sentence about SSD caching.",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
DEFAULT_PROMPTS = STRICT_PROMPTS + LENIENT_PROMPTS
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def assistant_text(stdout: str) -> str:
|
| 46 |
+
"""只取模型產生的文字(去掉我們自己的 [sdq] 診斷輸出)。"""
|
| 47 |
+
out, started = [], False
|
| 48 |
+
for line in stdout.splitlines():
|
| 49 |
+
if line.startswith("assistant:"):
|
| 50 |
+
started = True
|
| 51 |
+
rest = line[len("assistant:"):].strip()
|
| 52 |
+
if rest:
|
| 53 |
+
out.append(rest)
|
| 54 |
+
continue
|
| 55 |
+
if started:
|
| 56 |
+
if line.startswith("[sdq]") or line.startswith("~llama") or not line.strip():
|
| 57 |
+
continue
|
| 58 |
+
out.append(line)
|
| 59 |
+
return "\n".join(out).strip()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def sh(cmd: list[str], env: dict | None = None, check: bool = False) -> subprocess.CompletedProcess:
|
| 63 |
+
r = subprocess.run([str(c) for c in cmd], capture_output=True, text=True, env=env)
|
| 64 |
+
if check and r.returncode != 0:
|
| 65 |
+
raise RuntimeError(f"命令失敗 ({r.returncode}): {' '.join(str(c) for c in cmd)}\n{r.stderr}")
|
| 66 |
+
return r
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def ensure_index(model: str) -> str:
|
| 70 |
+
"""Always regenerate. gguf_index.py only reads the GGUF header (~11 MB), so this
|
| 71 |
+
is cheap; reusing a stale index silently verifies the wrong byte offsets."""
|
| 72 |
+
idx = REPO / "bench" / "expert_index.json"
|
| 73 |
+
idx.parent.mkdir(parents=True, exist_ok=True)
|
| 74 |
+
sh([sys.executable, REPO / "tools" / "gguf_index.py", model, "--json", idx], check=True)
|
| 75 |
+
return str(idx)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ---------------------------------------------------------------- 1+2
|
| 79 |
+
def kernel_and_layer_checks(binary: str, model: str, layers: list[int]) -> bool:
|
| 80 |
+
ok = True
|
| 81 |
+
print("=" * 72)
|
| 82 |
+
print("[1/3] kernel 層級驗證(單一 expert vs NumPy)")
|
| 83 |
+
print("=" * 72)
|
| 84 |
+
# (39, 100) 是刻意的:層 34/38/39 的 expert 是 Q6_K,比其它層大 6.8%,
|
| 85 |
+
# 因此需要「大槽位」。 arena 改成大小兩種槽位之後,這一組是唯一會走到
|
| 86 |
+
# 大槽位配置路徑的測試 —— 少了它,槽位配置器的錯誤不會被發現。
|
| 87 |
+
for i, (il, ie) in enumerate([(0, 3), (17, 200), (33, 7), (39, 100)]):
|
| 88 |
+
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
| 89 |
+
out = f.name
|
| 90 |
+
env = dict(os.environ, SDQ_ARENA_MB="800")
|
| 91 |
+
sh([binary, "-m", model, "-c", "512", "-t", "4", "--no-interactive",
|
| 92 |
+
"--selftest", f"{il}:{ie}:7:{out}"], env=env)
|
| 93 |
+
r = sh([sys.executable, REPO / "tools" / "moe_ref.py",
|
| 94 |
+
"--model", model, "--index", ensure_index(model), "--bin", out,
|
| 95 |
+
"--layer", str(il), "--expert", str(ie)])
|
| 96 |
+
passed = "PASS" in r.stdout
|
| 97 |
+
ok &= passed
|
| 98 |
+
print(f" L{il} E{ie}: {'✅ PASS' if passed else '❌ FAIL'}")
|
| 99 |
+
if not passed:
|
| 100 |
+
print(r.stdout, r.stderr)
|
| 101 |
+
|
| 102 |
+
print()
|
| 103 |
+
print("=" * 72)
|
| 104 |
+
print("[2/3] 整層層級驗證(真實路由輸入 vs NumPy)")
|
| 105 |
+
print("=" * 72)
|
| 106 |
+
with tempfile.TemporaryDirectory() as td:
|
| 107 |
+
prefix = Path(td) / "dump"
|
| 108 |
+
env = dict(os.environ, SDQ_ARENA_MB="1500", SDQ_MOE_DUMP=str(prefix),
|
| 109 |
+
SDQ_PREFETCH="0")
|
| 110 |
+
prompt = DEFAULT_PROMPTS[1] * 3
|
| 111 |
+
sh([binary, "-m", model, "-p", prompt, "-n", "0", "--temp", "0",
|
| 112 |
+
"-c", "2048", "-t", "8", "--no-chat", "--no-interactive"], env=env)
|
| 113 |
+
for il in layers:
|
| 114 |
+
f = f"{prefix}.{il}"
|
| 115 |
+
if not Path(f).exists():
|
| 116 |
+
print(f" L{il}: ❌ 沒有產生 dump 檔")
|
| 117 |
+
ok = False
|
| 118 |
+
continue
|
| 119 |
+
r = sh([sys.executable, REPO / "tools" / "moe_layer_check.py",
|
| 120 |
+
"--model", model, "--index", ensure_index(model), "--dump", f])
|
| 121 |
+
passed = "PASS" in r.stdout
|
| 122 |
+
ok &= passed
|
| 123 |
+
tail = " | ".join(l.strip() for l in r.stdout.splitlines()[-2:])
|
| 124 |
+
print(f" L{il}: {'✅ PASS' if passed else '❌ FAIL'} {tail}")
|
| 125 |
+
|
| 126 |
+
ok &= prefetch_stress_check(binary, model, layers)
|
| 127 |
+
return ok
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def prefetch_stress_check(binary: str, model: str, layers: list[int]) -> bool:
|
| 131 |
+
"""預取路徑的正確性:預取開著時,結果必須和 NumPy 一致。
|
| 132 |
+
|
| 133 |
+
為什麼需要這一層(它是 S7 補上的):S6 引進「大小兩種槽位」時,
|
| 134 |
+
`prefetch_expert()` 沒有跟著檢查槽位尺寸,於是可能把**小槽位發給大 expert**,
|
| 135 |
+
`read_expert` 寫出槽位邊界 139 264 B → 靜默寫壞鄰居槽位。
|
| 136 |
+
這個錯誤在 [1/3] 與 [2/3] 都抓不到,因為那兩層都用 `SDQ_PREFETCH=0`。
|
| 137 |
+
|
| 138 |
+
做法:開預取 + **把 arena 壓到很小**(強制發生回收,否則預取永遠搶不到槽位、
|
| 139 |
+
錯誤路徑不會被執行)+ 開跨層預取讓預取流量最大,再拿輸出和 NumPy 比。
|
| 140 |
+
比對基準是 NumPy(決定性),不是「與 prefetch=off 的輸出比較」——
|
| 141 |
+
後者會被既有的執行緒非確定性弄成假失敗。
|
| 142 |
+
"""
|
| 143 |
+
print()
|
| 144 |
+
print("=" * 72)
|
| 145 |
+
print("[2b/3] 預取路徑驗證(預取開 + 極小 arena 強制回收 vs NumPy)")
|
| 146 |
+
print("=" * 72)
|
| 147 |
+
ok = True
|
| 148 |
+
# 512 MB arena ≈ 220 個槽位 < 一個 token 就要的 320 個 →一定會回收
|
| 149 |
+
probe_layers = [il for il in (0, 39) if il in layers] or [0, 39]
|
| 150 |
+
# 兩個變體:
|
| 151 |
+
# mixed = 大小兩種槽位(現行預設)
|
| 152 |
+
# uniform = SDQ_FORCE_UNIFORM_SLOT=1(全部照大的配)
|
| 153 |
+
# 統一尺寸這條分支要單獨測的原因:S6 的第一版在這條分支上把 n_small_ 記成全部,
|
| 154 |
+
# 結果大 expert 永遠配不到槽位、整個推論卡死。那個 bug 是**手動跑**才發現的,
|
| 155 |
+
# 沒有任何測試涵蓋 → 這裡把它補起來,避免同型錯誤再次逃過驗證。
|
| 156 |
+
variants = [
|
| 157 |
+
("mixed(大小兩種槽位)", {}),
|
| 158 |
+
("uniform(SDQ_FORCE_UNIFORM_SLOT=1)", {"SDQ_FORCE_UNIFORM_SLOT": "1"}),
|
| 159 |
+
]
|
| 160 |
+
with tempfile.TemporaryDirectory() as td:
|
| 161 |
+
prompt = DEFAULT_PROMPTS[2] * 3
|
| 162 |
+
for vname, vextra in variants:
|
| 163 |
+
prefix = Path(td) / ("pf_" + vname.split("(")[0])
|
| 164 |
+
env = dict(os.environ, SDQ_ARENA_MB="512", SDQ_MOE_DUMP=str(prefix),
|
| 165 |
+
SDQ_PREFETCH="1", SDQ_PREFETCH_LAYER_AHEAD="1",
|
| 166 |
+
SDQ_PREFETCH_QUEUE="256", SDQ_PREFETCH_STALE_TOKENS="1")
|
| 167 |
+
env.update(vextra)
|
| 168 |
+
r = sh([binary, "-m", model, "-p", prompt, "-n", "0", "--temp", "0",
|
| 169 |
+
"-c", "2048", "-t", "8", "--no-chat", "--no-interactive"], env=env)
|
| 170 |
+
# 槽位相關的致命訊息一出現就算失敗(寧可失敗也不要靜默的錯誤結果)
|
| 171 |
+
fatal = [l for l in r.stderr.splitlines()
|
| 172 |
+
if ("致命" in l or "嚴重" in l or "找不到能放" in l)]
|
| 173 |
+
if fatal:
|
| 174 |
+
ok = False
|
| 175 |
+
print(f" ❌ {vname} 回報槽位錯誤:{fatal[0][:160]}")
|
| 176 |
+
for il in probe_layers:
|
| 177 |
+
f = f"{prefix}.{il}"
|
| 178 |
+
if not Path(f).exists():
|
| 179 |
+
print(f" {vname} L{il}: ❌ 沒有產生 dump 檔")
|
| 180 |
+
ok = False
|
| 181 |
+
continue
|
| 182 |
+
r = sh([sys.executable, REPO / "tools" / "moe_layer_check.py",
|
| 183 |
+
"--model", model, "--index", ensure_index(model), "--dump", f])
|
| 184 |
+
passed = "PASS" in r.stdout
|
| 185 |
+
ok &= passed
|
| 186 |
+
tail = " | ".join(l.strip() for l in r.stdout.splitlines()[-2:])
|
| 187 |
+
print(f" {vname} L{il}: {'✅ PASS' if passed else '❌ FAIL'} {tail}")
|
| 188 |
+
return ok
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# ---------------------------------------------------------------- 3
|
| 192 |
+
def load_logits(path: str) -> np.ndarray:
|
| 193 |
+
b = Path(path).read_bytes()
|
| 194 |
+
n = struct.unpack_from("<i", b, 0)[0]
|
| 195 |
+
a = np.frombuffer(b, dtype=np.float32, offset=4)
|
| 196 |
+
k = a.size // n
|
| 197 |
+
return a[: k * n].reshape(k, n)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def end_to_end(binary: str, model: str, threads: int, prompts: list[str]) -> bool:
|
| 201 |
+
print()
|
| 202 |
+
print("=" * 72)
|
| 203 |
+
print("[3/3] 端到端 A/B(分頁路徑 vs llama.cpp 原生 mul_mat_id)")
|
| 204 |
+
print("=" * 72)
|
| 205 |
+
ok = True
|
| 206 |
+
with tempfile.TemporaryDirectory() as td:
|
| 207 |
+
for pi, prompt in enumerate(prompts):
|
| 208 |
+
la, lb = Path(td) / f"a{pi}.bin", Path(td) / f"b{pi}.bin"
|
| 209 |
+
base = [binary, "-m", model, "-p", prompt, "-n", "32", "--temp", "0",
|
| 210 |
+
"-c", "2048", "-t", str(threads), "--no-chat", "--no-interactive",
|
| 211 |
+
"--dump-logits", "--logits-out"]
|
| 212 |
+
ra = sh(base + [la, "--no-pager"])
|
| 213 |
+
rb = sh(base + [lb], env=dict(os.environ, SDQ_ARENA_MB="3000"))
|
| 214 |
+
ta, tb = assistant_text(ra.stdout), assistant_text(rb.stdout)
|
| 215 |
+
strict = pi < len(STRICT_PROMPTS)
|
| 216 |
+
same_text = ta == tb
|
| 217 |
+
n_cmp = 1 # 多步 greedy 解碼會放大浮點差,只比第一個生成 token 的 logits
|
| 218 |
+
same_argmax, rel, exact = False, None, False
|
| 219 |
+
if la.exists() and lb.exists():
|
| 220 |
+
la_, lb_ = load_logits(str(la)), load_logits(str(lb))
|
| 221 |
+
n = min(la_.shape[0], lb_[:n_cmp].shape[0])
|
| 222 |
+
if n:
|
| 223 |
+
same_argmax = bool((la_[:n].argmax(1) == lb_[:n].argmax(1)).all())
|
| 224 |
+
exact = bool(np.array_equal(la_[:n], lb_[:n]))
|
| 225 |
+
diff = np.abs(la_[:n] - lb_[:n])
|
| 226 |
+
rel = float(diff.mean() / (np.abs(la_[:n]).mean() + 1e-9))
|
| 227 |
+
mode = "嚴格(要求逐位元組一致)" if strict else "寬鬆(多 token prefill 的浮點累加順序會有差)"
|
| 228 |
+
first_ok = (la_.shape[0] > 0 and lb_.shape[0] > 0 and
|
| 229 |
+
la_[0].argmax() == lb_[0].argmax()) if (la.exists() and lb.exists()) else False
|
| 230 |
+
print(f" 提示詞 #{pi+1} [{mode}]")
|
| 231 |
+
print(f" 文字一致={'✅' if same_text else '❌'}"
|
| 232 |
+
f" 第 1 個生成 token 的 argmax 一致={'✅' if first_ok else '❌'}"
|
| 233 |
+
f" 前 {n} 個 token 的 argmax 全一致={'✅' if same_argmax else '❌'}"
|
| 234 |
+
f" logits 逐位元組相同={'✅' if exact else '否'}"
|
| 235 |
+
f" 平均相對差={rel:.2e}")
|
| 236 |
+
if not same_text:
|
| 237 |
+
print(f" 原生: {ta[:160]}")
|
| 238 |
+
print(f" 分頁: {tb[:160]}")
|
| 239 |
+
# 判定標準:以「第一個生成 token 正確」為主(全 argmax 一致在多步
|
| 240 |
+
# greedy 解碼下會被浮點差放大而失真)。完整 32 token 文字一致另外報告。
|
| 241 |
+
ok &= first_ok
|
| 242 |
+
return ok
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def main() -> int:
|
| 246 |
+
ap = argparse.ArgumentParser(description="分頁路徑的三層正確性驗證")
|
| 247 |
+
ap.add_argument("--model", required=True)
|
| 248 |
+
ap.add_argument("--bin", required=True, help="epq-chat 路徑")
|
| 249 |
+
ap.add_argument("--fast", action="store_true", help="略過端到端 A/B")
|
| 250 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 251 |
+
ap.add_argument("--layers", type=int, nargs="*", default=None)
|
| 252 |
+
a = ap.parse_args()
|
| 253 |
+
layers = a.layers if a.layers else ([0, 39] if a.fast else [0, 19, 39])
|
| 254 |
+
return run_verification(a.bin, a.model, None, threads=a.threads,
|
| 255 |
+
fast=a.fast, layers=layers)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def run_verification(binary: str, model: str, code_dir: Path = None, threads: int = 8,
|
| 259 |
+
fast: bool = False, layers: list[int] = None) -> int:
|
| 260 |
+
binary = str(binary)
|
| 261 |
+
model = str(model)
|
| 262 |
+
if layers is None:
|
| 263 |
+
layers = [0, 39] if fast else [0, 19, 39]
|
| 264 |
+
ok = True
|
| 265 |
+
ok &= kernel_and_layer_checks(binary, model, layers)
|
| 266 |
+
if fast:
|
| 267 |
+
# The end-to-end A/B compares against llama.cpp's native mul_mat_id,
|
| 268 |
+
# which needs the whole 20.6 GiB of expert weights resident. That is by
|
| 269 |
+
# construction impossible on an 8 GiB machine -- it is the whole problem
|
| 270 |
+
# EPM exists to solve -- so `--fast` skips it rather than pretending it
|
| 271 |
+
# ran. Equivalence is covered by layers 1/2 plus determinism_check.py.
|
| 272 |
+
print()
|
| 273 |
+
print("=" * 72)
|
| 274 |
+
print("[3/3] 端到端 A/B —— 已略過(--fast)")
|
| 275 |
+
print(" 這一層需要整個 22 GiB 模型常駐 RAM 作為對照組,")
|
| 276 |
+
print(" 在 8 GiB 上不可能。等價性改由 kernel / 整層 / 預取壓力")
|
| 277 |
+
print(" 三層,加上 tools/determinism_check.py 的逐位元組證據支撐。")
|
| 278 |
+
print("=" * 72)
|
| 279 |
+
else:
|
| 280 |
+
ok &= end_to_end(binary, model, threads, DEFAULT_PROMPTS)
|
| 281 |
+
print()
|
| 282 |
+
print("=" * 72)
|
| 283 |
+
print("結論:", "全部通過 ✅" if ok else "有項目失敗 ❌")
|
| 284 |
+
print("=" * 72)
|
| 285 |
+
if ok:
|
| 286 |
+
print("提醒:分頁路徑與原生路徑的 logits 會有 1e-2~1e-1 的相對差。")
|
| 287 |
+
print(" 這不是缺陷:多 token prefill 時兩條路徑的浮點累加順序不同,")
|
| 288 |
+
print(" L0 的 3.7e-09 會被 40 層非線性放大到 L39 的 9.6e-02。")
|
| 289 |
+
print(" 細節見 docs/CORRECTNESS.md §3.3。")
|
| 290 |
+
return 0 if ok else 1
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
if __name__ == "__main__":
|
| 294 |
+
sys.exit(main())
|
tools/determinism_check.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""決定性檢查:同一組條件重複執行,逐位元組比對每一個 token 的 logits。
|
| 3 |
+
|
| 4 |
+
為什麼需要這個工具
|
| 5 |
+
------------------
|
| 6 |
+
`sdq/experiments.py` 會用「同一個提示詞跑幾次,第一個生成 token 是否相同」當
|
| 7 |
+
正確性指紋。但在 S7/S8 那兩輪,這個指紋偶爾會跳出「⚠️輸出不一致」,
|
| 8 |
+
看起來像是 prefill 的累加順序或執行緒交錯造成的非確定性。
|
| 9 |
+
|
| 10 |
+
實際上那個警告是 **harness 自己的解析問題**:`re.search(r"assistant:\\s*(.*)")`
|
| 11 |
+
的 `\\s*` 會跨過換行,assistant 第一行是空的時候就抓到下一行的內容,
|
| 12 |
+
而下一行在哪裡換行取決於 stdout 有沒有剛好切成兩個 chunk。
|
| 13 |
+
|
| 14 |
+
這個工具直接比對 logits(`--dump-logits --logits-out`),把「是不是真的非確定」
|
| 15 |
+
用數據回答,而不是靠字串。量測結果(2026-10-04,本機,預設條件):
|
| 16 |
+
|
| 17 |
+
4 次相同執行 → 48 個 token × 248320 個 vocab 的 logits **全部逐位元組相同**
|
| 18 |
+
|
| 19 |
+
也就是說分頁路徑在同一台機器、同一組條件下是**決定性**的;
|
| 20 |
+
設定 `--pager-off` 之外的任何變數才會讓輸出改變。
|
| 21 |
+
|
| 22 |
+
用法
|
| 23 |
+
----
|
| 24 |
+
python tools/determinism_check.py --model <gguf> --bin <epq-chat> -n 4
|
| 25 |
+
python tools/determinism_check.py --model <gguf> --bin <epq-chat> \\
|
| 26 |
+
--runs 3 --env SDQ_FORCE_UNIFORM_SLOT=1 # 比較不同設定是否也一致
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import argparse
|
| 32 |
+
import os
|
| 33 |
+
import struct
|
| 34 |
+
import subprocess
|
| 35 |
+
import sys
|
| 36 |
+
import tempfile
|
| 37 |
+
import time
|
| 38 |
+
from pathlib import Path
|
| 39 |
+
|
| 40 |
+
import numpy as np
|
| 41 |
+
|
| 42 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 43 |
+
DEFAULT_PROMPT = "請用三句話說明什麼是 SSD、RAM、CPU 之間的資料分層。"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_logits(path: str) -> np.ndarray:
|
| 47 |
+
"""讀 epq-chat --logits-out 寫的檔案:(n_token, vocab) 的 float32。"""
|
| 48 |
+
b = Path(path).read_bytes()
|
| 49 |
+
n = struct.unpack_from("<i", b, 0)[0]
|
| 50 |
+
a = np.frombuffer(b, dtype=np.float32, offset=4)
|
| 51 |
+
k = a.size // n
|
| 52 |
+
return a[: k * n].reshape(k, n)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def run_once(binary: str, model: str, prompt: str, out: str, n_predict: int,
|
| 56 |
+
env_extra: dict, threads: int, ctx: int) -> float:
|
| 57 |
+
env = dict(os.environ)
|
| 58 |
+
env.update(env_extra)
|
| 59 |
+
cmd = [binary, "-m", model, "-p", prompt, "-n", str(n_predict), "-c", str(ctx),
|
| 60 |
+
"-t", str(threads), "--temp", "0", "--no-chat", "--no-interactive",
|
| 61 |
+
"--dump-logits", "--logits-out", out, "--ram-budget-mb", "8192"]
|
| 62 |
+
t0 = time.time()
|
| 63 |
+
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
| 64 |
+
if p.returncode != 0:
|
| 65 |
+
print(f"[det] 執行失敗 rc={p.returncode}\n{p.stderr[-1500:]}")
|
| 66 |
+
sys.exit(2)
|
| 67 |
+
return time.time() - t0
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def main() -> int:
|
| 71 |
+
ap = argparse.ArgumentParser(description="SDQ 決定性檢查(逐位元組比對 logits)")
|
| 72 |
+
ap.add_argument("--model", required=True)
|
| 73 |
+
ap.add_argument("--bin", default=str(REPO.parent / "llama.cpp" / "build" / "bin" / "epq-chat"),
|
| 74 |
+
help="epq-chat 路徑(預設 <repo>/../llama.cpp/build/bin/epq-chat)")
|
| 75 |
+
ap.add_argument("--runs", type=int, default=4, help="重複執行幾次")
|
| 76 |
+
ap.add_argument("-n", "--n-predict", type=int, default=16,
|
| 77 |
+
help="生成幾個 token(每個都會存一份 logits)")
|
| 78 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 79 |
+
ap.add_argument("--ctx", type=int, default=4096)
|
| 80 |
+
ap.add_argument("--prompt", default=DEFAULT_PROMPT)
|
| 81 |
+
ap.add_argument("--env", action="append", default=[],
|
| 82 |
+
help="額外的環境變數(可重複),用來比較不同設定")
|
| 83 |
+
args = ap.parse_args()
|
| 84 |
+
|
| 85 |
+
env_extra = dict(kv.split("=", 1) for kv in args.env)
|
| 86 |
+
print(f"[det] 條件:runs={args.runs} n_predict={args.n_predict} "
|
| 87 |
+
f"threads={args.threads} ctx={args.ctx} env={env_extra or '{}'}")
|
| 88 |
+
|
| 89 |
+
with tempfile.TemporaryDirectory() as td:
|
| 90 |
+
mats = []
|
| 91 |
+
for r in range(args.runs):
|
| 92 |
+
out = str(Path(td) / f"logits_{r}.bin")
|
| 93 |
+
dt = run_once(args.bin, args.model, args.prompt, out, args.n_predict,
|
| 94 |
+
env_extra, args.threads, args.ctx)
|
| 95 |
+
m = load_logits(out)
|
| 96 |
+
mats.append(m)
|
| 97 |
+
print(f" run {r}: {dt:5.1f}s logits shape={m.shape}")
|
| 98 |
+
|
| 99 |
+
ref = mats[0]
|
| 100 |
+
all_same = True
|
| 101 |
+
for r, m in enumerate(mats[1:], start=1):
|
| 102 |
+
if m.shape != ref.shape:
|
| 103 |
+
print(f" ❌ run{r} 的 shape 與 run0 不同:{m.shape} vs {ref.shape}")
|
| 104 |
+
all_same = False
|
| 105 |
+
continue
|
| 106 |
+
diff_rows = np.where(np.abs(ref - m).max(axis=1) > 0)[0]
|
| 107 |
+
if diff_rows.size == 0:
|
| 108 |
+
print(f" ✅ run{r} 與 run0 **逐位元組完全相同**"
|
| 109 |
+
f"({ref.shape[0]} token × {ref.shape[1]} vocab)")
|
| 110 |
+
else:
|
| 111 |
+
all_same = False
|
| 112 |
+
i = int(diff_rows[0])
|
| 113 |
+
d = float(np.abs(ref[i] - m[i]).max())
|
| 114 |
+
print(f" ❌ run{r} 從第 {i} 個 token 開始有差異(max abs={d:.3e},"
|
| 115 |
+
f"argmax run0={ref[i].argmax()} run{r}={m[i].argmax()})")
|
| 116 |
+
|
| 117 |
+
print()
|
| 118 |
+
print("[det] 結論:", "決定性 ✅(同條件重複執行逐位元組一致)" if all_same
|
| 119 |
+
else "非決定性 ❌(同條件重複執行會有差異)")
|
| 120 |
+
return 0 if all_same else 1
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
sys.exit(main())
|
tools/drop_model_cache.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""丟掉模型檔的 kernel page cache(不需 root),讓 SSD 基準測量可重現。
|
| 3 |
+
|
| 4 |
+
為什麼需要:Linux 會把讀過的檔案留在 page cache,下一次 benchmark 就會「命中核心的
|
| 5 |
+
RAM」而不是我們的 expert cache,測出來的 tok/s 就不是真的 SSD 速度。
|
| 6 |
+
我們的分頁器本身在每次 pread 之後也會對該範圍丟 DONTNEED(SDQ_FADV_DONTNEED=1),
|
| 7 |
+
這個工具則是用來清掉「上一次殘留」的部分。
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import ctypes
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
POSIX_FADV_DONTNEED = 4
|
| 18 |
+
libc = ctypes.CDLL("libc.so.6", use_errno=True)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main() -> None:
|
| 22 |
+
ap = argparse.ArgumentParser(description="丟掉某個檔案的 page cache")
|
| 23 |
+
ap.add_argument("path", help="要清掉的檔案(通常是 .gguf)")
|
| 24 |
+
args = ap.parse_args()
|
| 25 |
+
|
| 26 |
+
p = Path(args.path)
|
| 27 |
+
fd = os.open(p, os.O_RDONLY)
|
| 28 |
+
try:
|
| 29 |
+
# 先整個檔案丟一次
|
| 30 |
+
rc = libc.posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)
|
| 31 |
+
# 再逐 256 MB 丟一次(某些核心對大範圍的處理不一致)
|
| 32 |
+
chunk = 256 << 20
|
| 33 |
+
size = p.stat().st_size
|
| 34 |
+
off = 0
|
| 35 |
+
while off < size:
|
| 36 |
+
libc.posix_fadvise(fd, off, min(chunk, size - off), POSIX_FADV_DONTNEED)
|
| 37 |
+
off += chunk
|
| 38 |
+
rc2 = libc.posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)
|
| 39 |
+
print(f"已對 {p} 發出 POSIX_FADV_DONTNEED(rc={rc}, rc2={rc2}),"
|
| 40 |
+
f"{size / 2**30:.2f} GiB 的 page cache 應已釋放")
|
| 41 |
+
finally:
|
| 42 |
+
os.close(fd)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
tools/epq_lock.so.c
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* EPQ strict-mode helper — anonymous-memory locker.
|
| 2 |
+
*
|
| 3 |
+
* Linked via LD_PRELOAD. Purpose: make "no swap" a *kernel* guarantee for this
|
| 4 |
+
* process instead of something we infer afterwards by reading VmSwap.
|
| 5 |
+
*
|
| 6 |
+
* Why mlockall() is the wrong tool here
|
| 7 |
+
* -------------------------------------
|
| 8 |
+
* The obvious one-liner, mlockall(MCL_CURRENT|MCL_FUTURE), is actively wrong
|
| 9 |
+
* for this workload. MCL_FUTURE also locks file-backed mappings, so every
|
| 10 |
+
* GGUF page the process touches through mmap becomes pinned, non-reclaimable
|
| 11 |
+
* RSS. Measured on this model, that pushed a run whose real footprint is ~5.5 GiB
|
| 12 |
+
* up to 16.5 GiB of RSS — and a machine with 8 GiB of RAM does not fail because
|
| 13 |
+
* of clean page cache, it reclaims it. Locking the file mapping would measure
|
| 14 |
+
* the wrong quantity and fail a process that actually fits.
|
| 15 |
+
*
|
| 16 |
+
* What this does instead
|
| 17 |
+
* ----------------------
|
| 18 |
+
* Wrap mmap/mmap64 and lock a region only when it is anonymous
|
| 19 |
+
* (MAP_ANONYMOUS) and private. Those are exactly the pages the kernel cannot
|
| 20 |
+
* reclaim under pressure and would otherwise write to swap: the expert arena,
|
| 21 |
+
* the KV cache, the compute buffers and the C++ runtime heap. File-backed
|
| 22 |
+
* mappings stay clean and reclaimable, matching real hardware and matching what
|
| 23 |
+
* a cgroup memory.max does (reclaim cache, never swap it out).
|
| 24 |
+
*
|
| 25 |
+
* Locked pages can never be written to a swap device, so once the arena is
|
| 26 |
+
* faulted in the process is physically unswappable. A brk()-grown main heap is
|
| 27 |
+
* not covered (glibc calls the syscall internally), but it is the small
|
| 28 |
+
* allocations; every large allocation routes through mmap and is covered.
|
| 29 |
+
* /proc/<pid>/status VmLck reports exactly how much is locked, so the harness
|
| 30 |
+
* can verify the coverage numerically instead of trusting this comment.
|
| 31 |
+
*
|
| 32 |
+
* Progress is appended to $EPQ_MLOCK_STATUS so a run that is SIGKILLed still
|
| 33 |
+
* leaves evidence. Nothing else about the process is modified.
|
| 34 |
+
*/
|
| 35 |
+
#define _GNU_SOURCE
|
| 36 |
+
#include <dlfcn.h>
|
| 37 |
+
#include <stdint.h>
|
| 38 |
+
#include <stdio.h>
|
| 39 |
+
#include <stdlib.h>
|
| 40 |
+
#include <string.h>
|
| 41 |
+
#include <sys/mman.h>
|
| 42 |
+
#include <sys/types.h>
|
| 43 |
+
#include <unistd.h>
|
| 44 |
+
|
| 45 |
+
static size_t g_locked = 0;
|
| 46 |
+
static int g_lock_fail = 0;
|
| 47 |
+
static FILE * g_status = NULL;
|
| 48 |
+
static void * (*real_mmap)(void *, size_t, int, int, int, off_t) = NULL;
|
| 49 |
+
static void * (*real_mmap64)(void *, size_t, int, int, int, off_t) = NULL;
|
| 50 |
+
|
| 51 |
+
static void status_init(void) {
|
| 52 |
+
const char *path = getenv("EPQ_MLOCK_STATUS");
|
| 53 |
+
if (path && !g_status) {
|
| 54 |
+
g_status = fopen(path, "w");
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
static void status_report(void) {
|
| 59 |
+
if (!g_status) return;
|
| 60 |
+
fprintf(g_status, "anon_locked_bytes=%zu lock_failures=%d\n", g_locked, g_lock_fail);
|
| 61 |
+
fflush(g_status);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
__attribute__((constructor))
|
| 65 |
+
static void epq_lock_init(void) {
|
| 66 |
+
real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap");
|
| 67 |
+
real_mmap64 = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap64");
|
| 68 |
+
status_init();
|
| 69 |
+
atexit(status_report);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
static void lock_if_anon(void *p, size_t len, int flags) {
|
| 73 |
+
/* MAP_ANONYMOUS alone: shared anonymous mappings would double-count, and
|
| 74 |
+
* MAP_SHARED file mappings are the page cache we deliberately leave alone. */
|
| 75 |
+
if (!p || MAP_FAILED == p || !(flags & MAP_ANONYMOUS) || (flags & MAP_SHARED)) return;
|
| 76 |
+
size_t page = 4096;
|
| 77 |
+
if (mlock(p, len) == 0) g_locked += len;
|
| 78 |
+
else g_lock_fail++;
|
| 79 |
+
(void) page;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t off) {
|
| 83 |
+
if (!real_mmap) real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap");
|
| 84 |
+
void *p = real_mmap(addr, length, prot, flags, fd, off);
|
| 85 |
+
lock_if_anon(p, length, flags);
|
| 86 |
+
return p;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off_t off) {
|
| 90 |
+
if (!real_mmap64) real_mmap64 = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap64");
|
| 91 |
+
void *p = real_mmap64(addr, length, prot, flags, fd, off);
|
| 92 |
+
lock_if_anon(p, length, flags);
|
| 93 |
+
return p;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* Fallbacks used when a glibc build routes through posix_memalign rather than
|
| 97 |
+
* mmap for large alignments; locking an explicit range is the same guarantee. */
|
| 98 |
+
int posix_memalign(void **out, size_t align, size_t size) {
|
| 99 |
+
static int (*real)(void **, size_t, size_t) = NULL;
|
| 100 |
+
if (!real) real = (int (*)(void **, size_t, size_t))dlsym(RTLD_NEXT, "posix_memalign");
|
| 101 |
+
int rc = real(out, align, size);
|
| 102 |
+
if (rc == 0 && *out) lock_if_anon(*out, size, MAP_ANONYMOUS | MAP_PRIVATE);
|
| 103 |
+
return rc;
|
| 104 |
+
}
|
tools/gguf_index.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""GGUF 專家索引工具:把 19.71 GB 的 MiniCPM-V-4.7-35B-A3B GGUF 拆成「常駐 RAM 部分」與「可分頁專家部分」。
|
| 3 |
+
|
| 4 |
+
只用標準函式庫讀檔頭(不做任何假設、不下載模型本體),輸出:
|
| 5 |
+
|
| 6 |
+
1. 常駐 RAM 的權重(非專家張量)總量 —— 這是 8 GB 預算裡扣不掉的部分
|
| 7 |
+
2. 每個 expert 的位元組範圍(layer, gate/up/down, 檔案 offset, 大小)—— SSD 分頁的最小單位
|
| 8 |
+
3. 一個 token 的專家工作集(working set)大小 —— RAM 快取至少要多大才不會每 token 全部重讀
|
| 9 |
+
|
| 10 |
+
輸出 JSON(--json 路徑)與人類可讀摘要(stdout)。
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import struct
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
# ggml_type -> (名稱, block 大小, 每 block 位元組數);K-quant 的 block 都是 QK_K=256
|
| 21 |
+
GGML_TYPE_INFO = {
|
| 22 |
+
0: ("F32", 1, 4),
|
| 23 |
+
1: ("F16", 1, 2),
|
| 24 |
+
2: ("Q4_0", 32, 18),
|
| 25 |
+
3: ("Q4_1", 32, 20),
|
| 26 |
+
6: ("Q5_0", 32, 22),
|
| 27 |
+
7: ("Q5_1", 32, 24),
|
| 28 |
+
8: ("Q8_0", 32, 34),
|
| 29 |
+
9: ("Q8_1", 32, 36),
|
| 30 |
+
10: ("Q2_K", 256, 84),
|
| 31 |
+
11: ("Q3_K", 256, 110),
|
| 32 |
+
12: ("Q4_K", 256, 144),
|
| 33 |
+
13: ("Q5_K", 256, 176),
|
| 34 |
+
14: ("Q6_K", 256, 210),
|
| 35 |
+
15: ("Q8_K", 256, 292),
|
| 36 |
+
16: ("IQ2_XXS", 256, 66),
|
| 37 |
+
17: ("IQ2_XS", 256, 74),
|
| 38 |
+
18: ("IQ3_XXS", 256, 98),
|
| 39 |
+
19: ("IQ1_S", 256, 50),
|
| 40 |
+
20: ("IQ4_NL", 32, 18),
|
| 41 |
+
21: ("IQ3_S", 256, 110),
|
| 42 |
+
22: ("IQ2_S", 256, 82),
|
| 43 |
+
23: ("IQ4_XS", 256, 136),
|
| 44 |
+
24: ("I8", 1, 1),
|
| 45 |
+
25: ("I16", 1, 2),
|
| 46 |
+
26: ("I32", 1, 4),
|
| 47 |
+
28: ("BF16", 1, 2),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
_SCALAR_FMT = {
|
| 51 |
+
0: ("u8", "<B", 1), 1: ("i8", "<b", 1), 2: ("u16", "<H", 2), 3: ("i16", "<h", 2),
|
| 52 |
+
4: ("u32", "<I", 4), 5: ("i32", "<i", 4), 6: ("f32", "<f", 4), 7: ("bool", "<B", 1),
|
| 53 |
+
10: ("u64", "<Q", 8), 11: ("i64", "<q", 8), 12: ("f64", "<d", 8),
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class Reader:
|
| 58 |
+
"""只讀檔頭的 GGUF 解析器(避免把 22 GB 讀進 RAM)。"""
|
| 59 |
+
|
| 60 |
+
def __init__(self, data: bytes):
|
| 61 |
+
self.b = data
|
| 62 |
+
self.o = 0
|
| 63 |
+
|
| 64 |
+
def u32(self) -> int:
|
| 65 |
+
v = struct.unpack_from("<I", self.b, self.o)[0]
|
| 66 |
+
self.o += 4
|
| 67 |
+
return v
|
| 68 |
+
|
| 69 |
+
def u64(self) -> int:
|
| 70 |
+
v = struct.unpack_from("<Q", self.b, self.o)[0]
|
| 71 |
+
self.o += 8
|
| 72 |
+
return v
|
| 73 |
+
|
| 74 |
+
def val(self, t: int):
|
| 75 |
+
if t == 8:
|
| 76 |
+
n = self.u64()
|
| 77 |
+
v = self.b[self.o:self.o + n].decode("utf-8", "replace")
|
| 78 |
+
self.o += n
|
| 79 |
+
return v
|
| 80 |
+
if t == 9:
|
| 81 |
+
et, n = self.u32(), self.u64()
|
| 82 |
+
return [self.val(et) for _ in range(n)]
|
| 83 |
+
name, fmt, size = _SCALAR_FMT[t]
|
| 84 |
+
v = struct.unpack_from(fmt, self.b, self.o)[0]
|
| 85 |
+
self.o += size
|
| 86 |
+
return v
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def read_header(path: Path, probe: int = 64 << 20):
|
| 90 |
+
with open(path, "rb") as f:
|
| 91 |
+
head = f.read(probe)
|
| 92 |
+
|
| 93 |
+
r = Reader(head)
|
| 94 |
+
if head[:4] != b"GGUF":
|
| 95 |
+
raise SystemExit(f"{path} 不是 GGUF 檔")
|
| 96 |
+
r.o = 4
|
| 97 |
+
version = r.u32()
|
| 98 |
+
n_tensors, n_kv = r.u64(), r.u64()
|
| 99 |
+
|
| 100 |
+
kv = {}
|
| 101 |
+
for _ in range(n_kv):
|
| 102 |
+
k = r.st_key()
|
| 103 |
+
t = r.u32()
|
| 104 |
+
kv[k] = r.val(t)
|
| 105 |
+
|
| 106 |
+
tensors = []
|
| 107 |
+
for _ in range(n_tensors):
|
| 108 |
+
name = r.st_key()
|
| 109 |
+
nd = r.u32()
|
| 110 |
+
dims = [r.u64() for _ in range(nd)]
|
| 111 |
+
ttype = r.u32()
|
| 112 |
+
offset = r.u64()
|
| 113 |
+
tensors.append({"name": name, "dims": dims, "type": ttype, "offset": offset})
|
| 114 |
+
|
| 115 |
+
align = kv.get("general.alignment", 32)
|
| 116 |
+
data_start = (r.o + align - 1) // align * align
|
| 117 |
+
if r.o > probe:
|
| 118 |
+
raise SystemExit(f"檔頭 {probe} bytes 不夠(需要 {r.o}),請加大 --probe")
|
| 119 |
+
return version, kv, tensors, data_start, align
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
Reader.st_key = Reader.val # placeholder, replaced below
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _st_key(self) -> str:
|
| 126 |
+
n = self.u64()
|
| 127 |
+
v = self.b[self.o:self.o + n].decode("utf-8", "replace")
|
| 128 |
+
self.o += n
|
| 129 |
+
return v
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
Reader.st_key = _st_key
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def tensor_nbytes(t: dict) -> int:
|
| 136 |
+
name, blck, tsize = GGML_TYPE_INFO[t["type"]]
|
| 137 |
+
n = 1
|
| 138 |
+
for d in t["dims"]:
|
| 139 |
+
n *= d
|
| 140 |
+
if n % blck:
|
| 141 |
+
raise SystemExit(f"{t['name']}: 元素數 {n} 不是 block 大小 {blck} 的整數")
|
| 142 |
+
return n // blck * tsize
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def n_expert(t: dict) -> int | None:
|
| 146 |
+
"""三維張量的第三維在這個模型裡就是專家編號。"""
|
| 147 |
+
return t["dims"][2] if len(t["dims"]) == 3 else None
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def analyze(path: Path) -> dict:
|
| 151 |
+
version, kv, tensors, data_start, align = read_header(path)
|
| 152 |
+
file_size = path.stat().st_size
|
| 153 |
+
arch = kv.get("general.architecture", "?")
|
| 154 |
+
prefix = f"{arch}."
|
| 155 |
+
|
| 156 |
+
def k(key, default=None):
|
| 157 |
+
return kv.get(prefix + key, default)
|
| 158 |
+
|
| 159 |
+
resident, expert_tensors = [], []
|
| 160 |
+
for t in tensors:
|
| 161 |
+
if "_exps." in t["name"] and n_expert(t):
|
| 162 |
+
expert_tensors.append(t)
|
| 163 |
+
else:
|
| 164 |
+
resident.append(t)
|
| 165 |
+
|
| 166 |
+
resident_bytes = sum(tensor_nbytes(t) for t in resident)
|
| 167 |
+
expert_bytes = sum(tensor_nbytes(t) for t in expert_tensors)
|
| 168 |
+
|
| 169 |
+
# 每個 (layer, 3 個矩陣) 的專家��分
|
| 170 |
+
layers = {}
|
| 171 |
+
for t in expert_tensors:
|
| 172 |
+
parts = t["name"].split(".")
|
| 173 |
+
layer = int(parts[1])
|
| 174 |
+
which = parts[2] # ffn_gate_exps / ffn_up_exps / ffn_down_exps
|
| 175 |
+
name, blck, tsize = GGML_TYPE_INFO[t["type"]]
|
| 176 |
+
ne = n_expert(t)
|
| 177 |
+
per = tensor_nbytes(t) // ne
|
| 178 |
+
layers.setdefault(layer, {})[which] = {
|
| 179 |
+
"file_offset": data_start + t["offset"],
|
| 180 |
+
"file_end": data_start + t["offset"] + tensor_nbytes(t),
|
| 181 |
+
"type": name,
|
| 182 |
+
"type_id": t["type"],
|
| 183 |
+
"dims": t["dims"],
|
| 184 |
+
"bytes_per_expert": per,
|
| 185 |
+
"n_expert": ne,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
per_layer = []
|
| 189 |
+
for layer in sorted(layers):
|
| 190 |
+
e = layers[layer]
|
| 191 |
+
row = {"layer": layer, "experts": e}
|
| 192 |
+
row["bytes_per_expert_total"] = sum(v["bytes_per_expert"] for v in e.values())
|
| 193 |
+
per_layer.append(row)
|
| 194 |
+
|
| 195 |
+
n_layer = k("block_count", 0)
|
| 196 |
+
n_expert_total = k("expert_count", 0)
|
| 197 |
+
n_expert_used = k("expert_used_count", 0)
|
| 198 |
+
|
| 199 |
+
# 一個 decode token 需要把所有層的 top-k 專家各讀一次
|
| 200 |
+
per_token_bytes = [r["bytes_per_expert_total"] * n_expert_used for r in per_layer]
|
| 201 |
+
|
| 202 |
+
return {
|
| 203 |
+
"file": str(path),
|
| 204 |
+
"file_size": file_size,
|
| 205 |
+
"gguf_version": version,
|
| 206 |
+
"architecture": arch,
|
| 207 |
+
"model_name": kv.get("general.name"),
|
| 208 |
+
"quant_file_type": kv.get("general.file_type"),
|
| 209 |
+
"n_layer": n_layer,
|
| 210 |
+
"n_expert": n_expert_total,
|
| 211 |
+
"n_expert_used": n_expert_used,
|
| 212 |
+
"n_embd": k("embedding_length"),
|
| 213 |
+
"n_ff_exp": k("expert_feed_forward_length"),
|
| 214 |
+
"n_ff_shexp": k("expert_shared_feed_forward_length"),
|
| 215 |
+
"context_length": k("context_length"),
|
| 216 |
+
"n_head": k("attention.head_count"),
|
| 217 |
+
"n_head_kv": k("attention.head_count_kv"),
|
| 218 |
+
"head_dim": k("attention.key_length"),
|
| 219 |
+
"rope_sections": k("rope.dimension_sections"),
|
| 220 |
+
"full_attention_interval": k("full_attention_interval"),
|
| 221 |
+
"data_start": data_start,
|
| 222 |
+
"alignment": align,
|
| 223 |
+
"resident_ram_bytes": resident_bytes,
|
| 224 |
+
"expert_total_bytes": expert_bytes,
|
| 225 |
+
"accounted_bytes": resident_bytes + expert_bytes,
|
| 226 |
+
"per_layer": per_layer,
|
| 227 |
+
"decode_working_set_bytes_max": max(per_token_bytes) if per_token_bytes else 0,
|
| 228 |
+
"decode_working_set_bytes_mean": (
|
| 229 |
+
sum(per_token_bytes) / len(per_token_bytes) if per_token_bytes else 0
|
| 230 |
+
),
|
| 231 |
+
"expert_slot_count": sum(1 for _ in expert_tensors),
|
| 232 |
+
"kv_bytes_per_token_f16": (
|
| 233 |
+
n_layer // max(k("full_attention_interval", 1), 1)
|
| 234 |
+
* max(k("attention.head_count_kv", 1), 1)
|
| 235 |
+
* max(k("attention.value_length", 1), 1)
|
| 236 |
+
* 2
|
| 237 |
+
* 2 # K + V,各 f16
|
| 238 |
+
),
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def human(n: float) -> str:
|
| 243 |
+
for unit in ("B", "KiB", "MiB", "GiB"):
|
| 244 |
+
if abs(n) < 1024 or unit == "GiB":
|
| 245 |
+
return f"{n:.2f} {unit}" if unit != "B" else f"{int(n)} B"
|
| 246 |
+
n /= 1024
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def main() -> None:
|
| 250 |
+
ap = argparse.ArgumentParser(description="MiniCPM-V-4.7-35B-A3B GGUF 專家索引 / RAM 預算分析")
|
| 251 |
+
ap.add_argument("model", help="GGUF 檔路徑")
|
| 252 |
+
ap.add_argument("--json", help="輸出完整索引 JSON")
|
| 253 |
+
ap.add_argument("--probe", type=int, default=64 << 20, help="讀取檔頭的位元組數")
|
| 254 |
+
args = ap.parse_args()
|
| 255 |
+
|
| 256 |
+
info = analyze(Path(args.model))
|
| 257 |
+
if args.json:
|
| 258 |
+
Path(args.json).write_text(json.dumps(info, indent=1), encoding="utf-8")
|
| 259 |
+
print(f"[index] 已寫出 {args.json}")
|
| 260 |
+
|
| 261 |
+
print("=" * 68)
|
| 262 |
+
print(f"模型 : {info['model_name']} ({info['architecture']})")
|
| 263 |
+
print(f"檔案大小 : {human(info['file_size'])}")
|
| 264 |
+
print(f"層數 : {info['n_layer']} 專家數/層: {info['n_expert']} 每 token 啟用: {info['n_expert_used']}")
|
| 265 |
+
print(f"隱層維度 : {info['n_embd']} 專家 FF: {info['n_ff_exp']} 共用專家 FF: {info['n_ff_shexp']}")
|
| 266 |
+
print(f"注意力 : {info['n_head']} head / {info['n_head_kv']} kv-head, head_dim={info['head_dim']},"
|
| 267 |
+
f" 每 {info['full_attention_interval']} 層一次全注意力")
|
| 268 |
+
print(f"RoPE : sections={info['rope_sections']} 原生 context={info['context_length']}")
|
| 269 |
+
print("-" * 68)
|
| 270 |
+
print(f"常駐 RAM : {human(info['resident_ram_bytes'])} (非專家權重,無法分頁)")
|
| 271 |
+
print(f"專家總量 : {human(info['expert_total_bytes'])} ({info['expert_slot_count']} 個 expert 槽)")
|
| 272 |
+
print("單一專家 : ", end="")
|
| 273 |
+
sizes = {}
|
| 274 |
+
for row in info["per_layer"]:
|
| 275 |
+
for which, v in row["experts"].items():
|
| 276 |
+
sizes.setdefault(v["type"], set()).add(v["bytes_per_expert"])
|
| 277 |
+
for t, s in sizes.items():
|
| 278 |
+
print(f"{t}={human(sorted(s)[0])}/expert ", end="")
|
| 279 |
+
print()
|
| 280 |
+
print(f"每 token 專家工作集: 平均 {human(info['decode_working_set_bytes_mean'])}"
|
| 281 |
+
f" 最大 {human(info['decode_working_set_bytes_max'])}")
|
| 282 |
+
print(f"KV cache : f16 約 {human(info['kv_bytes_per_token_f16'])}/token"
|
| 283 |
+
f" → 4K={human(info['kv_bytes_per_token_f16']*4096)}"
|
| 284 |
+
f" 1M={human(info['kv_bytes_per_token_f16']*1048576)}")
|
| 285 |
+
print("=" * 68)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
if __name__ == "__main__":
|
| 289 |
+
main()
|
tools/io_probe.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""量測「這個儲存裝置到底能有多快」——本專案所有 I/O 最佳化的前提。
|
| 3 |
+
|
| 4 |
+
為什麼需要這個工具:分頁引擎的每一層都要等 SSD。如果不知道單次讀取的延遲與
|
| 5 |
+
頻寬,就只能靠猜(例如「多開執行緒會更快」)。實測結果常常相反:
|
| 6 |
+
**單一 1.9 MB 隨機讀約 7 ms,而且執行緒再多也不會變快**(見下方輸出),
|
| 7 |
+
因為瓶頸是單流頻寬(約 275 MB/s),不是延遲也不是佇列深度。這正是
|
| 8 |
+
「I/O 段平行」這個最佳化能生效的原因:它不是開更多執行緒,而是把
|
| 9 |
+
「一個 expert 的三段循序讀」變成「同時讀」。
|
| 10 |
+
|
| 11 |
+
用法
|
| 12 |
+
----
|
| 13 |
+
python tools/io_probe.py --model /path/to/model.gguf
|
| 14 |
+
python tools/io_probe.py --model xxx.gguf --chunk 1900544 --parallel 1,2,4,8,16,24
|
| 15 |
+
|
| 16 |
+
輸出
|
| 17 |
+
----
|
| 18 |
+
每個併發度一列:median / mean / p90 的單次讀取延遲,以及整體聚合頻寬。
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import os
|
| 25 |
+
import random
|
| 26 |
+
import statistics
|
| 27 |
+
import threading
|
| 28 |
+
import time
|
| 29 |
+
|
| 30 |
+
PAGE = os.sysconf("SC_PAGE_SIZE")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def drop_cache(fd: int, off: int, length: int) -> None:
|
| 34 |
+
"""把剛讀過的範圍從 kernel page cache 丟掉,否則下一輪量到的是 RAM 不是 SSD。"""
|
| 35 |
+
start = off - (off % PAGE)
|
| 36 |
+
end = ((off + length + PAGE - 1) // PAGE) * PAGE
|
| 37 |
+
try:
|
| 38 |
+
os.posix_fadvise(fd, start, end - start, os.POSIX_FADV_DONTNEED)
|
| 39 |
+
except OSError:
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def measure(fd: int, size: int, chunk: int, parallel: int, n: int) -> dict:
|
| 44 |
+
random.seed(11)
|
| 45 |
+
max_off = max(1, size - chunk - PAGE)
|
| 46 |
+
offsets = [random.randrange(0, max_off) for _ in range(n)]
|
| 47 |
+
bufs = [bytearray(chunk) for _ in range(parallel)]
|
| 48 |
+
idx = [0]
|
| 49 |
+
lock = threading.Lock()
|
| 50 |
+
lat: list[float] = []
|
| 51 |
+
|
| 52 |
+
def work(tid: int) -> None:
|
| 53 |
+
local: list[float] = []
|
| 54 |
+
while True:
|
| 55 |
+
with lock:
|
| 56 |
+
i = idx[0]
|
| 57 |
+
if i >= len(offsets):
|
| 58 |
+
break
|
| 59 |
+
idx[0] += 1
|
| 60 |
+
off = offsets[i]
|
| 61 |
+
mv = memoryview(bufs[tid])
|
| 62 |
+
t0 = time.perf_counter()
|
| 63 |
+
os.preadv(fd, [mv], off)
|
| 64 |
+
local.append((time.perf_counter() - t0) * 1000.0)
|
| 65 |
+
drop_cache(fd, off, chunk)
|
| 66 |
+
with lock:
|
| 67 |
+
lat.extend(local)
|
| 68 |
+
|
| 69 |
+
ths = [threading.Thread(target=work, args=(i,)) for i in range(parallel)]
|
| 70 |
+
t_start = time.perf_counter()
|
| 71 |
+
for t in ths:
|
| 72 |
+
t.start()
|
| 73 |
+
for t in ths:
|
| 74 |
+
t.join()
|
| 75 |
+
wall = time.perf_counter() - t_start
|
| 76 |
+
lat.sort()
|
| 77 |
+
return {
|
| 78 |
+
"parallel": parallel,
|
| 79 |
+
"n": len(lat),
|
| 80 |
+
"median_ms": round(statistics.median(lat), 2),
|
| 81 |
+
"mean_ms": round(statistics.mean(lat), 2),
|
| 82 |
+
"p90_ms": round(lat[int(len(lat) * 0.9)], 2),
|
| 83 |
+
"wall_s": round(wall, 3),
|
| 84 |
+
"agg_MBps": round(len(lat) * chunk / wall / 1e6, 1),
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def main() -> int:
|
| 89 |
+
ap = argparse.ArgumentParser(description="SDQ 儲存裝置 I/O 特性量測")
|
| 90 |
+
ap.add_argument("--model", required=True, help="拿來當讀取目標的檔案(通常就是 gguf)")
|
| 91 |
+
ap.add_argument("--chunk", type=int, default=1_900_000, help="單次讀取的位元組數")
|
| 92 |
+
ap.add_argument("--parallel", default="1,2,4,8,16,24", help="要量哪幾種併發度")
|
| 93 |
+
ap.add_argument("--n", type=int, default=64, help="每個併發度讀幾次")
|
| 94 |
+
args = ap.parse_args()
|
| 95 |
+
|
| 96 |
+
size = os.path.getsize(args.model)
|
| 97 |
+
fd = os.open(args.model, os.O_RDONLY)
|
| 98 |
+
print(f"檔案:{args.model}({size / 2**30:.1f} GiB)")
|
| 99 |
+
print(f"單次讀取 {args.chunk} B,每個併發度 {args.n} 次")
|
| 100 |
+
print()
|
| 101 |
+
print(f"{'併發':>5} {'中位延遲':>10} {'平均':>8} {'p90':>8} {'聚合頻寬':>12}")
|
| 102 |
+
try:
|
| 103 |
+
for p in [int(x) for x in args.parallel.split(",")]:
|
| 104 |
+
r = measure(fd, size, args.chunk, p, args.n)
|
| 105 |
+
print(f"{r['parallel']:5d} {r['median_ms']:9.2f}ms {r['mean_ms']:7.2f}ms "
|
| 106 |
+
f"{r['p90_ms']:7.2f}ms {r['agg_MBps']:9.1f} MB/s")
|
| 107 |
+
finally:
|
| 108 |
+
os.close(fd)
|
| 109 |
+
print()
|
| 110 |
+
print("讀法:如果『中位延遲』不隨併發度下降,代表單次讀取是頻寬受限的;")
|
| 111 |
+
print(" 這時候開更多執行緒只會讓聚合頻寬變高,不會縮短關鍵路徑。")
|
| 112 |
+
return 0
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
raise SystemExit(main())
|
tools/moe_layer_check.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""用 epq-chat 的 SDQ_MOE_DUMP 檔,驗證「整層」MoE 的計算(真實路由輸入)。
|
| 3 |
+
|
| 4 |
+
這是最接近模型實際執行的驗證:不是隨機 activation,而是模型自己算出來的
|
| 5 |
+
cur / selected_experts / weights,然後檢查我們的輸出是否等於 NumPy 重算的結果。
|
| 6 |
+
"""
|
| 7 |
+
import argparse, json, struct, sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import numpy as np
|
| 10 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 11 |
+
from moe_ref import quant_q8_K, read_expert
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
ap = argparse.ArgumentParser()
|
| 15 |
+
ap.add_argument("--model", required=True)
|
| 16 |
+
ap.add_argument("--index", required=True)
|
| 17 |
+
ap.add_argument("--dump", required=True)
|
| 18 |
+
ap.add_argument("--tol", type=float, default=3e-3)
|
| 19 |
+
a = ap.parse_args()
|
| 20 |
+
|
| 21 |
+
b = open(a.dump, "rb").read()
|
| 22 |
+
n_embd, n_tok, n_used, n_expert, _, _ = struct.unpack_from("<6i", b, 0)
|
| 23 |
+
off = 24
|
| 24 |
+
cur = np.frombuffer(b, dtype=np.float32, count=n_embd * n_tok, offset=off).reshape(n_tok, n_embd)
|
| 25 |
+
off += 4 * n_embd * n_tok
|
| 26 |
+
ids = np.frombuffer(b, dtype=np.int32, count=n_used * n_tok, offset=off).reshape(n_tok, n_used)
|
| 27 |
+
off += 4 * n_used * n_tok
|
| 28 |
+
wts = np.frombuffer(b, dtype=np.float32, count=n_used * n_tok, offset=off).reshape(n_tok, n_used)
|
| 29 |
+
off += 4 * n_used * n_tok
|
| 30 |
+
got = np.frombuffer(b, dtype=np.float32, count=n_embd * n_tok, offset=off).reshape(n_tok, n_embd)
|
| 31 |
+
|
| 32 |
+
idx = json.loads(Path(a.index).read_text())
|
| 33 |
+
il = int(Path(a.dump).name.rsplit(".", 1)[1])
|
| 34 |
+
layer = idx["per_layer"][il]["experts"]
|
| 35 |
+
n_ff = idx["n_ff_exp"]
|
| 36 |
+
out = np.zeros((n_tok, n_embd), dtype=np.float32)
|
| 37 |
+
|
| 38 |
+
# Worst-case effect on one output element of a single Q8_K LSB flip in h.
|
| 39 |
+
# ggml accumulates the gate/up dots in float32 while this reference is exact,
|
| 40 |
+
# so the two can disagree by ~1e-6 on h. When that crosses a Q8_K rounding
|
| 41 |
+
# boundary one code moves by 1 and the layer output jumps by far more than
|
| 42 |
+
# float noise. See docs/CORRECTNESS.md: a 1-LSB flip alone produced 7.9e-4.
|
| 43 |
+
lsb = np.zeros(n_ff, dtype=np.float64)
|
| 44 |
+
|
| 45 |
+
with open(a.model, "rb") as fh:
|
| 46 |
+
cache = {}
|
| 47 |
+
for t in range(n_tok):
|
| 48 |
+
for k in range(n_used):
|
| 49 |
+
e = int(ids[t, k])
|
| 50 |
+
if e not in cache:
|
| 51 |
+
g, u, d = layer["ffn_gate_exps"], layer["ffn_up_exps"], layer["ffn_down_exps"]
|
| 52 |
+
Wg = read_expert(fh, g["file_offset"] + e * g["bytes_per_expert"], n_ff * n_embd, g["type"]).reshape(n_ff, n_embd)
|
| 53 |
+
Wu = read_expert(fh, u["file_offset"] + e * u["bytes_per_expert"], n_ff * n_embd, u["type"]).reshape(n_ff, n_embd)
|
| 54 |
+
Wd = read_expert(fh, d["file_offset"] + e * d["bytes_per_expert"], n_embd * n_ff, d["type"]).reshape(n_embd, n_ff)
|
| 55 |
+
cache[e] = (Wg, Wu, Wd)
|
| 56 |
+
Wg, Wu, Wd = cache[e]
|
| 57 |
+
xq = quant_q8_K(cur[t])
|
| 58 |
+
# float64 for the gate/up dots: this is the reference, so it should
|
| 59 |
+
# be the more accurate of the two.
|
| 60 |
+
go = Wg.astype(np.float64) @ xq.astype(np.float64)
|
| 61 |
+
uo = Wu.astype(np.float64) @ xq.astype(np.float64)
|
| 62 |
+
h = ((go / (1.0 + np.exp(-go))) * uo).astype(np.float32)
|
| 63 |
+
if t == 0 and k == 0:
|
| 64 |
+
ab = np.abs(h)
|
| 65 |
+
for bi in range(h.size // 256):
|
| 66 |
+
blk = ab[bi * 256:(bi + 1) * 256]
|
| 67 |
+
lsb[bi * 256:(bi + 1) * 256] = blk.max() / 127.0 if blk.max() > 0 else 0.0
|
| 68 |
+
out[t] += (Wd @ quant_q8_K(h)) * wts[t, k]
|
| 69 |
+
|
| 70 |
+
diff = np.abs(out - got)
|
| 71 |
+
scale = np.maximum(np.abs(out), 1e-2)
|
| 72 |
+
rel = (diff / scale).max()
|
| 73 |
+
# bound[r] = sum_j |Wd[r,j]| * d_h(block(j)) = worst case for one flipped code
|
| 74 |
+
bound = np.zeros((n_embd, n_ff), dtype=np.float64)
|
| 75 |
+
for t in range(n_tok):
|
| 76 |
+
for k in range(n_used):
|
| 77 |
+
Wd = cache[int(ids[t, k])][2]
|
| 78 |
+
bound += np.abs(Wd.astype(np.float64)) * lsb[None, :]
|
| 79 |
+
print(f"layer {il} n_tok={n_tok} n_used={n_used} ids[0]={ids[0].tolist()}")
|
| 80 |
+
print(f" NumPy out[0,:5]={out[0,:5]}")
|
| 81 |
+
print(f" SDQ out[0,:5]={got[0,:5]}")
|
| 82 |
+
print(f" max abs diff={diff.max():.6g} max rel={rel:.6g} 1-LSB-flip bound={bound.max():.6g}")
|
| 83 |
+
ok = rel <= a.tol
|
| 84 |
+
verdict = "PASS" if ok else ("PASS (quantization-boundary)" if diff.max() <= bound.max() else "FAIL")
|
| 85 |
+
print(" 結果:", verdict + (" ✅" if ok else (" ✅" if verdict != "FAIL" else " ❌")))
|
| 86 |
+
sys.exit(0 if ok or verdict != "FAIL" else 1)
|
| 87 |
+
|
| 88 |
+
main()
|
tools/moe_ref.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""SDQ MoE kernel 的 NumPy 對照實作(單元測試)。
|
| 3 |
+
|
| 4 |
+
用途:拿 C++ 端 `epq-chat --selftest IL:IE:SEED:OUT.bin` 算出來的結果,
|
| 5 |
+
用純 NumPy 從 GGUF 的同一段位元組重算一次,確認分頁 + 量化點積 + SiLU + 加權
|
| 6 |
+
這條路徑的數學完全正確。
|
| 7 |
+
|
| 8 |
+
實作完全照抄 ggml 的定義:
|
| 9 |
+
* dequantize_row_q4_K / dequantize_row_q5_K / dequantize_row_q6_K
|
| 10 |
+
* quantize_row_q8_K_ref(Q8_K activation)
|
| 11 |
+
* SiLU(x) = x * sigmoid(x)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import struct
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
QK_K = 256
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_scale_min_k4(j: int, q: np.ndarray) -> tuple[int, int]:
|
| 27 |
+
if j < 4:
|
| 28 |
+
return int(q[j] & 63), int(q[j + 4] & 63)
|
| 29 |
+
return ((int(q[j + 4] & 0xF) | ((int(q[j - 4]) >> 6) << 4)),
|
| 30 |
+
((int(q[j + 4]) >> 4) | ((int(q[j]) >> 6) << 4)))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def fp16_to_fp32(raw: bytes) -> float:
|
| 34 |
+
return struct.unpack("<e", raw)[0]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def dequant_q4_K(raw: bytes, n: int) -> np.ndarray:
|
| 38 |
+
nb = n // QK_K
|
| 39 |
+
out = np.empty(n, dtype=np.float32)
|
| 40 |
+
for i in range(nb):
|
| 41 |
+
d = fp16_to_fp32(raw[i * 144:i * 144 + 2])
|
| 42 |
+
dmin = fp16_to_fp32(raw[i * 144 + 2:i * 144 + 4])
|
| 43 |
+
scales = np.frombuffer(raw, dtype=np.uint8, count=12, offset=i * 144 + 4)
|
| 44 |
+
qs = np.frombuffer(raw, dtype=np.uint8, count=128, offset=i * 144 + 16)
|
| 45 |
+
base = i * QK_K
|
| 46 |
+
is_ = 0
|
| 47 |
+
for j in range(0, QK_K, 64):
|
| 48 |
+
sc1, m1 = get_scale_min_k4(is_ + 0, scales)
|
| 49 |
+
sc2, m2 = get_scale_min_k4(is_ + 1, scales)
|
| 50 |
+
d1, mm1 = d * sc1, dmin * m1
|
| 51 |
+
d2, mm2 = d * sc2, dmin * m2
|
| 52 |
+
q = qs[j // 2: j // 2 + 32]
|
| 53 |
+
out[base + j: base + j + 32] = d1 * (q & 0xF).astype(np.float32) - mm1
|
| 54 |
+
out[base + j + 32: base + j + 64] = d2 * (q >> 4).astype(np.float32) - mm2
|
| 55 |
+
is_ += 2
|
| 56 |
+
return out
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def dequant_q5_K(raw: bytes, n: int) -> np.ndarray:
|
| 60 |
+
nb = n // QK_K
|
| 61 |
+
out = np.empty(n, dtype=np.float32)
|
| 62 |
+
for i in range(nb):
|
| 63 |
+
o = i * 176
|
| 64 |
+
d = fp16_to_fp32(raw[o:o + 2])
|
| 65 |
+
dmin = fp16_to_fp32(raw[o + 2:o + 4])
|
| 66 |
+
scales = np.frombuffer(raw, dtype=np.uint8, count=12, offset=o + 4)
|
| 67 |
+
qh = np.frombuffer(raw, dtype=np.uint8, count=32, offset=o + 16)
|
| 68 |
+
qs = np.frombuffer(raw, dtype=np.uint8, count=128, offset=o + 48)
|
| 69 |
+
base = i * QK_K
|
| 70 |
+
is_ = 0
|
| 71 |
+
u1, u2 = 1, 2
|
| 72 |
+
for j in range(0, QK_K, 64):
|
| 73 |
+
sc1, m1 = get_scale_min_k4(is_ + 0, scales)
|
| 74 |
+
sc2, m2 = get_scale_min_k4(is_ + 1, scales)
|
| 75 |
+
d1, mm1 = d * sc1, dmin * m1
|
| 76 |
+
d2, mm2 = d * sc2, dmin * m2
|
| 77 |
+
q = qs[j // 2: j // 2 + 32]
|
| 78 |
+
# ggml 的迴圈只推進 ql,qh 永遠從 l=0 開始讀
|
| 79 |
+
h1 = ((qh[0:32] & u1) != 0).astype(np.float32) * 16
|
| 80 |
+
h2 = ((qh[0:32] & u2) != 0).astype(np.float32) * 16
|
| 81 |
+
out[base + j: base + j + 32] = d1 * ((q & 0xF).astype(np.float32) + h1) - mm1
|
| 82 |
+
out[base + j + 32: base + j + 64] = d2 * ((q >> 4).astype(np.float32) + h2) - mm2
|
| 83 |
+
is_ += 2
|
| 84 |
+
u1 <<= 2
|
| 85 |
+
u2 <<= 2
|
| 86 |
+
return out
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def dequant_q6_K(raw: bytes, n: int) -> np.ndarray:
|
| 90 |
+
"""逐位元組對照 ggml/src/ggml-quants.c 的 dequantize_row_q6_K。
|
| 91 |
+
|
| 92 |
+
block_q6_K(210 bytes):ql[128] qh[64] scales[16(int8)] d(fp16)
|
| 93 |
+
對每個 128 元素半區塊(n 迴圈,ql += 64 / qh += 32 / sc += 8):
|
| 94 |
+
q1 = (ql[l] & 0xF) | ((qh[l] >> 0 & 3) << 4) → y[l]
|
| 95 |
+
q2 = (ql[l+32] & 0xF) | ((qh[l] >> 2 & 3) << 4) → y[l+32]
|
| 96 |
+
q3 = (ql[l] >> 4) | ((qh[l] >> 4 & 3) << 4) → y[l+64]
|
| 97 |
+
q4 = (ql[l+32] >> 4) | ((qh[l] >> 6 & 3) << 4) → y[l+96]
|
| 98 |
+
縮放:y[l] 用 sc[is+0]、y[l+32] 用 sc[is+2]、y[l+64] 用 sc[is+4]、y[l+96] 用 sc[is+6],
|
| 99 |
+
其中 is = l / 16(l<16 用偶數、l>=16 用緊接的奇數)→ 等價於「每 16 個元素一個 scale」。
|
| 100 |
+
"""
|
| 101 |
+
nb = n // QK_K
|
| 102 |
+
out = np.empty(n, dtype=np.float32)
|
| 103 |
+
for i in range(nb):
|
| 104 |
+
o = i * 210
|
| 105 |
+
ql = np.frombuffer(raw, dtype=np.uint8, count=128, offset=o)
|
| 106 |
+
qh = np.frombuffer(raw, dtype=np.uint8, count=64, offset=o + 128)
|
| 107 |
+
sc = np.frombuffer(raw, dtype=np.int8, count=16, offset=o + 192).astype(np.float32)
|
| 108 |
+
d = fp16_to_fp32(raw[o + 208:o + 210])
|
| 109 |
+
base = i * QK_K
|
| 110 |
+
for half in range(2):
|
| 111 |
+
qa = ql[half * 64: half * 64 + 64].astype(np.int32) # ql[l], ql[l+32]
|
| 112 |
+
ha = qh[half * 32: half * 32 + 32].astype(np.int32) # qh[l]
|
| 113 |
+
sa = sc[half * 8: half * 8 + 8] # 8 個 scale,每 16 元素一個
|
| 114 |
+
scale = np.repeat(sa, 16) # 長度 128,對應整個 128 元素半區塊
|
| 115 |
+
|
| 116 |
+
lo = qa[:32] & 0xF # ql[l] 低 4 bit
|
| 117 |
+
hi = qa[32:] & 0xF # ql[l+32] 低 4 bit
|
| 118 |
+
blo = qa[:32] >> 4 # ql[l] 高 4 bit
|
| 119 |
+
bhi = qa[32:] >> 4 # ql[l+32] 高 4 bit
|
| 120 |
+
|
| 121 |
+
q1 = lo | ((ha & 0x3) << 4)
|
| 122 |
+
q2 = hi | (((ha >> 2) & 0x3) << 4)
|
| 123 |
+
q3 = blo | (((ha >> 4) & 0x3) << 4)
|
| 124 |
+
q4 = bhi | (((ha >> 6) & 0x3) << 4)
|
| 125 |
+
|
| 126 |
+
p = base + half * 128
|
| 127 |
+
out[p + 0: p + 32] = d * (q1 - 32) * scale[0:32]
|
| 128 |
+
out[p + 32: p + 64] = d * (q2 - 32) * scale[32:64]
|
| 129 |
+
out[p + 64: p + 96] = d * (q3 - 32) * scale[64:96]
|
| 130 |
+
out[p + 96: p + 128] = d * (q4 - 32) * scale[96:128]
|
| 131 |
+
return out
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def quant_q8_K(x: np.ndarray) -> np.ndarray:
|
| 135 |
+
"""照抄 quantize_row_q8_K_ref:回傳反量化後的 activation(float32)。"""
|
| 136 |
+
n = x.size
|
| 137 |
+
out = np.empty(n, dtype=np.float32)
|
| 138 |
+
for i in range(n // QK_K):
|
| 139 |
+
blk = x[i * QK_K:(i + 1) * QK_K]
|
| 140 |
+
ax = np.abs(blk)
|
| 141 |
+
amax = ax.max()
|
| 142 |
+
if amax == 0:
|
| 143 |
+
out[i * QK_K:(i + 1) * QK_K] = 0
|
| 144 |
+
continue
|
| 145 |
+
# ggml 取「第一個達到 amax 的元素」作為 max(含正負號)
|
| 146 |
+
idx = int(np.argmax(ax))
|
| 147 |
+
maxv = float(blk[idx])
|
| 148 |
+
iscale = -127.0 / maxv
|
| 149 |
+
q = np.clip(np.rint(iscale * blk), -127, 127).astype(np.int8)
|
| 150 |
+
out[i * QK_K:(i + 1) * QK_K] = q.astype(np.float32) / iscale
|
| 151 |
+
return out
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def read_expert(fh, off: int, n: int, typ: str) -> np.ndarray:
|
| 155 |
+
fh.seek(off)
|
| 156 |
+
raw = fh.read(n)
|
| 157 |
+
if typ == "Q4_K":
|
| 158 |
+
return dequant_q4_K(raw, n)
|
| 159 |
+
if typ == "Q5_K":
|
| 160 |
+
return dequant_q5_K(raw, n)
|
| 161 |
+
if typ == "Q6_K":
|
| 162 |
+
return dequant_q6_K(raw, n)
|
| 163 |
+
raise SystemExit(f"selftest 暫不支援 {typ}")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def main() -> None:
|
| 167 |
+
ap = argparse.ArgumentParser(description="SDQ MoE kernel 的 NumPy 對照")
|
| 168 |
+
ap.add_argument("--model", required=True)
|
| 169 |
+
ap.add_argument("--index", required=True, help="tools/gguf_index.py 產生的 JSON")
|
| 170 |
+
ap.add_argument("--bin", required=True, help="epq-chat --selftest 的輸出")
|
| 171 |
+
ap.add_argument("--layer", type=int, required=True)
|
| 172 |
+
ap.add_argument("--expert", type=int, required=True)
|
| 173 |
+
ap.add_argument("--n-tok", type=int, default=1, help="幾個 token 共用同一個 expert")
|
| 174 |
+
ap.add_argument("--tol", type=float, default=2e-3, help="相對誤差上限")
|
| 175 |
+
args = ap.parse_args()
|
| 176 |
+
|
| 177 |
+
idx = json.loads(Path(args.index).read_text())
|
| 178 |
+
layer = idx["per_layer"][args.layer]["experts"]
|
| 179 |
+
n_embd = idx["n_embd"]
|
| 180 |
+
n_ff = idx["n_ff_exp"]
|
| 181 |
+
e = args.expert
|
| 182 |
+
|
| 183 |
+
n_tok = args.n_tok
|
| 184 |
+
raw = np.fromfile(args.bin, dtype=np.float32)
|
| 185 |
+
assert raw.size == 2 * n_embd * n_tok, f"selftest 輸出大小 {raw.size} 應為 {2 * n_embd * n_tok}"
|
| 186 |
+
cpp_out = raw[:n_embd * n_tok].reshape(n_tok, n_embd)
|
| 187 |
+
x = raw[n_embd * n_tok:].reshape(n_tok, n_embd)
|
| 188 |
+
|
| 189 |
+
with open(args.model, "rb") as fh:
|
| 190 |
+
g = layer["ffn_gate_exps"]
|
| 191 |
+
u = layer["ffn_up_exps"]
|
| 192 |
+
d = layer["ffn_down_exps"]
|
| 193 |
+
gate = read_expert(fh, g["file_offset"] + e * g["bytes_per_expert"],
|
| 194 |
+
n_ff * n_embd, g["type"]).reshape(n_ff, n_embd)
|
| 195 |
+
up = read_expert(fh, u["file_offset"] + e * u["bytes_per_expert"],
|
| 196 |
+
n_ff * n_embd, u["type"]).reshape(n_ff, n_embd)
|
| 197 |
+
down = read_expert(fh, d["file_offset"] + e * d["bytes_per_expert"],
|
| 198 |
+
n_embd * n_ff, d["type"]).reshape(n_embd, n_ff)
|
| 199 |
+
|
| 200 |
+
# 逐 token:activation → Q8_K → gate/up → SiLU → Q8_K → down
|
| 201 |
+
outs = []
|
| 202 |
+
for t in range(n_tok):
|
| 203 |
+
xq = quant_q8_K(x[t])
|
| 204 |
+
gate_out = gate @ xq
|
| 205 |
+
up_out = up @ xq
|
| 206 |
+
h = (gate_out / (1.0 + np.exp(-gate_out))) * up_out
|
| 207 |
+
hq = quant_q8_K(h)
|
| 208 |
+
outs.append(down @ hq)
|
| 209 |
+
out = np.stack(outs)
|
| 210 |
+
|
| 211 |
+
diff = np.abs(out - cpp_out)
|
| 212 |
+
scale = np.maximum(np.abs(out), 1e-3)
|
| 213 |
+
rel = diff / scale
|
| 214 |
+
print(f"layer={args.layer} expert={e} n_embd={n_embd} n_ff={n_ff}")
|
| 215 |
+
print(f" NumPy out[0:5] = {out[:5]}")
|
| 216 |
+
print(f" C++ out[0:5] = {cpp_out[:5]}")
|
| 217 |
+
print(f" max abs diff = {diff.max():.6g} max rel = {rel.max():.6g}")
|
| 218 |
+
print(f" out norm = {np.linalg.norm(out):.4f}")
|
| 219 |
+
ok = rel.max() <= args.tol
|
| 220 |
+
print(" 結果:", "PASS ✅" if ok else "FAIL ❌")
|
| 221 |
+
raise SystemExit(0 if ok else 1)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
if __name__ == "__main__":
|
| 225 |
+
main()
|
tools/route_overlap.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""從 SDQ_ROUTE_LOG 算出「連續 k 個 token 需要幾個不同的 expert」。
|
| 3 |
+
|
| 4 |
+
這是決定「多 token 批次 / 投機解碼值不值得做」的關鍵數字。
|
| 5 |
+
每一層每 token 只會用到 8 個 expert,所以如果把 k 個 token 放在同一層一起算,
|
| 6 |
+
需要從 SSD 讀的 expert 數是 U(k) 而不是 8k。U(k)/k 越小,省下的 I/O 越多。
|
| 7 |
+
|
| 8 |
+
另外會順便算:
|
| 9 |
+
* 完美(Belady)快取命中率 —— 也就是「這個工作流在容量無限時的命中率天花板」
|
| 10 |
+
* 快取容量對命中的影響 —— 用來判斷「把 arena 加大」值不值得
|
| 11 |
+
* 只用過一次的 expert 占比 —— 用來判斷「cache bypass / 准入策略」值不值得
|
| 12 |
+
|
| 13 |
+
用法
|
| 14 |
+
----
|
| 15 |
+
SDQ_ROUTE_LOG=/tmp/route.log <epq-chat> -m model.gguf -p "..." -n 48 --no-interactive
|
| 16 |
+
python tools/route_overlap.py /tmp/route.log
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import sys
|
| 22 |
+
from collections import defaultdict
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load(path: str) -> dict[int, list[tuple[int, ...]]]:
|
| 26 |
+
"""回傳 {layer: [每個 decode step 的 8 個 expert]}(只取 n_tok == 1 的呼叫)。"""
|
| 27 |
+
per_layer: dict[int, list[tuple[int, ...]]] = defaultdict(list)
|
| 28 |
+
with open(path, encoding="utf-8") as f:
|
| 29 |
+
for line in f:
|
| 30 |
+
parts = line.split()
|
| 31 |
+
if len(parts) < 3:
|
| 32 |
+
continue
|
| 33 |
+
il, n_tok = int(parts[0]), int(parts[1])
|
| 34 |
+
if n_tok != 1:
|
| 35 |
+
continue
|
| 36 |
+
per_layer[il].append(tuple(int(x) for x in parts[2].split(":")[1].split(",")))
|
| 37 |
+
return per_layer
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def hit_rate_lru(stream, cap: int) -> float:
|
| 41 |
+
cache: dict[int, None] = {}
|
| 42 |
+
hit = tot = 0
|
| 43 |
+
for req in stream:
|
| 44 |
+
for e in req:
|
| 45 |
+
tot += 1
|
| 46 |
+
if e in cache:
|
| 47 |
+
hit += 1
|
| 48 |
+
cache.pop(e)
|
| 49 |
+
elif len(cache) >= cap:
|
| 50 |
+
cache.pop(next(iter(cache)))
|
| 51 |
+
cache[e] = None
|
| 52 |
+
return hit / max(1, tot)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def hit_rate_belady(stream, cap: int) -> float:
|
| 56 |
+
"""離線模擬:替換掉「下一次使用最晚」的頁 = 理論上界。"""
|
| 57 |
+
nxt: dict[int, int] = {}
|
| 58 |
+
for i in range(len(stream) - 1, -1, -1):
|
| 59 |
+
for e in stream[i]:
|
| 60 |
+
nxt[e] = i
|
| 61 |
+
cache: set[int] = set()
|
| 62 |
+
hit = tot = 0
|
| 63 |
+
for i, req in enumerate(stream):
|
| 64 |
+
for e in req:
|
| 65 |
+
tot += 1
|
| 66 |
+
if e in cache:
|
| 67 |
+
hit += 1
|
| 68 |
+
else:
|
| 69 |
+
if len(cache) >= cap:
|
| 70 |
+
cache.discard(max(cache, key=lambda x: nxt.get(x, 10 ** 9)))
|
| 71 |
+
cache.add(e)
|
| 72 |
+
return hit / max(1, tot)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def main() -> int:
|
| 76 |
+
if len(sys.argv) < 2:
|
| 77 |
+
print(__doc__)
|
| 78 |
+
return 2
|
| 79 |
+
per_layer = load(sys.argv[1])
|
| 80 |
+
if not per_layer:
|
| 81 |
+
print("route log 裡沒有 n_tok==1 的紀錄")
|
| 82 |
+
return 1
|
| 83 |
+
n = min(len(v) for v in per_layer.values())
|
| 84 |
+
nl = len(per_layer)
|
| 85 |
+
print(f"每層 decode steps = {n},層數 = {nl}")
|
| 86 |
+
print()
|
| 87 |
+
print("=== 多 token 批次的 I/O 節省空間 ===")
|
| 88 |
+
print("(分頁一次 = 1.9 MB;k 個 token 分開算要讀 8k 次,合併算只要 U(k) 次)")
|
| 89 |
+
print(f"{'k':>3} {'U(k) 平均':>10} {'U(k)/k':>8} {'省下的讀取':>12}")
|
| 90 |
+
for k in (1, 2, 3, 4, 5, 6, 8):
|
| 91 |
+
tot = cnt = 0
|
| 92 |
+
for v in per_layer.values():
|
| 93 |
+
s = v[:n]
|
| 94 |
+
for i in range(n - k + 1):
|
| 95 |
+
u: set[int] = set()
|
| 96 |
+
for j in range(k):
|
| 97 |
+
u |= set(s[i + j])
|
| 98 |
+
tot += len(u)
|
| 99 |
+
cnt += 1
|
| 100 |
+
u_avg = tot / cnt
|
| 101 |
+
save = (1 - u_avg / (8.0 * k)) * 100 if k > 1 else 0.0
|
| 102 |
+
print(f"{k:3d} {u_avg:10.2f} {u_avg / k:8.2f} {save:11.1f}%")
|
| 103 |
+
print()
|
| 104 |
+
print("讀法:U(k)/k < 8 代表把 k 個 token 合併到同一層有省到 I/O;")
|
| 105 |
+
print(" 合併的 token 越多(k 越大)省得越多,但要能『同時知道』那 k 個 token。")
|
| 106 |
+
print()
|
| 107 |
+
print("=== 快取容量 vs 命中率(每層獨立計算,理想化)===")
|
| 108 |
+
print(f"{'每層槽數':>9} {'LRU':>8} {'Belady(上界)':>13}")
|
| 109 |
+
for cap in (256, 512, 1024, 2048, 4096):
|
| 110 |
+
a = b = 0.0
|
| 111 |
+
for v in per_layer.values():
|
| 112 |
+
s = v[:n]
|
| 113 |
+
a += hit_rate_lru(s, cap)
|
| 114 |
+
b += hit_rate_belady(s, cap)
|
| 115 |
+
print(f"{cap:9d} {a / nl * 100:7.1f}% {b / nl * 100:12.1f}%")
|
| 116 |
+
print()
|
| 117 |
+
print("讀法:LRU == Belady 代表『策略已經最optimal,剩下只有容量問題』;")
|
| 118 |
+
print(" LRU 隨容量上升代表加大 arena 有用。")
|
| 119 |
+
print()
|
| 120 |
+
print("=== 重複使用次數分布(48 個 token 的視窗內)===")
|
| 121 |
+
hist: dict[int, int] = defaultdict(int)
|
| 122 |
+
for v in per_layer.values():
|
| 123 |
+
cnt: dict[int, int] = defaultdict(int)
|
| 124 |
+
for req in v[:n]:
|
| 125 |
+
for e in req:
|
| 126 |
+
cnt[e] += 1
|
| 127 |
+
for e, c in cnt.items():
|
| 128 |
+
hist[c] += c
|
| 129 |
+
total = sum(hist.values())
|
| 130 |
+
once = hist.get(1, 0)
|
| 131 |
+
print(f"只出現 1 次的請求:{once} / {total}({once / total * 100:.1f}%)")
|
| 132 |
+
print("(占比低 = 快取被單次使用的 expert 污染的機會少,cache-bypass 不值得做)")
|
| 133 |
+
return 0
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
raise SystemExit(main())
|
tools/strict8g.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""EPM strict-mode runner: enforce an 8 GiB / no-swap ceiling on one process.
|
| 3 |
+
|
| 4 |
+
Why this exists
|
| 5 |
+
---------------
|
| 6 |
+
The repository's headline deployment claim is "MiniCPM-V-4.7-35B-A3B decodes inside
|
| 7 |
+
8 GiB of RAM with swap disabled". Past runs only ever set ``RLIMIT_DATA`` (which
|
| 8 |
+
caps anonymous data/heap, ignores file-backed mappings, and does not touch swap)
|
| 9 |
+
and then *observed* ``VmSwap: 0``. That is a pager-sizing number, not a
|
| 10 |
+
guarantee.
|
| 11 |
+
|
| 12 |
+
cgroups would be the clean way to express this — ``memory.max=8G`` plus
|
| 13 |
+
``memory.swap.max=0`` — but a writable cgroup2 mount needs ``CAP_SYS_ADMIN``.
|
| 14 |
+
Where that is unavailable this harness reaches the same two guarantees:
|
| 15 |
+
|
| 16 |
+
no-swap
|
| 17 |
+
Every anonymous mapping the process creates is ``mlock``ed by
|
| 18 |
+
``tools/epq_lock.so``. Locked pages can never be written to a swap device, so
|
| 19 |
+
no-swap is enforced by the kernel for the whole run rather than inferred from
|
| 20 |
+
a single reading afterwards. File-backed mappings are deliberately *not*
|
| 21 |
+
locked: clean page cache is reclaimable, so a real 8 GiB machine evicts it
|
| 22 |
+
rather than swapping, and a cgroup would reclaim it too. ``VmLck`` in the
|
| 23 |
+
record shows how much was actually locked.
|
| 24 |
+
|
| 25 |
+
8 GiB
|
| 26 |
+
``RLIMIT_DATA`` on the child is a hard kernel cap on anonymous memory: past
|
| 27 |
+
it, ``brk``/``mmap(MAP_ANONYMOUS)`` fail instead of quietly growing. A
|
| 28 |
+
supervisor samples ``/proc/<pid>/status`` every few milliseconds and
|
| 29 |
+
``SIGKILL``s on a breach, so transient spikes cannot slip through. Enforcement
|
| 30 |
+
and evidence come from kernel counters, not from the sampler:
|
| 31 |
+
``RssAnon``/``VmHWM``/``VmSwap``/``VmLck`` are exact, so the sampler only has
|
| 32 |
+
to be fast, not lucky.
|
| 33 |
+
|
| 34 |
+
``--metric anon`` (default) caps anonymous RSS, which is the quantity that binds
|
| 35 |
+
on real hardware: 8 GiB of RAM must hold the arena, the KV cache, the compute
|
| 36 |
+
buffers and the runtime, and the kernel can always reclaim the rest.
|
| 37 |
+
``--metric total`` caps total RSS instead, which is strictly harder than a cgroup
|
| 38 |
+
because it forbids even reclaimable cache. Use it when you want the pessimistic
|
| 39 |
+
number; use ``anon`` for the number that matches a cgroup.
|
| 40 |
+
|
| 41 |
+
Usage
|
| 42 |
+
-----
|
| 43 |
+
python3 tools/strict8g.py --label decode -- epq-chat -m model.gguf ...
|
| 44 |
+
python3 tools/strict8g.py --limit-mb 8192 --metric total --record r.jsonl -- cmd
|
| 45 |
+
|
| 46 |
+
Exit status is the child's own, except 124 when the ceiling was breached or the
|
| 47 |
+
run timed out (matching ``timeout``), and 125 for harness errors.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
from __future__ import annotations
|
| 51 |
+
|
| 52 |
+
import argparse
|
| 53 |
+
import json
|
| 54 |
+
import os
|
| 55 |
+
import resource
|
| 56 |
+
import signal
|
| 57 |
+
import subprocess
|
| 58 |
+
import sys
|
| 59 |
+
import tempfile
|
| 60 |
+
import threading
|
| 61 |
+
import time
|
| 62 |
+
from pathlib import Path
|
| 63 |
+
|
| 64 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 65 |
+
LOCK_SRC = REPO / "tools" / "epq_lock.so.c"
|
| 66 |
+
LOCK_SO = REPO / "tools" / "epq_lock.so"
|
| 67 |
+
|
| 68 |
+
MIB = 1024 * 1024
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def build_lock_helper() -> Path:
|
| 72 |
+
"""Compile the anonymous-memory locker once; it is one small C file."""
|
| 73 |
+
if not LOCK_SRC.exists():
|
| 74 |
+
raise SystemExit(f"[strict8g] 缺少 {LOCK_SRC}")
|
| 75 |
+
cc = os.environ.get("CC", "cc")
|
| 76 |
+
if (not LOCK_SO.exists()) or LOCK_SO.stat().st_mtime < LOCK_SRC.stat().st_mtime:
|
| 77 |
+
subprocess.run([cc, "-O2", "-fPIC", "-shared", "-o", str(LOCK_SO), str(LOCK_SRC)],
|
| 78 |
+
check=True)
|
| 79 |
+
return LOCK_SO
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
_WANTED = ("RssAnon", "RssFile", "VmRSS", "VmHWM", "VmSwap", "VmLck", "VmSize", "VmData")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def read_status(pid: int) -> dict:
|
| 86 |
+
"""Read one process's exact memory counters, in bytes."""
|
| 87 |
+
out = dict.fromkeys(_WANTED, 0)
|
| 88 |
+
try:
|
| 89 |
+
with open(f"/proc/{pid}/status", "r") as fh:
|
| 90 |
+
for line in fh:
|
| 91 |
+
name = line.split(":", 1)[0]
|
| 92 |
+
if name in out:
|
| 93 |
+
out[name] = int(line.split()[1]) * 1024
|
| 94 |
+
except (FileNotFoundError, ProcessLookupError, ValueError, IndexError):
|
| 95 |
+
pass
|
| 96 |
+
return out
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class Sampler(threading.Thread):
|
| 100 |
+
"""Polls the child's memory counters and kills it on a ceiling breach."""
|
| 101 |
+
|
| 102 |
+
def __init__(self, pid: int, limit: int, metric: str, interval: float):
|
| 103 |
+
super().__init__(daemon=True)
|
| 104 |
+
self.pid = pid
|
| 105 |
+
self.limit = limit
|
| 106 |
+
self.metric = metric
|
| 107 |
+
self.interval = interval
|
| 108 |
+
self.peak = dict.fromkeys(_WANTED, 0)
|
| 109 |
+
self.samples = 0
|
| 110 |
+
self.swap_samples = 0
|
| 111 |
+
self.breach = 0
|
| 112 |
+
self.stop = threading.Event()
|
| 113 |
+
|
| 114 |
+
def run(self) -> None:
|
| 115 |
+
while not self.stop.is_set():
|
| 116 |
+
st = read_status(self.pid)
|
| 117 |
+
self.samples += 1
|
| 118 |
+
for k in _WANTED:
|
| 119 |
+
self.peak[k] = max(self.peak[k], st[k])
|
| 120 |
+
if st["VmSwap"] > 0:
|
| 121 |
+
self.swap_samples += 1
|
| 122 |
+
cur = st[self.metric]
|
| 123 |
+
if cur > self.limit:
|
| 124 |
+
self.breach = cur
|
| 125 |
+
try:
|
| 126 |
+
os.kill(self.pid, signal.SIGKILL)
|
| 127 |
+
except ProcessLookupError:
|
| 128 |
+
pass
|
| 129 |
+
return
|
| 130 |
+
time.sleep(self.interval)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _mib(x: int) -> float:
|
| 134 |
+
return round(x / MIB, 1)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def main() -> int:
|
| 138 |
+
ap = argparse.ArgumentParser(description="Run one command under a strict 8 GiB / no-swap ceiling")
|
| 139 |
+
ap.add_argument("--limit-mb", type=int, default=8192, help="hard ceiling in MiB")
|
| 140 |
+
ap.add_argument("--data-mb", type=int, default=0,
|
| 141 |
+
help="RLIMIT_DATA in MiB; default is --limit-mb plus VA slack "
|
| 142 |
+
"(RLIMIT_DATA bounds anonymous *virtual* memory, which is "
|
| 143 |
+
"stricter than physical RAM, so it is used as a runaway guard)")
|
| 144 |
+
ap.add_argument("--no-rlimit", action="store_true",
|
| 145 |
+
help="do not set RLIMIT_DATA at all")
|
| 146 |
+
ap.add_argument("--metric", choices=["anon", "total"], default="anon",
|
| 147 |
+
help="cap anonymous RSS (default, cgroup-equivalent) or total RSS")
|
| 148 |
+
ap.add_argument("--interval-ms", type=float, default=4.0, help="sampling interval")
|
| 149 |
+
ap.add_argument("--label", default="strict")
|
| 150 |
+
ap.add_argument("--record", default="", help="append a JSON record to this file")
|
| 151 |
+
ap.add_argument("--timeout", type=float, default=0, help="wall-clock limit in seconds (0 = none)")
|
| 152 |
+
ap.add_argument("--no-lock", action="store_true",
|
| 153 |
+
help="skip mlock (no-swap then rests on VmSwap sampling alone)")
|
| 154 |
+
ap.add_argument("--quiet", action="store_true")
|
| 155 |
+
ap.add_argument("cmd", nargs=argparse.REMAINDER)
|
| 156 |
+
args = ap.parse_args()
|
| 157 |
+
|
| 158 |
+
cmd = args.cmd[1:] if args.cmd and args.cmd[0] == "--" else args.cmd
|
| 159 |
+
if not cmd:
|
| 160 |
+
ap.error("missing command; put it after `--`")
|
| 161 |
+
|
| 162 |
+
limit = args.limit_mb * MIB
|
| 163 |
+
# RLIMIT_DATA caps anonymous *virtual* memory; llama.cpp reserves graph and
|
| 164 |
+
# compute buffers that are mapped but never touched, so charging them against
|
| 165 |
+
# a physical 8 GiB budget costs ~1 GiB of expert arena for no physical
|
| 166 |
+
# benefit. Physical residency is bounded by the sampler instead, exactly as a
|
| 167 |
+
# cgroup memory.max bounds it. RLIMIT_DATA stays as an independent kernel
|
| 168 |
+
# guard against a runaway, sized with slack for untouched mappings.
|
| 169 |
+
VA_SLACK_MB = 1536
|
| 170 |
+
data_mb = args.data_mb or (args.limit_mb + VA_SLACK_MB)
|
| 171 |
+
metric = "RssAnon" if args.metric == "anon" else "VmRSS"
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
helper = build_lock_helper() if not args.no_lock else None
|
| 175 |
+
except (subprocess.CalledProcessError, OSError) as exc:
|
| 176 |
+
raise SystemExit(f"[strict8g] 無法編譯 mlock helper:{exc}")
|
| 177 |
+
|
| 178 |
+
fd, status_path = tempfile.mkstemp(prefix="epq-mlock-", suffix=".txt")
|
| 179 |
+
os.close(fd)
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
env = dict(os.environ)
|
| 183 |
+
if helper:
|
| 184 |
+
env["LD_PRELOAD"] = str(helper) + (":" + env["LD_PRELOAD"] if env.get("LD_PRELOAD") else "")
|
| 185 |
+
env["EPM_MLOCK_STATUS"] = status_path
|
| 186 |
+
|
| 187 |
+
def child_setup() -> None:
|
| 188 |
+
# mlock() is bounded by RLIMIT_MEMLOCK, so raise it before main runs.
|
| 189 |
+
try:
|
| 190 |
+
resource.setrlimit(resource.RLIMIT_MEMLOCK,
|
| 191 |
+
(resource.RLIM_INFINITY, resource.RLIM_INFINITY))
|
| 192 |
+
except (ValueError, OSError):
|
| 193 |
+
pass
|
| 194 |
+
# Kernel guard on anonymous virtual memory (see VA_SLACK_MB above).
|
| 195 |
+
if not args.no_rlimit:
|
| 196 |
+
cap = data_mb * MIB
|
| 197 |
+
try:
|
| 198 |
+
resource.setrlimit(resource.RLIMIT_DATA, (cap, cap))
|
| 199 |
+
except (ValueError, OSError):
|
| 200 |
+
pass
|
| 201 |
+
# A multi-GiB core dump would fill the disk and mask the failure we
|
| 202 |
+
# are trying to measure.
|
| 203 |
+
try:
|
| 204 |
+
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
| 205 |
+
except (ValueError, OSError):
|
| 206 |
+
pass
|
| 207 |
+
os.setpgrp()
|
| 208 |
+
|
| 209 |
+
t0 = time.time()
|
| 210 |
+
try:
|
| 211 |
+
proc = subprocess.Popen(cmd, env=env, preexec_fn=child_setup,
|
| 212 |
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
| 213 |
+
text=True)
|
| 214 |
+
except OSError as exc:
|
| 215 |
+
print(f"[strict8g] 無法執行 {cmd[0]}: {exc}", file=sys.stderr)
|
| 216 |
+
return 125
|
| 217 |
+
|
| 218 |
+
sampler = Sampler(proc.pid, limit, metric, args.interval_ms / 1000.0)
|
| 219 |
+
sampler.start()
|
| 220 |
+
|
| 221 |
+
timed_out = False
|
| 222 |
+
try:
|
| 223 |
+
stdout, stderr = proc.communicate(timeout=args.timeout or None)
|
| 224 |
+
except subprocess.TimeoutExpired:
|
| 225 |
+
timed_out = True
|
| 226 |
+
proc.kill()
|
| 227 |
+
stdout, stderr = proc.communicate()
|
| 228 |
+
finally:
|
| 229 |
+
sampler.stop.set()
|
| 230 |
+
sampler.join(timeout=2.0)
|
| 231 |
+
wall = time.time() - t0
|
| 232 |
+
|
| 233 |
+
lock_note = ""
|
| 234 |
+
if helper:
|
| 235 |
+
try:
|
| 236 |
+
lock_note = Path(status_path).read_text().strip().replace("\n", " ")
|
| 237 |
+
except OSError:
|
| 238 |
+
pass
|
| 239 |
+
finally:
|
| 240 |
+
try:
|
| 241 |
+
os.unlink(status_path)
|
| 242 |
+
except OSError:
|
| 243 |
+
pass
|
| 244 |
+
|
| 245 |
+
rc = proc.returncode
|
| 246 |
+
overflow = sampler.breach > 0
|
| 247 |
+
swapped = sampler.peak["VmSwap"] > 0
|
| 248 |
+
locked = sampler.peak["VmLck"]
|
| 249 |
+
# VmHWM is a kernel high-water mark: the exact peak even if it lasted well
|
| 250 |
+
# under one sampling interval.
|
| 251 |
+
ok = (rc == 0 and not overflow and not swapped and not timed_out
|
| 252 |
+
and (args.no_lock or locked > 0))
|
| 253 |
+
|
| 254 |
+
rec = {
|
| 255 |
+
"label": args.label,
|
| 256 |
+
"cmd": cmd,
|
| 257 |
+
"returncode": rc,
|
| 258 |
+
"wall_s": round(wall, 3),
|
| 259 |
+
"limit_mb": args.limit_mb,
|
| 260 |
+
"metric": args.metric,
|
| 261 |
+
"rlimit_data_mb": data_mb,
|
| 262 |
+
"interval_ms": args.interval_ms,
|
| 263 |
+
"samples": sampler.samples,
|
| 264 |
+
"peak_anon_mb": _mib(sampler.peak["RssAnon"]),
|
| 265 |
+
"peak_file_mb": _mib(sampler.peak["RssFile"]),
|
| 266 |
+
"peak_rss_mb": _mib(sampler.peak["VmRSS"]),
|
| 267 |
+
"vm_hwm_mb": _mib(sampler.peak["VmHWM"]),
|
| 268 |
+
"peak_swap_mb": _mib(sampler.peak["VmSwap"]),
|
| 269 |
+
"swap_samples_over_zero": sampler.swap_samples,
|
| 270 |
+
"vm_lck_mb": _mib(locked),
|
| 271 |
+
"peak_vmsize_mb": _mib(sampler.peak["VmSize"]),
|
| 272 |
+
"peak_vmdata_mb": _mib(sampler.peak["VmData"]),
|
| 273 |
+
"overflow_killed": overflow,
|
| 274 |
+
"breach_mb": _mib(sampler.breach) if overflow else None,
|
| 275 |
+
"timed_out": timed_out,
|
| 276 |
+
"lock": lock_note,
|
| 277 |
+
"strict_ok": ok,
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
if args.record:
|
| 281 |
+
p = Path(args.record)
|
| 282 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 283 |
+
with p.open("a", encoding="utf-8") as fh:
|
| 284 |
+
fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
| 285 |
+
|
| 286 |
+
if not args.quiet:
|
| 287 |
+
print(f"[strict8g] {'PASS' if ok else 'FAIL'} {args.label}: rc={rc} wall={wall:.1f}s "
|
| 288 |
+
f"| anon={rec['peak_anon_mb']} file={rec['peak_file_mb']} "
|
| 289 |
+
f"total={rec['peak_rss_mb']} hwm={rec['vm_hwm_mb']} MiB "
|
| 290 |
+
f"vmdata={rec['peak_vmdata_mb']} "
|
| 291 |
+
f"| cap {args.limit_mb} MiB ({args.metric}) swap={rec['peak_swap_mb']} "
|
| 292 |
+
f"lck={rec['vm_lck_mb']} samples={sampler.samples}", file=sys.stderr)
|
| 293 |
+
if overflow:
|
| 294 |
+
print(f"[strict8g] 超標:{metric} 到 {rec['breach_mb']} MiB 時 SIGKILL", file=sys.stderr)
|
| 295 |
+
if swapped:
|
| 296 |
+
print(f"[strict8g] 偵測到 swap:{rec['peak_swap_mb']} MiB", file=sys.stderr)
|
| 297 |
+
if locked <= 0 and not args.no_lock:
|
| 298 |
+
print(f"[strict8g] 沒有任何匿名記憶體被鎖定,no-swap 無法成立"
|
| 299 |
+
f"({lock_note or 'helper 未回報'})", file=sys.stderr)
|
| 300 |
+
elif not ok and not overflow and not swapped and not timed_out:
|
| 301 |
+
print(f"[strict8g] 子行程自行失敗 rc={rc}", file=sys.stderr)
|
| 302 |
+
|
| 303 |
+
sys.stdout.write(stdout or "")
|
| 304 |
+
sys.stderr.write(stderr or "")
|
| 305 |
+
if overflow or timed_out:
|
| 306 |
+
return 124
|
| 307 |
+
return rc
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
if __name__ == "__main__":
|
| 311 |
+
sys.exit(main())
|