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 で再開できるので、必ず付けておくことをお勧めします。
学習率とイテレーション数の現実的な決め方
公式サンプルの --learning-rate 1e-4 は、データが数千件ある前提の値だと考えた方が安全です。数百件規模のデータで 1e-4 のまま回したところ、loss の下がり方が急すぎて、推論時に学習データの言い回しをそのまま繰り返す「暗記」に寄りました。5e-5 まで落とすと、表現の自然さを保ったまま狙ったスタイルに寄っていく感触が得られています。迷ったら 5e-5 から始めて、loss の動きが鈍すぎる場合にだけ上げる、という順番が安全です。
イテレーション数は「データ件数 × 2〜3 エポック相当 ÷ バッチサイズ」を起点にしています。例えば 300 件のデータで --batch-size 4 なら、1 エポックが約 75 イテレーション。150〜225 イテレーションから様子を見るのが現実的で、最初から 1,000 回す必要はありません。
判断のよりどころは valid loss です。チェックポイントごとに表示される valid loss が下がり止まったら、それ以上回しても暗記が進むだけです。train loss だけを見ていると「順調に下がっている」ように見えてしまうのが落とし穴で、私自身、最初の数回はこれで無駄に長く回していました。
深夜に回すための 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 の自動アップデートは、学習を回す晩だけ一時的にオフにしておくと安心です
学習結果を試す
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 を減らすか、データを増やすサインです。
アダプタを統合して 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 変換後、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 登録)を通してみてください。一度パイプラインが通れば、あとはデータの質を上げる作業に集中できます。同じ構成で試している方の参考になれば幸いです。