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
v11: 方向修正(hot expert 實體放 RAM,非 page 轉換)+ b9acf138 vanilla 基線 82.1 tok/s / peak RSS 2.674 GiB
a035243 |
Download STATUS.md from HelloSun/SmallThinker4b: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/HelloSun/SmallThinker4b/resolve/main/STATUS.md
- Command line
-
hf download hf://HelloSun/SmallThinker4b/STATUS.md
-
curl -L -o STATUS.md https://huggingface.co/HelloSun/SmallThinker4b/resolve/main/STATUS.md
14.6 kB
| # STATUS — SmallThinker-4B-A0.6B:熱參數在 RAM、冷參數到 SSD | |
| 進度真相來源。**每次改完程式、編譯過、驗證過就更新本檔並 `./sync.sh`。** | |
| ## 一句話 | |
| `Tiiny/SmallThinker-4BA0.6B-Instruct-GGUF` 的 `SmallThinker-4B-A0.6B-Instruct.Q4_K.gguf` | |
| (2,630,212,704 bytes = 2508 MiB)跑在 llama.cpp 上,權重永遠是 SSD 上的 | |
| file-backed `mmap`;RAM 只留「熱的」expert 權重 + KV + compute buffer, | |
| 用不到的 expert 會被**明確丟回 SSD**(`madvise` + `posix_fadvise`)。 | |
| ## 模型事實(全部從 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 | | |
| **expert 權重總量 2092.5 MiB / 2508 MiB = 83.4%。** 也就是說「熱在 RAM、冷在 SSD」 | |
| 這件事基本上就是「expert 權重怎麼放」。 | |
| 每個 expert 是三段連續位元組(`blk.N.ffn_{gate,up,down}_exps.weight`, | |
| expert 維度是最外層): | |
| | 段 | shape | 每個 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,294,784 B = 2.19 MiB** | | |
| 共 32 × 32 = 1024 個 expert,整體平均 2.04 MiB/個(因為 16 層的 down 是 Q4_K)。 | |
| 逐層實測(`tools` 之外,用 gguf-py 直接算):Q6_K down 的層 70.03 MiB/層、 | |
| Q4_K down 的層 60.75 MiB/層;Q6_K 的層號是 0,1,2,3,6,9,12,15,18,21,24,27,28,29,30,31。 | |
| ⚠️ **三段的量化型別逐層可能不同**(實測有些層的 `down_exps` 是 Q4_K 而非 Q6_K, | |
| `nb[2` 就不同)。所以「每個 expert 多大」必須**逐 slot 算**, | |
| 拿 layer 0 當全模型常數會算錯。 | |
| ## 硬約束 | |
| - **行程總 RSS**(含 file-backed mmap 的權重頁)要受 RAM 預算約束。 | |
| 只看匿名記憶體會嚴重低估 —— 這是本專案第一版的錯。 | |
| - **VmSwap = 0**。 | |
| - **輸出必須與上游逐字相同**。這是分頁機制唯一不能妥協的正確性條件。 | |
| ## 目前狀態(2026-10-07) | |
| **兩個目標都达成了**:`validate/ab-test.json` → `all_passed: true`。 | |
| | 設定 | peak 總 RSS | swap | SSD 重讀 | 命中率 | 淘汰次數 | 輸出逐字相同 | | |
| | --- | --- | --- | --- | --- | --- | --- | | |
| | `ST_PAGER=0`(上游行為) | **2.565 GiB** | 0 | — | — | — | 基準 | | |
| | 預算 1024 MiB | **1.301 GiB** | 0 | 2317 MiB | 57.4% | 2894 | ✅ | | |
| | 預算 512 MiB | **0.833 GiB** | 0 | 2316 MiB | 55.1% | 4572 | ✅ | | |
| | 預算 256 MiB | **0.596 GiB** | 0 | 2320 MiB | 53.7% | 5625 | ✅ | | |
| | 預算 128 MiB | **0.661 GiB** | 0 | 2317 MiB | 54.1% | 5298 | ✅ | | |
| 同一份 `llama-cli`、同一個 prompt、同一個 seed,四種設定的輸出**完全相同**: | |
| > Hello! 😊 I'm DeepSeek-R1, your friendly AI assistant. I'm here to help with all | |
| > kinds of questions, learning, and creative tasks | |
| `--budget 128` 的 RSS 反而比 `256` 高一點:預算已經逼近「hot window 內必須 | |
| 保留的量」(`ST_HOT_TOKENS=8` × 32 層 × 4 experts/層 ≈ 66 MiB 的下限), | |
| 再往下壓沒有意義,反而讓命中率掉下來。 | |
| ### 端到端(`llama_server.sh --verify`,ST_RAM_BUDGET_MB=512) | |
| 證據:`validate/last-run.json` | |
| | 項目 | 值 | | |
| | --- | --- | | |
| | peak 總 RSS | **1.090 GiB**(非 expert 權重 410 MiB + expert 分頁 510 MiB + KV/compute/sampler) | | |
| | VmSwap | **0** | | |
| | expert 常駐 / 預算 | **510.3 / 512.0 MiB** ✅ | | |
| | 命中率 | 41.2% | | |
| | `read_bytes`(整個行程) | 2440 MiB | | |
| | chat 輸出 | `A **MoE (Model Parallelism) layer** enables a neural network to` | | |
| ### 設計取捨(為什麼不照抄 sddqwen35a3b_v01 的 pread arena) | |
| | | sddqwen35a3b_v01 | 本專案 | | |
| | --- | --- | --- | | |
| | 權重怎麼進 RAM | `pread` 進自己配置的 arena | 留在 llama.cpp 自己的 mmap | | |
| | MoE 計算 | **自訂 ggml op**,自己重寫 Q4_K 點積 | **不動**,用上游 `mul_mat_id` | | |
| | 輸出正確性 | 要另外驗證 kernel 寫對沒有 | 逐字等於上游,結構上不可能錯 | | |
| | SSD 讀取 | 預取可與計算重疊 | demand paging(第一次碰到缺頁才讀) | | |
| | 程式碼量 | 約 2800 行 | 約 550 行 | | |
| 取捨很清楚:**放棄 I/O 預取,換取「計算路徑完全沒動過」的保證。** | |
| 模型只有 2.45 GiB、SSD 讀取 300+ MB/s,demand paging 的penalty 可接受。 | |
| ## 本機 SSD 讀取能力(實測,`validate/io-probe.json`) | |
| | threads | 1 | 2 | 4 | 8 | | |
| | --- | --- | --- | --- | --- | | |
| | 循序讀取 | 337 MB/s | 667 MB/s | 1328 MB/s | 2558 MB/s | | |
| 4K 隨機讀 ≈ 1767 IOPS。檔案系統是 **overlayfs**。 | |
| 單執行緒 337 MB/s 是「每個 decode step 重讀 4 experts × 32 層」這個負載的基準, | |
| SSD 讀取頻寬是本模型 decode 速度的主要上限。 | |
| ## 踩過的坑(全部有實測證據,不要重蹈) | |
| | 坑 | 真相 | 怎麼知道的 | | |
| | --- | --- | --- | | |
| | **`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 是在載入時進去的 | | |
| | **`will_page()` 不能看「權重載入了沒」** | `init_mappings()` 在 `load_all_data()` 之前,那時 `register_model()` 還沒跑過,所以 `enabled()` 一定是 false → 判斷會永遠回「不分頁」→ 回到 `MAP_POPULATE` | 同上症狀(改了之後峰值立刻掉到 0.83 GiB) | | |
| | **開 `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`) | | |
| | **`fscanf("%63[^:]: %llu")` 讀 `/proc/self/io` 永遠讀不到 `read_bytes`** | 沒有開頭空格時 `%[^:]` 會把上一行的 `\n` 讀進 key,`strcmp` 永遠比不到 | 量到 `read_bytes = 0`,但 C 測試明明有讀 | | |
| | **子行程退出後才讀 `/proc/<pid>/io`** | 行程一退出 `/proc/<pid>` 就消失,外部量測得到 `{}`,看起來像「完全沒 I/O」 | `io_delta: {}`。SSD 讀取量只能由**行程自己**在 `atexit` 寫出來 | | |
| | **`fscanf(" %63[^:]: %lld")` 的空格不能省** | 對比上面兩條:同一個檔案,同一個 key,一個有空格一個沒有,結果差 100% | 純 C 測試 `/tmp/ev6.c` 兩種寫法並排跑 | | |
| | **用 peak RSS 判定分頁有沒生效** | 峰值取決於 prefetch 有沒有關,跟 prefill 長度無關(實測 prompt 2 token 與 211 token 的峰值幾乎一樣) | `rss_timeline_mib` | | |
| | **`nb[2] != ne[0]*nb[0]` 檢查會誤判量化張量** | 量化張量 `nb[0]` 是**一個 block** 的位元組數,不是 1。只要檢查 `nb[2] == ne[1]*nb[1]` | 「layer 0 不連續」誤報 | | |
| | **第一輪 sweep 永遠什麼都不做** | 條件寫成 `last_used < now - hot_tokens`,第一輪時門檻是 0 而 `last_used` 也是 0,`0 < 0` 為假 | sweep 計數有跳、但 resident 不降 | | |
| | **拿 layer 0 的 `nb[2]` 當全模型常數** | `down_exps` 逐層量化型別不同(實測 layer 4 是 Q4_K、layer 0 是 Q6_K) | 「layer 4 part 2 形狀不一致」 | | |
| | **`os.pread()` 回傳資料本身,不是讀到的位元組數** | 寫成 `got += n` 會得到 `TypeError: int + bytes`,而**執行緒裡的例外不會讓主程式失敗** → `io_probe` 印出 `bytes: 0、0.015 秒、0.0 MB/s`,看起來像「SSD 完全沒速度」 | `bytes: 0` 搭配「只花 15 ms」這個不合理組合才察覺 | | |
| | **外部取樣 RSS 抓不到 decode 階段** | 一次 generation 的樣本數太少。改由**行程自己**在 sweep 裡讀 `/proc/self/status` | 兩邊數字對不起來時才發現 | | |
| (本專案被「量測方式/語意/時序」教訓共 6 次:① 只看外部取樣的 RSS | |
| ② 讀 `/proc/self/io` 的格式錯 ③ 在行程退出後才讀它 ④ 把 prefetch 語意寫反 | |
| ⑤ 分層時把「有打算分頁」和「已註冊分頁」混為一談 ⑥ 在執行緒裡把 `pread` 的 | |
| 回傳值當成位元組數,例外被吞掉後看起來像「量到 0」。 | |
| 共同特徵都是「看起來有在做事,但量錯了地方」。每次都是靠實測抓到的。) | |
| ,不要重蹈) | |
| | 坑 | 真相 | 怎麼知道的 | | |
| | --- | --- | --- | | |
| | **開 `GGML_CPU_REPACK` 時權重不在 mmap 內** | ggml 把 Q4_K 轉成 repack 格式放進**匿名**緩衝區。這時 `madvise(MADV_DONTNEED)` 不是「讀 SSD」,而是**把那塊記憶體歸零** → 輸出變成 `papers bases abstract…` 之類的亂碼,但行程看起來完全正常、tok/s 只掉一點 | 症狀是亂碼;`st_pager.cpp` 現在有 `in_map` 檢查會直接擋下並印出 buffer 型別 | | |
| | **`fscanf("%63[^:]: %llu")` 讀 `/proc/self/io` 永遠讀不到 `read_bytes`** | 沒有開頭空格時 `%[^:]` 會把上一行的 `\n` 讀進 key,`strcmp` 永遠比不到 | 量到 `read_bytes = 0`,但 C 測試明明有讀 | | |
| | **子行程退出後才讀 `/proc/<pid>/io`** | 行程一退出 `/proc/<pid>` 就消失,外部量測得到 `{}`,看起來像「完全沒 I/O」 | `io_delta: {}`。SSD 讀取量只能由**行程自己**在 `atexit` 寫出來 | | |
| | **用 peak RSS 判定分頁有沒生效** | peak 由 prefill 決定,而 prefill 的工作集 ≈ 整個模型。要看 decode 階段 | `rss_timeline_mib` | | |
| | **`nb[2] != ne[0]*nb[0]` 檢查會誤判量化張量** | 量化張量 `nb[0]` 是**一個 block** 的位元組數,不是 1。只要檢查 `nb[2] == ne[1]*nb[1]` | 「layer 0 不連續」誤報 | | |
| | **第一輪 sweep 永遠什麼都不做** | 條件寫成 `last_used < now - hot_tokens`,第一輪時門檻是 0 而 `last_used` 也是 0,`0 < 0` 為假 | sweep 計數有跳、但 resident 不降 | | |
| | **拿 layer 0 的 `nb[2]` 當全模型常數** | `down_exps` 逐層量化型別不同 | 「layer 4 part 2 形狀不一致」 | | |
| ## 常用指令 | |
| ```bash | |
| # 完整流程(自動抓 llama.cpp / 套 patch / 編譯 / 下載權重 / 啟動 / 驗證) | |
| ST_HOME=~/.cache/smallthinker4b ./llama_server.sh --verify | |
| # 只看推導出來的參數,不動任何東西 | |
| ./llama_server.sh --plan | |
| # A/B 驗證(baseline vs 各種預算) | |
| python3 tools/ab_test.py --bin <llama-cli> --model <gguf> \ | |
| --budget 1024 --budget 512 --budget 256 --out validate/ab-test.json | |
| # 對照組:關掉分頁 | |
| ST_PAGER=0 ./llama_server.sh | |
| # 手動壓低 RAM 預算 | |
| ST_RAM_BUDGET_MB=512 ./llama_server.sh | |
| # 上傳(最重要的一步) | |
| ./sync.sh "改了什麼" | |
| ``` | |
| ## 關鍵環境變數 | |
| | 變數 | 預設 | 說明 | | |
| | --- | --- | --- | | |
| | `ST_PAGER` | `1` | `0` = 關閉分頁(上游行為,做 A/B 用) | | |
| | `ST_RAM_BUDGET_MB` | 自動偵測 | RAM 總額 | | |
| | `ST_RESERVE_MB` | 0 | KV + compute 預留,從預算扣掉 | | |
| | `ST_ARENA_MB` | 0 | 直接指定 expert 可常駐量(有值優先) | | |
| | `ST_HOT_TOKENS` | `8` | 最近 N 個 tick 用過的 expert 不淘汰 | | |
| | `ST_VERBOSE` | `0` | 印每次淘汰 | | |
| | `ST_STATS_FILE` | — | 行程退出時把統計寫成 JSON(`SIGUSR2` 也會寫) | | |
| | `ST_EVICT_MODE` | `both` | 診斷用:`both`/`madvise`/`fadvise`/`none` | | |
| ## 檔案地圖 | |
| | 檔案 | 用途 | | |
| | --- | --- | | |
| | `llama_server.sh` | 一鍵啟動 + 自動推導參數 | | |
| | `patches/0001-st-expert-pager.patch` | llama.cpp 改動(expert 分頁) | | |
| | `tools/ab_test.py` | A/B 驗證(RSS / swap / SSD 讀取 / 輸出比對) | | |
| | `tools/verify_run.py` | 對 llama-server 打一次 chat 並量記憶體 | | |
| | `tools/io_probe.py` | 實測本機 SSD 讀取能力(**換機器必跑**) | | |
| | `tools/drop_model_cache.py` | 量測前丟掉模型檔的 page cache | | |
| | `validate/` | 驗證證據 | | |
| | `sync.sh` | 推回 HF(崩潰後的唯一保險) | | |
| --- | |
| ## 2026-10-07 重開:方向修正(使用者明確要求) | |
| **使用者指示:**「我要的是 **hot expert 放記憶體**,**不是 page 轉換**」。 | |
| 因此 `patches/0001-st-expert-pager.patch`(v1–v10 的做法)**方向被否決**: | |
| 它讓權重留在 llama.cpp 自己的 file-backed mmap,用 | |
| `madvise(MADV_DONTNEED)` + `posix_fadvise(DONTNEED)` 把冷 expert **逐出 page cache**。 | |
| 那確實是「page 轉換」:hot expert 只是恰好還在 page cache 裡,不是被程式**放進 RAM**。 | |
| ### 修正後的設計(本次目標) | |
| | 參數 | 住哪 | | |
| | --- | --- | | |
| | **啟動參數**(attn / norm / router / token_embd / output,410.19 MiB) | **匿名 RAM**(`use_mmap=false` 走 pread→匿名緩衝區,不依賴 page cache) | | |
| | **hot experts** | **RAM arena**(程式自己配置、`pread` 進來的**實體 RAM**) | | |
| | **cold experts** | **SSD**(留在 GGUF 檔案裡,只在需要時 `pread` 進 arena 槽位) | | |
| 即:RAM arena + 槽位淘汰 + 自訂 MoE op(執行期才知道哪些 expert 被選到, | |
| 所以算權重時必須由 pager 提供 arena 裡的 bytes)。 | |
| **這是 `HelloSun/sddqwen35a3b_v01` 的做法,也是本專案現在要採用的做法。** | |
| ### 為什麼不能沿用參考專案的 commit | |
| 參考專案 `sddqwen35a3b_v01` 的 base commit 是 `836d57176dc699a726c55418e4f96b8ca628e1bf`。 | |
| 實測在該 commit 上 **原生路徑(`SDQ_PAGER=0`)對 smallthinker 就已經是亂碼** | |
| (輸出 `and and the and all achieving and`),pager 路徑同樣是亂碼。 | |
| → 該 commit 的 smallthinker 支援有問題,**不能**用它當 base。 | |
| **本專案 base commit 改用 `b9acf138a1e28ce1fc23b5a4fc4b12444b50f7ea`**(v1–v10 用過、 | |
| 輸出正確的那個)。 | |
| ### v11 基線(vanilla,尚未套 arena patch) | |
| `validate/baseline-vanilla.json` | |
| | 項目 | 值 | | |
| | --- | --- | | |
| | 輸出 | `Hello! I'm DeepSeek-R1, your friendly AI assistant. 😊 ...` ✅ | | |
| | decode | **82.1 tok/s**(16 threads,權重全在 page cache) | | |
| | peak 總 RSS | **2.674 GiB**(= 整份 2.45 GiB 權重都在 RAM 裡) | | |
| | swap | 0 | | |
| 這是 arena 版本的**正確性基準**:同一 prompt / temp 0 / seed,輸出必須逐字相同。 | |