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
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
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 形狀不一致」 |
常用指令
# 完整流程(自動抓 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,輸出必須逐字相同。