gemma4:e4b 在 AMD Renoir iGPU Vulkan 輸出亂碼的完整排查

重點摘要

  • gemma4:e4b 在 AMD Renoir iGPU(Vulkan)超過 30 層就輸出亂碼,低層數正常
  • 根本原因:RADV Vulkan shader compiler 的精度優化破壞 f16 運算順序,誤差隨層數滾雪球
  • iGPU 沒有獨立 VRAM,記憶體從系統 RAM 借,RAM 不足會讓模型載入循環崩潰
  • Mac Metal / 純 CPU 不受影響;GGML_VK_DISABLE_F16 等環境變數無法修復底層 compiler bug

在本地端跑大型語言模型時,GPU 加速聽起來理所當然——更快嘛。但如果 GPU 算出來的答案是錯的,快有什麼用?這篇文章記錄了一次真實的踩坑:gemma4:e4b 在 AMD Renoir iGPU 上透過 Vulkan 跑出亂碼的完整排查過程,以及背後一層比一層深的技術原因。

事件起因:模型跑得快,答案全錯

測試環境是一台搭載 AMD Renoir APU 的 mini PC,透過 Ollama 跑 gemma4:e4b 模型。Ollama 偵測到 iGPU 後自動啟用 Vulkan 加速,把 42/43 層 offload 到 GPU。

速度看起來不錯,約 8 tok/s。但仔細看輸出內容,發現問題大了:

  • 問「什麼是 Kafka Consumer Group Rebalancing」→ 回答泰文的學習理論
  • 問 Python 日期解析函數 → 回答 JavaScript offsetTop 的用法
  • 問燈泡謎題 → 回答哲學框架

模型完全沒理解問題,像是跑在另一個平行宇宙一樣。

第一個坑:RAM 不足讓模型連載入都不穩定

在問為什麼答案亂之前,先碰到一個更早的問題:模型根本載入不起來。第一次啟動時,Ollama 日誌出現了這個循環:

16:35:53  offloaded 42/43 layers to GPU(模型開始載入)
16:38:53  llm server not responding
16:38:54  llm server loading model
16:39:02  llm server not responding
16:39:03  llm server loading model
...(反覆了將近 6 分鐘)
16:42:28  llama runner started in 410.58 seconds(終於啟動)
17:04:49  -- Boot --(機器直接重開機)

當時系統狀況:OMS 全套服務正在運行,RAM 只剩 1.7 GiB 可用。停掉 OMS 釋放記憶體後重跑:

llama runner started in 42.04 seconds

差了 10 倍。問題不在模型,在記憶體。

為什麼 iGPU 需要大量系統 RAM?

AMD Renoir iGPU 沒有獨立顯存,它的記憶體架構分兩層:

類型 來源 大小 特性
UMA(Unified Memory) BIOS 開機時從系統 RAM 切出 2 GiB(此台設定) 固定預留,GPU 專用,穩定
GTT(Graphics Translation Table) 從剩餘系統 RAM 動態借用 最多 ~6.8 GiB 依可用 RAM 決定,借不到就崩

Ollama 顯示「8.8 GiB 可用 VRAM」= UMA(2 GiB)+ GTT 上限(~6.8 GiB)合計。

跑 gemma4:e4b 需要 2.8 GiB GPU weights + 224 MB KV cache + 289 MB compute graph ≈ 3.3 GiB。流程是:

  1. 先用完 2 GiB UMA
  2. 再向 GTT 借 ~1.3 GiB 的系統 RAM 頁面
  3. GTT 的頁面必須來自空閒的系統 RAM

OMS 開著只剩 1.7 GB 可用,GPU 借不到足夠的 GTT 頁面,runner 一直崩潰重試,才出現那個 6 分鐘的死循環。最後機器因為 OOM 直接 reboot。

換句話說:iGPU 的「VRAM」是假的,它在跟你的程式搶同一塊系統 RAM。獨顯不會有這個問題,因為它有真正的 GDDR 記憶體。

GPU 與 GPU 的差異:不是每顆都一樣精準

解決了載入問題,答案還是亂的。這時才是 GPU 精度的核心問題。

GPU API f16 硬體支援 ML 成熟度
NVIDIA RTX CUDA 原生 Tensor Core,為 ML 設計
Apple M 系列 Metal Neural Engine + Metal 緊密整合
AMD RX 獨顯 ROCm / Vulkan 原生支援,ROCm 驗證過 中高
AMD Renoir iGPU Vulkan(RADV) Vega 架構,遊戲導向,未針對 ML 驗證

f16 規格一樣,為什麼結果不同?

很多人的疑問:IEEE 754 fp16 是標準規格,大家都遵守,為什麼不同 GPU 算出來不一樣?

因為規格只定義單一運算的結果,但沒有規定三件事:

  1. Denormal 數的處理方式:非常接近 0 的浮點數(小於 6×10⁻⁸)稱為 denormal。IEEE 標準要求保留精度處理,但 AMD Vega 架構預設會把這些數 flush to zero(FTZ)——直接歸零——以換取效能。NVIDIA 不做 FTZ。這在遊戲中完全沒影響,但 LLM attention 的 softmax 運算大量出現這個範圍的數值。
  2. 運算順序:浮點數不符合結合律(a + b) + c ≠ a + (b + c) 在 fp16 中是真的。Shader compiler 為了讓 GPU 並行效率最大化,會自由重排運算順序,這對遊戲結果沒有可見影響,但 LLM 的 attention 矩陣乘法有幾萬次浮點運算,順序一改,誤差模式就完全不同。
  3. FMA(Fused Multiply-Add)的使用時機a × b + c 可以分兩步算(兩次取整),也可以融合成一步(只取整一次,精度更高)。NVIDIA CUDA 強制所有路徑用 FMA。RADV 的 ACO shader compiler 根據優化情境決定,在某些 LLM 矩陣模式下選了非 FMA 路徑。

這三個問題疊在一起,每一層的誤差模式都不同,而且誤差會被下一層當成合法輸入繼續放大。

為什麼低層數正常、高層數亂碼?

什麼是 Transformer 的「層」

gemma4:e4b 是 Transformer 架構,共 43 層。每個 token 的生成,資料要依序流過全部 43 層,每一層的輸出是下一層的輸入:

Token 輸入
  → Layer 1(基礎語法、token 特徵)
  → Layer 2 ~ 10(語法結構、基礎語意)
  → Layer 10 ~ 25(上下文理解、語意推理)
  → Layer 25 ~ 42(高階抽象、輸出生成)
  → 下一個 token

當設定 num_gpu=N 時,前 N 層在 GPU 算,剩下的在 CPU 算

誤差如何滾雪球

Layer  1:正確值 2.000,Vulkan 算出 2.001 → 誤差 0.001(無感)
Layer  5:誤差疊加,下層把這個「略偏的 2.005」當成正確輸入繼續算
Layer 10:誤差影響語意方向判斷
Layer 20:模型對問題的理解開始偏移
Layer 30:完全脫離正確答案空間
Layer 42:輸出隨機語言的 token,變成泰文或法文

這就像傳話遊戲:每個人傳話都有一點偏差,傳到第 43 個人時,內容已經面目全非。

為什麼簡單問題(1+1)還能過

「1+1 等於幾」的答案空間極小——就算誤差很大,最終 token 機率分布仍然在「2」附近。但「請用 Python 寫一個函數處理三種日期格式」的答案空間是整個程式語言的 token 集合,一旦偏移,就隨機落到任何地方。這也是為什麼診斷時不能只用簡單問題測試,必須用複雜 prompt 才能暴露問題。

診斷過程:二分法找臨界點

for layers in 1 5 10 20 30 40 42; do
  echo -n "=== num_gpu=$layers === "
  curl -s http://localhost:11434/api/chat \
    -d "{\"model\":\"gemma4:e4b\",
         \"messages\":[{\"role\":\"user\",\"content\":\"1+1等於幾?\"}],
         \"stream\":false,
         \"options\":{\"num_gpu\":$layers,\"num_predict\":30}}" \
    | python3 -c "import json,sys; print(json.load(sys.stdin)['message']['content'])"
done
num_gpu 結果 狀態
1 1+1 等於 2 ✅ 正確
5 1+1 等於 2 ✅ 正確
10 (空白) ⚠️ 不穩定
20 簡單問題偶爾正確 ⚠️ 不穩定
30 法文回應 ❌ 亂碼
42 隨機英文片段 ❌ 亂碼

嘗試修復:三種方案全部失敗,以及為什麼

方案一:GGML_VK_DISABLE_F16=1

理論上這個環境變數應該告訴 llama.cpp 在 Vulkan 路徑使用 f32 計算,避開 f16 精度問題。但沒有效果。原因在於 Vulkan 推論的整個管線分四層:

1. llama.cpp         → 寫 GLSL compute shader 原始碼        ← 環境變數影響到這層
2. glslang           → 編譯成 SPIR-V bytecode
3. RADV / ACO        → 把 SPIR-V JIT 編譯成 AMD GCN 機器碼  ← 精度 bug 在這層
4. AMD Vega 硬體     → 執行機器碼

精度 bug 在第 3 層——RADV 的 ACO shader compiler 在把 SPIR-V 轉成 GCN ISA 時,對某些矩陣運算的指令做了重排或合併,這個行為由 RADV 自己決定,應用層的環境變數完全碰不到。就算第 1 層寫了 f32,RADV 也可能在編譯時把某些中間值降轉成 f16——這對遊戲是合法的效能優化,對 LLM 是致命的。

方案二:OLLAMA_KV_CACHE_TYPE=f32

只影響 KV cache 的儲存精度,不影響主要的矩陣乘法計算路徑。輸出直接變成旁遮普語。

方案三:num_gpu=20 折衷

1+1 這種極簡問題可以過,但稍微複雜的 prompt 仍然亂答。誤差臨界點比簡單測試顯示的還要低,不穩定。

最終結論

方案 速度 正確性 建議
純 CPU(num_gpu=0) ~1 tok/s ✅ 正確 唯一可用方案
Vulkan 全層(num_gpu=42) ~8 tok/s ❌ 亂碼 不可用
Mac Metal(同款模型) ~8 tok/s ✅ 正確 最佳選擇

快 8 倍但答案全錯,還不如 1 tok/s 的純 CPU。資料正確性永遠優先於速度。

如果你也遇到類似問題:排查步驟

  1. 先確認系統有足夠空閒 RAM(至少要比模型大小多 2 GiB),不然連載入都會失敗或 OOM reboot
  2. num_gpu=0 跑純 CPU,確認模型本身答案正確
  3. 逐步增加 num_gpu(1 → 5 → 10 → 20 → …),用複雜問題測試(不要只用 1+1),找到亂碼的臨界層數
  4. 環境變數(GGML_VK_DISABLE_F16、OLLAMA_KV_CACHE_TYPE)對 RADV 底層 compiler bug 無效,不要在這裡浪費時間
  5. 若根本無法修,接受純 CPU 或換有 CUDA / Metal 支援的環境

這不是 Ollama 的 bug,也不是模型的問題。是 AMD Renoir Vega iGPU + RADV Vulkan shader compiler 在 LLM 工作負載下的精度限制,加上 iGPU 共享系統 RAM 的架構特性,兩個問題同時踩到。

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *