Google Colab で Gemma 4 のファインチューニングができるのは知っていました。ただ Colab には制約があります。無料枠のランタイムは途中で切れるし、Pro 版でもセッション時間は限られています。手元の Mac で動かせたら、深夜に走らせたまま寝られるのに——そう思って試したのが Apple Silicon + MLX の組み合わせです。
M3 Max(96GB ユニファイドメモリ)で実際にファインチューニングを走らせてみたので、その手順と感触をそのままお伝えします。個人開発の立場では、クラウドの固定費を増やさずに学習の実験環境を手元に持てること自体が、静かに大きな変化です。後半では、学習したアダプタを Ollama 経由で Antigravity から使うところまでつなげます。
なぜ MLX なのか
MLX は Apple が公開した機械学習フレームワークで、Apple Silicon の ユニファイドメモリアーキテクチャを直接活用します。GPU と CPU がメモリを共有するため、96GB 搭載の M3 Max なら、量子化なしの Gemma 4(27B)もギリギリ動きます。
PyTorch + MPS(Metal Performance Shaders)という選択肢もありましたが、MLX の方がメモリ効率が良く、ファインチューニング用の専用ライブラリ mlx-lm が使いやすかったので、今回は MLX を選びました。
なお、M1/M2/M3 の Pro 以下(ユニファイドメモリ 32GB 未満)の場合は、Gemma 4 の 4bit 量子化版(約 14GB)を使えば動作します。それ以上のモデルサイズは Colab の方が現実的です。
ユニファイドメモリ別の現実的な構成目安
手元の Mac でどこまでできるかは、ほぼユニファイドメモリ容量で決まります。私が試した範囲と、MLX コミュニティで報告されている構成を合わせると、目安は次の通りです。
- 96GB 以上(M3 Max / M4 Max 上位): 27B の 8bit 量子化 + LoRA が安定圏。
--batch-size 4/--lora-layers 16から始められます - 64GB: 27B の 4bit 量子化 + LoRA。
--batch-size 2に落とすとスワップを避けやすくなります - 32〜36GB: 12B クラスの 4bit 量子化が現実的。27B は推論はできても学習中の勾配でメモリが尽きます
- 16〜24GB: 4B クラスで実験するか、データセット整備だけ Mac で行い学習は Colab に任せる構成が無難です
迷ったら「モデルの量子化サイズ × 2 + 8GB」くらいを学習時の必要メモリとして見積もると、大きく外れません。LoRA はベースモデルを凍結したまま小さなアダプタ行列だけを学習するので、フルファインチューニングよりはるかに軽いのですが、それでも勾配と optimizer state の分は確実に上乗せされます。
量子化のビット数でどこまで品質が変わるのかが気になる方は、Gemma 4 を Antigravity で動かすとき、Q4 と Q5 のどちらを選ぶか — M2 Mac で実測した3軸の結果もあわせてどうぞ。推論側の比較記事ですが、量子化がどこで効いてくるかの感覚はファインチューニングでも共通です。
環境構築
# Homebrew がない場合はインストール
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Python 3.11 以上を推奨
brew install python@3.11
# 仮想環境を作成
python3.11 -m venv gemma4-mlx-env
source gemma4-mlx-env/bin/activate
# MLX 関連ライブラリのインストール
pip install mlx-lm
pip install huggingface-hub datasets
# Hugging Face にログイン(Gemma 4 のダウンロードに必要)
huggingface-cli loginGemma 4 は huggingface.co にアクセスして利用規約に同意しておく必要があります。未同意だとダウンロードで 401 エラーが出ます。
データセットの準備
学習データは Alpaca 形式(instruction / input / output)を使います。自前のデータを使う場合は同じ形式で JSON を用意します。
# custom_dataset.json の例
[
{
"instruction": "このコードのバグを指摘してください",
"input": "def add(a, b):\n return a - b",
"output": "演算子が誤っています。`a - b` ではなく `a + b` が正しいです。"
},
{
"instruction": "次のコードを Python 3 スタイルに書き換えてください",
"input": "print 'Hello, World!'",
"output": "print('Hello, World!')"
}
]# データセットをMLX用に変換するスクリプト
import json
from pathlib import Path
def convert_to_mlx_format(input_file: str, output_file: str):
"""Alpaca形式のJSONをMLX training形式に変換"""
with open(input_file, "r", encoding="utf-8") as f:
data = json.load(f)
converted = []
for item in data:
prompt = f"<start_of_turn>user\n{item['instruction']}"
if item.get("input"):
prompt += f"\n\n{item['input']}"
prompt += "<end_of_turn>\n<start_of_turn>model\n"
response = item["output"] + "<end_of_turn>"
converted.append({"text": prompt + response})
# train/valid 分割(90/10)
split = int(len(converted) * 0.9)
train_data = converted[:split]
valid_data = converted[split:]
output_dir = Path(output_file).parent
output_dir.mkdir(exist_ok=True)
with open(f"{output_dir}/train.jsonl", "w") as f:
for item in train_data:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
with open(f"{output_dir}/valid.jsonl", "w") as f:
for item in valid_data:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
print(f"変換完了: train={len(train_data)}件, valid={len(valid_data)}件")
convert_to_mlx_format("custom_dataset.json", "data/train.jsonl")ここで一つ、私が最初にやってしまった失敗を共有します。Gemma 系のチャットテンプレート(<start_of_turn> / <end_of_turn>)を付けずに素のテキストで学習させると、loss は下がるのに推論で会話が崩れます。ベースが instruction-tuned モデル(-it)の場合、学習データも必ず同じテンプレートに揃えてください。
LoRA ファインチューニングの実行
# モデルのダウンロード(初回のみ、約 14GB)
python -c "
from huggingface_hub import snapshot_download
snapshot_download('google/gemma-4-27b-it', local_dir='./gemma-4-27b-it')
"
# LoRA ファインチューニング開始
mlx_lm.lora \
--model ./gemma-4-27b-it \
--train \
--data ./data \
--iters 1000 \
--batch-size 4 \
--lora-layers 16 \
--learning-rate 1e-4 \
--adapter-path ./adapters \
--save-every 100M3 Max で 1,000 イテレーション走らせると約 40 分かかりました。Colab の Pro+ で A100 を使った場合と比べると 1.5〜2 倍の時間ですが、途切れる心配がないのは大きなメリットです。
--save-every 100 を付けておくと 100 イテレーションごとにアダプタのチェックポイントが保存されます。深夜に回す運用では、万一途中で落ちても最後のチェックポイントから --resume-adapter-file で再開できるので、必ず付けておくことをお勧めします。
学習中のメモリと所要時間の目安(M3 Max / 96GB)
同じ 1,000 イテレーションでも、量子化とバッチサイズで所要時間もメモリの余裕もかなり変わります。M3 Max(96GB)で私が回した範囲の、おおよその実測レンジをまとめておきます。手元の一台での傾向値ですが、構成を決める最初の当たりには十分使えるはずです。
| 構成 | 1,000 iters の所要時間 | ピークメモリ | メモリプレッシャー |
|---|---|---|---|
| 27B / 8bit / batch 4 / lora-layers 16 | 約 40 分 | 約 58〜62GB | 黄に触れることがある |
| 27B / 8bit / batch 2 / lora-layers 16 | 約 55 分 | 約 40〜44GB | 緑を維持しやすい |
| 27B / 4bit / batch 4 / lora-layers 16 | 約 28 分 | 約 28〜32GB | 緑で安定 |
読み取れるのは「batch-size を倍にしても所要時間は半分にはならない」という点です。8bit の batch 4 と batch 2 で所要時間の差が 1.4 倍程度に収まるのは、Apple Silicon ではメモリ帯域がボトルネックになりやすく、バッチを増やしても計算が線形には詰まらないためだと考えています。裏を返せば、メモリに余裕がなければ batch 2 に落としても時間的な損は思ったほど大きくありません。まずスワップを避けることを優先した方が、結果的に早く終わります。
学習率とイテレーション数の現実的な決め方
公式サンプルの --learning-rate 1e-4 は、データが数千件ある前提の値だと考えた方が安全です。数百件規模のデータで 1e-4 のまま回したところ、loss の下がり方が急すぎて、推論時に学習データの言い回しをそのまま繰り返す「暗記」に寄りました。5e-5 まで落とすと、表現の自然さを保ったまま狙ったスタイルに寄っていく感触が得られています。迷ったら 5e-5 から始めて、loss の動きが鈍すぎる場合にだけ上げる、という順番が安全です。
学習率ごとの挙動を、同じ 300 件のデータで観察した範囲で整理すると次の通りです。
| 学習率 | valid loss の下がり方 | 推論時の傾向 |
|---|---|---|
| 1e-4 | 急に下がるが早い段階で頭打ち | 学習データの言い回しをそのまま反復(暗記寄り) |
| 5e-5 | 緩やかに、長く下がり続ける | スタイルは寄るが表現の自然さを保つ |
| 2e-5 | 下がりが鈍く収束に時間がかかる | 変化は控えめ。データが多いときの微調整向き |
イテレーション数は「データ件数 × 2〜3 エポック相当 ÷ バッチサイズ」を起点にしています。例えば 300 件のデータで --batch-size 4 なら、1 エポックが約 75 イテレーション。150〜225 イテレーションから様子を見るのが現実的で、最初から 1,000 回す必要はありません。
判断のよりどころは valid loss です。チェックポイントごとに表示される valid loss が下がり止まったら、それ以上回しても暗記が進むだけです。train loss だけを見ていると「順調に下がっている」ように見えてしまうのが落とし穴で、私自身、最初の数回はこれで無駄に長く回していました。
LoRA の rank と scale をどう決めるか
--lora-layers は「モデルの何層にアダプタを差すか」を決める指定で、アダプタ自体の表現力を決めるのは rank です。コマンドラインの引数だけで回していると、この rank が既定値のまま固定されていることに気づきにくく、私も途中まで見落としていました。
MLX では細かいハイパーパラメータを YAML の設定ファイルにまとめて渡せます。実験を重ねるなら、こちらに移した方が再現性が上がります。
# lora_config.yaml
model: "./gemma-4-27b-it"
train: true
data: "./data"
adapter_path: "./adapters"
iters: 300
batch_size: 2
learning_rate: 5e-5
save_every: 100
lora_parameters:
keys: ["self_attn.q_proj", "self_attn.v_proj"]
rank: 16
scale: 20.0
dropout: 0.05# 設定ファイルを指定して実行(コマンドライン引数は設定ファイルを上書きします)
caffeinate -i mlx_lm.lora --config lora_config.yaml 2>&1 | tee train_log.txtここで一つ、他のフレームワークから来た方がつまずくところがあります。Hugging Face の PEFT では lora_alpha を指定しますが、MLX の lora_parameters にあるのは scale です。名前が違うだけでなく、PEFT の実装が内部で alpha / rank を掛けるのに対し、MLX の scale はそのまま倍率として使われます。PEFT の設定をそのまま数値だけ移植すると、意図した強さの数倍で学習が進んでしまいます。移植するなら scale ≒ lora_alpha / rank と読み替えるのが出発点です。
rank をいくつにするかは、学習させたいものの性質で変えています。同じ 300 件のデータで rank だけを振ってみた範囲では、次のような傾向がありました。
| rank | アダプタのファイルサイズ | 向いている用途 |
|---|---|---|
| 4〜8 | 数 MB 台 | 語尾や出力フォーマットなど、文体だけを寄せたいとき |
| 16 | 10〜20MB 前後 | 社内の書き方や独自ライブラリの使い方まで覚えさせたいとき |
| 32 以上 | 数十 MB | データが数千件ある場合。数百件だと過学習が早い |
数百件のデータで rank 32 を試したときは、valid loss が 150 イテレーションあたりから上向きに転じました。表現力が上がった分だけデータを覚え込むのが早く、汎化が先に頭打ちになった形です。データ件数が三桁のうちは rank 8〜16 に留めておき、rank を上げる前にまずデータを増やす。この順番の方が、結果的に遠回りが少なく済みます。
keys に何を指定するかも効きます。既定の q_proj / v_proj は最も軽い構成で、出力の癖を寄せる用途ならこれで足ります。知識寄りの内容を入れたい場合は mlp.gate_proj などを足す選択もありますが、メモリと所要時間は素直に増えます。まず既定で 1 サイクル通してから広げる方が、どの変更が効いたのかを切り分けられます。
深夜に回すための Mac 側の設定
Colab に対する Mac の最大の利点は「寝ている間に回せる」ことです。ただし無防備に走らせると、朝起きたらスリープで止まっていた、ということが起きます。私は次の形に落ち着きました。
# スリープを抑止したまま学習を実行(プロセス終了で抑止も自動解除)
caffeinate -i mlx_lm.lora \
--model ./gemma-4-27b-it \
--train \
--data ./data \
--iters 1000 \
--batch-size 4 \
--lora-layers 16 \
--learning-rate 5e-5 \
--adapter-path ./adapters \
--save-every 100 2>&1 | tee train_log.txtcaffeinate -iは、コマンドが生きている間だけアイドルスリープを抑止します。システム設定を変えないので、戻し忘れがありません。ディスプレイは消えても構いません。止めたいのはシステムスリープだけです- 必ず電源アダプタに接続します。バッテリー駆動では macOS が省電力側に倒れ、学習速度が安定しません
teeでログをファイルに残しておくと、朝に loss の推移を最初から確認できます。ターミナルのスクロールバックだけに頼ると、途中経過が流れて消えます- 再起動を伴う macOS の自動アップデートは、学習を回す晩だけ一時的にオフにしておくと安心です
長時間回したときに効いてくる熱と実効速度
夜通しの学習で意外だったのが、時間の経過とともに速度が少しずつ落ちることでした。M3 Max で同じ設定を回したとき、開始 10 分時点と 3 時間経過時点の iters/sec を比べると 1 割弱ほど遅くなっています。数字としては小さいのですが、所要時間を最初の 100 イテレーションから線形に見積もると、実際より 5〜10% 短く出ます。夜中に終わる想定が朝までずれ込む原因は、たいていここです。
温度の推移は次のコマンドで確認できます。sudo が必要なので、長時間の観測は別ターミナルで走らせておくのが楽です。
# SMC センサーを 5 秒間隔でサンプリング(Ctrl-C で終了)
sudo powermetrics --samplers smc -i 5000 | grep -i -E "temp|fan"やっている対策は地味なものばかりです。ノート型なら閉じたまま外部ディスプレイで使うより、開いた状態で底面に空間をつくった方が安定しました。電源アダプタは必ず接続し、他の重いアプリは終了しておきます。そして何より、--save-every を付けておけば、仮に熱で不安定になって落ちても、そこまでの学習は失われません。
再開はチェックポイントのファイルを指定するだけです。
# 直近のアダプタから学習を再開する
caffeinate -i mlx_lm.lora \
--model ./gemma-4-27b-it \
--train \
--data ./data \
--iters 400 \
--batch-size 2 \
--lora-layers 16 \
--learning-rate 5e-5 \
--adapter-path ./adapters \
--resume-adapter-file ./adapters/adapters.safetensors--iters は再開後に追加で回す回数として指定します。累計回数ではない点に注意してください。私は一度ここを勘違いして、意図の倍のイテレーションを回してしまいました。
学習結果を試す
from mlx_lm import load, generate
# ベースモデル + アダプターを読み込む
model, tokenizer = load(
"./gemma-4-27b-it",
adapter_path="./adapters"
)
prompt = """<start_of_turn>user
このPythonコードのバグを指摘してください
def calculate_average(numbers):
total = sum(numbers)
return total / len(numbers)
<end_of_turn>
<start_of_turn>model
"""
response = generate(
model,
tokenizer,
prompt=prompt,
max_tokens=512,
temp=0.7
)
print(response)確認のコツは、学習データに含めた質問の「言い換え」を投げることです。学習データそのままの質問に答えられるのは当然なので、表現を変えても意図した応答スタイルが維持されるかで汎化を判断します。数百件規模の小さいデータセットでは過学習が起きやすく、valid loss が途中から上がり始めたら --iters を減らすか、データを増やすサインです。
学習の効果を測る — ベースモデルと並べて確認する
推論を数回試して「良くなった気がする」で終わってしまうのは、小規模なファインチューニングで最も起きやすい失敗だと感じています。学習前後を同じ条件で並べないと、変化がアダプタによるものなのか、単に温度パラメータのゆらぎなのかが区別できません。
そこで、学習データに含めていないプロンプトを 10 件ほど用意して、ベースモデルと学習済みモデルの応答を並べて書き出すスクリプトを挟むようにしました。
# eval_ab.py — 同じプロンプトでベースと学習済みの応答を並べる
import json
from mlx_lm import load, generate
HELD_OUT = [
"この関数の戻り値がおかしい理由を指摘してください\n\ndef ratio(a, b):\n return a / b",
"次のコードをリスト内包表記に書き換えてください\n\nout = []\nfor x in xs:\n out.append(x * 2)",
]
def build_prompt(question: str) -> str:
return f"<start_of_turn>user\n{question}<end_of_turn>\n<start_of_turn>model\n"
def run(model_path: str, adapter_path):
model, tokenizer = load(model_path, adapter_path=adapter_path)
return [
# temp=0.0 で固定しないと、差がゆらぎに埋もれます
generate(model, tokenizer, prompt=build_prompt(q), max_tokens=384, temp=0.0)
for q in HELD_OUT
]
base = run("./gemma-4-27b-it", None)
tuned = run("./gemma-4-27b-it", "./adapters")
rows = [
{"prompt": q, "base": b, "tuned": t, "base_len": len(b), "tuned_len": len(t)}
for q, b, t in zip(HELD_OUT, base, tuned)
]
with open("ab_result.json", "w", encoding="utf-8") as f:
json.dump(rows, f, ensure_ascii=False, indent=2)
for r in rows:
print(f"{r['prompt'][:20]}... base={r['base_len']}字 / tuned={r['tuned_len']}字")温度を 0.0 に固定しているのが要点です。生成のたびに文言が変わる状態で比較しても、良し悪しの判断が揺れます。出力を JSON に残しておけば、翌朝あらためて読み返せますし、設定を変えて回した回どうしの比較もできます。
見るべきは次の 3 点だと考えています。
| 観点 | 確認すること | 悪化していたときの対処 |
|---|---|---|
| スタイルの一致 | 語尾・構成・説明の粒度が狙い通りに寄っているか | 寄っていなければ学習率を上げるか、イテレーションを増やす |
| 事実の維持 | ベースでは正しかった内容が壊れていないか | 壊れていれば過学習。イテレーションを減らすかデータを増やす |
| 長さの変化 | tuned 側だけ極端に長い/短いものが混ざっていないか | 極端に長い場合は学習データの <end_of_turn> 欠落を疑う |
3 点目は、私が実際に引っかかったところです。応答が延々と続いて止まらなくなったとき、最初は過学習を疑ったのですが、原因は変換スクリプトの分岐で一部のレコードだけ終端タグが抜けていたことでした。学習が終わってから気づくと丸ごと回し直しになるので、train.jsonl の末尾数行を目視で確認する手間は惜しまない方がよいです。
用途別にアダプタを複数持つ — fuse しない運用
fuse はモデル全体を書き出す操作なので、27B なら 1 回で数十 GB が増えます。用途ごとに fuse していると、ディスクがあっという間に埋まります。
実際に運用してみて落ち着いたのは、fuse するのは配布や Ollama 登録の直前だけにして、普段はアダプタのまま切り替える形でした。アダプタは rank 16 で 10〜20MB 程度しかないので、用途別に何本持っても負担になりません。
adapters/
├── code-review/ # コードレビューの指摘スタイルを学習
├── commit-message/ # コミットメッセージの書式を学習
└── doc-ja/ # ドキュメントの日本語文体を学習
読み込み時に adapter_path を差し替えるだけで切り替わります。ベースモデルは共通なので、ディスク上の実体は 1 つのままです。
# switch_adapter.py — 同じベースモデルにアダプタを差し替えて試す
import sys
from mlx_lm import load, generate
ADAPTERS = {
"code-review": "./adapters/code-review",
"commit-message": "./adapters/commit-message",
"doc-ja": "./adapters/doc-ja",
"base": None, # アダプタなし(比較用)
}
name = sys.argv[1] if len(sys.argv) > 1 else "base"
question = sys.argv[2] if len(sys.argv) > 2 else "この差分のコミットメッセージを書いてください"
model, tokenizer = load("./gemma-4-27b-it", adapter_path=ADAPTERS[name])
prompt = f"<start_of_turn>user\n{question}<end_of_turn>\n<start_of_turn>model\n"
print(generate(model, tokenizer, prompt=prompt, max_tokens=384, temp=0.0))python switch_adapter.py code-review "このコードの問題点を指摘してください"
python switch_adapter.py base "このコードの問題点を指摘してください"同じ質問をアダプタ名だけ変えて投げられるので、前節の効果測定がそのまま日常の確認作業になります。学習を回すたびに新しいディレクトリへ保存しておけば、rank16-300iter と rank8-500iter のような命名で世代を並べて比較することもできます。
注意点は 2 つあります。1 つは、アダプタは学習時のベースモデルと組でしか使えないことです。ベースを 8bit から 4bit に入れ替えると読み込みで落ちます。ディレクトリ名にベースの量子化を含めておくと、後から取り違えずに済みます。もう 1 つは、複数のアダプタを同時に重ねることはできない点です。文体と知識を両方入れたい場合は、アダプタを分けるのではなく、学習データの側で混ぜる必要があります。
アダプタを統合して Ollama / Antigravity から使う
学習したアダプタは fuse(統合)すると、単体のモデルとして配布・利用できるようになります。
# LoRA アダプタをベースモデルに統合
mlx_lm.fuse \
--model ./gemma-4-27b-it \
--adapter-path ./adapters \
--save-path ./gemma-4-27b-custom統合後のモデルは mlx_lm.generate で直接使えるほか、llama.cpp の変換スクリプトで GGUF に変換すれば Ollama に載せられます。Ollama に載れば、Antigravity のローカル LLM 接続からそのまま呼び出せるので、「自分のコードベースの流儀を学習させた補完モデル」を IDE に組み込む、という流れが Mac 1台で完結します。Antigravity 側の接続手順は Antigravity でローカル LLM を設定して活用する手順にまとめています。
GGUF への変換は、fuse したモデルに対して llama.cpp の変換スクリプトを使います。手順としては短いのですが、先に 2 点だけ押さえておくと詰まりません。
# llama.cpp を取得(ここでは変換スクリプトのみ使用)
git clone --depth 1 https://github.com/ggml-org/llama.cpp
pip install -r llama.cpp/requirements.txt
# fuse 済みモデルを f16 の GGUF へ変換
python llama.cpp/convert_hf_to_gguf.py ./gemma-4-27b-custom \
--outfile ./gemma-4-27b-custom-f16.gguf \
--outtype f16
# 実用サイズへ量子化(Q4_K_M は容量と品質のバランスが取りやすい)
llama.cpp/build/bin/llama-quantize \
./gemma-4-27b-custom-f16.gguf \
./gemma-4-27b-custom-q4_k_m.gguf Q4_K_M押さえておきたい 1 点目は、ベースに量子化済みモデルを使った場合、mlx_lm.fuse に --de-quantize を付けないと変換スクリプトが重みを解釈できずに落ちることです。2 点目は中間ファイルの容量で、27B の f16 GGUF はおよそ 50GB を超えます。量子化後のファイルと合わせて 70GB 程度の空きを見ておくと安全です。変換が終わったら f16 の中間ファイルは削除して構いません。
# GGUF 変換後、Modelfile を作って Ollama に登録
cat > Modelfile <<'EOF'
FROM ./gemma-4-27b-custom.gguf
PARAMETER temperature 0.7
EOF
ollama create gemma4-custom -f Modelfile
ollama run gemma4-customなお、Ollama 経由で使い始めてから長い応答が途中で切れる症状に出会ったら、Antigravity で Ollama の応答が途中で途切れる・長時間実行で固まる問題を直す実践ガイドに対処法をまとめています。自作モデルに限らず Ollama 連携全般で起きる症状です。
つまずきやすいエラーと対処
私が実際に踏んだもの、検証中に再現できたものをまとめます。
- 学習開始直後に Out of Memory: まず
--batch-sizeを半分に。それでも落ちる場合は--lora-layersを 16 → 8 に減らすか、4bit 量子化版のベースモデルに切り替えます。他のアプリ(特にブラウザ)を閉じるだけで通ることもあります。推論側(Antigravity から使う段階)で同種のエラーが出る場合は Antigravity で Gemma 4 が Out of Memory エラーになる問題の解決法が参考になります - ダウンロードで 401 Unauthorized: Hugging Face 上で Gemma のライセンスに未同意か、
huggingface-cli loginのトークンが read 権限を持っていないかのどちらかです - 学習は進むのに応答が壊れる: 前述のチャットテンプレート不一致が典型原因です。
train.jsonlの中身を 1 行取り出して、テンプレートタグが正しく入っているか目視確認してください - 途中から極端に遅くなる: メモリ圧迫でスワップに入っています。アクティビティモニタの「メモリプレッシャー」が黄色になったら、batch-size を下げた方がトータルでは速く終わります
Colab と Mac の使い分け判断基準
どちらを使うべきかは、データ量とモデルサイズで決まります。
Mac(MLX)を選ぶ場面:
- 学習データが 1,000 件以下の小規模実験
- 深夜〜翌朝にかけて長時間回したい
- クラウドコストを使いたくない
- ユニファイドメモリが 64GB 以上ある
Colab を選ぶ場面:
- データが 10,000 件以上の本格学習
- Gemma 4 より大きいモデルを試したい
- ユニファイドメモリが 32GB 未満の Mac
個人開発の用途では、試行錯誤の初期段階は Mac でやって、本番用の学習だけ Colab Pro+ に切り替えるというハイブリッドな運用が現実的でした。MLX のおかげで Mac がただの「確認用端末」ではなく、実用的なファインチューニング環境になっています。
まずは 100 件程度の小さなデータセットで 1 サイクル(変換 → 学習 → fuse → Ollama 登録)を通してみてください。一度パイプラインが通れば、あとはデータの質を上げる作業に集中できます。同じ構成で試している方の参考になれば幸いです。