QLoRA(4-bit 量化 base model + LoRA adapter)讓消費級硬體也能 fine-tune 7B-32B 模型、是 2026/5 本地 fine-tuning 的主流方法。「在本機 fine-tune 一個小 coding 模型懂我 codebase 的慣例」是個人 dev 的合理目標、特別是在「本地 RAG 不夠精準、prompt engineering 已到天花板」的場景。本篇用 QLoRA 把 fine-tuning 的最短路徑走完:環境準備、資料蒐集、訓練、evaluation、合併權重、部署到 Ollama / llama.cpp 配 VS Code Continue.dev。

本篇 framing 是「真實會跑、不只跑 demo」、所以包含:硬體預算估算、catastrophic forgetting 防護、evaluation 確認真的有提升、回退方案(fine-tune 失敗時怎麼辦)。

驗證日期:2026-05-12 環境:M4 Max 64GB + Hugging Face PEFT 0.13、或 5090 24GB + bitsandbytes 目標模型:Qwen3-Coder-7B-Instruct(fine-tune 後輸出符合自己 codebase 慣例的 code)

為什麼這個議題重要

寫 code 場景的常見 fine-tune 動機:

  1. 私有 codebase 慣例:自家專案有特殊 naming、特殊 design pattern、prompt engineering 拉不到、希望模型「自然知道」
  2. 特殊框架 / library:用 obscure 的內部 framework、通用模型沒看過、補完品質差
  3. 特定文檔風格:commit message、PR description、code comment 有 team-specific 格式
  4. Reduce RAG dependence:把高頻 knowledge 編進模型權重、減少每次 query 都要 retrieve

不該 fine-tune的情境(先排除):

  1. 新增世界知識:fine-tune 不擅長加新事實、用 RAG 即可
  2. 複雜 reasoning 能力:fine-tune 一般不會讓模型變更會 reason、reasoning 來自 pre-training + RL
  3. 改善通用對話品質:通用對話品質取決於 RLHF、fine-tune 多半會 catastrophic forgetting
  4. 資料太少(< 500 對):fine-tune 收益低、不如優化 prompt + RAG

整體流程

11. 硬體預算估算       → 知道能跑哪個 size 的 base model
22. 蒐集 fine-tune 資料 → 50-5000 對 (prompt, response)
33. 環境準備           → Python + bitsandbytes / PEFT / transformers
44. 跑 QLoRA 訓練      → 1-3 epochs、看 loss 趨勢
55. Evaluation         → 在 held-out set + 通用 benchmark 都跑
66. Merge LoRA → base  → 得到合併權重 .safetensors
77. Convert → GGUF     → 用 llama.cpp convert 工具
88. Deploy 到 Ollama   → ollama create my-coder -f Modelfile
99. 配 Continue.dev    → config.json 加新 provider

Step 1:硬體預算估算

QLoRA 訓練的記憶體需求(粗略估算):

 1記憶體 ≈ N (B 參數) × 0.6 GB     ← 訓練時
 2        ≈ N (B 參數) × 0.3 GB     ← 推論(4-bit)
 3
 4Apple Silicon Mac:
 5  M4 Pro 24GB → 訓 7B 可、訓 14B 緊
 6  M4 Pro 36GB → 訓 7B 寬鬆、訓 14B 可
 7  M4 Max 64GB+ → 訓 30B 可、推論 70B 可
 8
 9PC 獨立 GPU:
10  RTX 4090 / 5090 24GB → 訓 7B 寬鬆、訓 14B / 30B with `--n-cpu-moe` 可
11  RTX A6000 48GB → 訓 30-32B 寬鬆

事實查核註:Apple Silicon 上的 QLoRA 支援度跟 bitsandbytes / MLX 工具鏈版本相關、2026/5 主流是用 MLX 自己的 LoRA 實作(mlx-lm)、CUDA 路線用 transformers + bitsandbytes + PEFT。具體支援度以對應 release 為準。

本篇假設 fine-tune Qwen3-Coder-7B、所以 24GB+ Mac 或 16GB+ GPU 都能跑。

Step 2:蒐集 fine-tune 資料

最關鍵的 step。資料品質決定 fine-tune 成敗。

資料格式(典型 SFT format)

1[
2  {
3    "instruction": "用我們 codebase 的慣例寫一個 REST endpoint 處理 user signup",
4    "input": "需求:accept email + password、回 JWT",
5    "output": "// 完整符合我們慣例的 code..."
6  },
7  ...
8]

或對話格式(ChatML):

1[
2  {
3    "messages": [
4      {"role": "system", "content": "你是我們 codebase 的 coding assistant"},
5      {"role": "user", "content": "..."},
6      {"role": "assistant", "content": "..."}
7    ]
8  }
9]

資料來源

來源取得方式品質
過往 commit 的「good code」從 main branch 抽函式 + git log message中(人工挑)
Code review 通過的 PR diff從 GitHub API 抽 merged PR
內部 wiki 跟 design docs轉成 Q&A 對
Synthetic data:用大模型生給雲端旗艦 prompt「以這個 codebase 風格寫 X」中(要 review)
Pair programming 紀錄自己跟 IDE 互動的 log高(最貼近真實使用)

資料量門檻

資料量預期效果
< 50 對通常無感、不如優化 prompt + RAG
50-500 對開始有 in-domain 效果、但易 forgetting
500-5000 對顯著效果、QLoRA fine-tune 甜蜜點
5000+ 對邊際收益遞減、開始接近 full fine-tune 效果

資料 mixing(防 catastrophic forgetting

訓練 batch 內 mix 通用資料、避免 fine-tune 把通用能力洗掉:

180% in-domain data(你的 codebase 範例)
220% 通用 instruction data(如 Alpaca、ShareGPT subset)

通用 data 可從 Hugging Face datasets 抓(如 tatsu-lab/alpacateknium/OpenHermes-2.5)。

Step 3:環境準備

Apple Silicon Mac(用 MLX)

1# MLX 是 Apple 的 ML framework、原生支援 Apple Silicon
2pip install mlx mlx-lm
3
4# 或用 conda(推薦)
5conda create -n llm-ft python=3.11
6conda activate llm-ft
7pip install mlx-lm

PC(CUDA + transformers + bitsandbytes)

1# 安裝 CUDA 12.x(依 GPU 驅動)
2
3# Python 套件
4pip install torch transformers peft bitsandbytes accelerate datasets trl

Step 4:跑 QLoRA 訓練

Apple Silicon(MLX)方式

 1# 把 base model 下載到本機
 2huggingface-cli download Qwen/Qwen3-Coder-7B-Instruct \
 3  --local-dir ~/models/qwen3-coder-7b
 4
 5# 把資料整理成 JSONL(一行一筆)
 6# data/train.jsonl、data/valid.jsonl
 7
 8# 跑 LoRA fine-tune(MLX 內建 4-bit)
 9mlx_lm.lora \
10  --train \
11  --model ~/models/qwen3-coder-7b \
12  --data data/ \
13  --batch-size 4 \
14  --lora-layers 16 \
15  --iters 1000 \
16  --learning-rate 1e-4 \
17  --steps-per-eval 100 \
18  --adapter-path ./adapters

PC(CUDA)方式

 1# train.py(簡化版)
 2from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, BitsAndBytesConfig
 3from peft import LoraConfig, get_peft_model
 4from trl import SFTTrainer
 5from datasets import load_dataset
 6
 7# 4-bit 量化載入 base
 8bnb_config = BitsAndBytesConfig(
 9    load_in_4bit=True,
10    bnb_4bit_quant_type="nf4",
11    bnb_4bit_compute_dtype="bfloat16",
12)
13model = AutoModelForCausalLM.from_pretrained(
14    "Qwen/Qwen3-Coder-7B-Instruct",
15    quantization_config=bnb_config,
16)
17
18# LoRA 配置
19lora_config = LoraConfig(
20    r=16,
21    lora_alpha=32,
22    target_modules=["q_proj", "v_proj"],
23    lora_dropout=0.05,
24    task_type="CAUSAL_LM",
25)
26model = get_peft_model(model, lora_config)
27
28# 資料
29dataset = load_dataset("json", data_files="data/train.jsonl")
30
31# 訓練
32training_args = TrainingArguments(
33    output_dir="./checkpoints",
34    learning_rate=1e-4,
35    num_train_epochs=2,
36    per_device_train_batch_size=4,
37    gradient_accumulation_steps=4,
38    save_steps=200,
39    logging_steps=20,
40    optim="paged_adamw_8bit",
41    bf16=True,
42)
43trainer = SFTTrainer(
44    model=model,
45    args=training_args,
46    train_dataset=dataset["train"],
47    max_seq_length=2048,
48)
49trainer.train()
50trainer.save_model("./adapters")

關鍵超參數的判讀邏輯:

參數預設怎麼調
r(LoRA rank)16小 dataset(< 1000 對)可降到 8、大 dataset 升到 32 / 64
lora_alpha32(通常 = 2 × r)增大會放大 LoRA 影響、太大易 catastrophic forgetting
target_modulesq_proj, v_proj8B+ 模型可加 k_proj + o_proj 提品質、加 ffn 是進階
lora_dropout0.05dataset 小時加大(0.1)防 overfit
num_train_epochs21-3 是常見範圍、看 validation loss 何時開始升
per_device_train_batch_size4視 GPU 記憶體;不夠用 gradient_accumulation_steps
learning_rate1e-4LoRA 適合較大 lr(vs full fine-tune 的 1e-5)、初值可 1e-4 ~ 5e-4

看 training loss 趨勢

訓練過程中、loss 應該:

 1Initial:~2.5(cross-entropy on next-token)
 21/4 訓練:降到 ~1.5
 31/2 訓練:降到 ~1.0
 43/4 訓練:降到 ~0.7
 5末段:穩定在 ~0.5
 6
 7警示訊號:
 8- Loss 不降(≈ 2.0+ 持平) → lr 太小、或資料品質差、或 base 跟資料分佈完全不合
 9- Loss 降到 < 0.1 → over-fit、validation loss 應該已升、stop training
10- Loss 出 NaN → lr 太大、降 lr 重來

Step 5:Evaluation

訓練完不能只看 training loss、要實測:

1. Held-out test set(你自己的 in-domain 資料)

1# 拿 valid.jsonl 跑、看模型輸出 vs expected
2# 用 BLEU / ROUGE / 或 LLM-as-judge 評分
3mlx_lm.generate \
4  --model ~/models/qwen3-coder-7b \
5  --adapter ./adapters \
6  --prompt "<test prompt from valid.jsonl>"

2. 通用 benchmark(防 catastrophic forgetting)

跑通用 HumanEval、看分數有沒有崩:

1# 用 lm-evaluation-harness
2git clone https://github.com/EleutherAI/lm-evaluation-harness
3cd lm-evaluation-harness
4pip install -e .
5
6lm_eval --model hf \
7  --model_args pretrained=~/models/qwen3-coder-7b,peft=./adapters \
8  --tasks humaneval \
9  --batch_size 8

判讀:

  • HumanEval 從 75% → 75%:通用能力保留、in-domain 提升、成功
  • HumanEval 從 75% → 55%:catastrophic forgetting、要重新 fine-tune(用 LoRA + 資料 mixing 加強)

3. 自己工作流測試(最重要)

實際在 Continue.dev 用幾天、看:

  • In-domain 任務輸出是否確實貼近 codebase 慣例
  • 通用 coding 任務(如「寫一個 helper function」)是否仍 OK
  • 對話流暢度有沒有變差
  • 出現怪行為的頻率

Step 6:合併 LoRA 跟 base model

訓練完得到 adapter(小檔、< 100MB)。要用於日常推論、通常 merge 進 base:

 1# MLX 方式
 2mlx_lm.fuse \
 3  --model ~/models/qwen3-coder-7b \
 4  --adapter-path ./adapters \
 5  --save-path ~/models/qwen3-coder-7b-mycodebase
 6
 7# PEFT 方式
 8python -c "
 9from peft import AutoPeftModelForCausalLM
10import torch
11
12model = AutoPeftModelForCausalLM.from_pretrained('./adapters', torch_dtype=torch.bfloat16)
13merged = model.merge_and_unload()
14merged.save_pretrained('./merged-model')
15"

Step 7:Convert 成 GGUF(給 Ollama / llama.cpp 用)

 1# 安裝 llama.cpp
 2git clone https://github.com/ggml-org/llama.cpp
 3cd llama.cpp
 4pip install -r requirements.txt
 5
 6# Convert HF → GGUF
 7python convert_hf_to_gguf.py ~/models/qwen3-coder-7b-mycodebase \
 8  --outfile ~/models/qwen3-coder-7b-mycodebase.gguf
 9
10# 量化(可選、Q4_K_M 是甜蜜點)
11./llama-quantize \
12  ~/models/qwen3-coder-7b-mycodebase.gguf \
13  ~/models/qwen3-coder-7b-mycodebase-Q4_K_M.gguf \
14  Q4_K_M

Step 8:Deploy 到 Ollama

 1# 寫 Modelfile
 2cat > ~/models/Modelfile-mycodebase <<EOF
 3FROM ~/models/qwen3-coder-7b-mycodebase-Q4_K_M.gguf
 4
 5TEMPLATE """<|im_start|>system
 6{{ .System }}<|im_end|>
 7<|im_start|>user
 8{{ .Prompt }}<|im_end|>
 9<|im_start|>assistant
10"""
11
12PARAMETER temperature 0.3
13PARAMETER top_p 0.9
14PARAMETER num_ctx 32768
15EOF
16
17# 註冊到 Ollama
18ollama create mycodebase-coder -f ~/models/Modelfile-mycodebase
19
20# 測試
21ollama run mycodebase-coder "寫一個 user signup endpoint"

Step 9:配 Continue.dev

 1// ~/.continue/config.json 加:
 2{
 3  "models": [
 4    {
 5      "title": "My Codebase Coder",
 6      "provider": "ollama",
 7      "model": "mycodebase-coder",
 8      "apiBase": "http://localhost:11434"
 9    },
10    // ... 既有 models
11  ]
12}

VS Code restart 後、Continue panel 下拉就能切換。

失敗模式跟回退

失敗 1:訓練 loss 不降

可能原因:

  • 資料品質差 → 人工 review 50 對、看 instruction-response 是否真有對應
  • 資料 token 太短 → 多數 < 100 token、模型學不到複雜 pattern
  • lr 太小 → 試 lr 5e-4

回退:把資料品質提升、或放棄 fine-tune 用 RAG。

失敗 2:HumanEval 大幅下降(catastrophic forgetting)

緩解:

  • 加入 20% 通用 data mixing、重訓
  • 降低 epochs(從 3 → 1)
  • 降低 LoRA rank(從 16 → 8)

失敗 3:In-domain test 進步、但日常用感覺沒變

可能原因:

  • Test set 跟真實工作流分佈不符
  • Prompt template 在訓練跟推論不一致

緩解:實際在 Continue.dev 跑 1-2 週、看真實效果再判斷。

失敗 4:訓練爆 OOM

緩解:

  • 降 batch size(4 → 2 → 1)
  • 加 gradient_accumulation_steps(保持 effective batch size)
  • 用更小的 LoRA rank
  • 換更小的 base model(7B → 3B)

何時不該繼續 fine-tune 路線

跑完一次 fine-tune 評估後、若:

  1. In-domain 提升 < 10%:相對成本(時間 + 維護)不划算、用 RAG
  2. Catastrophic forgetting > 10%:跟其他能力 trade-off 不值得
  3. 資料量不夠(< 500 對):RAG 比 fine-tune 更有效
  4. 工作流變化快(codebase 慣例每月變):fine-tune 過時得快、RAG 更靈活

跟其他模組的關係