重點摘要
- 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。流程是:
- 先用完 2 GiB UMA
- 再向 GTT 借 ~1.3 GiB 的系統 RAM 頁面
- 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 算出來不一樣?
因為規格只定義單一運算的結果,但沒有規定三件事:
- Denormal 數的處理方式:非常接近 0 的浮點數(小於 6×10⁻⁸)稱為 denormal。IEEE 標準要求保留精度處理,但 AMD Vega 架構預設會把這些數 flush to zero(FTZ)——直接歸零——以換取效能。NVIDIA 不做 FTZ。這在遊戲中完全沒影響,但 LLM attention 的 softmax 運算大量出現這個範圍的數值。
- 運算順序:浮點數不符合結合律。
(a + b) + c ≠ a + (b + c)在 fp16 中是真的。Shader compiler 為了讓 GPU 並行效率最大化,會自由重排運算順序,這對遊戲結果沒有可見影響,但 LLM 的 attention 矩陣乘法有幾萬次浮點運算,順序一改,誤差模式就完全不同。 - 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。資料正確性永遠優先於速度。
如果你也遇到類似問題:排查步驟
- 先確認系統有足夠空閒 RAM(至少要比模型大小多 2 GiB),不然連載入都會失敗或 OOM reboot
- 用
num_gpu=0跑純 CPU,確認模型本身答案正確 - 逐步增加 num_gpu(1 → 5 → 10 → 20 → …),用複雜問題測試(不要只用 1+1),找到亂碼的臨界層數
- 環境變數(GGML_VK_DISABLE_F16、OLLAMA_KV_CACHE_TYPE)對 RADV 底層 compiler bug 無效,不要在這裡浪費時間
- 若根本無法修,接受純 CPU 或換有 CUDA / Metal 支援的環境
這不是 Ollama 的 bug,也不是模型的問題。是 AMD Renoir Vega iGPU + RADV Vulkan shader compiler 在 LLM 工作負載下的精度限制,加上 iGPU 共享系統 RAM 的架構特性,兩個問題同時踩到。