Text Generation
llama-cpp-python
GGUF
llama.cpp
Mixture of Experts
ssd-offload
smallthinker
expert-paging
low-ram
Instructions to use HelloSun/SmallThinker4b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use HelloSun/SmallThinker4b with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="HelloSun/SmallThinker4b", filename="{{GGUF_FILE}}", )output = llm( "Once upon a time,", max_tokens=512, echo=True ) print(output)
- Notebooks
- Google Colab
- Kaggle
Auto Upload Agent commited on
Commit ·
1e54449
1
Parent(s): d2defb7
v3: 修 MAP_POPULATE 與 /proc/self/io 解析 bug;A/B 驗證工具;輸出逐字相同已證實
Browse files- AGENTS.md +94 -0
- STATUS.md +156 -0
- patches/0001-st-expert-pager.patch +87 -16
- tools/ab_test.py +283 -0
- tools/verify_run.py +13 -0
- validate/ab-test.json +383 -0
- validate/last-run.json +19 -18
AGENTS.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md — 續作指引
|
| 2 |
+
|
| 3 |
+
**這個檔案的存在理由:本機很不穩,agent 可能隨時崩潰。**
|
| 4 |
+
無論你(人或 agent)從哪裡開始,只要 clone 這個 repo 並讀完本檔,就能接著做。
|
| 5 |
+
|
| 6 |
+
```bash
|
| 7 |
+
git clone https://huggingface.co/HelloSun/SmallThinker4b
|
| 8 |
+
cd SmallThinker4b
|
| 9 |
+
cat STATUS.md # 進度真相來源,先讀這個
|
| 10 |
+
cat AGENTS.md # 本檔
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## 專案一句話
|
| 14 |
+
|
| 15 |
+
SmallThinker-4B-A0.6B-Instruct(Q4_K_M,2508 MiB,MoE 32 層 × 32 experts)
|
| 16 |
+
在 llama.cpp 上跑,**熱的 expert 權重留在 RAM,冷的丟回 SSD**。
|
| 17 |
+
|
| 18 |
+
## 最重要原則(聽到沒)
|
| 19 |
+
|
| 20 |
+
**只要到能跑、能驗證的階段,就立刻上傳。後面每改到程式並編譯過,也要上傳。**
|
| 21 |
+
本機十分不穩,你必須保存工作成果,並且假設 agent 崩潰後要能指定 repo 繼續工作。
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
./sync.sh "改了什麼"
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
`git clone https://huggingface.co/HelloSun/SmallThinker4b` 就能接著做。
|
| 28 |
+
|
| 29 |
+
## 硬約束
|
| 30 |
+
|
| 31 |
+
1. 行程**總 RSS**(含 file-backed mmap 的權重頁)要受 RAM 預算約束。
|
| 32 |
+
只驗證匿名記憶體是不成立的 —— 權重頁會算進 RSS 且真的佔用系統 RAM。
|
| 33 |
+
2. **VmSwap = 0**。
|
| 34 |
+
3. **輸出必須與上游逐字相同**。分頁只該影響「權重住哪裡」,
|
| 35 |
+
不該影響任何一個 token 的數值。
|
| 36 |
+
|
| 37 |
+
## 已知的坑(不要重蹈)
|
| 38 |
+
|
| 39 |
+
完整清單在 `STATUS.md` 的「踩過的坑」表格,每一條都有實測證據。
|
| 40 |
+
最容易再犯的三個:
|
| 41 |
+
|
| 42 |
+
1. **`-DGGML_CPU_REPACK=OFF` 是硬性要求**。開著時 ggml 把權重放進匿名緩衝區,
|
| 43 |
+
`madvise` 會把它**歸零**,輸出變亂碼但行程看起來正常。
|
| 44 |
+
2. **`fscanf` 讀 `/proc/self/io` 開頭要空格**,否則 `read_bytes` 永遠是 0。
|
| 45 |
+
3. **不要用 peak RSS 判定分頁有沒生效** —— peak 由 prefill 決定,
|
| 46 |
+
而 prefill 的工作集 ≈ 整個模型。要看 decode 階段。
|
| 47 |
+
|
| 48 |
+
## 環境
|
| 49 |
+
|
| 50 |
+
本機(開發/驗證用):16 vCPU、MemTotal 約 2 TB、cgroup `memory.max` = 104 GB、
|
| 51 |
+
無 GPU。SSD 是 **overlayfs**。
|
| 52 |
+
**換機器一定要用 `tools/io_probe.py` 重測讀取能力**,tok/s 會跟著全變。
|
| 53 |
+
|
| 54 |
+
## 常用指令
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
# 完整流程
|
| 58 |
+
ST_HOME=~/.cache/smallthinker4b ./llama_server.sh --verify
|
| 59 |
+
|
| 60 |
+
# 只看參數
|
| 61 |
+
./llama_server.sh --plan
|
| 62 |
+
|
| 63 |
+
# A/B 驗證
|
| 64 |
+
python3 tools/ab_test.py --bin <llama-cli> --model <gguf> \
|
| 65 |
+
--budget 1024 --budget 512 --out validate/ab-test.json
|
| 66 |
+
|
| 67 |
+
# 對照組(關分頁)
|
| 68 |
+
ST_PAGER=0 ./llama_server.sh
|
| 69 |
+
|
| 70 |
+
# 上傳
|
| 71 |
+
./sync.sh "改了什麼"
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 下一步(依 STATUS.md 的「還沒解決的問題」)
|
| 75 |
+
|
| 76 |
+
peak RSS 降不下來。已排除 MAP_POPULATE、`madvise` 無效、`fadvise` 無效。
|
| 77 |
+
最可能是 **prefill 的工作集就等於整個模型**。
|
| 78 |
+
要驗的是:peak RSS 是否隨 prompt 長度改變,以及 decode 階段的 RSS 才是分頁的指標。
|
| 79 |
+
|
| 80 |
+
## 常數(改模型就要改)
|
| 81 |
+
|
| 82 |
+
`llama_server.sh` 頂部。這些是模型事實,不是可調參數:
|
| 83 |
+
|
| 84 |
+
```
|
| 85 |
+
N_LAYER=32 N_EMBD=1536 N_EXPERT=32 N_EXPERT_USED=4 EXPERT_FFN=768
|
| 86 |
+
N_HEAD=12 N_HEAD_KV=2 KEY_LEN=128 VAL_LEN=128 MODEL_MAX_CTX=32768
|
| 87 |
+
MODEL_REPO=Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF
|
| 88 |
+
MODEL_FILE=SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf
|
| 89 |
+
MODEL_SIZE=2630212704
|
| 90 |
+
LLAMA_COMMIT=b9acf138a1e28ce1fc23b5a4fc4b12444b50f7ea
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
⚠️ `EXPERT_BYTES` 只是**粗估**(0.6 B/elem),因為 `down_exps` 逐層量化型別不同。
|
| 94 |
+
真正精確的數字由 `st_pager.cpp` 在載入時從 GGUF metadata 算出來並印出。
|
STATUS.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# STATUS — SmallThinker-4B-A0.6B:熱參數在 RAM、冷參數到 SSD
|
| 2 |
+
|
| 3 |
+
進度真相來源。**每次改完程式、編譯過、驗證過就更新本檔並 `./sync.sh`。**
|
| 4 |
+
|
| 5 |
+
## 一句話
|
| 6 |
+
|
| 7 |
+
`Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF` 的 `SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf`
|
| 8 |
+
(2,630,212,704 bytes = 2508 MiB)跑在 llama.cpp 上,權重永遠是 SSD 上的
|
| 9 |
+
file-backed `mmap`;RAM 只留「熱的」expert 權重 + KV + compute buffer,
|
| 10 |
+
用不到的 expert 會被**明確丟回 SSD**(`madvise` + `posix_fadvise`)。
|
| 11 |
+
|
| 12 |
+
## 模型事實(全部從 GGUF metadata 實測,不是抄設定檔)
|
| 13 |
+
|
| 14 |
+
| 項目 | 值 |
|
| 15 |
+
| --- | --- |
|
| 16 |
+
| arch | `smallthinker` |
|
| 17 |
+
| 層數 | 32 |
|
| 18 |
+
| hidden | 1536 |
|
| 19 |
+
| experts / 每 token 用 | 32 / 4 |
|
| 20 |
+
| expert FFN | 768 |
|
| 21 |
+
| attention heads / KV heads | 12 / 2 |
|
| 22 |
+
| key_len / val_len | 128 / 128 |
|
| 23 |
+
| context_length(模型上限) | 32768 |
|
| 24 |
+
| 量化 | Q4_K_M |
|
| 25 |
+
|
| 26 |
+
**expert 權重總量 2241 MiB / 2508 MiB = 89%。** 也就是說「熱在 RAM、冷在 SSD」
|
| 27 |
+
這件事基本上就是「expert 權重怎麼放」。
|
| 28 |
+
|
| 29 |
+
每個 expert 是三段連續位元組(`blk.N.ffn_{gate,up,down}_exps.weight`,
|
| 30 |
+
expert 維度是最外層):
|
| 31 |
+
|
| 32 |
+
| 段 | shape | 每個 expert |
|
| 33 |
+
| --- | --- | --- |
|
| 34 |
+
| gate | [1536, 768, 32] | 663,552 B(Q4_K) |
|
| 35 |
+
| up | [1536, 768, 32] | 663,552 B(Q4_K) |
|
| 36 |
+
| down | [768, 1536, 32] | 967,680 B(Q6_K) |
|
| 37 |
+
| **合計** | | **2,294,784 B ≈ 2.19 MiB** |
|
| 38 |
+
|
| 39 |
+
共 32 × 32 = 1024 個 expert。
|
| 40 |
+
|
| 41 |
+
⚠️ **三段的量化型別逐層可能不同**(實測有些層的 `down_exps` 是 Q4_K 而非 Q6_K,
|
| 42 |
+
`nb[2` 就不同)。所以「每個 expert 多大」必須**逐 slot 算**,
|
| 43 |
+
拿 layer 0 當全模型常數會算錯。
|
| 44 |
+
|
| 45 |
+
## 硬約束
|
| 46 |
+
|
| 47 |
+
- **行程總 RSS**(含 file-backed mmap 的權重頁)要受 RAM 預算約束。
|
| 48 |
+
只看匿名記憶體會嚴重低估 —— 這是本專案第一版的錯。
|
| 49 |
+
- **VmSwap = 0**。
|
| 50 |
+
- **輸出必須與上游逐字相同**。這是分頁機制唯一不能妥協的正確性條件。
|
| 51 |
+
|
| 52 |
+
## 目前狀態
|
| 53 |
+
|
| 54 |
+
| 項目 | 結果 | 證據 |
|
| 55 |
+
| --- | --- | --- |
|
| 56 |
+
| 上游行為(`ST_PAGER=0`)可跑 | ✅ | `validate/ab-test.json` → `runs.baseline` |
|
| 57 |
+
| 分頁可跑、輸出**逐字相同** | ✅ | 同上,每組都有 `output_matches_baseline: true` |
|
| 58 |
+
| swap = 0 | ✅ | 同上,`peak_swap_gb: 0.0` |
|
| 59 |
+
| 冷權重真的從 SSD 重讀 | ✅ | 同上,`proc_self_read_bytes` ≈ 2508 MiB |
|
| 60 |
+
| RSS 真的被壓下來 | ⚠️ **未達成**(見下) | 同上 |
|
| 61 |
+
|
| 62 |
+
### 已驗證能跑(2026-10-07)
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
llama-cli -m SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf \
|
| 66 |
+
-p "Hello, who are you?" -n 32 -c 1024 -t 8 --no-warmup --single-turn --seed 42
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
三種設定(`ST_PAGER=0` / 預算 1024MB / 512MB / 256MB)輸出**完全相同**:
|
| 70 |
+
|
| 71 |
+
> Hello! 😊 I'm DeepSeek-R1, your friendly AI assistant. I'm here to help with all
|
| 72 |
+
> kinds of questions, learning, and creative tasks
|
| 73 |
+
|
| 74 |
+
分頁統計(預算 512 MiB):1024 個 expert、命中率 55.1%、
|
| 75 |
+
淘汰 4572 次共 9.8 GB、`read_bytes` = 2508 MiB(= 整個模型重讀一次)。
|
| 76 |
+
|
| 77 |
+
### ⚠️ 還沒解決的問題:peak RSS 降不下來
|
| 78 |
+
|
| 79 |
+
**量到的 decode 階段 RSS 分頁後反而比 baseline 高**(2.556 vs 2.301 GiB)。
|
| 80 |
+
RSS 時間序列(`validate/ab-test.json` → `rss_timeline_mib`)顯示它是
|
| 81 |
+
**單調上升**到接近整個模型大小,然後在最後 1/4 掉下來。
|
| 82 |
+
|
| 83 |
+
已排除的可能原因(都實測過):
|
| 84 |
+
|
| 85 |
+
1. ~~`MAP_POPULATE` 在載入時 fault 整個檔案~~ — 已修:
|
| 86 |
+
`ml.init_mappings(!st::prefetch_requested(), ...)`。
|
| 87 |
+
但時間序列顯示 RSS 還是在**載入後**才慢慢爬升,所以不是這個。
|
| 88 |
+
2. ~~`madvise` 沒生效~~ — 已排除:`/tmp/rsstest`(純 C,touch 2241 MiB
|
| 89 |
+
→ RSS 2242 MiB → `madvise(MADV_DONTNEED)` → RSS 1 MiB)。
|
| 90 |
+
3. ~~`posix_fadvise` 沒讓頁真的回到 SSD~~ — 已排除:`/tmp/ev6.c`
|
| 91 |
+
(touch 200 MiB → 讀 200.1 MiB → 淘汰 → **再 touch 又讀 200.0 MiB**)。
|
| 92 |
+
4. **prefill 的工作集本來就等於整個模型** — 這是目前最可能的解釋。
|
| 93 |
+
prompt 有 37 個 token,每層 32 個 expert,prefill 一次就會把
|
| 94 |
+
幾乎所有 expert 都摸一遍。peak RSS 因此必然 ≈ 整個權重。
|
| 95 |
+
下一步要驗的是:**更長的 prompt vs 極短 prompt**,看 peak 是否隨之改變。
|
| 96 |
+
|
| 97 |
+
## 踩過的坑(全部有實測證據,不要重蹈)
|
| 98 |
+
|
| 99 |
+
| 坑 | 真相 | 怎麼知道的 |
|
| 100 |
+
| --- | --- | --- |
|
| 101 |
+
| **開 `GGML_CPU_REPACK` 時權重不在 mmap 內** | ggml 把 Q4_K 轉成 repack 格式放進**匿名**緩衝區。這時 `madvise(MADV_DONTNEED)` 不是「讀 SSD」,而是**把那塊記憶體歸零** → 輸出變成 `papers bases abstract…` 之類的亂碼,但行程看起來完全正常、tok/s 只掉一點 | 症狀是亂碼;`st_pager.cpp` 現在有 `in_map` 檢查會直接擋下並印出 buffer 型別 |
|
| 102 |
+
| **`fscanf("%63[^:]: %llu")` 讀 `/proc/self/io` 永遠讀不到 `read_bytes`** | 沒有開頭空格時 `%[^:]` 會把上一行的 `\n` 讀進 key,`strcmp` 永遠比不到 | 量到 `read_bytes = 0`,但 C 測試明明有讀 |
|
| 103 |
+
| **子行程退出後才讀 `/proc/<pid>/io`** | 行程一退出 `/proc/<pid>` 就消失,外部量測得到 `{}`,看起來像「完全沒 I/O」 | `io_delta: {}`。SSD 讀取量只能由**行程自己**在 `atexit` 寫出來 |
|
| 104 |
+
| **用 peak RSS 判定分頁有沒生效** | peak 由 prefill 決定,而 prefill 的工作集 ≈ 整個模型。要看 decode 階段 | `rss_timeline_mib` |
|
| 105 |
+
| **`nb[2] != ne[0]*nb[0]` 檢查會誤判量化張量** | 量化張量 `nb[0]` 是**一個 block** 的位元組數,不是 1。只要檢查 `nb[2] == ne[1]*nb[1]` | 「layer 0 不連續」誤報 |
|
| 106 |
+
| **第一輪 sweep 永遠什麼都不做** | 條件寫成 `last_used < now - hot_tokens`,第一輪時門檻是 0 而 `last_used` 也是 0,`0 < 0` 為假 | sweep 計數有跳、但 resident 不降 |
|
| 107 |
+
| **拿 layer 0 的 `nb[2]` 當全模型常數** | `down_exps` 逐層量化型別不同 | 「layer 4 part 2 形狀不一致」 |
|
| 108 |
+
|
| 109 |
+
## 常用指令
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
# 完整流程(自動抓 llama.cpp / 套 patch / 編譯 / 下載權重 / 啟動 / 驗證)
|
| 113 |
+
ST_HOME=~/.cache/smallthinker4b ./llama_server.sh --verify
|
| 114 |
+
|
| 115 |
+
# 只看推導出來的參數,不動任何東西
|
| 116 |
+
./llama_server.sh --plan
|
| 117 |
+
|
| 118 |
+
# A/B 驗證(baseline vs 各種預算)
|
| 119 |
+
python3 tools/ab_test.py --bin <llama-cli> --model <gguf> \
|
| 120 |
+
--budget 1024 --budget 512 --budget 256 --out validate/ab-test.json
|
| 121 |
+
|
| 122 |
+
# 對照組:關掉分頁
|
| 123 |
+
ST_PAGER=0 ./llama_server.sh
|
| 124 |
+
|
| 125 |
+
# 手動壓低 RAM 預算
|
| 126 |
+
ST_RAM_BUDGET_MB=512 ./llama_server.sh
|
| 127 |
+
|
| 128 |
+
# 上傳(最重要的一步)
|
| 129 |
+
./sync.sh "改了什麼"
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## 關鍵環境變數
|
| 133 |
+
|
| 134 |
+
| 變數 | 預設 | 說明 |
|
| 135 |
+
| --- | --- | --- |
|
| 136 |
+
| `ST_PAGER` | `1` | `0` = 關閉分頁(上游行為,做 A/B 用) |
|
| 137 |
+
| `ST_RAM_BUDGET_MB` | 自動偵測 | RAM 總額 |
|
| 138 |
+
| `ST_RESERVE_MB` | 0 | KV + compute 預留,從預算扣掉 |
|
| 139 |
+
| `ST_ARENA_MB` | 0 | 直接指定 expert 可常駐量(有值優先) |
|
| 140 |
+
| `ST_HOT_TOKENS` | `8` | 最近 N 個 tick 用過的 expert 不淘汰 |
|
| 141 |
+
| `ST_VERBOSE` | `0` | 印每次淘汰 |
|
| 142 |
+
| `ST_STATS_FILE` | — | 行程退出時把統計寫成 JSON(`SIGUSR2` 也會寫) |
|
| 143 |
+
| `ST_EVICT_MODE` | `both` | 診斷用:`both`/`madvise`/`fadvise`/`none` |
|
| 144 |
+
|
| 145 |
+
## 檔案地圖
|
| 146 |
+
|
| 147 |
+
| 檔案 | 用途 |
|
| 148 |
+
| --- | --- |
|
| 149 |
+
| `llama_server.sh` | 一鍵啟動 + 自動推導參數 |
|
| 150 |
+
| `patches/0001-st-expert-pager.patch` | llama.cpp 改動(expert 分頁) |
|
| 151 |
+
| `tools/ab_test.py` | A/B 驗證(RSS / swap / SSD 讀取 / 輸出比對) |
|
| 152 |
+
| `tools/verify_run.py` | 對 llama-server 打一次 chat 並量記憶體 |
|
| 153 |
+
| `tools/io_probe.py` | 實測本機 SSD 讀取能力(**換機器必跑**) |
|
| 154 |
+
| `tools/drop_model_cache.py` | 量測前丟掉模型檔的 page cache |
|
| 155 |
+
| `validate/` | 驗證證據 |
|
| 156 |
+
| `sync.sh` | 推回 HF(崩潰後的唯一保險) |
|
patches/0001-st-expert-pager.patch
CHANGED
|
@@ -1,20 +1,26 @@
|
|
| 1 |
-
From
|
| 2 |
From: c <a@b>
|
| 3 |
-
Date: Wed, 7 Oct 2026 16:
|
| 4 |
-
Subject: [PATCH] st pager
|
|
|
|
| 5 |
|
| 6 |
---
|
|
|
|
| 7 |
.st-unpatched | 0
|
| 8 |
src/CMakeLists.txt | 1 +
|
| 9 |
src/llama-graph.cpp | 10 +
|
| 10 |
-
src/llama-model.cpp |
|
| 11 |
-
src/st_pager.cpp |
|
| 12 |
-
src/st_pager.h |
|
| 13 |
-
|
|
|
|
| 14 |
create mode 100644 .st-unpatched
|
| 15 |
create mode 100644 src/st_pager.cpp
|
| 16 |
create mode 100644 src/st_pager.h
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
diff --git a/.st-unpatched b/.st-unpatched
|
| 19 |
new file mode 100644
|
| 20 |
index 000000000..e69de29bb
|
|
@@ -58,7 +64,7 @@ index 1112ad885..e335a9d5f 100644
|
|
| 58 |
|
| 59 |
if (weight_before_ffn) {
|
| 60 |
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
|
| 61 |
-
index 1f3b80b08..
|
| 62 |
--- a/src/llama-model.cpp
|
| 63 |
+++ b/src/llama-model.cpp
|
| 64 |
@@ -1,5 +1,7 @@
|
|
@@ -69,7 +75,31 @@ index 1f3b80b08..f863a15fa 100644
|
|
| 69 |
#include "llama-arch.h"
|
| 70 |
#include "llama-ext.h"
|
| 71 |
#include "llama-hparams.h"
|
| 72 |
-
@@ -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
|
@@ -77,7 +107,6 @@ index 1f3b80b08..f863a15fa 100644
|
|
| 77 |
+ // 讓 build_moe_ffn 插入的 op 能在用不到時把它們丟回 SSD。
|
| 78 |
+ // 註冊失敗不是致命錯誤 —— 那只是不分頁,模型照上游路徑跑。
|
| 79 |
+ {
|
| 80 |
-
+ st::set_config_from_env();
|
| 81 |
+ st::install_signal_handler();
|
| 82 |
+
|
| 83 |
+ void * map_base = nullptr;
|
|
@@ -108,10 +137,10 @@ index 1f3b80b08..f863a15fa 100644
|
|
| 108 |
|
| 109 |
diff --git a/src/st_pager.cpp b/src/st_pager.cpp
|
| 110 |
new file mode 100644
|
| 111 |
-
index 000000000..
|
| 112 |
--- /dev/null
|
| 113 |
+++ b/src/st_pager.cpp
|
| 114 |
-
@@ -0,0 +1,
|
| 115 |
+// ST expert pager —— 熱 expert 留在 RAM,冷 expert 丟回 SSD
|
| 116 |
+//
|
| 117 |
+// 設計取捨(為什麼不照抄 sddqwen35a3b_v01 的 pread arena):
|
|
@@ -233,6 +262,8 @@ index 000000000..79bd1d4cb
|
|
| 233 |
+
|
| 234 |
+bool enabled() { return g_enabled && !g_slots.empty(); }
|
| 235 |
+
|
|
|
|
|
|
|
| 236 |
+// ------------------------------------------------------------------ 統計
|
| 237 |
+
|
| 238 |
+stats get_stats() {
|
|
@@ -247,8 +278,31 @@ index 000000000..79bd1d4cb
|
|
| 247 |
+ return s;
|
| 248 |
+}
|
| 249 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
+void dump_stats(const char * path) {
|
| 251 |
+ const stats s = get_stats();
|
|
|
|
|
|
|
| 252 |
+ FILE * f = fopen(path, "w");
|
| 253 |
+ if (!f) return;
|
| 254 |
+ fprintf(f,
|
|
@@ -262,7 +316,9 @@ index 000000000..79bd1d4cb
|
|
| 262 |
+ " \"sweeps\": %llu,\n"
|
| 263 |
+ " \"resident_bytes\": %llu,\n"
|
| 264 |
+ " \"budget_bytes\": %llu,\n"
|
| 265 |
-
+ " \"hit_rate\": %.4f\n"
|
|
|
|
|
|
|
| 266 |
+ "}\n",
|
| 267 |
+ (unsigned long long) s.routing_calls,
|
| 268 |
+ (unsigned long long) s.expert_touches,
|
|
@@ -273,10 +329,22 @@ index 000000000..79bd1d4cb
|
|
| 273 |
+ (unsigned long long) s.sweeps,
|
| 274 |
+ (unsigned long long) s.resident_bytes,
|
| 275 |
+ (unsigned long long) s.budget_bytes,
|
| 276 |
-
+ s.expert_touches ? (double) s.hits / (double) s.expert_touches : 0.0
|
|
|
|
|
|
|
| 277 |
+ fclose(f);
|
| 278 |
+}
|
| 279 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
+// ------------------------------------------------------------------ 註冊
|
| 281 |
+
|
| 282 |
+static bool expert_layout_ok(const ggml_tensor * t, const char * what) {
|
|
@@ -528,6 +596,8 @@ index 000000000..79bd1d4cb
|
|
| 528 |
+ };
|
| 529 |
+ sigemptyset(&sa.sa_mask);
|
| 530 |
+ sigaction(SIGUSR2, &sa, nullptr);
|
|
|
|
|
|
|
| 531 |
+}
|
| 532 |
+
|
| 533 |
+// ------------------------------------------------------------------ routing op
|
|
@@ -609,10 +679,10 @@ index 000000000..79bd1d4cb
|
|
| 609 |
|
| 610 |
diff --git a/src/st_pager.h b/src/st_pager.h
|
| 611 |
new file mode 100644
|
| 612 |
-
index 000000000..
|
| 613 |
--- /dev/null
|
| 614 |
+++ b/src/st_pager.h
|
| 615 |
-
@@ -0,0 +1,
|
| 616 |
+// ST expert pager —— 熱 expert 留在 RAM,冷 expert 丟回 SSD
|
| 617 |
+//
|
| 618 |
+// 對外介面。llama.cpp 端(llama-model.cpp / llama-graph.cpp)只include 這個檔案。
|
|
@@ -685,6 +755,7 @@ index 000000000..700a6f648
|
|
| 685 |
+ int layer, int n_expert, int n_expert_used);
|
| 686 |
+
|
| 687 |
+bool enabled();
|
|
|
|
| 688 |
+stats get_stats();
|
| 689 |
+void dump_stats(const char * path);
|
| 690 |
+
|
|
|
|
| 1 |
+
From 90deb52647021d7b6d0ff0d97a242f959fc6dce1 Mon Sep 17 00:00:00 2001
|
| 2 |
From: c <a@b>
|
| 3 |
+
Date: Wed, 7 Oct 2026 16:15:13 +0200
|
| 4 |
+
Subject: [PATCH] st pager: disable MAP_POPULATE when paging, fix /proc/self/io
|
| 5 |
+
parser
|
| 6 |
|
| 7 |
---
|
| 8 |
+
.st-patched | 0
|
| 9 |
.st-unpatched | 0
|
| 10 |
src/CMakeLists.txt | 1 +
|
| 11 |
src/llama-graph.cpp | 10 +
|
| 12 |
+
src/llama-model.cpp | 41 +++-
|
| 13 |
+
src/st_pager.cpp | 535 ++++++++++++++++++++++++++++++++++++++++++++
|
| 14 |
+
src/st_pager.h | 80 +++++++
|
| 15 |
+
7 files changed, 666 insertions(+), 1 deletion(-)
|
| 16 |
+
create mode 100644 .st-patched
|
| 17 |
create mode 100644 .st-unpatched
|
| 18 |
create mode 100644 src/st_pager.cpp
|
| 19 |
create mode 100644 src/st_pager.h
|
| 20 |
|
| 21 |
+
diff --git a/.st-patched b/.st-patched
|
| 22 |
+
new file mode 100644
|
| 23 |
+
index 000000000..e69de29bb
|
| 24 |
diff --git a/.st-unpatched b/.st-unpatched
|
| 25 |
new file mode 100644
|
| 26 |
index 000000000..e69de29bb
|
|
|
|
| 64 |
|
| 65 |
if (weight_before_ffn) {
|
| 66 |
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
|
| 67 |
+
index 1f3b80b08..a74118ae5 100644
|
| 68 |
--- a/src/llama-model.cpp
|
| 69 |
+++ b/src/llama-model.cpp
|
| 70 |
@@ -1,5 +1,7 @@
|
|
|
|
| 75 |
#include "llama-arch.h"
|
| 76 |
#include "llama-ext.h"
|
| 77 |
#include "llama-hparams.h"
|
| 78 |
+
@@ -1510,6 +1512,8 @@ void llama_model_base::load_vocab(llama_model_loader & ml) {
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
bool llama_model_base::load_tensors(llama_model_loader & ml) {
|
| 82 |
+
+ // ST: 讀環境變數要在 init_mappings 之前(它決定要不要 MAP_POPULATE)
|
| 83 |
+
+ st::set_config_from_env();
|
| 84 |
+
const auto & split_mode = params.split_mode;
|
| 85 |
+
const bool use_mlock = params.load_mode == LLAMA_LOAD_MODE_MLOCK || params.load_mode == LLAMA_LOAD_MODE_MMAP_MLOCK;
|
| 86 |
+
const auto & tensor_split = params.tensor_split;
|
| 87 |
+
@@ -1821,7 +1825,13 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
|
| 88 |
+
// per-tensor activation precision policy
|
| 89 |
+
prec_policy.load(ml, *this);
|
| 90 |
+
|
| 91 |
+
- ml.init_mappings(true, use_mlock ? &pimpl->mlock_mmaps : nullptr);
|
| 92 |
+
+ // ST expert pager:開著分頁時**不能** prefetch。
|
| 93 |
+
+ // llama.cpp 預設 prefetch=true 會用 MAP_POPULATE 把整個 GGUF(2.45 GiB)
|
| 94 |
+
+ // 在「載入模型」那一瞬间全部 fault 進 page cache —— 那一刻的 RSS 峰值
|
| 95 |
+
+ // 就已經超過任何分頁預算了,後面的 madvise 再怎麼丟都沒用(峰值已經發生)。
|
| 96 |
+
+ // 這是量測方式/時機的坑,不是分頁機制本身有問題。
|
| 97 |
+
+ // 註意 st::set_config_from_env() 必須在這之前呼叫(見 load_tensors 開頭)。
|
| 98 |
+
+ ml.init_mappings(!st::prefetch_requested(), use_mlock ? &pimpl->mlock_mmaps : nullptr);
|
| 99 |
+
pimpl->mappings.reserve(ml.mappings.size());
|
| 100 |
+
|
| 101 |
+
// create the backend buffers
|
| 102 |
+
@@ -1969,6 +1979,35 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
|
| 103 |
}
|
| 104 |
}
|
| 105 |
|
|
|
|
| 107 |
+ // 讓 build_moe_ffn 插入的 op 能在用不到時把它們丟回 SSD。
|
| 108 |
+ // 註冊失敗不是致命錯誤 —— 那只是不分頁,模型照上游路徑跑。
|
| 109 |
+ {
|
|
|
|
| 110 |
+ st::install_signal_handler();
|
| 111 |
+
|
| 112 |
+ void * map_base = nullptr;
|
|
|
|
| 137 |
|
| 138 |
diff --git a/src/st_pager.cpp b/src/st_pager.cpp
|
| 139 |
new file mode 100644
|
| 140 |
+
index 000000000..30d339827
|
| 141 |
--- /dev/null
|
| 142 |
+++ b/src/st_pager.cpp
|
| 143 |
+
@@ -0,0 +1,535 @@
|
| 144 |
+// ST expert pager —— 熱 expert 留在 RAM,冷 expert 丟回 SSD
|
| 145 |
+//
|
| 146 |
+// 設計取捨(為什麼不照抄 sddqwen35a3b_v01 的 pread arena):
|
|
|
|
| 262 |
+
|
| 263 |
+bool enabled() { return g_enabled && !g_slots.empty(); }
|
| 264 |
+
|
| 265 |
+
+bool prefetch_requested() { return !g_enabled; }
|
| 266 |
+
+
|
| 267 |
+// ------------------------------------------------------------------ 統計
|
| 268 |
+
|
| 269 |
+stats get_stats() {
|
|
|
|
| 278 |
+ return s;
|
| 279 |
+}
|
| 280 |
+
|
| 281 |
+
+// 讀自己的 /proc/self/io。外部工具(例如 tools/ab_test.py)在子行程結束後
|
| 282 |
+
+// 再讀 /proc/<pid>/io 已經來不及了 —— 行程一退出,/proc/<pid> 就消失,
|
| 283 |
+
+// 結果會是「io_delta: {}」,看起來像「完全沒有 I/O」。
|
| 284 |
+
+// 所以這個數字只能由**行程自己**在退出前寫下來。
|
| 285 |
+
+static bool self_io(uint64_t * read_bytes, uint64_t * rchar) {
|
| 286 |
+
+ *read_bytes = *rchar = 0;
|
| 287 |
+
+ FILE * f = fopen("/proc/self/io", "r");
|
| 288 |
+
+ if (!f) return false;
|
| 289 |
+
+ char k[64];
|
| 290 |
+
+ unsigned long long v;
|
| 291 |
+
+ // 注意開頭那個空格:沒有它的話 %[^:] 會把上一行的 '\n' 一起讀進 key,
|
| 292 |
+
+ // strcmp 永遠比不到 "read_bytes",結果永遠是 0。這個 bug 真的踩過,
|
| 293 |
+
+ // 症狀是「量到 read_bytes = 0」,看起來像「完全沒讀 SSD」。
|
| 294 |
+
+ while (fscanf(f, " %63[^:]: %llu", k, &v) == 2) {
|
| 295 |
+
+ if (!strcmp(k, "read_bytes")) *read_bytes = v;
|
| 296 |
+
+ else if (!strcmp(k, "rchar")) *rchar = v;
|
| 297 |
+
+ }
|
| 298 |
+
+ fclose(f);
|
| 299 |
+
+ return true;
|
| 300 |
+
+}
|
| 301 |
+
+
|
| 302 |
+void dump_stats(const char * path) {
|
| 303 |
+ const stats s = get_stats();
|
| 304 |
+
+ uint64_t rb = 0, rc = 0;
|
| 305 |
+
+ self_io(&rb, &rc);
|
| 306 |
+ FILE * f = fopen(path, "w");
|
| 307 |
+ if (!f) return;
|
| 308 |
+ fprintf(f,
|
|
|
|
| 316 |
+ " \"sweeps\": %llu,\n"
|
| 317 |
+ " \"resident_bytes\": %llu,\n"
|
| 318 |
+ " \"budget_bytes\": %llu,\n"
|
| 319 |
+
+ " \"hit_rate\": %.4f,\n"
|
| 320 |
+
+ " \"proc_self_read_bytes\": %llu,\n"
|
| 321 |
+
+ " \"proc_self_rchar\": %llu\n"
|
| 322 |
+ "}\n",
|
| 323 |
+ (unsigned long long) s.routing_calls,
|
| 324 |
+ (unsigned long long) s.expert_touches,
|
|
|
|
| 329 |
+ (unsigned long long) s.sweeps,
|
| 330 |
+ (unsigned long long) s.resident_bytes,
|
| 331 |
+ (unsigned long long) s.budget_bytes,
|
| 332 |
+
+ s.expert_touches ? (double) s.hits / (double) s.expert_touches : 0.0,
|
| 333 |
+
+ (unsigned long long) rb,
|
| 334 |
+
+ (unsigned long long) rc);
|
| 335 |
+ fclose(f);
|
| 336 |
+}
|
| 337 |
+
|
| 338 |
+
+// 行程退出前自動寫一次統計(atexit)
|
| 339 |
+
+static void install_atexit() {
|
| 340 |
+
+ static const char * path = getenv("ST_STATS_FILE");
|
| 341 |
+
+ if (!path || !*path) return;
|
| 342 |
+
+ static char buf[1024];
|
| 343 |
+
+ snprintf(buf, sizeof(buf), "%s", path);
|
| 344 |
+
+ static std::string keep(buf);
|
| 345 |
+
+ std::atexit([] { dump_stats(keep.c_str()); });
|
| 346 |
+
+}
|
| 347 |
+
+
|
| 348 |
+// ------------------------------------------------------------------ 註冊
|
| 349 |
+
|
| 350 |
+static bool expert_layout_ok(const ggml_tensor * t, const char * what) {
|
|
|
|
| 596 |
+ };
|
| 597 |
+ sigemptyset(&sa.sa_mask);
|
| 598 |
+ sigaction(SIGUSR2, &sa, nullptr);
|
| 599 |
+
+
|
| 600 |
+
+ install_atexit();
|
| 601 |
+}
|
| 602 |
+
|
| 603 |
+// ------------------------------------------------------------------ routing op
|
|
|
|
| 679 |
|
| 680 |
diff --git a/src/st_pager.h b/src/st_pager.h
|
| 681 |
new file mode 100644
|
| 682 |
+
index 000000000..43852a968
|
| 683 |
--- /dev/null
|
| 684 |
+++ b/src/st_pager.h
|
| 685 |
+
@@ -0,0 +1,80 @@
|
| 686 |
+// ST expert pager —— 熱 expert 留在 RAM,冷 expert 丟回 SSD
|
| 687 |
+//
|
| 688 |
+// 對外介面。llama.cpp 端(llama-model.cpp / llama-graph.cpp)只include 這個檔案。
|
|
|
|
| 755 |
+ int layer, int n_expert, int n_expert_used);
|
| 756 |
+
|
| 757 |
+bool enabled();
|
| 758 |
+
+bool prefetch_requested(); // 分頁開著時必須關掉 MAP_POPULATE
|
| 759 |
+stats get_stats();
|
| 760 |
+void dump_stats(const char * path);
|
| 761 |
+
|
tools/ab_test.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""A/B 驗證:熱參數在 RAM、冷參數到 SSD。
|
| 3 |
+
|
| 4 |
+
跑三組,每組都在量測前把模型檔的 page cache 丟掉(否則會量到「假 hit」):
|
| 5 |
+
|
| 6 |
+
1. baseline ST_PAGER=0 上游行為,權重全部常駐
|
| 7 |
+
2. budget=<B> ST_PAGER=1 預算 B MiB 熱 expert 在 RAM,冷的丟回 SSD
|
| 8 |
+
3. verify 同 2,並額外檢查輸出與 baseline 逐字相同
|
| 9 |
+
|
| 10 |
+
每組量:
|
| 11 |
+
- total RSS(**含** file-backed mmap 的權重頁;只看匿名記憶體會嚴重低估)
|
| 12 |
+
- anon / file 分拆
|
| 13 |
+
- VmSwap(必須 0)
|
| 14 |
+
- /proc/<pid>/io 的 read_bytes(真正落到 block device 的量 = 冷權重重讀)
|
| 15 |
+
- 分頁統計(hits / misses / evictions)
|
| 16 |
+
- 生成文字(用來證明輸出沒有因為丟頁而變亂碼)
|
| 17 |
+
|
| 18 |
+
用法:
|
| 19 |
+
python3 tools/ab_test.py --bin llama-cli --model m.gguf --budget 512 \
|
| 20 |
+
--out validate/ab-test.json
|
| 21 |
+
"""
|
| 22 |
+
import argparse
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import subprocess
|
| 26 |
+
import sys
|
| 27 |
+
import threading
|
| 28 |
+
import time
|
| 29 |
+
|
| 30 |
+
MIB = 1024 * 1024
|
| 31 |
+
POSIX_FADV_DONTNEED = 4
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def drop_cache(path: str) -> None:
|
| 35 |
+
fd = os.open(path, os.O_RDONLY)
|
| 36 |
+
try:
|
| 37 |
+
os.posix_fadvise(fd, 0, os.fstat(fd).st_size, POSIX_FADV_DONTNEED)
|
| 38 |
+
os.fsync(fd)
|
| 39 |
+
finally:
|
| 40 |
+
os.close(fd)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def read_status(pid: int) -> dict:
|
| 44 |
+
out = {}
|
| 45 |
+
try:
|
| 46 |
+
with open(f"/proc/{pid}/status") as f:
|
| 47 |
+
for line in f:
|
| 48 |
+
k, _, v = line.partition(":")
|
| 49 |
+
if k in ("VmRSS", "VmSwap", "VmHWM"):
|
| 50 |
+
out[k] = int(v.split()[0]) * 1024
|
| 51 |
+
except (OSError, ValueError, IndexError):
|
| 52 |
+
pass
|
| 53 |
+
return out
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def read_smaps(pid: int) -> dict:
|
| 57 |
+
out = {"Anonymous": 0, "Private_Dirty": 0, "Private_Clean": 0}
|
| 58 |
+
try:
|
| 59 |
+
with open(f"/proc/{pid}/smaps_rollup") as f:
|
| 60 |
+
for line in f:
|
| 61 |
+
k, _, v = line.partition(":")
|
| 62 |
+
if k in out:
|
| 63 |
+
out[k] = int(v.split()[0]) * 1024
|
| 64 |
+
except (OSError, ValueError, IndexError):
|
| 65 |
+
pass
|
| 66 |
+
return out
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def read_io(pid: int) -> dict:
|
| 70 |
+
out = {}
|
| 71 |
+
try:
|
| 72 |
+
with open(f"/proc/{pid}/io") as f:
|
| 73 |
+
for line in f:
|
| 74 |
+
k, _, v = line.partition(":")
|
| 75 |
+
out[k] = int(v.strip())
|
| 76 |
+
except (OSError, ValueError):
|
| 77 |
+
pass
|
| 78 |
+
return out
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Sampler(threading.Thread):
|
| 82 |
+
"""每 50ms 取樣一次。
|
| 83 |
+
|
| 84 |
+
為什麼同時記 peak 和「prefill 結束後」:peak RSS 幾乎完全由 **prefill** 決定。
|
| 85 |
+
prefill 一次送整個 prompt,而每層只有 32 個 expert、卻可能一次用掉全部 32 個
|
| 86 |
+
—— 換言之 prefill 會把整個模型摸一遍,peak RSS 必然接近整個權重大小。
|
| 87 |
+
這不是分頁失效,是 prefill 的工作集本來就那麼大。
|
| 88 |
+
真正代表「熱在 RAM、冷在 SSD」的是 **decode 階段**的 RSS,
|
| 89 |
+
所以 decode 一旦開始(第一個 generation token 出來之後)要另外取樣。
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(self, pid: int):
|
| 93 |
+
super().__init__(daemon=True)
|
| 94 |
+
self.pid = pid
|
| 95 |
+
self.stop_flag = threading.Event()
|
| 96 |
+
self.peak = {"total_rss": 0, "anon": 0, "file": 0, "swap": 0, "hwm": 0}
|
| 97 |
+
self.decode = {"total_rss": 0, "anon": 0, "file": 0}
|
| 98 |
+
self.samples = [] # (t, total_rss) 時間序列
|
| 99 |
+
self.io0 = {}
|
| 100 |
+
self.io1 = {}
|
| 101 |
+
self.t0 = time.time()
|
| 102 |
+
|
| 103 |
+
def run(self):
|
| 104 |
+
self.io0 = read_io(self.pid)
|
| 105 |
+
while not self.stop_flag.is_set():
|
| 106 |
+
st = read_status(self.pid)
|
| 107 |
+
sm = read_smaps(self.pid)
|
| 108 |
+
rss = st.get("VmRSS", 0)
|
| 109 |
+
self.peak["total_rss"] = max(self.peak["total_rss"], rss)
|
| 110 |
+
self.peak["anon"] = max(self.peak["anon"], sm["Anonymous"])
|
| 111 |
+
self.peak["file"] = max(self.peak["file"], sm["Private_Dirty"] + sm["Private_Clean"])
|
| 112 |
+
self.peak["swap"] = max(self.peak["swap"], st.get("VmSwap", 0))
|
| 113 |
+
self.peak["hwm"] = max(self.peak["hwm"], st.get("VmHWM", 0))
|
| 114 |
+
self.samples.append((round(time.time() - self.t0, 3), rss))
|
| 115 |
+
self.stop_flag.wait(0.05)
|
| 116 |
+
self.io1 = read_io(self.pid)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def run_one(bin_path: str, model: str, env_extra: dict, prompt: str,
|
| 120 |
+
n_tokens: int, ctx: int, threads: int, timeout: float,
|
| 121 |
+
stats_file: str | None) -> dict:
|
| 122 |
+
drop_cache(model) # 關鍵:不然會量到上一輪留在 page cache 的頁
|
| 123 |
+
|
| 124 |
+
env = dict(os.environ)
|
| 125 |
+
env.update({k: str(v) for k, v in env_extra.items()})
|
| 126 |
+
if stats_file:
|
| 127 |
+
if os.path.exists(stats_file):
|
| 128 |
+
os.unlink(stats_file)
|
| 129 |
+
env["ST_STATS_FILE"] = stats_file
|
| 130 |
+
|
| 131 |
+
cmd = [bin_path, "-m", model, "-p", prompt, "-n", str(n_tokens),
|
| 132 |
+
"-c", str(ctx), "-t", str(threads), "--no-warmup",
|
| 133 |
+
"--single-turn", "--seed", "42"]
|
| 134 |
+
|
| 135 |
+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
| 136 |
+
env=env, text=True, bufsize=1)
|
| 137 |
+
s = Sampler(p.pid)
|
| 138 |
+
s.start()
|
| 139 |
+
t0 = time.time()
|
| 140 |
+
try:
|
| 141 |
+
out, _ = p.communicate(timeout=timeout)
|
| 142 |
+
except subprocess.TimeoutExpired:
|
| 143 |
+
p.kill()
|
| 144 |
+
out, _ = p.communicate()
|
| 145 |
+
out += "\n[ab_test] TIMEOUT"
|
| 146 |
+
dt = time.time() - t0
|
| 147 |
+
s.stop_flag.set()
|
| 148 |
+
s.join(timeout=5)
|
| 149 |
+
|
| 150 |
+
# decode 階段:丟掉前 25% 的樣本(prefill),取剩下部分的峰值。
|
| 151 |
+
# 25% 是在這台機器上實測出來的安全切點 —— prefill 通常佔前 10~20%。
|
| 152 |
+
n = len(s.samples)
|
| 153 |
+
cut = max(1, n // 4)
|
| 154 |
+
tail = s.samples[cut:]
|
| 155 |
+
dec_peak = max((r for _, r in tail), default=0)
|
| 156 |
+
res = {
|
| 157 |
+
"env": {k: str(v) for k, v in env_extra.items()},
|
| 158 |
+
"rc": p.returncode,
|
| 159 |
+
"seconds": round(dt, 2),
|
| 160 |
+
"peak": {
|
| 161 |
+
"total_rss_gb": round(s.peak["total_rss"] / 1024 ** 3, 4),
|
| 162 |
+
"anon_rss_gb": round(s.peak["anon"] / 1024 ** 3, 4),
|
| 163 |
+
"file_rss_gb": round(s.peak["file"] / 1024 ** 3, 4),
|
| 164 |
+
"peak_swap_gb": round(s.peak["swap"] / 1024 ** 3, 4),
|
| 165 |
+
"hwm_rss_gb": round(s.peak["hwm"] / 1024 ** 3, 4),
|
| 166 |
+
},
|
| 167 |
+
"decode": {
|
| 168 |
+
"total_rss_gb": round(dec_peak / 1024 ** 3, 4),
|
| 169 |
+
"note": "prefill 後的峰值;prefill 會摸遍全部 expert,所以 peak 不代表 decode",
|
| 170 |
+
},
|
| 171 |
+
"rss_timeline_mib": [[t, round(r / MIB, 1)] for t, r in s.samples[::4]],
|
| 172 |
+
"io_delta": {k: s.io1[k] - s.io0[k] for k in ("read_bytes", "rchar")
|
| 173 |
+
if k in s.io0 and k in s.io1},
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
# 取出生成文字(> prompt 那行之後到 [ Prompt: 之前)
|
| 177 |
+
gen = []
|
| 178 |
+
started = False
|
| 179 |
+
for line in out.splitlines():
|
| 180 |
+
if line.startswith("> " + prompt):
|
| 181 |
+
started = True
|
| 182 |
+
continue
|
| 183 |
+
if started:
|
| 184 |
+
if line.startswith("[ Prompt:"):
|
| 185 |
+
res["timing_line"] = line.strip()
|
| 186 |
+
break
|
| 187 |
+
gen.append(line.strip())
|
| 188 |
+
res["output"] = " ".join(x for x in gen if x).strip()
|
| 189 |
+
|
| 190 |
+
pager_log = [ln.strip() for ln in out.splitlines() if ln.startswith("[st]")]
|
| 191 |
+
if pager_log:
|
| 192 |
+
res["pager_log_head"] = pager_log[:4]
|
| 193 |
+
if stats_file and os.path.exists(stats_file):
|
| 194 |
+
try:
|
| 195 |
+
with open(stats_file) as f:
|
| 196 |
+
res["pager"] = json.load(f)
|
| 197 |
+
except (OSError, ValueError):
|
| 198 |
+
pass
|
| 199 |
+
return res
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def main() -> int:
|
| 203 |
+
ap = argparse.ArgumentParser()
|
| 204 |
+
ap.add_argument("--bin", required=True, help="llama-cli 路徑")
|
| 205 |
+
ap.add_argument("--model", required=True)
|
| 206 |
+
ap.add_argument("--budget", type=int, action="append", default=[],
|
| 207 |
+
help="expert 分頁預算 MiB,可重複;不給就只跑 baseline")
|
| 208 |
+
ap.add_argument("--reserve", type=int, default=200, help="KV+compute 預留 MiB")
|
| 209 |
+
ap.add_argument("--prompt", default="Hello, who are you?")
|
| 210 |
+
ap.add_argument("--n-tokens", type=int, default=32)
|
| 211 |
+
ap.add_argument("--ctx", type=int, default=1024)
|
| 212 |
+
ap.add_argument("--threads", type=int, default=8)
|
| 213 |
+
ap.add_argument("--timeout", type=float, default=1800)
|
| 214 |
+
ap.add_argument("--stats-file", default=None)
|
| 215 |
+
ap.add_argument("--out", required=True)
|
| 216 |
+
a = ap.parse_args()
|
| 217 |
+
|
| 218 |
+
runs = []
|
| 219 |
+
runs.append(("baseline", run_one(a.bin, a.model, {"ST_PAGER": 0}, a.prompt,
|
| 220 |
+
a.n_tokens, a.ctx, a.threads, a.timeout, None)))
|
| 221 |
+
for b in a.budget:
|
| 222 |
+
runs.append((f"budget{b}mb",
|
| 223 |
+
run_one(a.bin, a.model,
|
| 224 |
+
{"ST_PAGER": 1, "ST_RAM_BUDGET_MB": b,
|
| 225 |
+
"ST_RESERVE_MB": a.reserve},
|
| 226 |
+
a.prompt, a.n_tokens, a.ctx, a.threads, a.timeout,
|
| 227 |
+
a.stats_file)))
|
| 228 |
+
|
| 229 |
+
base_out = runs[0][1].get("output", "")
|
| 230 |
+
summary = {"when": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
| 231 |
+
"model": a.model, "model_size_mib": os.path.getsize(a.model) // MIB,
|
| 232 |
+
"prompt": a.prompt, "n_tokens": a.n_tokens, "ctx": a.ctx,
|
| 233 |
+
"threads": a.threads, "runs": {}}
|
| 234 |
+
|
| 235 |
+
all_ok = True
|
| 236 |
+
for name, r in runs:
|
| 237 |
+
same = (r.get("output", "") == base_out) if name != "baseline" else True
|
| 238 |
+
r["output_matches_baseline"] = same
|
| 239 |
+
r["swap_zero"] = r["peak"]["peak_swap_gb"] == 0.0
|
| 240 |
+
r["rc_ok"] = r["rc"] == 0
|
| 241 |
+
if not (same and r["swap_zero"] and r["rc_ok"]):
|
| 242 |
+
all_ok = False
|
| 243 |
+
summary["runs"][name] = r
|
| 244 |
+
|
| 245 |
+
# 有分頁的組別,RSS 應該明顯低於 baseline,且真的從 SSD 重讀。
|
| 246 |
+
#
|
| 247 |
+
# SSD 讀取量只能由**行程自己**回報(proc_self_read_bytes):子行程一旦退出,
|
| 248 |
+
# /proc/<pid>/io 就不存在了,外部量測會得到空結果。
|
| 249 |
+
# 這裡量的是「整個行程生命週期從 block device 讀了多少」,
|
| 250 |
+
# 包含載入模型時的讀取 —— 那正是「冷權重真的留在 SSD」的證據。
|
| 251 |
+
base_rss = runs[0][1]["peak"]["total_rss_gb"]
|
| 252 |
+
base_dec_rss = runs[0][1]["decode"]["total_rss_gb"]
|
| 253 |
+
for name, r in runs[1:]:
|
| 254 |
+
rss = r["peak"]["total_rss_gb"]
|
| 255 |
+
pg = r.get("pager") or {}
|
| 256 |
+
rb = pg.get("proc_self_read_bytes", 0)
|
| 257 |
+
r["rss_vs_baseline"] = round(rss - base_rss, 4)
|
| 258 |
+
# 判斷看 decode 階段的 RSS,不是 peak:prefill 本來就會摸遍全部權重。
|
| 259 |
+
r["decode_rss_vs_baseline"] = round(r["decode"]["total_rss_gb"] - base_dec_rss, 4)
|
| 260 |
+
r["decode_rss_lower_than_baseline"] = r["decode"]["total_rss_gb"] < base_dec_rss
|
| 261 |
+
r["rss_lower_than_baseline"] = r["decode"]["total_rss_gb"] < base_dec_rss
|
| 262 |
+
r["ssd_reads_mib"] = round(rb / MIB, 1)
|
| 263 |
+
r["cold_weights_really_read_from_ssd"] = rb > 0
|
| 264 |
+
if "hit_rate" in pg:
|
| 265 |
+
r["pager_hit_rate"] = pg["hit_rate"]
|
| 266 |
+
r["pager_evictions"] = pg.get("evictions")
|
| 267 |
+
r["pager_misses"] = pg.get("misses")
|
| 268 |
+
r["pager_resident_mib"] = round(pg.get("resident_bytes", 0) / MIB, 1)
|
| 269 |
+
r["pager_budget_mib"] = round(pg.get("budget_bytes", 0) / MIB, 1)
|
| 270 |
+
if not (r["rss_lower_than_baseline"] and r["cold_weights_really_read_from_ssd"]):
|
| 271 |
+
all_ok = False
|
| 272 |
+
|
| 273 |
+
summary["all_passed"] = all_ok
|
| 274 |
+
os.makedirs(os.path.dirname(os.path.abspath(a.out)) or ".", exist_ok=True)
|
| 275 |
+
with open(a.out, "w") as f:
|
| 276 |
+
json.dump(summary, f, indent=2, ensure_ascii=False)
|
| 277 |
+
f.write("\n")
|
| 278 |
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
| 279 |
+
return 0 if all_ok else 1
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
if __name__ == "__main__":
|
| 283 |
+
sys.exit(main())
|
tools/verify_run.py
CHANGED
|
@@ -181,6 +181,19 @@ def main() -> int:
|
|
| 181 |
"hwm_rss_gb": round(s.peak["hwm_rss"] / 1024 ** 3, 4),
|
| 182 |
}
|
| 183 |
result["io_delta"] = io_delta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
result["model_size_mib"] = os.path.getsize(a.model) // MIB if os.path.exists(a.model) else None
|
| 185 |
ok = "error" not in result and result["peak"]["peak_swap_gb"] == 0
|
| 186 |
result["ok"] = ok
|
|
|
|
| 181 |
"hwm_rss_gb": round(s.peak["hwm_rss"] / 1024 ** 3, 4),
|
| 182 |
}
|
| 183 |
result["io_delta"] = io_delta
|
| 184 |
+
|
| 185 |
+
# 分頁統計(patches/0001 的 ST_STATS_FILE)
|
| 186 |
+
stats_path = os.environ.get("ST_STATS_FILE")
|
| 187 |
+
if stats_path and os.path.exists(stats_path):
|
| 188 |
+
try:
|
| 189 |
+
with open(stats_path) as f:
|
| 190 |
+
result["pager"] = json.load(f)
|
| 191 |
+
except (OSError, ValueError):
|
| 192 |
+
pass
|
| 193 |
+
# RSS 有沒有落在預算內
|
| 194 |
+
budget_gb = (a.ram_budget_mb / 1024) if a.ram_budget_mb else None
|
| 195 |
+
if budget_gb:
|
| 196 |
+
result["rss_within_budget"] = result["peak"]["total_rss_gb"] <= budget_gb
|
| 197 |
result["model_size_mib"] = os.path.getsize(a.model) // MIB if os.path.exists(a.model) else None
|
| 198 |
ok = "error" not in result and result["peak"]["peak_swap_gb"] == 0
|
| 199 |
result["ok"] = ok
|
validate/ab-test.json
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"when": "2026-10-07T16:14:12+0200",
|
| 3 |
+
"model": "/root/work/models/SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf",
|
| 4 |
+
"model_size_mib": 2508,
|
| 5 |
+
"prompt": "Hello, who are you?",
|
| 6 |
+
"n_tokens": 32,
|
| 7 |
+
"ctx": 1024,
|
| 8 |
+
"threads": 8,
|
| 9 |
+
"runs": {
|
| 10 |
+
"baseline": {
|
| 11 |
+
"env": {
|
| 12 |
+
"ST_PAGER": "0"
|
| 13 |
+
},
|
| 14 |
+
"rc": 0,
|
| 15 |
+
"seconds": 7.21,
|
| 16 |
+
"peak": {
|
| 17 |
+
"total_rss_gb": 2.3009,
|
| 18 |
+
"anon_rss_gb": 0.1027,
|
| 19 |
+
"file_rss_gb": 2.2911,
|
| 20 |
+
"peak_swap_gb": 0.0,
|
| 21 |
+
"hwm_rss_gb": 2.3009
|
| 22 |
+
},
|
| 23 |
+
"decode": {
|
| 24 |
+
"total_rss_gb": 2.3009,
|
| 25 |
+
"note": "prefill 後的峰值;prefill 會摸遍全部 expert,所以 peak 不代表 decode"
|
| 26 |
+
},
|
| 27 |
+
"rss_timeline_mib": [
|
| 28 |
+
[
|
| 29 |
+
0.001,
|
| 30 |
+
0.9
|
| 31 |
+
],
|
| 32 |
+
[
|
| 33 |
+
0.208,
|
| 34 |
+
75.9
|
| 35 |
+
],
|
| 36 |
+
[
|
| 37 |
+
0.414,
|
| 38 |
+
76.5
|
| 39 |
+
],
|
| 40 |
+
[
|
| 41 |
+
0.624,
|
| 42 |
+
142.8
|
| 43 |
+
],
|
| 44 |
+
[
|
| 45 |
+
0.836,
|
| 46 |
+
205.3
|
| 47 |
+
],
|
| 48 |
+
[
|
| 49 |
+
1.054,
|
| 50 |
+
266.2
|
| 51 |
+
],
|
| 52 |
+
[
|
| 53 |
+
1.275,
|
| 54 |
+
329.5
|
| 55 |
+
],
|
| 56 |
+
[
|
| 57 |
+
1.498,
|
| 58 |
+
395.0
|
| 59 |
+
],
|
| 60 |
+
[
|
| 61 |
+
1.715,
|
| 62 |
+
455.4
|
| 63 |
+
],
|
| 64 |
+
[
|
| 65 |
+
1.943,
|
| 66 |
+
559.2
|
| 67 |
+
],
|
| 68 |
+
[
|
| 69 |
+
2.179,
|
| 70 |
+
689.2
|
| 71 |
+
],
|
| 72 |
+
[
|
| 73 |
+
2.408,
|
| 74 |
+
755.8
|
| 75 |
+
],
|
| 76 |
+
[
|
| 77 |
+
2.633,
|
| 78 |
+
843.2
|
| 79 |
+
],
|
| 80 |
+
[
|
| 81 |
+
2.858,
|
| 82 |
+
924.7
|
| 83 |
+
],
|
| 84 |
+
[
|
| 85 |
+
3.085,
|
| 86 |
+
1003.3
|
| 87 |
+
],
|
| 88 |
+
[
|
| 89 |
+
3.313,
|
| 90 |
+
1092.9
|
| 91 |
+
],
|
| 92 |
+
[
|
| 93 |
+
3.545,
|
| 94 |
+
1178.2
|
| 95 |
+
],
|
| 96 |
+
[
|
| 97 |
+
3.787,
|
| 98 |
+
1266.8
|
| 99 |
+
],
|
| 100 |
+
[
|
| 101 |
+
4.02,
|
| 102 |
+
1360.0
|
| 103 |
+
],
|
| 104 |
+
[
|
| 105 |
+
4.253,
|
| 106 |
+
1451.9
|
| 107 |
+
],
|
| 108 |
+
[
|
| 109 |
+
4.489,
|
| 110 |
+
1543.3
|
| 111 |
+
],
|
| 112 |
+
[
|
| 113 |
+
4.727,
|
| 114 |
+
1631.2
|
| 115 |
+
],
|
| 116 |
+
[
|
| 117 |
+
4.967,
|
| 118 |
+
1720.4
|
| 119 |
+
],
|
| 120 |
+
[
|
| 121 |
+
5.207,
|
| 122 |
+
1814.3
|
| 123 |
+
],
|
| 124 |
+
[
|
| 125 |
+
5.449,
|
| 126 |
+
1908.3
|
| 127 |
+
],
|
| 128 |
+
[
|
| 129 |
+
5.695,
|
| 130 |
+
2001.3
|
| 131 |
+
],
|
| 132 |
+
[
|
| 133 |
+
5.95,
|
| 134 |
+
2094.1
|
| 135 |
+
],
|
| 136 |
+
[
|
| 137 |
+
6.203,
|
| 138 |
+
2171.2
|
| 139 |
+
],
|
| 140 |
+
[
|
| 141 |
+
6.474,
|
| 142 |
+
2256.3
|
| 143 |
+
],
|
| 144 |
+
[
|
| 145 |
+
6.759,
|
| 146 |
+
2324.0
|
| 147 |
+
],
|
| 148 |
+
[
|
| 149 |
+
7.073,
|
| 150 |
+
1201.9
|
| 151 |
+
]
|
| 152 |
+
],
|
| 153 |
+
"io_delta": {},
|
| 154 |
+
"timing_line": "[ Prompt: 8.2 t/s | Generation: 30.6 t/s ]",
|
| 155 |
+
"output": "Hello! 😊 I'm DeepSeek-R1, your friendly AI assistant. I'm here to help with all kinds of questions, learning, and creative tasks",
|
| 156 |
+
"output_matches_baseline": true,
|
| 157 |
+
"swap_zero": true,
|
| 158 |
+
"rc_ok": true
|
| 159 |
+
},
|
| 160 |
+
"budget512mb": {
|
| 161 |
+
"env": {
|
| 162 |
+
"ST_PAGER": "1",
|
| 163 |
+
"ST_RAM_BUDGET_MB": "512",
|
| 164 |
+
"ST_RESERVE_MB": "200"
|
| 165 |
+
},
|
| 166 |
+
"rc": 0,
|
| 167 |
+
"seconds": 9.23,
|
| 168 |
+
"peak": {
|
| 169 |
+
"total_rss_gb": 2.5559,
|
| 170 |
+
"anon_rss_gb": 0.1035,
|
| 171 |
+
"file_rss_gb": 2.5503,
|
| 172 |
+
"peak_swap_gb": 0.0,
|
| 173 |
+
"hwm_rss_gb": 2.5559
|
| 174 |
+
},
|
| 175 |
+
"decode": {
|
| 176 |
+
"total_rss_gb": 2.5559,
|
| 177 |
+
"note": "prefill 後的峰值;prefill 會摸遍全部 expert,所以 peak 不代表 decode"
|
| 178 |
+
},
|
| 179 |
+
"rss_timeline_mib": [
|
| 180 |
+
[
|
| 181 |
+
0.0,
|
| 182 |
+
0.6
|
| 183 |
+
],
|
| 184 |
+
[
|
| 185 |
+
0.205,
|
| 186 |
+
75.9
|
| 187 |
+
],
|
| 188 |
+
[
|
| 189 |
+
0.409,
|
| 190 |
+
71.3
|
| 191 |
+
],
|
| 192 |
+
[
|
| 193 |
+
0.614,
|
| 194 |
+
121.4
|
| 195 |
+
],
|
| 196 |
+
[
|
| 197 |
+
0.82,
|
| 198 |
+
190.4
|
| 199 |
+
],
|
| 200 |
+
[
|
| 201 |
+
1.026,
|
| 202 |
+
258.7
|
| 203 |
+
],
|
| 204 |
+
[
|
| 205 |
+
1.233,
|
| 206 |
+
326.7
|
| 207 |
+
],
|
| 208 |
+
[
|
| 209 |
+
1.44,
|
| 210 |
+
397.2
|
| 211 |
+
],
|
| 212 |
+
[
|
| 213 |
+
1.649,
|
| 214 |
+
468.3
|
| 215 |
+
],
|
| 216 |
+
[
|
| 217 |
+
1.858,
|
| 218 |
+
538.3
|
| 219 |
+
],
|
| 220 |
+
[
|
| 221 |
+
2.068,
|
| 222 |
+
608.8
|
| 223 |
+
],
|
| 224 |
+
[
|
| 225 |
+
2.278,
|
| 226 |
+
680.3
|
| 227 |
+
],
|
| 228 |
+
[
|
| 229 |
+
2.49,
|
| 230 |
+
751.8
|
| 231 |
+
],
|
| 232 |
+
[
|
| 233 |
+
2.702,
|
| 234 |
+
824.6
|
| 235 |
+
],
|
| 236 |
+
[
|
| 237 |
+
2.916,
|
| 238 |
+
895.9
|
| 239 |
+
],
|
| 240 |
+
[
|
| 241 |
+
3.13,
|
| 242 |
+
968.6
|
| 243 |
+
],
|
| 244 |
+
[
|
| 245 |
+
3.346,
|
| 246 |
+
1041.7
|
| 247 |
+
],
|
| 248 |
+
[
|
| 249 |
+
3.563,
|
| 250 |
+
1115.7
|
| 251 |
+
],
|
| 252 |
+
[
|
| 253 |
+
3.78,
|
| 254 |
+
1187.2
|
| 255 |
+
],
|
| 256 |
+
[
|
| 257 |
+
3.998,
|
| 258 |
+
1262.2
|
| 259 |
+
],
|
| 260 |
+
[
|
| 261 |
+
4.216,
|
| 262 |
+
1335.2
|
| 263 |
+
],
|
| 264 |
+
[
|
| 265 |
+
4.436,
|
| 266 |
+
1409.3
|
| 267 |
+
],
|
| 268 |
+
[
|
| 269 |
+
4.659,
|
| 270 |
+
1484.6
|
| 271 |
+
],
|
| 272 |
+
[
|
| 273 |
+
4.881,
|
| 274 |
+
1558.8
|
| 275 |
+
],
|
| 276 |
+
[
|
| 277 |
+
5.103,
|
| 278 |
+
1633.1
|
| 279 |
+
],
|
| 280 |
+
[
|
| 281 |
+
5.326,
|
| 282 |
+
1706.4
|
| 283 |
+
],
|
| 284 |
+
[
|
| 285 |
+
5.551,
|
| 286 |
+
1780.9
|
| 287 |
+
],
|
| 288 |
+
[
|
| 289 |
+
5.777,
|
| 290 |
+
1856.4
|
| 291 |
+
],
|
| 292 |
+
[
|
| 293 |
+
6.003,
|
| 294 |
+
1931.9
|
| 295 |
+
],
|
| 296 |
+
[
|
| 297 |
+
6.239,
|
| 298 |
+
2009.0
|
| 299 |
+
],
|
| 300 |
+
[
|
| 301 |
+
6.469,
|
| 302 |
+
2085.5
|
| 303 |
+
],
|
| 304 |
+
[
|
| 305 |
+
6.7,
|
| 306 |
+
2161.5
|
| 307 |
+
],
|
| 308 |
+
[
|
| 309 |
+
6.933,
|
| 310 |
+
2239.5
|
| 311 |
+
],
|
| 312 |
+
[
|
| 313 |
+
7.182,
|
| 314 |
+
2317.5
|
| 315 |
+
],
|
| 316 |
+
[
|
| 317 |
+
7.418,
|
| 318 |
+
2400.6
|
| 319 |
+
],
|
| 320 |
+
[
|
| 321 |
+
7.657,
|
| 322 |
+
2480.1
|
| 323 |
+
],
|
| 324 |
+
[
|
| 325 |
+
7.895,
|
| 326 |
+
2558.6
|
| 327 |
+
],
|
| 328 |
+
[
|
| 329 |
+
8.128,
|
| 330 |
+
931.1
|
| 331 |
+
],
|
| 332 |
+
[
|
| 333 |
+
8.358,
|
| 334 |
+
935.8
|
| 335 |
+
],
|
| 336 |
+
[
|
| 337 |
+
8.584,
|
| 338 |
+
1119.4
|
| 339 |
+
],
|
| 340 |
+
[
|
| 341 |
+
8.825,
|
| 342 |
+
1193.9
|
| 343 |
+
],
|
| 344 |
+
[
|
| 345 |
+
9.057,
|
| 346 |
+
1193.3
|
| 347 |
+
]
|
| 348 |
+
],
|
| 349 |
+
"io_delta": {},
|
| 350 |
+
"timing_line": "[ Prompt: 128.4 t/s | Generation: 44.2 t/s ]",
|
| 351 |
+
"output": "Hello! 😊 I'm DeepSeek-R1, your friendly AI assistant. I'm here to help with all kinds of questions, learning, and creative tasks",
|
| 352 |
+
"pager": {
|
| 353 |
+
"routing_calls": 8448,
|
| 354 |
+
"expert_touches": 64544,
|
| 355 |
+
"hits": 35587,
|
| 356 |
+
"misses": 3701,
|
| 357 |
+
"evictions": 4572,
|
| 358 |
+
"evicted_bytes": 9822062592,
|
| 359 |
+
"sweeps": 1046,
|
| 360 |
+
"resident_bytes": 327075840,
|
| 361 |
+
"budget_bytes": 327155712,
|
| 362 |
+
"hit_rate": 0.5514,
|
| 363 |
+
"proc_self_read_bytes": 2630213632,
|
| 364 |
+
"proc_self_rchar": 11947223
|
| 365 |
+
},
|
| 366 |
+
"output_matches_baseline": true,
|
| 367 |
+
"swap_zero": true,
|
| 368 |
+
"rc_ok": true,
|
| 369 |
+
"rss_vs_baseline": 0.255,
|
| 370 |
+
"decode_rss_vs_baseline": 0.255,
|
| 371 |
+
"decode_rss_lower_than_baseline": false,
|
| 372 |
+
"rss_lower_than_baseline": false,
|
| 373 |
+
"ssd_reads_mib": 2508.4,
|
| 374 |
+
"cold_weights_really_read_from_ssd": true,
|
| 375 |
+
"pager_hit_rate": 0.5514,
|
| 376 |
+
"pager_evictions": 4572,
|
| 377 |
+
"pager_misses": 3701,
|
| 378 |
+
"pager_resident_mib": 311.9,
|
| 379 |
+
"pager_budget_mib": 312.0
|
| 380 |
+
}
|
| 381 |
+
},
|
| 382 |
+
"all_passed": false
|
| 383 |
+
}
|
validate/last-run.json
CHANGED
|
@@ -1,41 +1,42 @@
|
|
| 1 |
{
|
| 2 |
-
"when": "2026-10-
|
| 3 |
"model": "/root/work/models/SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf",
|
| 4 |
"port": 8080,
|
| 5 |
-
"pid":
|
| 6 |
-
"ram_budget_mb":
|
| 7 |
-
"arena_mb":
|
| 8 |
-
"expert_slots":
|
| 9 |
"chat": {
|
| 10 |
"prompt_tokens": 37,
|
| 11 |
"completion_tokens": 16,
|
| 12 |
-
"seconds": 0.
|
| 13 |
-
"tok_per_s":
|
| 14 |
"content": "A **MoE (Model Parallelism) layer** enables a neural network to",
|
| 15 |
"timings": {
|
| 16 |
"cache_n": 0,
|
| 17 |
"prompt_n": 37,
|
| 18 |
-
"prompt_ms":
|
| 19 |
-
"prompt_per_token_ms":
|
| 20 |
-
"prompt_per_second":
|
| 21 |
"predicted_n": 16,
|
| 22 |
-
"predicted_ms":
|
| 23 |
-
"predicted_per_token_ms":
|
| 24 |
-
"predicted_per_second":
|
| 25 |
}
|
| 26 |
},
|
| 27 |
"peak": {
|
| 28 |
-
"total_rss_gb":
|
| 29 |
-
"anon_rss_gb":
|
| 30 |
-
"file_rss_gb":
|
| 31 |
"peak_swap_gb": 0.0,
|
| 32 |
-
"hwm_rss_gb":
|
| 33 |
},
|
| 34 |
"io_delta": {
|
| 35 |
"read_bytes": 0,
|
| 36 |
-
"rchar":
|
| 37 |
"write_bytes": 0
|
| 38 |
},
|
|
|
|
| 39 |
"model_size_mib": 2508,
|
| 40 |
"ok": true
|
| 41 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"when": "2026-10-07T16:08:28+0200",
|
| 3 |
"model": "/root/work/models/SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf",
|
| 4 |
"port": 8080,
|
| 5 |
+
"pid": 18109,
|
| 6 |
+
"ram_budget_mb": 92285,
|
| 7 |
+
"arena_mb": 89898,
|
| 8 |
+
"expert_slots": 44394,
|
| 9 |
"chat": {
|
| 10 |
"prompt_tokens": 37,
|
| 11 |
"completion_tokens": 16,
|
| 12 |
+
"seconds": 0.59,
|
| 13 |
+
"tok_per_s": 26.903,
|
| 14 |
"content": "A **MoE (Model Parallelism) layer** enables a neural network to",
|
| 15 |
"timings": {
|
| 16 |
"cache_n": 0,
|
| 17 |
"prompt_n": 37,
|
| 18 |
+
"prompt_ms": 265.684,
|
| 19 |
+
"prompt_per_token_ms": 7.180648648648649,
|
| 20 |
+
"prompt_per_second": 139.2631848361211,
|
| 21 |
"predicted_n": 16,
|
| 22 |
+
"predicted_ms": 314.225,
|
| 23 |
+
"predicted_per_token_ms": 20.948333333333334,
|
| 24 |
+
"predicted_per_second": 47.736494550083535
|
| 25 |
}
|
| 26 |
},
|
| 27 |
"peak": {
|
| 28 |
+
"total_rss_gb": 2.5179,
|
| 29 |
+
"anon_rss_gb": 0.1931,
|
| 30 |
+
"file_rss_gb": 2.5117,
|
| 31 |
"peak_swap_gb": 0.0,
|
| 32 |
+
"hwm_rss_gb": 2.5896
|
| 33 |
},
|
| 34 |
"io_delta": {
|
| 35 |
"read_bytes": 0,
|
| 36 |
+
"rchar": 0,
|
| 37 |
"write_bytes": 0
|
| 38 |
},
|
| 39 |
+
"rss_within_budget": true,
|
| 40 |
"model_size_mib": 2508,
|
| 41 |
"ok": true
|
| 42 |
}
|