q4f16 輸出亂碼,原因是一個被 tail 截掉的錯誤
我在做一個 browser extension,用 WebGPU 在本機跑語音辨識,幫影片即時上字幕。模型用的是 Qwen3-ASR,官方有現成的 ONNX 版。後來我想換成一個針對日文 fine-tune 的版本,它沒有 ONNX,所以我讓 AI 自己轉,再上傳到 Hugging Face。
轉出來的 q4 版正常,q4f16 版卻在我的顯示卡上輸出亂碼。官方的 q4f16 在同一張卡上是好的,問題只出在我轉的這份。
最後找到原因的方法很樸素:把兩份模型的計算圖拿來比。只下載結構檔,不含權重,每個才幾 MB。decoder 的節點數差很多,官方 q4 是 1,586 個,我的是 2,264 個。差在 RMSNorm:官方把每一層的 RMSNorm 合成一個運算,總共 113 個;我的則是拆成 6 個小運算。
合併 RMSNorm 是轉換流程裡「圖最佳化」那一步的工作。回頭翻當時的紀錄,encoder 處理完、印出 Optimizing decoder_init.onnx... 之後,就再也沒有任何輸出了,最可能是 memory 不夠,被系統砍掉。當時為了版面乾淨,輸出用 tail 截短了,所以沒人發現這步失敗,後面的量化照樣跑完,看起來一切正常。
至於為什麼會變亂碼,目前是推論,還沒直接量到數值。沒合併的 RMSNorm 在 q4f16 版裡,會先把「平方的平均」轉成 16 位元。fp16 最大只能表示 65504,某一層只要有一個維度的值大到一定程度,平方的平均就會溢位成無限大,正規化的結果變成 0,後面整個壞掉。大型語言模型本來就常有少數維度數值特別大,所以這件事很可能真的會發生。我在 Node 上跑 CPU 版是正常的,因為 CPU 版的 onnxruntime 很多 fp16 運算會偷偷轉回 32 位元算;顯示卡則是真的用 16 位元在算。官方版用的是合併後的單一運算,所以沒有這個問題。
修法不用重新匯出 7GB 的權重,只要重做 decoder 計算圖的合併就好。在 memory 裡試過一次,合併後節點數剛好是 1,586,跟官方一樣。順帶一提,現在用的 q4 每產生一個字要多跑 678 個節點,修好之後應該也會變快。不過這份修正還沒存檔,也還沒重跑辨識驗證,所以 q4f16 到底會不會恢復正常,現在還不能下結論。
這件事讓我記住兩點。第一,長時間的轉換流程,不要只看最後幾行輸出,被 OOM 砍掉的步驟不一定會報錯,它可能只是安靜地不見了。第二,有官方版可以對照時,先比結構。節點數這種很粗的數字,就足以指出問題在哪一層。
Open to conversations about engineering leadership and AI product engineering.