跳到主要內容
Lab Grimoire
TW EN
請喝咖啡
33 秒跑完一次微調:Apple Silicon 上的 LoRA 體驗
動手實作

33 秒跑完一次微調:Apple Silicon 上的 LoRA 體驗

Agent 工作流實戰 · 第 16/17 篇
本頁目錄

33 秒。我在 Mac 上按下 Enter,倒了杯水回來,模型已經學會輸出我要的 JSON schema 了。

不是雲端 GPU,不是租 A100,也不是那種「理論上可以但實際上沒人做」的概念驗證。就是一台 M4 Pro 的 MacBook Pro,跑完 60 步 LoRA 訓練,loss 從 1.06 降到 0.17,記憶體峰值不到 7GB。於是微調完的模型直接吐出乾淨的單行 JSON,不再包 markdown 圍欄,不再美化縮排,就是我要的格式。

這篇文章記錄整個過程:為什麼選 Gemma-4 E4B、踩了什麼坑、怎麼繞過去、結果長什麼樣。如果你也在 Mac 上跑本地模型,想讓它「越用越合手」,這條路已經被驗證過了。

為什麼選 Gemma-4 E4B

故事要從 oMLX 跑分說起。

我手邊常駐四顆本地模型,用 oMLX 社群跑分做了一輪效能測試。結果很明確:

模型 Prefill (tok/s) 生成 (tok/s) 記憶體占用
gemma-4-e2b 4bit 2,090 133.7 ~3.5GB
gemma-4-e4b 4bit 1,117 ~90 ~6.8GB
Qwen3.6-35B-A3B-UD 4bit 777 69.9 ~20GB
qwen3-4b 721 ~2.5GB

Gemma-4 E4B 在同級距裡推理速度明顯快過 qwen3-4b,而且長 context(到 64K)效能衰減平緩。更重要的是,它只吃 6.8GB,所以在 48GB 的機器上做 LoRA 訓練綽綽有餘。

但有件事我一開始沒注意到。

你以為它是文字模型,其實它是 VLM

翻開 Gemma-4 E4B 的 config.json,赫然發現 ForConditionalGenerationvision_configaudio_config。這不是純文字語言模型,它是一顆 omni 多模態模型,同時有 vision tower、audio tower 和 text tower。

更令人意外的是,我的主力模型 Qwen3.6-35B-A3B 也是 VLM。本地這四顆模型,沒有一顆是純文字 causal LM。

這代表什麼?代表你不能用 mlx_lm.lora 那條純文字微調路徑。既然是 VLM,就得走 mlx-vlm 的載入流程。但我要做的是純文字任務(NER 實體萃取),所以策略很簡單:凍結 vision tower 和 audio tower,只練 text tower 裡的語言層。

VLM 架構與純文字 LoRA 凍結策略示意

踩坑:mlx 0.31.2 的 KV-sharing 回歸

環境準備完畢。Python 3.12 虛擬環境、mlx-tune 0.6.0、mlx 0.31.2、mlx-vlm 0.6.3,全都是 PyPI 上的最新版。下載 lmstudio-community 的 gemma-4-E4B-it-MLX-4bit(flat 格式,6.8GB),準備開練。

結果一載入就炸了:

ValueError: Received 126 parameters not in model:
  layers.*.self_attn.k_norm
  layers.*.self_attn.k_proj
  layers.*.self_attn.v_proj

換成 e2b 試試?同樣的錯,只是數字變成 140。Gemma-4 的 e 系列在目前的 mlx 生態裡全軍覆沒。

根因

追查之後發現,這是 mlx 0.31.1 升到 0.31.2 時引入的回歸(mlx-lm issue #1242,2026 年 5 月開,截至本文撰寫仍未修復)。

Gemma-4 的架構用了 KV-sharing 機制:部分 attention 層共用 K/V 投影,量化匯出時會產生一批「round-trip 冗餘副本」。由於這些張量在推理時根本用不到,模型定義裡也沒有對應的參數名稱。mlx 0.31.1 會靜默忽略它們,0.31.2 開始嚴格檢查,strict=True 直接拒收。

為什麼「官方解法」都不管用

第一直覺是降版。但 mlx-vlm 和 mlx-lm 都 pin 了 mlx>=0.31.2,而且 PyPI 上 0.31.1 已經被移除。因此降版這條路被依賴鏈堵死了。

第二直覺是用 strict=False。mlx 的底層 nn.Module.load_weights() 確實有這個參數。但 mlx-vlm 在內部呼叫 load_weights 時沒有把 strict=False 往下傳,所以你在外層怎麼設都沒效果。

解法:monkeypatch

既然公開 API 走不通,那就直接 patch 底層。在訓練腳本最前面,對 mlx.nn.Module.load_weights 做 monkeypatch,強制把 strict 參數改成 False

import mlx.nn as nn
_orig_load = nn.Module.load_weights

def _patched_load(self, file_or_weights, strict=False):
    return _orig_load(self, file_or_weights, strict=False)

nn.Module.load_weights = _patched_load

於是那些冗餘張量被靜默丟棄,模型照常載入。實測生成結果是連貫的繁體中文,架構完全正確。這個 workaround 的道理很單純:那批 K/V 張量本來就是多餘的副本,丟掉不影響推理。

mlx 0.31.2 KV-sharing 回歸的根因與 monkeypatch 修法

訓練:45 筆資料、60 步、33 秒

載入問題解決之後,訓練本身反而簡單。

我的任務是六維 NER(命名實體辨識):給一段生醫文本,要求模型輸出包含六個維度的 JSON 物件。訓練資料用腳本產生,45 筆 train、5 筆 valid,格式是 ChatML 的 jsonl。

LoRA 超參數:

參數
rank (r) 16
alpha 16
dropout 0
目標模組 q/k/v/o/gate/up/down_proj
batch size 1
gradient accumulation 4
learning rate 2e-4
max steps 60
max sequence length 512
train_on_completions True

跑完 60 步,耗時約 33 秒。loss 從 1.06 一路降到平均 0.17。記憶體峰值約 6.9GB,離 48GB 上限差得遠,所以完全不會影響其他應用程式。

Before vs After:格式服從的威力

微調的效果在 before/after 對比裡一目了然。

Before(base model,未微調):

模型收到 NER 指令後,確實辨識出了正確的實體,但輸出格式完全不合規:

  • 用 ````json` 圍欄包裹
  • 多行美化縮排
  • 大小寫飄移(該小寫的寫成大寫)
  • 需要額外後處理才能餵進下游管線

After(掛上 LoRA adapter):

  • 直接吐出緊湊的單行 JSON
  • 沒有圍欄,沒有多餘空白
  • 大小寫正確
  • 4 筆驗證句中,3 筆與 gold standard 完全一致
  • 1 筆出現 HER2 的 protein/gene 分類混淆,屬於內容層的小錯,可以透過增加訓練資料修正

Before/After 對比:base model 與 LoRA adapter 的輸出差異

重點是:微調對「格式服從」和「風格貼合」的效果是立竿見影的。你不需要幾千筆資料、不需要訓練幾小時,45 筆範例、33 秒,模型就學會了你要的輸出結構。

至於內容正確度,那是資料品質和數量的問題,不是微調機制本身的限制。

「越用越合手」的迴圈

這次 MVP 驗證的核心命題不是「能不能微調」,而是「日常使用、定期微調、換 adapter」這個迴圈是否成立。

答案是成立的。而且成本低到令人意外。

這個迴圈長這樣:

  1. 日常使用:用本地模型處理日常任務(NER、分類、摘要、格式轉換)
  2. 收集樣本:把模型輸出好的結果標記起來,累積成訓練資料
  3. 定期重訓:每週或每月跑一次 LoRA,幾十秒到幾分鐘
  4. 換 adapter:新 adapter 上線,模型行為更貼合你的需求
  5. 回到第一步

這不是什麼線上學習或即時權重更新。那種做法在大型語言模型上不穩定,因此生產環境沒人用。實務上就是批次重訓加 adapter 熱抽換,簡單、可控、可回溯。

有幾件事要注意:

  • 微調學的是「貼合」(格式、文風、領域術語),不是「提升智商」。模型的能力天花板不會因為微調而提高。
  • 爛資料會導致災難性遺忘。一定要有品質校對和評測閘門。
  • 事實知識交給 RAG 和記憶層處理,微調只負責風格和技能。

與 T15 的對照:Mac vs Windows

如果你讀過 T15(Windows 上的 QLoRA 微調),會發現兩條路線的取捨很不一樣:

面向 T15 Windows QLoRA T16 Mac LoRA
硬體需求 NVIDIA GPU(RTX 3060 以上) Apple Silicon(M1 以上)
框架 Unsloth + PyTorch + CUDA mlx-tune + MLX
生態成熟度 主流,教學資源豐富 邊緣,需自己踩坑
微調速度 視 GPU 而定,通常較快 33 秒 / 60 步(E4B)
記憶體管理 VRAM 獨立,不影響系統 統一記憶體,需注意總量
最大痛點 驅動與 CUDA 版本衝突 mlx 生態回歸與相容性

兩條路都能到達「越用越合手」的終點。差別在於 Windows/NVIDIA 那邊路平、車多、導航清楚;Mac/MLX 這邊是山路,偶爾要下車搬石頭,但風景不錯,而且你不需要另外買一張顯示卡。

後續展望

這次 MVP 驗證了最小迴圈,接下來有幾件事要做:

  • 擴充訓練資料:修正 HER2 的 protein/gene 消歧問題,增加更多生醫領域的 NER 範例
  • 接入日常採集管線:把日常使用中品質好的 prompt-response 對自動收集起來,作為下一輪微調的素材
  • 雙模型分流:E4B 微調版負責高吞吐的預處理任務(NER、triage、標籤),35B 的 Qwen 繼續扛需要推理能力的重活
  • 評估更多任務:除了 NER,還有繁中科普初稿、IRB 學術文體等任務等著試

33 秒能做的事比你想像的多。不需要租 GPU、不需要等雲端排隊、不需要把資料上傳到任何地方。你的 Mac 就是你的訓練機,你的日常使用就是你的訓練資料。模型會越用越合手,前提是你願意花 33 秒讓它學一次。

常見問題

M1 或 M2 也能跑嗎?

可以。mlx-tune 支援 M1 以上的所有 Apple Silicon 晶片。差異在於記憶體和速度。 Gemma-4 E4B 的 4bit 量化版約占 6.8GB,訓練時峰值約 6.9GB。如果你的 Mac 有 16GB 以上的記憶體,基本上都跑得動。M1 / M2 的訓練速度會比 M4 Pro 慢,但 LoRA 本身計算量不大,60 步的訓練可能從 33 秒拉長到一兩分鐘,仍然在可接受範圍內。 如果記憶體只有 8GB,建議改用更小的模型(例如 gemma-4-e2b,約 3.5GB),或減少 `max_length` 參數。

跟 T15 的 Windows QLoRA 差在哪?

核心差異在硬體生態和框架: - **T15(Windows QLoRA)** 走 Unsloth + PyTorch + CUDA,需要 NVIDIA GPU(建議 RTX 3060 / 12GB VRAM 以上)。生態成熟、教學資源多、社群支援強,是目前微調的主流路線。 - **T16(Mac LoRA)** 走 mlx-tune + MLX,原生跑在 Apple Silicon 的統一記憶體上。不需要獨立顯示卡,但生態較新,偶爾會遇到相容性問題需要自己處理。 兩條路最終達成的效果相同:讓本地模型學會你的格式和風格。選哪條取決於你手邊的硬體。如果你有 NVIDIA GPU,T15 的路更平順;如果你只有 Mac,T16 證明了這條路也走得通。

為什麼不用 PyTorch 而用 MLX?

PyTorch 在 Mac 上可以透過 MPS(Metal Performance Shaders)後端運作,但微調場景的支援不完整。很多訓練用的運算子在 MPS 上沒有實作,會 fallback 到 CPU,速度大幅下降。Unsloth 和 PEFT 等主流微調框架也綁定 CUDA,Mac 上用不了。 MLX 是 Apple 專為 Apple Silicon 設計的機器學習框架,直接利用統一記憶體架構,不需要 CPU 和 GPU 之間的資料搬移。mlx-tune 建構在 MLX 之上,原生支援 LoRA / SFT / DPO / CPT 等微調方法,是目前在 Mac 上做本地微調最務實的選擇。

33 秒只是 60 步,實際場景資料量更大怎麼辦?

33 秒 / 60 步是 MVP 驗證,用 45 筆訓練資料。實際場景當然需要更多資料和更多步數。 按比例估算: - 500 筆資料、300 步:大約 3 到 5 分鐘 - 2,000 筆資料、1,000 步:大約 10 到 15 分鐘 - 10,000 筆以上:可能需要 1 小時左右(M3 Max 跑 1,000 步約 40 分鐘的社群回報可作為參考) 這些時間在本地訓練的情境下完全可以接受。你可以睡前按下 Enter,早上起來就有新的 adapter 了。重點不是一次訓練的速度,而是「收集樣本、定期重訓、換 adapter」這個迴圈能不能持續轉動。33 秒只是證明迴圈的摩擦力低到幾乎不存在。

adapter 可以分享給別人嗎?

可以。LoRA adapter 是一組獨立的小型權重檔(這次的 adapter 只有幾 MB),不包含完整的 base model 權重。你可以把 adapter 檔案分享給任何人,對方只要有相同的 base model(gemma-4-E4B-it-MLX-4bit),把 adapter 掛上去就能用。 需要注意的是: - adapter 綁定特定的 base model。不同量化版本或不同模型之間的 adapter 不能互換。 - 如果你的訓練資料包含敏感資訊,adapter 權重可能隱含這些資訊。分享前請確認資料的隱私性。 - adapter 的效果與 base model 版本有關。如果對方用的是不同來源或不同量化方式的同名模型,行為可能略有差異。

覺得這篇有幫助?

追蹤以收到新的 AI × 生醫研究筆記:

或請我喝杯咖啡,讓新內容持續產出。

☕ 請我喝杯咖啡