Auto Upload Agent commited on
Commit
370fce7
·
1 Parent(s): ff122a0

v7: STATUS/AGENTS 更新 — 目標達成(peak 2.56→0.60 GiB),補上完整踩坑紀錄

Browse files
Files changed (2) hide show
  1. AGENTS.md +24 -8
  2. STATUS.md +56 -33
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. **`fscanf` 讀 `/proc/self/io` 開頭要空格**,否則 `read_bytes` 永遠是 0。
45
- 3. **不要用 peak RSS 判定分頁有沒生效** —— peak 由 prefill 決定,
46
- 而 prefill 的工作集 ≈ 整個模型。要看 decode 階段。
 
 
 
 
47
 
48
  ## 環境
49
 
@@ -71,11 +79,19 @@ ST_PAGER=0 ./llama_server.sh
71
  ./sync.sh "改了什麼"
72
  ```
73
 
74
- ## 下一步(依 STATUS.md 的「還沒解決的問題」)
75
-
76
- peak RSS 降不下來。已排除 MAP_POPULATE、`madvise` 無效、`fadvise` 無效。
77
- 最可能是 **prefill 的工作集就等於整個模型**。
78
- 要驗的是:peak RSS 是否隨 prompt 長度改變,以及 decode 階段的 RSS 才是分頁的指標。
 
 
 
 
 
 
 
 
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
- ### 已驗證能跑(2026-10-07)
 
 
 
 
 
 
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
- 分頁統計(預算 512 MiB):1024 個 expert、命中率 55.1%、
75
- 淘汰 4572 次共 9.8 GB、`read_bytes` = 2508 MiB(= 整個模型重讀一次)。
 
76
 
77
- ### ⚠️ 還沒解決的問題:peak RSS 降不下來
78
 
79
- **量到的 decode 階段 RSS 分頁後反而比 baseline 高**(2.556 vs 2.301 GiB)。
80
- RSS 時間序列(`validate/ab-test.json` → `rss_timeline_mib`)顯示它是
81
- **單調上升**到接近整個模型大小,然後在最後 1/4 掉下來。
82
 
83
- 已排除的可能原因(都實測過):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- 1. ~~`MAP_POPULATE` 在載入時 fault 整個檔案~~ — 已修:
86
- `ml.init_mappings(!st::prefetch_requested(), ...)`。
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 型別 |