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,赫然發現 ForConditionalGeneration、vision_config、audio_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 裡的語言層。
![]()
踩坑: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 張量本來就是多餘的副本,丟掉不影響推理。
![]()
訓練: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 分類混淆,屬於內容層的小錯,可以透過增加訓練資料修正
![]()
重點是:微調對「格式服從」和「風格貼合」的效果是立竿見影的。你不需要幾千筆資料、不需要訓練幾小時,45 筆範例、33 秒,模型就學會了你要的輸出結構。
至於內容正確度,那是資料品質和數量的問題,不是微調機制本身的限制。
「越用越合手」的迴圈
這次 MVP 驗證的核心命題不是「能不能微調」,而是「日常使用、定期微調、換 adapter」這個迴圈是否成立。
答案是成立的。而且成本低到令人意外。
這個迴圈長這樣:
- 日常使用:用本地模型處理日常任務(NER、分類、摘要、格式轉換)
- 收集樣本:把模型輸出好的結果標記起來,累積成訓練資料
- 定期重訓:每週或每月跑一次 LoRA,幾十秒到幾分鐘
- 換 adapter:新 adapter 上線,模型行為更貼合你的需求
- 回到第一步
這不是什麼線上學習或即時權重更新。那種做法在大型語言模型上不穩定,因此生產環境沒人用。實務上就是批次重訓加 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 版本有關。如果對方用的是不同來源或不同量化方式的同名模型,行為可能略有差異。