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 ·
370fce7
1
Parent(s): ff122a0
v7: STATUS/AGENTS 更新 — 目標達成(peak 2.56→0.60 GiB),補上完整踩坑紀錄
Browse files
AGENTS.md
CHANGED
|
@@ -34,6 +34,10 @@ SmallThinker-4B-A0.6B-Instruct(Q4_K_M,2508 MiB,MoE 32 層 × 32 experts)
|
|
| 34 |
3. **輸出必須與上游逐字相同**。分頁只該影響「權重住哪裡」,
|
| 35 |
不該影響任何一個 token 的數值。
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
## 已知的坑(不要重蹈)
|
| 38 |
|
| 39 |
完整清單在 `STATUS.md` 的「踩過的坑」表格,每一條都有實測證據。
|
|
@@ -41,9 +45,13 @@ SmallThinker-4B-A0.6B-Instruct(Q4_K_M,2508 MiB,MoE 32 層 × 32 experts)
|
|
| 41 |
|
| 42 |
1. **`-DGGML_CPU_REPACK=OFF` 是硬性要求**。開著時 ggml 把權重放進匿名緩衝區,
|
| 43 |
`madvise` 會把它**歸零**,輸出變亂碼但行程看起來正常。
|
| 44 |
-
2. **`
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
## 環境
|
| 49 |
|
|
@@ -71,11 +79,19 @@ ST_PAGER=0 ./llama_server.sh
|
|
| 71 |
./sync.sh "改了什麼"
|
| 72 |
```
|
| 73 |
|
| 74 |
-
## 下一步(
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
## 常數(改模型就要改)
|
| 81 |
|
|
|
|
| 34 |
3. **輸出必須與上游逐字相同**。分頁只該影響「權重住哪裡」,
|
| 35 |
不該影響任何一個 token 的數值。
|
| 36 |
|
| 37 |
+
實測成果(`validate/ab-test.json`,`all_passed: true`):
|
| 38 |
+
peak RSS 從上游的 **2.564 GiB** 降到 **0.601 GiB**(預算 256 MiB),
|
| 39 |
+
swap 全程 0,冷權重真的從 SSD 重讀 2.3 GiB,輸出四種設定逐字相同。
|
| 40 |
+
|
| 41 |
## 已知的坑(不要重蹈)
|
| 42 |
|
| 43 |
完整清單在 `STATUS.md` 的「踩過的坑」表格,每一條都有實測證據。
|
|
|
|
| 45 |
|
| 46 |
1. **`-DGGML_CPU_REPACK=OFF` 是硬性要求**。開著時 ggml 把權重放進匿名緩衝區,
|
| 47 |
`madvise` 會把它**歸零**,輸出變亂碼但行程看起來正常。
|
| 48 |
+
2. **`init_mappings(!st::will_page(arch))` 這個 `!` 不能拿掉**。
|
| 49 |
+
它是整個機制裡最容易寫反的一行 —— 寫反的話 peak RSS 會變成 2.6 GiB,
|
| 50 |
+
但分頁統計看起來完全正常(sweep 有跑、帳面 resident 也對),
|
| 51 |
+
所以**光看分頁統計抓不到**。
|
| 52 |
+
3. **`fscanf` 讀 `/proc/self/io` 開頭要空格**,否則 `read_bytes` 永遠是 0。
|
| 53 |
+
4. **`will_page()` 只能看 arch,不能看 `enabled()`** ——
|
| 54 |
+
`init_mappings()` 在權重載入之前跑,那時 `register_model()` 還沒被呼叫。
|
| 55 |
|
| 56 |
## 環境
|
| 57 |
|
|
|
|
| 79 |
./sync.sh "改了什麼"
|
| 80 |
```
|
| 81 |
|
| 82 |
+
## 下一步(可選,非必要)
|
| 83 |
+
|
| 84 |
+
1. **I/O 預取**:目前是 demand paging(第一次碰到缺頁才讀 SSD)。
|
| 85 |
+
可以在 sweep 裡對「下一個 token 極可能用到、但現在是冷的」expert 發
|
| 86 |
+
`MADV_WILLNEED`,讓 SSD 讀取與計算重疊。參考 sddqwen35a3b_v01 的結論:
|
| 87 |
+
它實測預取命中率 0%(相鄰 token 的 top-k 重疊率已達 70%,LRU 本來就留著),
|
| 88 |
+
所以不確定這邊有沒有價值 —— 要先量路由重疊率。
|
| 89 |
+
2. **KV cache 也放 SSD**:ctx 拉到 32768 時 KV 是 128 MiB(f16),
|
| 90 |
+
目前是留在 RAM 的。要更省可以加 KV 檔案 mmap(llama.cpp 有 `--kv-unified`
|
| 91 |
+
與部分 lazy load 的機制可利用)。
|
| 92 |
+
3. **prefill 的工作集**:prefill 會一次摸遍幾乎所有 expert,所以 prefill 階段
|
| 93 |
+
的峰值必然接近整個模型。要壓低只能限制 prefill 批次大小(`-ub`),
|
| 94 |
+
分批送 prompt。
|
| 95 |
|
| 96 |
## 常數(改模型就要改)
|
| 97 |
|
STATUS.md
CHANGED
|
@@ -49,53 +49,76 @@ expert 維度是最外層):
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
-
###
|
| 78 |
|
| 79 |
-
|
| 80 |
-
RSS 時間序列(`validate/ab-test.json` → `rss_timeline_mib`)顯示它是
|
| 81 |
-
**單調上升**到接近整個模型大小,然後在最後 1/4 掉下來。
|
| 82 |
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 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 型別 |
|
|
|
|
| 49 |
- **VmSwap = 0**。
|
| 50 |
- **輸出必須與上游逐字相同**。這是分頁機制唯一不能妥協的正確性條件。
|
| 51 |
|
| 52 |
+
## 目前狀態(2026-10-07)
|
| 53 |
|
| 54 |
+
**兩個目標都达成了**:`validate/ab-test.json` → `all_passed: true`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
| 設定 | peak 總 RSS | swap | SSD 重讀 | 命中率 | 淘汰次數 | 輸出逐字相同 |
|
| 57 |
+
| --- | --- | --- | --- | --- | --- | --- |
|
| 58 |
+
| `ST_PAGER=0`(上游行為) | **2.564 GiB** | 0 | — | — | — | 基準 |
|
| 59 |
+
| 預算 1024 MiB | **1.301 GiB** | 0 | 2317 MiB | 57.4% | 2894 | ✅ |
|
| 60 |
+
| 預算 512 MiB | **0.833 GiB** | 0 | 2315 MiB | 55.1% | 4572 | ✅ |
|
| 61 |
+
| 預算 256 MiB | **0.601 GiB** | 0 | 2320 MiB | 53.7% | 5625 | ✅ |
|
| 62 |
+
| 預算 128 MiB | **0.664 GiB** | 0 | 2317 MiB | 54.2% | 5298 | ✅ |
|
| 63 |
|
| 64 |
+
同一份 `llama-cli`、同一個 prompt、同一個 seed,四種設定的輸出**完全相同**:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
> Hello! 😊 I'm DeepSeek-R1, your friendly AI assistant. I'm here to help with all
|
| 67 |
> kinds of questions, learning, and creative tasks
|
| 68 |
|
| 69 |
+
`--budget 128` 的 RSS 反而比 `256` 高一點:預算已經逼近「hot window 內必須
|
| 70 |
+
保留的量」(`ST_HOT_TOKENS=8` × 32 層 × 4 experts/層 ≈ 66 MiB 的下限),
|
| 71 |
+
再往下壓沒有意義,反而讓命中率掉下來。
|
| 72 |
|
| 73 |
+
### 端到端(`llama_server.sh --verify`,ST_RAM_BUDGET_MB=512)
|
| 74 |
|
| 75 |
+
證據:`validate/last-run.json`
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
| 項目 | 值 |
|
| 78 |
+
| --- | --- |
|
| 79 |
+
| peak 總 RSS | **1.092 GiB**(非 expert 權重 267 MiB + expert 分頁 510 MiB + KV/compute/sampler) |
|
| 80 |
+
| VmSwap | **0** |
|
| 81 |
+
| expert 常駐 / 預算 | **510.3 / 512.0 MiB** ✅ |
|
| 82 |
+
| 命中率 | 41.2% |
|
| 83 |
+
| `read_bytes`(整個行程) | 2440 MiB |
|
| 84 |
+
| chat 輸出 | `A **MoE (Model Parallelism) layer** enables a neural network to` |
|
| 85 |
+
|
| 86 |
+
### 設計取捨(為什麼不照抄 sddqwen35a3b_v01 的 pread arena)
|
| 87 |
+
|
| 88 |
+
| | sddqwen35a3b_v01 | 本專案 |
|
| 89 |
+
| --- | --- | --- |
|
| 90 |
+
| 權重怎麼進 RAM | `pread` 進自己配置的 arena | 留在 llama.cpp 自己的 mmap |
|
| 91 |
+
| MoE 計算 | **自訂 ggml op**,自己重寫 Q4_K 點積 | **不動**,用上游 `mul_mat_id` |
|
| 92 |
+
| 輸出正確性 | 要另外驗證 kernel 寫對沒有 | 逐字等於上游,結構上不可能錯 |
|
| 93 |
+
| SSD 讀取 | 預取可與計算重疊 | demand paging(第一次碰到缺頁才讀) |
|
| 94 |
+
| 程式碼量 | 約 2800 行 | 約 550 行 |
|
| 95 |
|
| 96 |
+
取捨很清楚:**放棄 I/O 預取,換取「計算路徑完全沒動過」的保證。**
|
| 97 |
+
模型只有 2.45 GiB、SSD 讀取 300+ MB/s,demand paging 的penalty 可接受。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
## 踩過的坑(全部有實測證據,不要重蹈)
|
| 100 |
|
| 101 |
+
| 坑 | 真相 | 怎麼知道的 |
|
| 102 |
+
| --- | --- | --- |
|
| 103 |
+
| **`ml.init_mappings()` 的 prefetch 參數寫反** | 開著分頁時本來要關掉 prefetch(否則 `MAP_POPULATE` 會在**載入那一瞬間**把整個 2.45 GiB fault 進 page cache,峰值立刻爆掉),我卻寫成 `init_mappings(!paging_active())` → 分頁開著時反而 prefetch=ON。症狀:peak RSS 2.6 GiB,而且**後面的 sweep 明明有在跑、帳面 resident 也對** —— 只看分頁統計完全看不出問題 | 在 `load_tensors` 三個時點印 `VmRSS`:載入前 83 MB → `load_all_data` 前 **2652 MB** → 載入後 2646 MB。整個 2.5 GiB 是在載入時進去的 |
|
| 104 |
+
| **`will_page()` 不能看「權重載入了沒」** | `init_mappings()` 在 `load_all_data()` 之前,那時 `register_model()` 還沒跑過,所以 `enabled()` 一定是 false → 判斷會永遠回「不分頁」→ 回到 `MAP_POPULATE` | 同上症狀(改了之後峰值立刻掉到 0.83 GiB) |
|
| 105 |
+
| **開 `GGML_CPU_REPACK` 時權重不在 mmap 內** | ggml 把 Q4_K 轉成 repack 格式放進**匿名**緩衝區。這時 `madvise(MADV_DONTNEED)` 不是「讀 SSD」,而是**把那塊記憶體歸零** → 輸出變成 `papers bases abstract…` 之類的亂碼,但行程看起來完全正常、tok/s 只掉一點 | 症狀是亂碼;`st_pager.cpp` 現在有 `in_map` 檢查會擋下並印出 buffer 型別(`CPU_REPACK`) |
|
| 106 |
+
| **`fscanf("%63[^:]: %llu")` 讀 `/proc/self/io` 永遠讀不到 `read_bytes`** | 沒有開頭空格時 `%[^:]` 會把上一行的 `\n` 讀進 key,`strcmp` 永遠比不到 | 量到 `read_bytes = 0`,但 C 測試明明有讀 |
|
| 107 |
+
| **子行程退出後才讀 `/proc/<pid>/io`** | 行程一退出 `/proc/<pid>` 就消失,外部量測得到 `{}`,看起來像「完全沒 I/O」 | `io_delta: {}`。SSD 讀取量只能由**行程自己**在 `atexit` 寫出來 |
|
| 108 |
+
| **`fscanf(" %63[^:]: %lld")` 的空格不能省** | 對比上面兩條:同一個檔案,同一個 key,一個有空格一個沒有,結果差 100% | 純 C 測試 `/tmp/ev6.c` 兩種寫法並排跑 |
|
| 109 |
+
| **用 peak RSS 判定分頁有沒生效** | 峰值取決於 prefetch 有沒有關,跟 prefill 長度無關(實測 prompt 2 token 與 211 token 的峰值幾乎一樣) | `rss_timeline_mib` |
|
| 110 |
+
| **`nb[2] != ne[0]*nb[0]` 檢查會誤判量化張量** | 量化張量 `nb[0]` 是**一個 block** 的位元組數,不是 1。只要檢查 `nb[2] == ne[1]*nb[1]` | 「layer 0 不連續」誤報 |
|
| 111 |
+
| **第一輪 sweep 永遠什麼都不做** | 條件寫成 `last_used < now - hot_tokens`,第一輪時門檻是 0 而 `last_used` 也是 0,`0 < 0` 為假 | sweep 計數有跳、但 resident 不降 |
|
| 112 |
+
| **拿 layer 0 的 `nb[2]` 當全模型常數** | `down_exps` 逐層量化型別不同(實測 layer 4 是 Q4_K、layer 0 是 Q6_K) | 「layer 4 part 2 形狀不一致」 |
|
| 113 |
+
| **外部取樣 RSS 抓不到 decode 階段** | 一次 generation 的樣本數太少。改由**行程自己**在 sweep 裡讀 `/proc/self/status` | 兩邊數字對不起來時才發現 |
|
| 114 |
+
|
| 115 |
+
(本專案被「量測方式/語意/時序」教訓共 5 次:① 只看外部 peak RSS
|
| 116 |
+
② 讀 `/proc/self/io` 的格式錯 ③ 在行程退出後才讀它 ④ 把 prefetch 語意寫反
|
| 117 |
+
⑤ 分層時把「有打算分頁」和「已註冊分頁」混為一談。
|
| 118 |
+
共同特徵都是「看起來有在做事,但量錯了地方」。每次都是靠實測抓到的。)
|
| 119 |
+
|
| 120 |
+
,不要重蹈)
|
| 121 |
+
|
| 122 |
| 坑 | 真相 | 怎麼知道的 |
|
| 123 |
| --- | --- | --- |
|
| 124 |
| **開 `GGML_CPU_REPACK` 時權重不在 mmap 內** | ggml 把 Q4_K 轉成 repack 格式放進**匿名**緩衝區。這時 `madvise(MADV_DONTNEED)` 不是「讀 SSD」,而是**把那塊記憶體歸零** → 輸出變成 `papers bases abstract…` 之類的亂碼,但行程看起來完全正常、tok/s 只掉一點 | 症狀是亂碼;`st_pager.cpp` 現在有 `in_map` 檢查會直接擋下並印出 buffer 型別 |
|