快轉到主要內容
  1. 教學文章/

MLX-LM LoRA 微調入門:Adapter、資料格式與 Apple Silicon 本機評測

·13 分鐘· loading · loading · ·
Mlx MLX-LM LoRA Fine-Tuning Apple-Silicon Local AI
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
科技觀點 - 本文屬於一個選集。
§ 23: 本文

本地 LLM 跑起來以後,下一個很自然的問題是: 「那我可以讓模型更懂自己的任務嗎?」 可以。 但拍拍君先把期待值放準一點。 LoRA 微調不是魔法。 它不是把一個小模型訓練成萬能博士。 它比較像是在模型旁邊加一組可拆卸的小筆記,讓模型在某個格式、語氣、分類規則、抽取任務上更穩。 這篇要做的是 MLX-LM 的 LoRA 入門工作流:

  1. 準備 train.jsonlvalid.jsonltest.jsonl
  2. mlx_lm.lora 訓練 adapter
  3. 用 adapter 做生成與評測
  4. 看懂什麼時候要 fuse
  5. 調整 Apple Silicon 上最常卡住的記憶體參數 如果你還沒跑過 MLX-LM,可以先看 MLX-LM 本地模型推論。 如果你已經有一批 prompts 想批次比較,可以看 MLX-LM 批次推論。 今天這篇不重講「怎麼聊天」,而是專心處理「怎麼讓模型學會一個窄任務」。

一. 前言:LoRA 微調適合解決什麼問題?
#

先講結論。 LoRA 適合這些情境:

  • 固定輸出格式
  • 特定分類標籤
  • 特定客服語氣
  • 小型領域問答
  • 資料抽取規則
  • 指令格式對齊 LoRA 不太適合這些期待:
  • 讓模型學會大量新知識
  • 修掉基礎模型所有幻覺
  • 用幾十筆資料讓模型變專家
  • 把小模型訓練成大模型等級
  • 取代 RAG、資料庫或搜尋系統 拍拍君自己的心法是: 如果問題是「模型不知道資料」,先考慮 RAG。 如果問題是「模型知道,但輸出格式老是不聽話」,再考慮 LoRA。 LoRA 的好處是它不用更新整個模型。 它只訓練一小組 adapter 權重。 所以檔案小、實驗快、可以保留多個任務版本。 這對 Mac 本機實驗很重要。 你不想每次都複製一整個 7B 模型,只是為了測一個分類 prompt。

二. 安裝:把訓練依賴裝起來
#

建立專案:

uv init mlx-lora-lab
cd mlx-lora-lab
uv add "mlx-lm[train]"

如果你不用 uv

python -m venv .venv
source .venv/bin/activate
pip install "mlx-lm[train]"

確認指令存在:

mlx_lm.lora --help
mlx_lm.generate --help
mlx_lm.fuse --help

今天的範例目標是做一個「短訊息分類器」。 輸入一段文字,模型要輸出其中一種標籤:

  • bug
  • feature
  • question
  • other 這種任務很適合入門。 因為資料短、格式固定、容易用 test set 檢查。 專案結構先長這樣:
mlx-lora-lab/
├── data/
│   ├── train.jsonl
│   ├── valid.jsonl
│   └── test.jsonl
├── adapters/
├── prompts/
│   └── classify.txt
└── scripts/
    └── make_data.py

adapters/ 可以先不存在。 MLX-LM 訓練完會幫你產生。

三. 選模型:先小再說,不要逞強
#

微調第一個常見錯誤是模型選太大。 本機實驗的第一輪不該追求最強。 你要追求的是:

  1. 訓練能跑完
  2. loss 有變化
  3. adapter 能載入
  4. test set 可以比較
  5. 整個流程可重現 拍拍君會先用小模型或量化模型試流程。 例如:
MODEL="mlx-community/Qwen2.5-1.5B-Instruct-4bit"

或:

MODEL="mlx-community/Llama-3.2-3B-Instruct-4bit"

模型名稱會隨 Hugging Face 上的轉換版本變動。 所以正式寫腳本時,請把模型名稱放在 .env 或 shell 變數,不要散落在每個指令裡。 如果你用的是量化模型,MLX-LM 會走 QLoRA 方向。 如果是非量化模型,則是一般 LoRA。 這裡先不用急著分太細。 先把資料格式準備好比較重要。

四. 資料格式:先從 chat JSONL 開始
#

MLX-LM 的 LoRA 訓練吃 JSONL。 本地資料夾裡通常放:

data/
├── train.jsonl
├── valid.jsonl
└── test.jsonl

每一行是一筆 JSON。 不要把一筆資料切成多行。 拍拍君建議初學先用 chat 格式。 它最接近現在 instruct model 的使用方式。 data/train.jsonl

{"messages":[{"role":"system","content":"你是一個 issue triage assistant。只能輸出 bug、feature、question、other 其中一個標籤。"},{"role":"user","content":"登入後按儲存會跳 500,而且 console 有 TypeError。"},{"role":"assistant","content":"bug"}]}
{"messages":[{"role":"system","content":"你是一個 issue triage assistant。只能輸出 bug、feature、question、other 其中一個標籤。"},{"role":"user","content":"可以支援匯出成 Excel 嗎?目前只有 CSV。"},{"role":"assistant","content":"feature"}]}
{"messages":[{"role":"system","content":"你是一個 issue triage assistant。只能輸出 bug、feature、question、other 其中一個標籤。"},{"role":"user","content":"請問 API token 要在哪裡設定?文件裡我找不到。"},{"role":"assistant","content":"question"}]}
{"messages":[{"role":"system","content":"你是一個 issue triage assistant。只能輸出 bug、feature、question、other 其中一個標籤。"},{"role":"user","content":"這個專案看起來很棒,謝謝維護。"},{"role":"assistant","content":"other"}]}

注意兩件事。 第一,assistant 的答案要短。 你希望模型學會的是標籤,不是長篇解釋。 第二,system prompt 要穩定。 訓練資料裡 system prompt 一直變,模型就很難知道規則在哪裡。 如果你做的是 prompt/completion 任務,也可以用:

{"prompt":"分類:登入後按儲存會跳 500。答案:","completion":"bug"}

但對 instruct model 來說,chat 格式通常比較直覺。

五. 切 train/valid/test:不要只準備 train
#

很多人第一次微調只準備 train.jsonl。 這樣可以跑,但你會很難判斷模型有沒有真的變好。 建議最少切三份:

  • train.jsonl:訓練用
  • valid.jsonl:訓練中觀察 validation loss
  • test.jsonl:最後才拿出來評估 小資料集可以先用這個比例:
  • train:80%
  • valid:10%
  • test:10% 如果總資料只有 100 筆,不要幻想結果很穩。 但你還是應該保留 test set,因為它能抓出「模型只是背 train」這種尷尬情況。切資料時請固定 random seed,並把切分腳本或資料 commit 記錄下來;不然下週重跑時資料分布變了,你會以為是參數造成差異。

六. 先做資料檢查:壞資料比壞參數更常見
#

LoRA 微調最常見的問題不是 rank 設錯。 是資料髒。 例如:

  • label 拼錯
  • assistant 回答太長
  • 某類資料太少
  • 一筆 JSON 跨多行
  • user message 空白
  • system prompt 版本混在一起
  • train 和 test 有重複樣本 至少先做兩個快速檢查。第一,確認每一行都是合法 JSON:
python -c 'import json, pathlib; [json.loads(line) for line in pathlib.Path("data/train.jsonl").read_text().splitlines()]'

第二,寫一個十幾行的小腳本統計每個 label 數量,並確認最後一則 message 是 assistant。 如果這一步就爆炸,很好。代表你在花 GPU/神經引擎時間之前先抓到問題了。資料檢查是本機微調最省錢的步驟。

七. 第一輪訓練:先小步跑通
#

先不要一開始就跑幾千 step。 第一輪只確認流程:

mlx_lm.lora \
  --model "$MODEL" \
  --train \
  --data data \
  --adapter-path adapters/issue-triage-v1 \
  --iters 100 \
  --batch-size 1 \
  --num-layers 4

幾個參數先看懂:

  • --model:Hugging Face repo 或本機模型路徑
  • --train:進入訓練模式
  • --data:包含 train.jsonl 的資料夾
  • --adapter-path:adapter 輸出位置
  • --iters:訓練迭代數
  • --batch-size:每次餵幾筆
  • --num-layers:套 LoRA 的層數 這不是最佳參數。 這是「讓你不要第一步就炸記憶體」的參數。 跑完後應該會看到 adapter 相關檔案。
find adapters/issue-triage-v1 -maxdepth 1 -type f -print

你可以把 adapter 當作一個小補丁。 基礎模型還是原本那個模型。 adapter 只是訓練出來的額外權重。

八. prompt masking:只讓模型學答案
#

分類任務常常只想讓模型學 assistant 的回答。 如果 loss 把 system prompt、user prompt、assistant answer 全部算進去,模型可能花力氣學「重建提示詞」。 這時可以用:

mlx_lm.lora \
  --model "$MODEL" \
  --train \
  --data data \
  --adapter-path adapters/issue-triage-v1 \
  --iters 300 \
  --batch-size 1 \
  --num-layers 4 \
  --mask-prompt

--mask-prompt 的意思是忽略 prompt 部分的 loss,只針對 completion 算 loss。 對 chat dataset 來說,最後一個 assistant message 會被視為 completion。 這很適合:

  • 分類
  • 摘要
  • 固定格式輸出
  • 資料抽取
  • 短答案任務 但如果你正在做純 text continuation,那就不一定適合。

九. 用 YAML config 固定實驗參數
#

命令列很適合快速試。 但實驗一多,參數會變成一坨長蛇。 拍拍君建議把正式訓練寫成 config。 lora.yaml

model: "mlx-community/Qwen2.5-1.5B-Instruct-4bit"
train: true
data: "data"
adapter_path: "adapters/issue-triage-v1"
iters: 600
batch_size: 1
num_layers: 4
learning_rate: 0.0001
mask_prompt: true

執行:

mlx_lm.lora --config lora.yaml

這樣 commit 到 repo 時,你至少知道這次 adapter 是怎麼來的。 更完整一點,可以把 config 分版本:

configs/
├── issue-triage-v1.yaml
├── issue-triage-v2-more-data.yaml
└── issue-triage-v3-mask-prompt.yaml

模型微調不是「按一次就成功」。 它比較像實驗紀錄。 沒有 config 的微調,很快就會變成玄學。

十. 用 adapter 做生成測試
#

訓練完後,不要急著 fuse。 先直接載 adapter 測一筆:

mlx_lm.generate \
  --model "$MODEL" \
  --adapter-path adapters/issue-triage-v1 \
  --prompt "你是一個 issue triage assistant。只能輸出 bug、feature、question、other 其中一個標籤。\n\n使用者:匯入 CSV 後中文欄位變成亂碼。\n答案:"

你要觀察的不是它有沒有講得很漂亮。 你要觀察的是:

  • 是否只輸出一個標籤
  • 是否輸出合法標籤
  • 是否比 base model 更穩
  • 是否對同類問題有一致反應 如果模型開始輸出:
bug,因為這看起來像編碼問題,建議檢查...

那對聊天來說可能很貼心。 但對分類 pipeline 來說就是麻煩。 你可能需要:

  • 更多短答案資料
  • 更強的 system prompt
  • --mask-prompt
  • 生成後處理
  • 或改成 structured output 流程 LoRA 不是免驗證通行證。 它只是讓模型更容易往你要的方向偏。

十一. 建立小型 test runner
#

單筆 generate 看感覺不夠。做一個簡單 test runner 比較實際:讀 data/test.jsonl,把每筆 user message 丟給 base model 和 adapter model,取回第一個合法 label,再計算 accuracy 與 invalid output 比例。這個 runner 可以很粗,但它已經比肉眼看三筆好很多。真實專案可以再加:

  • confusion matrix
  • 每個 label 的 precision/recall
  • invalid output 比例
  • base model vs adapter 比較
  • 多次 sampling 的穩定度 如果你的任務是生成長文,不要只看 accuracy。 那時候要用人工抽查、規則檢查、LLM judge 或 domain-specific metric。 但入門階段,先有一個 test runner 就贏很多人了。

十二. fuse:什麼時候要把 adapter 合併進模型?
#

adapter 訓練完後,有兩種使用方式。 第一種是保留 base model + adapter:

mlx_lm.generate \
  --model "$MODEL" \
  --adapter-path adapters/issue-triage-v1 \
  --prompt "..."

第二種是 fuse 成一個模型:

mlx_lm.fuse \
  --model "$MODEL" \
  --adapter-path adapters/issue-triage-v1 \
  --save-path fused/issue-triage-v1

保留 adapter 的好處:

  • 多任務版本容易管理
  • adapter 檔案小
  • 可以快速切換
  • 實驗紀錄清楚 fuse 的好處:
  • 部署時少帶一組 adapter
  • 載入流程比較單純
  • 分享給別人比較直覺 拍拍君通常會這樣分:
  • 研究和迭代階段:保留 adapter
  • 固定版本要交付:才 fuse
  • 要上傳或轉格式:再考慮 fuse 不要每次訓練完就 fuse。 先確定 adapter 真的有價值。

十三. 記憶體不夠時,先調這幾個旋鈕
#

Apple Silicon 的 unified memory 很方便。 但不是無限。 如果訓練卡住、爆記憶體、速度慢到想泡咖啡,先調這些:

1. 降低 batch size
#

--batch-size 1

這是最直接的降記憶體方法。 缺點是訓練可能比較慢,梯度也比較吵。

2. 降低 LoRA 層數
#

--num-layers 4

預設可能會套比較多層。 入門實驗先少一點沒關係。 但層數太少,模型可調整的空間也會變小。

3. 使用 gradient accumulation
#

--batch-size 1 \
--grad-accumulation-steps 4

這可以用多次小 batch 累積出比較大的有效 batch。 記憶體比較省,但訓練時間會增加。

4. 打開 gradient checkpointing
#

--grad-checkpoint

這是用計算換記憶體。 中間結果不全部存起來,需要時再重算。 大模型或長序列時比較有幫助。

5. 縮短樣本長度
#

如果一筆訓練資料塞了超長上下文,記憶體會很痛。 對分類任務來說,通常不需要把整篇文件塞進去。 先做摘要、切片或只保留相關段落。 資料長度常常比你想像中更影響訓練成本。

十四. 常見坑:不是每個 loss 下降都值得開心
#

坑一:資料太少,模型只是背答案
#

如果 train loss 下降、test 表現沒變,可能只是背 train。 解法:

  • 增加資料
  • 去重
  • 保留更乾淨的 test set
  • 檢查 train/test 是否洩漏

坑二:標籤定義不清楚
#

例如 questionfeature 混在一起:

可以請問未來會支援 Excel 嗎?

這到底是 question 還是 feature? 如果人類都標不一致,模型也不會穩。 請先寫標註規則。 這比改 learning rate 有用。

坑三:把 LoRA 當知識庫
#

如果你想讓模型回答公司內部文件,LoRA 通常不是第一選擇。 RAG 更適合。 LoRA 比較適合學「怎麼回答」。 RAG 比較適合補「回答需要的資料」。

坑四:只看漂亮案例
#

每個微調模型都可以挑出幾個漂亮例子。 不要只看漂亮例子。 要固定 test set。 要看失敗案例。 要看 invalid output。 工程上的微調,不是 demo 影片。

坑五:忘記記錄 base model
#

adapter 不能脫離 base model 討論。 請記錄:

  • base model repo/path
  • MLX-LM 版本
  • adapter path
  • config
  • data commit
  • 訓練日期
  • 評測結果 不然兩週後你只會得到一個神秘資料夾,名字叫 final-final-v3-good。 拍拍君看到這種命名會沉默三秒,然後打開咖啡。

十五. 一個可維護的本機微調流程
#

把今天的內容整理成一條實務路線:

  1. 先用 base model 跑 batch inference。
  2. 收集錯誤案例。
  3. 定義清楚標籤或輸出格式。
  4. 建立 train/valid/test JSONL。
  5. 寫資料檢查腳本。
  6. 小模型、小 iters 跑通 LoRA。
  7. 用 fixed test runner 比較 base vs adapter。
  8. 改資料,而不是先亂改參數。
  9. adapter 穩定後才 fuse。
  10. 把 config、data、result 都記錄起來。 其中第 2 點很重要。 不要一開始就微調。 先用 base model 跑你的真實任務。 看它到底失敗在哪裡。 如果失敗是因為 prompt 太鬆,先改 prompt。 如果失敗是因為缺資料,先做 RAG。 如果失敗是因為格式、語氣、標籤規則不穩,再上 LoRA。 這樣比較像工程,不像許願。

十六. 什麼時候值得做第二版?
#

第一版 LoRA 跑完後,不要急著宣布成功。 先問幾個問題:

  • test set 是否真的沒看過?
  • base model 的同一套測試結果是多少?
  • adapter 是否減少 invalid output?
  • 哪些 label 進步,哪些退步?
  • 有沒有對某一類過度偏好?
  • 長一點的輸入是否變差?
  • 換一個 prompt 寫法是否還穩? 如果答案看起來不錯,再做第二版。 第二版可以改:
  • 增加資料量
  • 平衡 label 分布
  • 清掉模糊標註
  • 增加 hard negatives
  • 調整 num_layers
  • 調整 learning_rate
  • 增加 iters
  • 改用更合適的 base model 拍拍君會優先改資料。 因為小型 LoRA 任務裡,資料品質通常比參數更有影響。 你可以把它想成: 壞資料加好參數,還是壞。 好資料加普通參數,常常已經能用。

結語:LoRA 是窄任務的調音旋鈕
#

MLX-LM 讓 Apple Silicon 上的本機微調變得很親切。 但「能訓練」不等於「應該訓練」。 LoRA 最適合拿來處理窄任務:

  • 固定格式
  • 固定語氣
  • 固定分類
  • 固定抽取
  • 固定 domain pattern 如果你只是想讓模型知道一堆新資料,先看 RAG。 如果你只是想讓模型回答更短,先改 prompt。 如果你已經知道 base model 的錯誤模式,而且那些錯誤和格式或任務習慣有關,LoRA 就很值得試。 拍拍君建議你從小任務開始。 不要第一天就訓練夢想中的全能助手。 先訓練一個會穩定輸出四個標籤的小 adapter。 它不華麗,但它會教你整條微調流程真正長什麼樣子。 本機 AI 最有趣的地方不是「一次成功」。 而是你可以很快失敗、很快修資料、很快再跑一次。 這才是實驗工作流該有的樣子。

延伸閱讀
#

科技觀點 - 本文屬於一個選集。
§ 23: 本文

相關文章

MLX-LM 批次推論實戰:Prompt Template、抽樣參數與本機評測流程
·9 分鐘· loading · loading
Mlx MLX-LM LLM Batch Inference Apple-Silicon Local AI
MLX-LM 實戰:在 Apple Silicon 上跑本地模型推論
·9 分鐘· loading · loading
Mlx MLX-LM LLM Apple-Silicon Python Local AI
在 Mac/iPhone 生態跑本地 AI:Ollama、MLX 與行動端工作流
·9 分鐘· loading · loading
LLM Ollama Mlx Apple-Silicon IPhone Local AI
本地 AI App 架構:Streamlit、Ollama、MLX 怎麼分工
·10 分鐘· loading · loading
LLM Local AI Streamlit Ollama Mlx Python Architecture
MLX + Embeddings:在 Apple Silicon 上打造本地語意搜尋
·7 分鐘· loading · loading
Mlx Embeddings Semantic Search Apple-Silicon Python
Streamlit + Ollama:打造本地 LLM Chatbot App
·11 分鐘· loading · loading
LLM Ollama Streamlit Python Local AI Chatbot