# Paper 說支援 ARM，我大概是第一個真的去編譯的人

> 微軟發了號稱 CPU 就能即時聽寫的 VibeVoice-ASR-BitNet，論文寫 targeting both ARM and x86。我在 M1 Max 上實測，從編譯失敗一路 debug 到 NaN 指紋，挖出三個上游 bug。這篇講整個偵探過程，以及 model card 的數字該怎麼讀。
> Author: Fred Chu (MumbleKey) · Published: 2026-07-27
> Canonical: https://blog.mumblekey.com/zh/posts/vibevoice-bitnet-arm-debug/
微軟上週丟出一個蠻誘人的東西：VibeVoice-ASR-BitNet，號稱不用 GPU、3 條 CPU thread 就能即時語音辨識，比 Whisper.cpp 快 1.6 到 2.3 倍。論文白紙黑字寫著 custom SIMD kernels「targeting both ARM and x86 platforms」。

我的 M1 Max 就是 ARM。玩玩看。

結果這一玩，我大概成了地球上第一個在 ARM 上編譯這個專案的人。不是什麼榮譽，是編譯器直接告訴我的。

## 這模型想做什麼

先講它為什麼值得測。現在的語音辨識分兩個世界：小模型能跑 CPU 但陽春，LLM 型的辨識（聽得懂長錄音、分得出講者、能塞術語提示）功能全但綁死 GPU。這個模型想把第二種塞進第一種的硬體。

做法是對症下藥的壓縮。他們先量出管線兩段的體質完全相反：前段的聲音編碼器卡在資料搬運，流過的資料量是權重的 16 倍，所以用溫和的 8-bit 加算子融合，少跑幾趟記憶體。後段的語言模型卡在權重太肥，每吐一個字都要把 15 億參數讀一遍，所以下猛藥，每個權重只留 -1、0、+1 三種值，壓成八分之一。這就是 BitNet，作者正是微軟提出 1-bit LLM 的那個團隊。

一個讀 model card 的小心得：WER 表左欄那個漂亮數字是 7B 大系統的，這個 checkpoint 是 7B 換成 1.5B 小腦再壓縮的版本，自己的數字在右欄，中文會議語料從 19.83 掉到 27.45。廠商的表格永遠把最大的系統放最顯眼的位置，checkpoint 自己的那一行要自己找。

## 第一關：編譯直接炸

```text
ggml-aarch64.c:1156:31: error: use of undeclared identifier 'VAE_ROW_BLOCK_SIZE'
```

追進去發現，這些巨集只在 x86 的條件編譯分支裡有定義。有趣的是，語言模型那側的設定檔有寫 ARM 分支，偏偏聲音編碼器這側的忘了。

這種錯誤騙不了人。程式碼可以宣稱支援任何平台，編譯器不會。這個 repo 從來沒有人在 ARM 上按下過 build。

補上缺的巨集，過關。

## 第二關：輸出全是驚嘆號

編譯過了，跑 30 秒音檔，模型輸出：

```text
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
```

而且一路輸出到天荒地老，20 分鐘停不下來。

跟 Claude Code 一路往下挖，先在聲音編碼器的輸出插檢查點，發現一個一眼假的現象：acoustic 和 semantic 兩個編碼器，權重完全不同，輸出的特徵向量卻幾乎一模一樣。正常運算下這不可能，唯一的解釋是矩陣乘法根本沒算，輸出只剩 bias 的殘影。

翻 kernel 原始碼，答案很直白：七個核心運算函式全部包在 `#if defined(__AVX2__)` 裡面，沒有 `#else`。在 ARM 上，這七個函式編譯出來是空的。不報錯，不警告，就是安靜地什麼都不做。

補了七個 scalar 版本，特徵有值了。但輸出還是驚嘆號。

## 第三關：NaN 的指紋

這關最好玩。logits 全部是 NaN，於是 greedy decoding 永遠選到第 0 號 token，就是那個驚嘆號。但 NaN 從哪來？

改變 batch 大小做實驗，得到一組漂亮的指紋：

```text
batch size 1 → 正常
batch size 2 → 正常
batch size 4 → 全部 NaN
batch size 5 → 全部 NaN
batch size 8 → 正常
```

1、2、8 好，4、5 炸。這個 pattern 直接指向答案：上游框架把矩陣運算的列數硬取整到 4 的倍數，在 x86 上這永遠安全，因為 x86 的平行度參數就是 4。但 ARM 的平行度參數是 8，於是 4 列的餘數塊通不過整除檢查，掉進一個語意完全不匹配的 fallback 函式：值寫錯位置，該寫的位置永遠沒寫到，殘留的未初始化記憶體就是 NaN 的來源。

最妙的是，另一個平行的函式裡有人加過完全對應的防護，還留了註解解釋為什麼要防。同一個 bug，一個地方修了，另一個地方忘了。x86 上永遠不會觸發，所以沒人發現。

照著那段註解的樣式補上防護，logits 正常了，decode 速度 71 tok/s，整體 RTF 0.75。論文宣稱的 real-time，在 ARM 上其實做得到。

## 第四關：停損

修完三個 bug，轉錄輸出從驚嘆號變成了流利的多語胡言亂語。模型在講話，但沒有在聽。

這代表還有至少一個數值 bug。我做了最後一個隔離實驗：把所有矩陣運算換成逐元素的笨版本（慢到哭但語意絕對正確），輸出跟我修的版本 bit 級相同。所以剩下的 bug 不在我碰過的地方，在更上游的卷積融合算子區，那裡也是一片從沒在 ARM 驗證過的手寫程式碼。

到這裡我停手了。不是修不動，是修好了也不改變結論：我家裡已經跑著它的 7B 老大哥（MLX 走 GPU，快它 4 倍，中文更準），這個 CPU 版是為沒有這種硬體的人設計的。繼續挖是別人的作業。

三個 bug 的修補和重現步驟都整理好了，準備發給上游。repo 才 13 個 star，可能這篇文比 issue 先到。

## 學到什麼

「支援 ARM」和「在 ARM 上編譯過」是兩件事。paper 可以寫 targeting，model card 可以列 NEON，但唯一可信的證據是 CI 裡有沒有那台機器。

另外，NaN 不可怕，可怕的是不留指紋的 NaN。這次 batch 1/2/8 好、4/5 炸的 pattern 一出來，答案就自己浮上來了。debug 的大半功夫不在盯程式碼，在設計那個能逼 bug 現形的實驗。

至於 1-bit 模型到底行不行，這個 checkpoint 的中文數字擺在那裡，我持保留態度。但邊緣 CPU 即時聽寫這個方向，值得繼續看。

## 後記（隔天）

寫完這篇的第二天，微軟推了一個 commit，訊息寫著 ARM NEON optimizations 和 fix overflow。點開一看，三個 bug 全修了，跟我挖到的一一對上。所以那些 bug 是真的，方向也是對的，只是他們的鍵盤比我的 issue 快。

我拉了新版在同一台機器重測。編譯直接過，速度更快了。然後，轉錄還是亂碼。第四關還在，而且證據反而更完整：亂碼在我的 scalar 實作和他們的 NEON 實作下長得一模一樣，指向共同的上游卷積路徑。

issue 發出去了，這次不用擔心比文章慢：[microsoft/VibeASR.cpp#2](https://github.com/microsoft/VibeASR.cpp/issues/2)

至於「第一個編譯的人」這個頭銜，保鮮期一天。開源世界跑得很快，這大概是被打臉的方式裡最好的一種。

## 後記二（又隔天）

不甘心，讓 agent 在背景把第四關也打完了。root cause 打臉了我自己在 issue 裡的猜測：不在卷積管線，在 LM kernel。NEON 路徑用 64 元素的區塊佈局去解三值權重，但檔案格式是 128 元素一組，等於每一層 transformer 都在用「洗過牌的權重矩陣」算矩陣乘法。這也解釋了為什麼輸出是流利的胡言亂語而非雜訊：模型本身是好的，只是它的腦被均勻打亂了。

修好之後，同一段 30 秒中文演講在 M1 Max 上轉出了正確的內容，RTF 0.50。修法和 kernel 對拍測試都在 [PR #3](https://github.com/microsoft/VibeASR.cpp/pull/3)。

所以最終回收：paper 宣稱的「ARM 即時聽寫」，現在真的成立了，四個 bug 之後。