# 在 32GB 筆電上跑 744B 參數，然後叫作者不要修那個 bug

> colibri 號稱 25GB 記憶體就能跑 GLM-5.2 的 744B 參數。我在 M1 Max 上真的跑起來了，一個字 7 秒。過程中挖到三件跟直覺相反的事，其中一件讓我發給上游的內容不是「請修這個 bug」，而是「拜託不要修」。
> Author: Fred Chu (MumbleKey) · Published: 2026-07-30
> Canonical: https://blog.mumblekey.com/zh/posts/colibri-744b-on-a-laptop/
有個叫 colibri 的專案最近衝到兩萬顆星，開頭第一句話是：只要約 25GB 記憶體，就能在消費級電腦上執行 GLM-5.2，744B 參數的 MoE 模型。純 C、零相依、專家從硬碟串流。

我的筆電有 32GB。玩玩看。

結果它真的跑起來了，一個字 7 秒。這篇要講的不是慢，慢是可以預期的。有意思的是過程中量到三件跟直覺相反的事，其中最後一件讓我發 issue 給作者的時候，寫的不是「請修這個 bug」，而是「拜託不要修」。

## 它憑什麼塞得進去

先講原理，這部分值得懂，因為後面三個發現全部從它衍生。

想像一間顧問公司，裡面有 19456 位專科顧問，每位的資料夾約 19MB。這種模型的特別之處是，每回答一個字只需要問其中 8 位的意見（實際上要跑 75 個樓層、每層問 8 位，所以一個字大約調 600 多份資料夾）。既然一次只用到極少數，那就不必把全部顧問請進辦公室，需要誰再從隔壁倉庫搬進來就好。

辦公室是記憶體，倉庫是硬碟上那 429GB 的模型檔。負責分派工作、不能離開的那位主管約 12GB，常駐辦公室。

問題就在這裡。辦公室 32GB，主管一個人占掉 12GB，作業空間再扣掉一些，幾乎沒有位子留給顧問。所以每回答一個字，600 多份資料夾裡有九成以上要現去倉庫搬。

這就是全部的故事。速度慢不是因為運算不夠力，是因為一直在搬東西。

## 先確認它是真的

編譯快得有點誇張，5.4 秒，零警告。Metal kernel 的對拍測試全過，誤差在 1e-6 等級，專家選路的序列版和平行版是位元相同。這種測試寫得出來，說明作者知道自己在幹什麼。

模型 429GB 下載完，149 個檔案逐一比對，位元全對。第一個問題丟給它：

```text
› What is 2+2?
◆ 2 + 2 = 4

prefill 13 tokens in 74.36s | decode 7 tokens in 58.24s (0.12 tok/s)
expert hit rate 7.6% | RSS 6.29 GB
```

744B 參數，在一台 32GB 的筆電上，花了兩分鐘算出 2+2 等於 4。

然後換繁體中文問它 MoE 為什麼能在小記憶體機器上跑，上限設 128 個字：

> 要理解這個問題，首先需要釐清一個核心概念：「總參數量」不等於「同時需要載入到記憶體（VRAM）的參數量」。
>
> MoE（Mixture of Experts，混合專家）模型之所以能在記憶體遠小於其總參數量的機器上執行，核心原因在於「稀疏激活」與「動態載入」。以下用繁體中文為您詳細解釋其運作原理：

跑了 20 分鐘，然後在 128 字上限處被切斷。中文品質是好的，術語正確、還自己排了標題結構。這個容器是壓縮過的版本（把權重精度壓低讓檔案變小），中文能力沒被打壞。

順便量一下搬運量。用 iostat 對拍前後，這 20 分鐘從硬碟真的讀了 1621GB。我另外用顧問人數乘資料夾大小推算應該是 1585GB，差 2%。

所以「每產生一個字要從硬碟搬 12GB」不是比喻，是字面上的事實。差不多是三部藍光電影。

## 意外一：顯示晶片加速等於沒用

作者提供了一個 Metal 後端，讓顯示晶片幫忙算專家的矩陣乘法。他自己在 128GB 的 M4 Max 上量到快 40%。

我在這台開起來，只快 1.2%。

翻 profile 才知道為什麼：

```text
METAL: GPU blocks 67 | CPU fallback 0 | experts on GPU 411 | gpu-wall 0.98s
```

整段生成要跑 19456 次專家運算，顯示晶片只處理了 411 次，占 2%。全程只忙了 0.98 秒，而 decode 花了 225 秒。

原因回到辦公室那個比喻。Metal 路徑處理的是已經常駐在辦公室裡的顧問（零複製直接從記憶體 slab 讀），而這台辦公室的常駐率只有 8%。其他九成是剛從倉庫搬進來、當場算完就丟掉的，根本沒有東西給顯示晶片加速。

這個結論反直覺的地方是：要讓顯示晶片派上用場，你需要的是更大的記憶體，不是更強的顯示晶片。作者那台 128GB 的能快 40%，是因為辦公室夠大放得下顧問；文件裡另一台 M5 Max 靠 46.9GB 的常駐快取衝到 2.06 tok/s，同一個道理。Metal 的收益跟常駐率成正比，跟 GPU 算力沒什麼關係。

## 意外二：軟體自己的調校建議是錯的

這軟體有個 doctor 命令做自我檢查，會給調校建議。它在這台機器上建議關掉投機解碼：

```text
auto-tune:
  DRAFT=0    low hit rate: MTP widens expert union, adds disk reads
```

投機解碼是這樣運作的：模型附了一個小幫手先猜下一個字，然後讓正式模型一次驗證兩個字，猜對就省一次工。doctor 的理由是，這個小幫手會讓每個字需要調的資料夾變多，在你這種命中率極低的機器上不划算。

前半段沒錯，我實測搶答確實讓每個字多調 4.6% 的資料夾（608 對 581）。但照它的建議關掉之後：

```text
                  decode     每次驗證產出   猜對率
搶答開（預設）    224.92s      2.00 個字     100%
DRAFT=0           262.26s      1.03 個字      --
```

慢了 17%。

它漏看的是猜對率。多調 4.6% 的資料夾換來一次做兩個字，而猜對率是 100%（換長一點的題目是 86%），明顯划算。doctor 的判斷只看了命中率一個指標，沒去看實際的接受率。

這其實是作者自己在 README 裡寫過的：投機解碼是否划算取決於快取熱度，請自己實測。他寫對了，只是 doctor 的啟發式沒跟上。

## 意外三：這個 bug 千萬不要修

這件最有意思。

我在讀原始碼的時候，看到一段 OpenMP 調校，作用是讓運算執行緒保持隨時待命不要睡著。註解寫得很清楚，是為了在細碎的 per-expert 矩陣乘法之間維持熱度。

問題是這段調校靠 self re-exec 才會生效，因為 libgomp 在 main() 之前就把環境變數讀完了，程式碼註解自己也說明「setenv-in-main is ignored by the already-initialised runtime」。而那個 execv 只有兩個分支：

```c
#ifdef __linux__
    ... execv("/proc/self/exe", argv);
#endif
#ifdef __FreeBSD__
    ... execv("/proc/curproc/file", argv);
#endif
```

沒有 macOS。看起來就是個該補的漏洞，補上 `_NSGetExecutablePath()` 就好，五行的事。

但我沒直接下結論。因為 macOS 用的是 LLVM libomp，它在第一個 parallel region 才初始化，那時候 main() 裡的 setenv 早就跑完了，理論上可能根本不需要 re-exec 就會生效。這兩種可能要靠實驗分辨，不能靠讀程式碼。

所以做了對照：A 是現況，B 是我自己在外面把同一組變數 export 進去。如果內部的 setenv 有效，兩組應該一樣。

```text
                                    r1         r2       平均      vs A
A  現況（10 執行緒）              218.55s   219.39s   218.97s      --
B  外部 export 同一組調校         481.20s   490.72s   485.96s    +122%
C  OMP_NUM_THREADS=8              209.57s   210.01s   209.79s     -4.2%
D  8 執行緒 + 調校                233.00s   234.51s   233.76s     +6.8%
```

打開之後慢了 2.2 倍。

而 A 不等於 B 這件事本身就是答案：內部的 setenv 確實沒有到達 OpenMP runtime。這個漏洞是真的，但它目前是在保護 macOS 使用者。

八組跑下來，專家命中率全部落在 9.9% 到 10.1% 之間，所以快取狀態不是變數；同一組配置跑兩輪差異在 2% 以內，這些落差遠超噪音。

單變數隔離之後，兇手指得更清楚：

```text
（什麼都不設）                     218.97s      --
OMP_WAIT_POLICY=active             485.76s    +122%
KMP_BLOCKTIME=200                  471.40s    +115%
OMP_DYNAMIC=FALSE                  220.78s     +0.8%（噪音）
GOMP_SPINCOUNT=200000              217.39s     -0.7%（噪音，libomp 不讀 GOMP_*）
```

兩個忙等旋鈕各自單獨都足以造成全部退化，另外兩個完全無害。

機制說得通。所謂保持待命的做法是讓運算執行緒空轉不睡，但這台機器最關鍵的工作是搬資料的那 8 個 I/O 執行緒，它們也需要核心才能動。運算執行緒佔滿全部 10 顆核心空轉，搬運工就沒核心可用，搬不動了，整體反而更慢。

C 和 D 那兩列剛好把這個機制釘死：同樣的調校，10 執行緒下慢 122%，只讓 8 顆空轉、留 2 顆給搬運工，就只慢 6.8%。

順帶說 C 這一列本身也是免費的 4.2%。這顆晶片有 8 顆快核心加 2 顆省電慢核心，而 plan 在 macOS 上查不到核心配置（它只會用 lscpu，Mac 上必然失敗），退而全用 10 顆。但這種運算是大家做完一起等，最慢的那個決定整組節奏。有趣的是，就算補上物理核心數的偵測也修不到這件事，因為 `hw.physicalcpu` 在 M1 Max 也回 10。要的是 `hw.perflevel0.logicalcpu`，才會回 8。

所以我給作者的回報不是「請補上 macOS 分支」，是反過來的：千萬不要順手把這個漏洞補好，實測會讓 Apple Silicon 低常駐率的機器慢一倍以上，除非先加判斷條件。

## 一件值得記的驗證

我用七種不同配置跑同一個問題，包含純 CPU、開 Metal、關掉投機解碼。七次產生的文字，一個位元都不差。

這件事有意義。作者的核心宣稱是「顧問放在辦公室還是倉庫，只影響速度，不改變路由決策與權重精度」。這是很容易在最佳化過程中偷偷破掉的承諾，而它成立。加上 Metal kernel 對拍到 1e-6、專家選路位元相同這些測試，這是個做得很誠實的專案。

## 學到什麼

看起來最該修的東西，可能正在保護你。這次差一點就直接寫「macOS 少了一個分支」發出去，五行的修法、機制清楚、程式碼註解都幫我背書。真正擋下來的是一個問句：如果它生效了，會怎樣？花了兩小時做對照，答案是慢 2.2 倍。

在低常駐率的機器上，加速器加速不了不存在的東西。Metal 那 0.98 秒是整篇最便宜的教訓。買硬體之前先問瓶頸在哪一層，這台機器缺的是記憶體不是 GPU。

自我檢查工具的建議也是啟發式，也會過期。doctor 的 `DRAFT=0` 前提正確、結論相反，因為它少看一個指標。工具給的建議跟工具給的量測，可信度不是一回事。

那 400GB 的模型檔我留著。一個字 7 秒沒辦法日常用，但 744B 參數在筆電上正確回答問題這件事，值得偶爾拿出來看一眼。

兩則回報都發出去了：[#706](https://github.com/JustVugg/colibri/issues/706) 是這台機器的完整實測（這個硬體等級先前沒人補過），[#707](https://github.com/JustVugg/colibri/issues/707) 是那個「拜託不要修」。

發完才發現 CONTRIBUTING 只有 37 行，而我只讀了前 30 行，漏掉的最後兩行剛好就是 benchmark 報告的規範，要求中位數吞吐量。補了一則 comment。所以這篇的最後一個教訓其實是：小孩子才選擇性讀文件。