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
File size: 10,830 Bytes
f279c31 faea13d f279c31 faea13d f279c31 faea13d f279c31 faea13d f279c31 833ef4d f279c31 833ef4d f279c31 833ef4d f279c31 833ef4d faea13d f279c31 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | ---
license: apache-2.0
library_name: llama-cpp-python
tags:
- gguf
- llama.cpp
- moe
- ssd-offload
- smallthinker
- expert-paging
- low-ram
base_model:
- Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF
pipeline_tag: text-generation
---
# SmallThinker-4B-A0.6B — 熱參數在 RAM、冷參數在 SSD
在 llama.cpp 上跑 [`Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF`](https://huggingface.co/Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF)
的 `SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf`(2,630,212,704 bytes = 2508 MiB),
**expert 級 SSD 分頁**:權重永遠是 SSD 上的 file-backed `mmap`,RAM 只留熱的
expert 權重,用不到的 expert 會被明確丟回 SSD。
**計算路徑完全沒動過** —— 仍然是 llama.cpp 自己的 `mul_mat_id`,
所以輸出與上游**逐字元相同**。這是本專案唯一不妥協的正確性條件。
```bash
./llama_server.sh # port 8080
./llama_server.sh --plan # 只印推導出來的參數
./llama_server.sh --verify # 啟動 + 打一次 chat + 量記憶體
ST_RAM_BUDGET_MB=256 ./llama_server.sh # 手動壓低 RAM 預算
ST_PAGER=0 ./llama_server.sh # 關閉分頁,回到上游行為
```
---
## 實測結果
本機:16 vCPU、**無 GPU**、檔案系統 **overlayfs**、2026-10-07。
完整證據:[`validate/ab-test.json`](validate/ab-test.json)(`all_passed: true`)、
[`validate/last-run.json`](validate/last-run.json)。
### 記憶體 vs 分頁預算
同一份 `llama-cli`、同一個 prompt、同一個 seed(`--seed 42`)、ctx 1024、8 threads:
| 設定 | peak 總 RSS | 匿名 | 檔案對映 | swap | SSD 重讀 | 命中率 | 淘汰次數 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `ST_PAGER=0`(上游) | **2.565 GiB** | 0.103 | 2.559 | **0** | — | — | — |
| 預算 1024 MiB | **1.301 GiB** | 0.103 | 1.300 | **0** | 2317 MiB | 57.4% | 2,894 |
| 預算 512 MiB | **0.833 GiB** | 0.103 | 0.836 | **0** | 2316 MiB | 55.1% | 4,572 |
| 預算 256 MiB | **0.596 GiB** | 0.103 | 0.592 | **0** | 2320 MiB | 53.7% | 5,625 |
| 預算 128 MiB | **0.661 GiB** | 0.103 | 0.657 | **0** | 2317 MiB | 54.1% | 5,298 |
**總 RSS 從 2.565 GiB 降到 0.596 GiB(−76.8%),swap 全程 0。**
> 量的是**總 RSS**(含 file-backed mmap 的權重頁),不是只有匿名記憶體 ——
> 權重頁會算進 RSS 且真的佔用系統 RAM。只看匿名記憶體會嚴重低估(本實測 0.103 GiB)。
> 峰值由**模型載入**決定,不是 prefill:實測 prompt 2 token 與 211 token 的峰值幾乎一樣。
`預算 128 MiB` 反而比 `256 MiB` 高一點:預算已逼近 hot window 內必須保留的下限
(`ST_HOT_TOKENS=8` × 32 層 × 4 experts/層),再壓沒有意義,命中率也沒比較好。
### 速度代價
| 設定 | prefill | decode | 耗時 |
| --- | --- | --- | --- |
| `ST_PAGER=0`(上游,權重全在 RAM) | 178.7 t/s | **55.4 t/s** | 9.06 s |
| 預算 1024 MiB | 7.8 t/s | **22.9 t/s** | 7.85 s |
| 預算 512 MiB | 7.9 t/s | **21.6 t/s** | 7.67 s |
| 預算 256 MiB | 8.1 t/s | **19.7 t/s** | 7.88 s |
| 預算 128 MiB | 7.9 t/s | **20.9 t/s** | 8.13 s |
**用約 2.4~2.8× 的 decode 速度換 3~4.3× 的記憶體。** 這是 demand paging 的必然代價:
被丟掉的 expert 下次要用時必須重新讀 SSD。
### 本機 SSD 讀取能力([`validate/io-probe.json`](validate/io-probe.json))
| threads | 1 | 2 | 4 | 8 |
| --- | --- | --- | --- | --- |
| 循序讀取 | 337 MB/s | 667 MB/s | 1328 MB/s | 2558 MB/s |
4K 隨機讀 ≈ 1767 IOPS。**換機器務必用 `tools/io_probe.py` 重測**,
decode 速度幾乎完全由它決定。
### 端到端(`llama_server.sh --verify`,`ST_RAM_BUDGET_MB=512`)
| 項目 | 值 |
| --- | --- |
| peak 總 RSS | 1.090 GiB |
| VmSwap | **0** |
| expert 常駐 / 預算 | **510.3 / 512.0 MiB** ✅ |
| 分頁命中率 | 41.2% |
| `read_bytes`(整個行程) | 2.44 GiB |
| chat 輸出 | `A **MoE (Model Parallelism) layer** enables a neural network to` |
1.090 GiB 的組成:非 expert 權重 410 MiB(常駐)+ expert 分頁 510 MiB
+ KV / compute buffer / 執行檔(llama-server 開 4 個 slot)。
---
## 模型事實(全部從 GGUF metadata 實測,不是抄設定檔)
| 項目 | 值 |
| --- | --- |
| arch | `smallthinker` |
| 層數 | 32 |
| hidden | 1536 |
| experts / 每 token 用 | 32 / 4 |
| expert FFN | 768 |
| attention heads / KV heads | 12 / 2 |
| key_len / val_len | 128 / 128 |
| context_length(模型上限) | 32768 |
| 量化 | Q4_K_M |
| 檔案 | 2,630,212,704 bytes(2508 MiB) |
### 權重組成
| | 位元組 | 佔檔案 |
| --- | --- | --- |
| **expert 權重(1024 個)** | 2,194,145,280(**2092.5 MiB**) | **83.4%** |
| 其餘(embedding / attention / norms / router) | 430,065,024(410.2 MiB) | 16.6% |
expert 佔 83.4% —— 所以「熱在 RAM、冷在 SSD」這件事基本上就是
「expert 權重怎麼放」。
**每個 expert 是三段連續位元組**(`blk.N.ffn_{gate,up,down}_exps.weight`,
expert 維度是最外層 `ne[2]`,所以單一 expert 是一段連續區間):
| 段 | shape | layer 0 每個 expert |
| --- | --- | --- |
| gate | [1536, 768, 32] | 663,552 B(Q4_K) |
| up | [1536, 768, 32] | 663,552 B(Q4_K) |
| down | [768, 1536, 32] | 967,680 B(**Q6_K**) |
| 合計 | | 2.19 MiB |
⚠️ **`down_exps` 的量化型別逐層不同**:32 層裡有 **16 層是 Q6_K、16 層是 Q4_K**
(層號 0,1,2,3,6,9,12,15,18,21,24,27,28,29,30,31 是 Q6_K)。
所以「一個 expert 多大」必須**逐層算**:Q6_K 層 70.03 MiB/層、Q4_K 層 60.75 MiB/層,
平均 2.04 MiB/個。拿 layer 0 當全模型常數會把總量高估 148 MiB(7%)。
---
## 機制
```
llama.cpp 正常載入(權重留在 SSD 的 file-backed mmap)
│
├─ 每層 top-k 選完 expert 之後,插入一個 ggml custom op
│ ├─ 讀出這一層這批 token 用到哪些 expert,標成「熱」
│ └─ 若 resident 超過預算 → 用 LFU→LRU 順序淘汰最冷的
│ a. madvise(MADV_DONTNEED) 拿掉本行程 PTE → RSS 下降
│ b. posix_fadvise(DONTNEED) 丟掉 page cache → 下次真的讀 SSD
│
└─ 計算本身:完全不動,仍是上游 ggml_mul_mat_id
```
淘汰的兩步驟**順序不能換**:
只做 (a) → RSS 下降但 `read_bytes` 不變(頁還在 page cache);
只做 (b) → page cache 下降但行程 RSS 不變(VMA 還映射著)。
(核心的 `invalidate_mapping_pages()` 會跳過仍被 VMA 映射的頁,所以必須先 (a)。)
### 與 `HelloSun/sddqwen35a3b_v01` 的取捨
| | sddqwen35a3b_v01 | 本專案 |
| --- | --- | --- |
| 權重怎麼進 RAM | `pread` 進自己配置的 arena | 留在 llama.cpp 自己的 mmap |
| MoE 計算 | **自訂 ggml op**,自己重寫 Q4_K 點積 | **不動**,用上游 `mul_mat_id` |
| 輸出正確性 | 要另外驗證 kernel 寫對沒有 | 逐字等於上游,結構上不可能錯 |
| SSD 讀取 | 預取可與計算重疊 | demand paging(碰到缺頁才讀) |
| 程式碼量 | ~2800 行 | ~550 行 |
**放棄 I/O 預取,換取「計算路徑完全沒動過」的保證。**
---
## 環境變數
| 變數 | 預設 | 說明 |
| --- | --- | --- |
| `ST_PAGER` | `1` | `0` = 關閉分頁(上游行為,做 A/B 用) |
| `ST_RAM_BUDGET_MB` | 自動偵測 | expert 權重可以常駐 RAM 的上限 |
| `ST_RESERVE_MB` | `0` | KV + compute 預留,從預算扣掉 |
| `ST_ARENA_MB` | `0` | 直接指定 expert 常駐量(有值優先) |
| `ST_HOT_TOKENS` | `8` | 最近 N 個 tick 用過的 expert 不淘汰 |
| `ST_VERBOSE` | `0` | 印每次淘汰 + 實際 RSS |
| `ST_STATS_FILE` | — | 行程退出時把統計寫成 JSON(`SIGUSR2` 也會寫) |
| `ST_EVICT_MODE` | `both` | 診斷用:`both`/`madvise`/`fadvise`/`none` |
## ⚠️ 必看的坑
1. **`-DGGML_CPU_REPACK=OFF` 是硬性要求**(`llama_server.sh` 已自動加上)。
開著時 ggml 把 Q4_K 轉成 repack 格式放進**匿名**緩衝區,權重就離開 mmap。
這時 `madvise(MADV_DONTNEED)` 不是「讀 SSD」,而是**把那塊記憶體歸零**
→ 輸出變成 `papers bases abstract…` 之類的亂碼,但行程看起來完全正常、
tok/s 只掉一點。patch 裡也有 `in_map` 檢查會擋下並印出 buffer 型別。
2. **分頁開著時一定要關掉 `MAP_POPULATE`**,否則整個 2.45 GiB 會在
「載入模型」那一瞬間全部 fault 進 page cache,峰值立刻爆掉 ——
而且分頁統計看起來完全正常(sweep 有跑、帳面 resident 也對),光看統計抓不到。
完整清單(每條都有實測證據)在 [`STATUS.md`](STATUS.md) 的「踩過的坑」表格。
## 檔案
| 檔案 | 用途 |
| --- | --- |
| [`llama_server.sh`](llama_server.sh) | 一鍵啟動(自動抓 llama.cpp / 套 patch / 編譯 / 下載權重) |
| [`patches/0001-st-expert-pager.patch`](patches/0001-st-expert-pager.patch) | llama.cpp 改動 |
| [`tools/ab_test.py`](tools/ab_test.py) | A/B 驗證(RSS / swap / SSD 讀取 / 輸出比對) |
| [`tools/verify_run.py`](tools/verify_run.py) | 對 llama-server 打 chat 並量記憶體 |
| [`tools/io_probe.py`](tools/io_probe.py) | 實測本機 SSD 讀取能力(**換機器必跑**) |
| [`tools/drop_model_cache.py`](tools/drop_model_cache.py) | 量測前丟掉模型檔的 page cache |
| [`STATUS.md`](STATUS.md) | **進度真相來源** |
| [`AGENTS.md`](AGENTS.md) | 續作指引(假設 agent 崩潰後只看這兩個檔案) |
## 重現
```bash
git clone https://huggingface.co/HelloSun/SmallThinker4b
cd SmallThinker4b
# 端到端驗證
ST_RAM_BUDGET_MB=512 ./llama_server.sh --verify
# A/B 驗證(baseline vs 各種預算)
python3 tools/ab_test.py --bin <path>/llama-cli --model <path>/model.gguf \
--budget 1024 --budget 512 --budget 256 --out validate/ab-test.json
# 本機 SSD 讀取能力
python3 tools/io_probe.py --model <path>/model.gguf --out validate/io-probe.json
```
## 進度與續作
工作成果保存在這個 repo(`./sync.sh "改了什麼"` 即可上傳)。
**假設 agent 崩潰**:clone 這個 repo → 讀 `STATUS.md` → 讀 `AGENTS.md` → 接著做。
本機很不穩,**每改完程式並編譯過就上傳,不要累積**。
尚未做(見 `STATUS.md` 的「下一步」):I/O 預取(讓 SSD 讀取與計算重疊)、
KV cache 也放 SSD、限制 prefill 批次大小以壓低 prefill 階段峰值。
## 授權
本 repo 的程式碼(`llama_server.sh` / `patches/` / `tools/`)沿用
llama.cpp 的 MIT。模型權重屬於
[`Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF`](https://huggingface.co/Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF)
(Apache-2.0),本 repo **不含**權重。 |