SDQwen38-27B
Qwen3.8-27B 在 3GB 總 RAM、禁止 swap 的條件下推論。權重與 KV cache 全部放在 SSD。
| 上下文 | 峰值總 RSS | swap | 推論 | |
|---|---|---|---|---|
| 驗證 | 1,024 | 0.35 – 0.67 GiB | 0 | ✅ all_passed: true |
| 理論最高 | 262,144 | 2.118 GiB | 0 | ✅ 實際推理成功 |
1K 的峰值每次跑會在 0.35–0.67 GiB 之間跳(validate/ 裡有多次紀錄)——
差異來自 SSD 上多少權重頁剛好還在 page cache。區間的上緣 0.67 GiB 才是要參考的數字,
因為約束要對最壞情況成立。三次量測的 swap 都是 0。
模型:unsloth/Qwen3.8-27B-GGUF /
Qwen3.8-27B-UD-Q4_K_M.gguf(15.33 GiB,dense)
引擎:llama.cpp 0504396 + 本專案兩個 patch(見下)
證據:validate/ 每一個數字都有對應檔案
這個專案在解決什麼
27B 模型在 3GB RAM 裡跑,權重本身就佔 15.33 GiB。Dense 模型的特性是 每個 token 都要用到全部權重,而且零重複使用 —— 所以權重不值得留在 RAM 裡 (留著只是多付 15.33 GiB),正確做法是每次從 SSD 重讀。
| 層 | 大小 | 位置 |
|---|---|---|
| 權重 | 15.33 GiB | SSD(mmap + 背景 sweeper 定期丟回) |
| KV cache @262144 | 4.78 GiB | SSD(檔案 mmap,本專案 patch 0002) |
| SSM 狀態 / compute buffer / 執行檔 | ~0.3 GiB | RAM |
RAM 裡只有「執行必須」的部分。
為什麼這不容易
三個每一個都真的踩過(詳見 AGENTS.md 的坑表):
mmap的權重頁會算進 RSS,而且真的佔用系統 RAM。只算匿名記憶體會得到 「0.84 GiB ✓」這種假通過(實際總 RSS 是 15.2 GiB)。- 行程內
madvise不夠。它能降低行程 RSS,但權重頁仍留在系統 page cache, 會把 16 GB 的 cgroup 灌滿 → 核心反過來把行程自己的匿名頁換出到 swap。 必須搭配外部posix_fadvise才算真的沒佔 RAM。 - KV 在 262144 下是 4.78 GiB,連最省的 q4_0 都超標 1.6 倍,而且禁止 swap 意味著「讓核心去換 KV」這條路被堵死 → 必須把 KV 本身搬到 SSD。
兩個 patch(都在 patches/,未改動任何上游原始碼)
| patch | 做什麼 |
|---|---|
0001-ssd-mmap-release.patch |
權重頁 madvise(MADV_DONTNEED),由背景 sweeper 執行緒在計算進行中定期丟回 SSD |
0002-kv-ssd-pager.patch |
KV buffer 改用檔案 mmap,llama_context 的同一個 sweeper 順手把 KV 丟回 SSD |
0002 的關鍵設計:不自己寫 buffer type,而是把 CPU buffer 的記憶體來源從
ggml_aligned_malloc 換成檔案 mmap(MAP_SHARED)。因為 set_tensor /
cpy_tensor / clear 全部是 memcpy,換掉來源後一行都不用動,
連 q4_0 的寫入路徑都不變 → 零行為差異。
安全性根據:MAP_SHARED 的檔案對映,資料權威副本在檔案,
MADV_DONTNEED 只丟 page table entry、不丟資料 —— 對「會被寫入」的 KV 也成立
(writeback 交給核心的 dirty-page 機制,不呼叫 msync)。
為什麼 sweeper 必須在計算進行中跑:單一 token 的 decode 會走完 65 層,
峰值發生在任何「算完才釋放」之前。實測放在 synchronize() 之後時峰值是 8.8 GiB,
改成背景 sweeper 後是 0.35–0.67 GiB。
使用方法
一鍵安裝 + 驗證
git clone https://huggingface.co/HelloSun/sddqwen38-27b
cd sddqwen38-27b
# 編譯 + 下載模型(15.33 GiB)+ 啟動 + 驗證 1K 上下文
export HF_TOKEN=hf_... # 模型是公開的,但設了會走 xet 加速
./installpi.sh
常用指令
./installpi.sh --validate-only # 只重跑 1K 驗證(約 8 分鐘)
./installpi.sh --probe-maxctx # 驗證理論最高 262144(含實際推理,約 10 分鐘)
./installpi.sh --ctx 8192 # 用任意上下文啟動
./sync.sh "改了什麼" # 推回 HF(崩潰後的唯一保險)
當成 OpenAI 相容 API 用
installpi.sh 會在 127.0.0.1:8080 啟動 llama-server:
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3.8-27b-ud-q4_k_m",
"messages":[{"role":"user","content":"用一句話介紹你自己"}],
"max_tokens":8}'
搭配 pi coding agent(installpi.sh 會自動寫 ~/.pi/agent/models.json,
contextWindow 填 262144):
pi-local -p "你好"
換機器之後
tok/s 是「模型 × 機器」的性質,不是模型的性質。 換機必跑:
python3 tools/io_probe.py ~/models/Qwen3.8-27B-UD-Q4_K_M.gguf
本機實測:325 MB/s(單執行緒)、1213 MB/s(4 threads)。 因為 dense 模型每個 token 都要重讀 15.33 GiB,這個數字直接決定速度 → 每 token 約 13–45 秒。程式碼不需要改,只會變快。
效能:誠實說明
| 數值 | |
|---|---|
| prefill | 0.46–0.48 tok/s |
| decode | 0.022 tok/s(每 token 約 45 秒) |
這不是「還沒調好」,是 dense 模型 + 這台磁碟的物理下限:每個 token 要從 SSD 讀完整份 15.33 GiB,權重之間零重複使用。
對照 HelloSun/sddqwen35a3b
能跑到 6.28 tok/s,那是因為它是 MoE(35B-A3B,每 token 只用約 3B 權重)。
這個模型沒有這個性質,兩者不可比。
⚠️ 舊版文件寫過「0.33 tok/s」,那是假的。那次驗證沒開 page cache 回收, 整份 GGUF 的 15.33 GiB page cache 就留在系統 RAM 裡 —— 等於偷偷用了 15 GB RAM, 正是 3GB 約束要禁止的事。修好之後速度掉 15 倍,但那才是真實數字。
證據
| 檔案 | 內容 |
|---|---|
validate/validation.json |
1K 驗證報告,all_passed: true |
validate/memwatch-ctx1024.json |
1K 峰值總 RSS / swap / 強制手段 |
validate/chat-ctx1024.json |
1K 實際推論輸出與 timings |
validate/maxctx-probe.json |
262144 結果摘要 |
validate/memwatch-maxctx.json |
262144 峰值總 RSS 2.118 GiB、swap 0 |
validate/maxctx-infer.json |
262144 實際推論輸出與 timings |
validate/io-probe.json |
本機 SSD 讀取實測 |
STATUS.md |
進度真相來源(含所有技術決定的理由) |
AGENTS.md |
續作指引 + 已知的坑(接手前必讀) |
接手前請先讀
本機很不穩,agent 可能隨時崩潰,所以 AGENTS.md 存在的唯一理由是
「換一個 agent 也能接著做」。其中記了 5 個「看起來對、實際不對」的坑,
全部都是靠實測抓到的 —— 接手前請讀一遍。
約束只有兩條:行程總 RSS(含 page cache)≤ 3GB,以及 swap 使用量 = 0。 驗證必須同時滿足這兩條,缺一不算通過。
參考資料(未修改)
- Niko1221/Strata — SSD 分層推論
- ggml-org/llama.cpp — 上游,固定 commit
0504396 - HelloSun/sddqwen35a3b — 主參考專案