Google Cloud で Llama 4 Scout を QLoRA でチューニングする手順
Posted by: sion
0. はじめに
こんにちは、インターンのsionです。
本記事では、MetaのLlama 4 Scout(17B-16E。MoE(Mixture of Experts)で、有効パラメータ17B、総パラメータ約109B)をGoogle Cloud上でQLoRA(Quantized Low-Rank Adaptation)によりファインチューニングする手順をまとめます。 題材はtext-to-SQLのデータセットb-mc2/sql-create-contextです。目的は精度ではなく、学習・保存・中断からの復帰・評価が一続きに動く手順を確立することでした。
手順には制約が2つあります。以下の通りです:
- 組織ポリシーでVMに外部IPを付けられない
- A100 80GBのオンデマンド枠が承認されず、Spot VMしか使えない
結果としては、評価用1,000件での完全一致率(ESM、Exact Set Match。正解SQLと値まで含めて同じ書き方かを見る指標)は0.514から0.744に上がりました。
1. 全体像
VMは使い捨てにし、残すものはすべてGCS(Google Cloud Storage)に置く方針としました。Spot VMはいつ回収されてもおかしくないため、VMのディスクには何も残らない前提で組みます[1]。
作業は次の3段で進めました。
- Stage A:A100 Spot VMの上で環境を作り、配線(モデルのロード、LoRA(Low-Rank Adaptation)の適用範囲、チェックポイントの保存と復帰)を確認
- Stage B:依存ライブラリとモデルの重みをカスタムイメージに焼き、Managed Instance Group(MIG)で学習を自走
- 評価:同じイメージから単発のVMを立て、学習前と学習後のモデルを同じ条件で評価

LoRAの設定はr=16、α=32で、対象はlanguage_model側のattention(q_proj / k_proj / v_proj / o_proj)だけです。expert、router、vision側には当てていません。
rは差分行列の階数、αはその差分に掛ける倍率の分子で、倍率α/r = 2で元の重みに足し込みます[2]。 attentionだけに絞ったのは、本記事の目的が精度ではなく手順の確立であり、学習可能パラメータの数を手で検算できる最小の構成にしたかったためです。 Scoutのattentionはq/oが5120→5120、k/vが5120→1024なので[3]、 r=16では1層あたり16 × (10240 + 6144 + 6144 + 10240) = 524,288、48層で25,165,824になります。 どの行列に当てるかとrの大きさについては、LoRA論文がGPT-3で比較しています[4]。
expertに当てなかった理由は3つあります。Scoutでは16個のexpertが総パラメータの約9割を占め[5]、当てると学習対象が一気に増えること。 各トークンが通るrouted expertは1個だけで[6]、1 expertあたりの学習信号が薄いこと。 そしてexpertの重みの持ち方が、素のモデルと今回使った4bit版とで違うことです。素のtransformersではexpertの重みは線形層ではなく、16個分を束ねた1本のテンソル(gate_up_proj、down_proj)として持たれており、LoRAを当てるにはpeftの別経路(target_parameters)が要ります[7]。 一方、今回の環境(transformers 5.5.0、bnb 4bit版)では、expertは1個ずつ別の線形層(experts.{0..15}.{gate,up,down}_proj)として組まれ、attentionと同じtarget_modulesで名前指定できる形になっていました。どちらの形で載るかはロードしてみるまで分からず、当て方もそれに引きずられます。本記事ではexpertには当てないと決めたうえで、ライブラリがMoEを検出してexpert側のLoRAを自動で有効にしていないことを確認の対象にしています(節4)。 routerはトークンをどのexpertに送るかを決める小さな線形層(5120→16)で、ここを動かすとexpertの使われ方そのものが変わります。今回の目的には不要なので凍結しました。vision側はtext-to-SQLでは使わないため対象外です。
検証環境(手元)
GPUを使う処理はすべてVMで行い、手元のMacではGPUの要らない作業だけを行いました。具体的には、スクリプトの単体テストとCPUでの事前検証、学習・評価データの抽出、評価結果の比較と判定、gcloudによるVMとGCSの操作です。
| 項目 | 値 |
|---|---|
| マシン | Mac(Apple M4、10コア、メモリ16GB) |
| OS | macOS Tahoe 26.6.2 |
| Python | 3.12.11(uv venvで作成) |
| uv | 0.12.13 |
| Google Cloud SDK | 582.0.0(beta 2026.08.21) |
| 主なパッケージ | transformers 5.15.1、sqlglot 30.17.0、scikit-learn 1.9.0、pytest 9.1.1 |
OSは検証時(2026年8月)の版です。その他の版は2026年9月に確認したもので、検証時と異なる可能性があります。手元のtransformersは、チャットテンプレートとトークン長の検査でトークナイザを読むためだけに使っており、VM側(5.5.0)とは版が異なります。採点に使うsqlglotは、VM側と同じ30.17.0に固定しています。
検証環境(VM)
学習と評価に使ったVMの構成は次のとおりです。ライブラリの版は、VM上のvenvから書き出したrequirements.lock(121行)の値です。
| 項目 | 値 |
|---|---|
| マシンタイプ | a2-ultragpu-1g(A100 80GB × 1、12 vCPU、メモリ170GB)、Spot |
| ブートイメージ | Deep Learning VM common-cu129-ubuntu-2204-nvidia-580(Ubuntu 22.04) |
| NVIDIAドライバ | 580.173.02 |
| Python | 3.12(uv venvで作成) |
| PyTorch | 2.11.0+cu128 |
| transformers / trl / peft | 5.5.0 / 0.24.0 / 0.20.0 |
| unsloth / bitsandbytes | 2026.8.22 / 0.50.2 |
| モデル | bnb-community/Llama-4-Scout-17B-16E-Instruct-bnb-4bit |
2. 下準備(一度きり)
請求先
請求先アカウントは有料アカウントに上げておきます。無料トライアルのままでは、GPUクォータの増加を申請できません[8]。
gcloud billing accounts list # OPEN が True であることを確認する
プロジェクトとgcloud
プロジェクトIDは後から変更できないため、最初に決めます[9]。gcloudにはbetaコンポーネントを入れておきます。Spotの中断予告時間を指定する--preemption-notice-durationが、検証時のgcloud(582.0.0)ではbetaにしか無かったためです[10]。
gcloud projects create "$PROJECT_ID"
gcloud billing projects link "$PROJECT_ID" --billing-account="$BILLING_ACCOUNT"
gcloud config set project "$PROJECT_ID"
gcloud auth login
gcloud auth application-default login
gcloud components install beta
APIの有効化
有効化していないAPIを叩くと、コマンドがAPI has not been used in project ...で失敗します。使うものは先にまとめて有効にします。
gcloud services enable compute.googleapis.com storage.googleapis.com \
secretmanager.googleapis.com cloudbilling.googleapis.com \
billingbudgets.googleapis.com logging.googleapis.com monitoring.googleapis.com
予算アラート
コンソールの「お支払い > 予算とアラート」から、予算に対して50%、90%、100%で通知を出すようにしました。注意点が2つあります。アラートは通知するだけで、支払いを止めません[11]。また、利用してから課金に反映されるまでには遅れがあります[12]。止め忘れたVMに気づく手段としては当てにできないため、作業が終わったらVMを停止または削除し、一覧で確かめます。
GPUクォータ
新規プロジェクトのGPUクォータは0から始まります。A100 80GBのクォータは次の3つで、それぞれ別に申請します。Spotだけで使う場合に要るのは、Spot用の枠と全リージョンの合計枠の2つです[13]。
| クォータ | スコープ | 用途 |
|---|---|---|
NVIDIA_A100_80GB_GPUS |
リージョン | オンデマンド |
PREEMPTIBLE_NVIDIA_A100_80GB_GPUS |
リージョン | Spot |
GPUS_ALL_REGIONS |
グローバル | 全リージョンの合計 |
コンソールで名前を絞り込むと、COMMITTED_NVIDIA_A100_80GB_GPUS(確約利用割引の枠[14])も並んで表示されますが、これは無関係ですので注意してください。
また申請確認時、gcloud compute regions describeが返すのは現在の上限値だけで、0.0と表示されても「申請して却下された」のか「そもそも申請していない」のか区別が付きません。申請の履歴は別のコマンドで見ます[15]。
gcloud quotas preferences list --project="$PROJECT_ID" \
--format="table(quotaId,dimensions.region,quotaConfig.preferredValue,quotaConfig.stateDetail)"
オンデマンド枠は2回申請し、2回とも却下されたため、Spotだけで進める方針に切り替えました。さらに、最初に選んだus-central1ではSpotの在庫切れ(ZONE_RESOURCE_POOL_EXHAUSTED)が続いたため、us-east5にSpot枠を追加で申請し、以降はそちらを使っています。GPUS_ALL_REGIONSは1のため、同時に持てるGPUは常に1枚です。
Hugging Faceの承認
Llama 4 Scoutはモデルページでライセンスに同意し、承認されるまでダウンロードできません。今回は承認まで約2時間かかりました。承認後にread権限のトークンを発行しておきます。
GCSバケット
VMは使い捨てにするので、残したいものはすべてバケットに置きます。パスは規約として最初に決めました。
| パス | 中身 |
|---|---|
$BUCKET/env/requirements.lock |
依存の解決結果。イメージ名の元になる |
$BUCKET/data/ |
データの分割結果と抽出したサンプル |
$BUCKET/code/lora-tuning.tar.gz |
スクリプト一式 |
$BUCKET/prompts/ |
凍結したプロンプトとそのハッシュ |
$BUCKET/checkpoints/<段階>/ |
チェックポイント(optimizerの状態を含む) |
$BUCKET/logs/、$BUCKET/metrics/ |
ログと実測値 |
gcloud storage buckets create "$BUCKET" --location="$REGION" \
--uniform-bucket-level-access --default-storage-class=STANDARD
本来、バケットとVMは同じリージョンに置くべきです[16]。今回はVMだけを在庫の都合でus-east5に移したため、バケット(us-central1)とVMのリージョンが分かれています。転送料と遅延が乗りますが、今回の規模では問題になりませんでした。
サービスアカウント
VMに付けるサービスアカウント(SA)を作ります。JSONキーは作りません。SAをVMに付けておけば、VM上のgcloudやPython SDKはメタデータサーバ経由で自動的に認証されるためです[17]。キーファイルを持ち回る必要がなく、漏洩の心配もありません。
付けるロールは、既定のロール3つとカスタムロール1つです。
gcloud iam service-accounts create "$SA_NAME"
gcloud storage buckets add-iam-policy-binding "$BUCKET" \
--member="serviceAccount:$SA_EMAIL" --role="roles/storage.objectAdmin"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:$SA_EMAIL" --role="roles/secretmanager.secretAccessor"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:$SA_EMAIL" --role="roles/logging.logWriter"
gcloud iam roles create migSelfStop --project="$PROJECT_ID" --title="MIG self-stop" \
--permissions=compute.instanceGroupManagers.get,compute.instanceGroupManagers.update
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:$SA_EMAIL" --role="projects/$PROJECT_ID/roles/migSelfStop"
migSelfStopは、学習を終えたVMが自分の属するMIGのサイズを0に落とすための権限です(節5)。これが無いと、学習が終わってもMIGはサイズ1のままVMを維持し、A100の課金が続きます。既定ロールのroles/compute.instanceAdmin.v1でも同じことはできますが、VMの作成や削除まで含まれます[18]。学習用VMに必要なのはMIGの参照と更新だけですので、その2つに絞ったロールを作りました。
Secret Manager
Hugging FaceのトークンはSecret Managerに置きます。VM上ではSAの権限で、追加の認証なしに読めます。手元の設定ファイルにトークンを書かずに済みます。
printf '%s' "$HF_TOKEN" | gcloud secrets create hf-token --data-file=- --replication-policy=automatic
# VM 上で
export HF_TOKEN=$(gcloud secrets versions access latest --secret=hf-token)
3. ネットワークとGPU VM
80GBが要る理由
4bitに量子化したScoutでも、GPUに載せた重みだけで54.5GBあります(ダウンロード量は59GB、/opt/hfのキャッシュは56GBです)。ロード直後のVRAMは55.08GB、配線確認の学習(系列長512)でのピークは62.4GBでした。L4(24GB)やA100 40GBでは載らないため、80GB級が必要です。今回のプロジェクトではA100 80GBを取得・使用しています。
外部IPが付けられない環境への対応
組織ポリシーcompute.vmExternalIpAccessにより、VMに外部IPを付けると作成自体が拒否されます[19]。そこで次の4つを組み合わせます。
--no-address:VMを外部IPなしで作ります。- IAP(Identity-Aware Proxy)経由のSSH:
gcloud compute ssh --tunnel-through-iapで入ります。IAPの送信元(35.235.240.0/20)から22番ポートへの接続を許可するファイアウォール規則が要ります[20]。今回はdefaultネットワークに最初からあるdefault-allow-ssh(すべての送信元からtcp:22を許可)がこれを満たしていたため、規則を追加せずに済みました[21]。 - Private Google Access(PGA):サブネットで有効にすると、外部IPの無いVMからもGCSやSecret Managerに届きます[22]。Stage B以降も使うため、引き続き有効にしておきます。
- Cloud NAT:PyPIやHugging Faceに出るための経路です。必要なのは、依存ライブラリの導入や重みのダウンロードをするStage Aと、イメージを焼くVMだけです。
gcloud services enable iap.googleapis.com
gcloud compute networks subnets update default --region="$VM_REGION" \
--enable-private-ip-google-access
gcloud compute routers create nat-router --network=default --region="$VM_REGION"
gcloud compute routers nats create nat --router=nat-router --region="$VM_REGION" \
--auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges
NATの料金は、NATを使っているVMの台数と時間、NATが使う外部IPの時間、処理したデータ量で決まります[23]。消し忘れを避けるため、作業が終わったら落とします。NATはリージョン単位で作るため、バケットではなくVMのリージョンに作ります[24]。
gcloud compute routers nats delete nat --router=nat-router --region="$VM_REGION"
gcloud compute routers delete nat-router --region="$VM_REGION"
在庫のあるゾーンを探す
A100 80GBを1枚積んだマシンタイプはa2-ultragpu-1gです。このマシンタイプはすべてのゾーンにあるわけではなく、たとえばus-central1-bには存在しません[25]。
gcloud compute machine-types list --filter="name=a2-ultragpu-1g" --format="table(name,zone)"
在庫切れとマシンタイプの不在は別物です。在庫切れ(ZONE_RESOURCE_POOL_EXHAUSTED)は時間を置けば通ることがありますが[26]、不在のゾーンは何度試しても通りません。候補は上のコマンドで先に絞っておきます。
VMを作る
gcloud beta compute instances create scout-phase0 --zone="$ZONE" \
--machine-type=a2-ultragpu-1g --provisioning-model=SPOT \
--instance-termination-action=STOP --preemption-notice-duration=120s \
--maintenance-policy=TERMINATE \
--image-family=common-cu129-ubuntu-2204-nvidia-580 \
--image-project=deeplearning-platform-release \
--boot-disk-size=200GB --boot-disk-type=pd-balanced \
--metadata="install-nvidia-driver=True" \
--service-account="$SA_EMAIL" --scopes=https://www.googleapis.com/auth/cloud-platform \
--no-address
主なフラグの意図は次のとおりです。
- イメージ:Deep Learning VMのうち、ドライバとCUDAだけが入りフレームワークの無いものを使います[27]。PyTorch入りのイメージは、後で入れるtorchと競合します。
--instance-termination-action=STOP:Spotが回収されてもVMを削除せず停止にし、構築した環境をディスクごと残します[28]。--maintenance-policy=TERMINATE:GPU付きのVMでは必須です[29]。--boot-disk-size=200GB:重みのキャッシュ(約60GB)、venv、データが収まる大きさです。
起動後の確認
SSHで入ったら、まずnvidia-smiで81920MiBと表示されることを確かめます。40960MiBならA100 40GBが割り当てられています。作業はtmuxの中で行います。IAP経由のSSHは切れることがあり、切れるとその中のプロセスも止まるためです。
gcloud compute ssh scout-phase0 --zone="$ZONE" --tunnel-through-iap
nvidia-smi
tmux new -A -s work
4. 環境構築と配線確認(Stage A)
先にL4でドライランする
A100に入る前に、L4のVMと4bitのLlama-3.1-8Bで同じスクリプトを一通り動かしました。L4のSpotはA100の4分の1程度の単価です。コードの不具合は安いGPUで出し切っておきます。
環境は/optに作る
venvとHugging Faceのキャッシュは/optの下に絶対パスで置きます。この環境は後でイメージに焼いてMIGのVMで使いますが、MIGがVMを作り直すとホームディレクトリが変わりうるためです。HF_HOMEを設定し忘れると、起動のたびに重みを再ダウンロードすることになります。
sudo mkdir -p /opt/lora-venv /opt/hf && sudo chown -R "$USER" /opt/lora-venv /opt/hf
export HF_HOME=/opt/hf
torchの導入
パッケージ管理にはuvを使いました。--torch-backend=autoを指定すると、警告もなくCPU版のtorch(2.11.0+cpu)が入ることがあります。CUDAの版はcu128と明示し、成否は「インストールが通ったか」ではなくtorch.cuda.is_available()で判定します。
curl -LsSf https://astral.sh/uv/install.sh | sh && source "$HOME/.local/bin/env"
uv venv /opt/lora-venv --python 3.12 --seed && source /opt/lora-venv/bin/activate
uv pip install --torch-backend=cu128 unsloth
uv pip install --torch-backend=cu128 trl peft transformers bitsandbytes datasets \
sqlglot scipy wandb google-cloud-storage
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
# 2.11.0+cu128 True
requirements.lock
venvに入った全パッケージの版をuv pip freezeで書き出し、GCSに置きます。このlockのsha256の先頭8文字が、後でイメージ名になります(節5)。
uv pip freeze > requirements.lock
gcloud storage cp requirements.lock "$BUCKET/env/requirements.lock"
モデルのロード
モデルはbnb-community/Llama-4-Scout-17B-16E-Instruct-bnb-4bitを使いました。ロード時のログでは、MISSING(モデル定義にあるのにチェックポイントに無い重み)とUNEXPECTED(チェックポイントにあるのにモデル定義に無い重み)がともに0件であることを確認します。
当初はUnslothが配布する4bit版を使っていましたが、expertの重みの並べ方が今のtransformersと合わず、expertがすべて捨てられてランダムに初期化されていました。例外は出なかったので、ログを確認するとよいです。
LoRAの配線
LoRAを当てたら、学習可能なパラメータの名前を全て列挙し、expert・router・vision・埋め込み層に当たっておらず、language_model側のq/k/v/oすべてに当たっていることを確かめます。
判定は必ずlanguage_modelのスコープに絞ります。Scoutはvisionタワーにも同じ名前のself_attn.{q,k,v,o}_projを持ち、全328個のうち136個がvision側です。名前の部分一致だけでは、vision側にしか当たっていなくても通ってしまいます。
あわせて、unslothのget_peft_modelが出すログにDetected MoE、Enabling LoRA on MoE、target_parametersのいずれも現れないことを確かめます。MoEを検出するとexpert側のLoRAを自動で有効にする経路があり、学習可能なパラメータの名前を列挙するだけでは見落とす可能性があるためです。今回は3つともヒットしませんでした。
学習可能なパラメータは全体の0.0232%(約2,517万個)でした。attentionだけにr=16で当てた場合の目安(0.01〜0.05%)に収まっています。
短い学習
50件のデータを50ステップ学習させ、train lossが0.1を下回れば配線は繋がっているとみなします。バッチは50件全部を1ステップに収めます(--batch-size 10 --grad-accum 5)。最初は30ステップ、実効バッチ16で試しましたがlossは0.22までしか下がらず、ステップを50に伸ばして0.0029になりました。
同時に、チェックポイントがGCSに出ることも確かめます。Trainerのoutput_dirにgs://を渡すと、例外は出ずにローカルにgs:/というディレクトリができてしまうので、保存はローカルに行い、保存のたびにGCSへ送る作りにします(--gcs-prefix)。gcloud storage lsで存在を確認できます。
python scripts/train_overfit.py --profile phase0 \
--data data/phase0_sample.jsonl --max-steps 50 --max-seq-length 512 \
--batch-size 10 --grad-accum 5 \
--output-dir ~/ckpt/phase0 --gcs-prefix "$BUCKET/checkpoints/phase0/"
gcloud storage ls -r "$BUCKET/checkpoints/phase0/checkpoint-*/"
各チェックポイントには、adapter_model.safetensors(LoRAの重み)、optimizer.pt、scheduler.pt、trainer_state.json、rng_state.pthの5ファイルが揃っている必要があります。optimizerの状態が欠けていると、再開直後にlossが跳ね上がります。
resume
再開の検証で比べるのは、保存された状態と再開直後の状態です。実測ではVMを停止・起動してから再開し、保存時と再開直後で5点(global_step、学習率、schedulerの進み、optimizerの状態のチェックサム、optimizerが持つテンソルの本数)がビット単位で一致しました。さらに、再開後の10ステップ分のlossと学習率が、中断せずに回した元の実行と最後の桁まで一致しました。RNGの状態まで復元され、データの順序も揃っている証拠です。
5. イメージ焼成、MIGでの本学習、評価
Spotの復帰のたびにpip installと59GBのダウンロードを繰り返すと、復帰が遅くなるうえにNATも要ります。そこで、依存と重みを一度イメージに焼き、そのイメージから起動したVMをMIGに管理させます。

置き場所の分担
変更の程度に応じて、以下のように分けました。
| 置き場所 | 中身 | 変更の程度 |
|---|---|---|
| イメージ | ドライバとCUDA、/opt/lora-venv、/opt/hf(重み56GB) |
変更しない |
| GCSのtar | 学習・評価のスクリプト、設定 | よく変更される |
| GCS | データ、チェックポイント、凍結プロンプト、ログ | 実行をまたいで引き継ぐ状態 |
イメージを焼く
イメージを焼く作業にGPUは要らないので、CPUのVM(e2-standard-8)で行いました。このVMにはNATが必要です。ビルドスクリプトは、GCSからlockを取って/optに同じ構成を作るだけのものです。
スクリプトには最初からset -euo pipefailとtrap ERRを入れておきます。/optへの書き込み権限が無いと、torchも重みも無い状態で進んでしまうためです。
#!/bin/bash
set -euo pipefail
trap 'echo "失敗: line $LINENO" >&2' ERR
sudo mkdir -p /opt/lora-venv /opt/hf && sudo chown -R "$USER" /opt/lora-venv /opt/hf
curl -LsSf https://astral.sh/uv/install.sh | sh && source "$HOME/.local/bin/env"
gcloud storage cp "$BUCKET/env/requirements.lock" "$BUCKET/code/lora-tuning.tar.gz" ~/
mkdir -p ~/w && tar xzf ~/lora-tuning.tar.gz -C ~/w && cd ~/w
uv venv /opt/lora-venv --python 3.12 --seed
source /opt/lora-venv/bin/activate
uv pip install --torch-backend=cu128 -r ~/requirements.lock
export HF_TOKEN=$(gcloud secrets versions access latest --secret=hf-token)
HF_HOME=/opt/hf python scripts/load_model.py --profile phase0 --download-only
導入が終わったらVMを止め、そのディスクからイメージを作ります[30]。 イメージ名にはlockのsha8を埋め込みます。CPUのVMではtorch.cuda.is_available()を確かめられないため、GPU上での確認は次のスモークテストに回します。
gcloud compute instances stop image-builder --zone="$ZONE"
LOCK_SHA=$(gcloud storage cat "$BUCKET/env/requirements.lock" | sha256sum | cut -c1-8)
gcloud compute images create "scout-qlora-${LOCK_SHA}" \
--source-disk=image-builder --source-disk-zone="$ZONE" --family=scout-qlora
gcloud compute instances delete image-builder --zone="$ZONE" --quiet
焼成は322秒、イメージのアーカイブは75.7GBでした。
スモークテスト
焼いたイメージからA100 Spotを1台立て、NATを外した状態で学習が始まるかを確かめます。外に出られない状態で動けば、実行時に何もダウンロードしておらず、必要なものがすべてイメージに揃っていることの証明になります。
このときHF_HUB_OFFLINE=1とTRANSFORMERS_OFFLINE=1を必ず設定します。重みがローカルにあってもライブラリはHugging Faceに問い合わせに行き、到達できないときに即座にエラーが返るとは限りません。TCPの接続待ちを繰り返し、起動が数分から十数分延びることがありました。
あわせて、VRAMがStage Aとほぼ一致すること、GCSのチェックポイントから再開できることを確かめました。
インスタンステンプレート
MIGがVMを作るときの設定は、インスタンステンプレートに書きます。
META="bucket=$BUCKET,ckpt-prefix=$BUCKET/checkpoints/phase1/,train-data=train10k.jsonl"
META+=",max-seq-length=192,max-steps=200,batch-size=10,grad-accum=5,save-steps=29"
META+=",allow-fresh-start=1,expect=train,wandb-mode=offline,mig-name=scout-qlora-mig"
gcloud beta compute instance-templates create scout-qlora-tmpl-p1 \
--machine-type=a2-ultragpu-1g --provisioning-model=SPOT \
--instance-termination-action=STOP --preemption-notice-duration=120s \
--maintenance-policy=TERMINATE \
--image=scout-qlora-1da88d94 --image-project="$PROJECT_ID" \
--boot-disk-size=200GB --boot-disk-type=pd-balanced \
--service-account="$SA_EMAIL" --scopes=https://www.googleapis.com/auth/cloud-platform \
--no-address \
--metadata-from-file=startup-script=scripts/startup.sh,shutdown-script=scripts/shutdown.sh \
--metadata="$META"
回収時の動作はSTOPにします。MIGに作り直させたいならDELETEが自然に思えますが、SpotとMIGの組み合わせではDELETEが拒否されます。しかもテンプレートの作成は通り、MIGを作る段階で初めてエラーになります[31]。
上のコマンドは修正後の版です。本学習を回したテンプレートにはexpect=trainが無く、startup-scriptは配線確認用の基準(train lossが0.1を下回ること)で本学習の成否を判定していました。200ステップを完走した健全な学習が失敗扱いになり、「失敗時はVMを残す」分岐でA100が26分空回りしました(節6)。配線確認の基準と本学習の基準は別物として、メタデータで明示的に渡します。
テンプレートからは環境変数を渡せないため、学習のパラメータはメタデータとして渡し、startup-scriptの中でメタデータサーバから読みます[32]。
md() { # md <キー> <既定値>
local v
v=$(curl -s -f -H 'Metadata-Flavor: Google' \
"http://metadata.google.internal/computeMetadata/v1/instance/attributes/$1")
echo "${v:-$2}"
}
MAX_STEPS=$(md max-steps 200)
startup-scriptは、コードとデータと最新のチェックポイントをGCSから取って学習を再開し、ログを送り、成功したら自分のMIGを0に落とすだけです。インストールは一切しません。
中断予告の120秒は、メタデータのpreemptedがTRUEになることで知らされるだけです。shutdown-scriptが走るのは予告が終わった後の停止処理の間で、長さはベストエフォートで最大30秒です[33]。今回のスクリプトはpreemptedを監視していないため、120秒の予告は使っていません。チェックポイント一式(optimizerの状態を含む)の送付は30秒では間に合わないので、shutdown-scriptで送るのは中断時のステップ数とログの末尾だけです。チェックポイントは学習中に29ステップ(約15分)ごとに送っています。
なお、shutdown-scriptが動くかどうかは確かめられていません。Stage BではVMを削除して中断を模擬しました。docsでは削除でもshutdown-scriptは走るとされていますが[34]、GCSに痕跡は残りませんでした。スクリプトがエラー出力を捨てていたため、走らなかったのか、走って失敗したのかは区別できません。本学習では中断そのものが起きませんでした。
コードの渡し方
リポジトリはprivateで、VMにはGitHubの鍵が無いためgit cloneできません。スクリプト一式をtarにまとめてGCSに置き、VMはPGA経由でそれを取ります。NATは要りません。
tar czf /tmp/lora-tuning.tar.gz scripts configs tests prompts
gcloud storage cp /tmp/lora-tuning.tar.gz "$BUCKET/code/lora-tuning.tar.gz"
注意点が1つあります。startup-scriptはテンプレートを作った時点の内容でテンプレートに埋め込まれます。startup.shを直したら、tarを上げ直すだけでは反映されず、テンプレートも作り直す必要があります[35]。
進捗の見方
進捗はGCSに出てくる成果物で見ます。RUNNINGでも学習が動いているとは限りません[36]。スモークテストではstartup-scriptが途中で落ち、VMはRUNNINGのままGPU使用率0%で止まっていました。
gcloud storage ls "$BUCKET/checkpoints/phase1/"
gcloud compute instances get-serial-port-output <VM名> --zone="$ZONE" | grep -a "startup-script:"
gcloud compute instance-groups managed describe scout-qlora-mig --zone="$ZONE" \
--format="value(targetSize)"
チェックポイントが29ステップごとに増えていれば学習は進んでいます。学習が終わるとtargetSizeが1から0に変わります。0に変わるまではA100が稼働しているので、ここまで見届けます。
自動復帰
Spotが回収されたときにMIGがVMを作り直し、最新のチェックポイントから学習を再開する仕組みは用意し、Stage Bで動作も確かめました。ただ、本学習では中断が一度も起きず、出番はありませんでした。
評価
評価は同じイメージから単発のVMを立てて行います。生成のパラメータを1つでも変えると比較が成り立たないため、学習後の評価はベースラインのコマンドに--adapterを足すだけにしました。
gcloud compute instances create scout-eval --zone="$ZONE" \
--machine-type=a2-ultragpu-1g --provisioning-model=SPOT \
--instance-termination-action=STOP --maintenance-policy=TERMINATE \
--image=scout-qlora-1da88d94 --image-project="$PROJECT_ID" \
--boot-disk-size=200GB --boot-disk-type=pd-balanced \
--service-account="$SA_EMAIL" --scopes=https://www.googleapis.com/auth/cloud-platform --no-address
# VM 上で
export HF_HOME=/opt/hf HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
source /opt/lora-venv/bin/activate
# ベースライン
python scripts/gen_eval.py --profile phase0 --data data/eval1000.jsonl \
--max-new-tokens 80 --out logs/base_pred.jsonl
# 学習後(VM を作り直し、GCS から checkpoint-200 を ~/w/adapter に取ってから)
python scripts/gen_eval.py --profile phase0 --data data/eval1000.jsonl \
--max-new-tokens 80 --adapter ~/w/adapter --out logs/ft_pred.jsonl
評価1回は1,000件で約30分でした。終わったら結果をGCSに送り、VMは削除します。
resizeの直後
MIGのサイズを0にした直後に評価用のVMを作ると、403 QUOTA_EXCEEDEDで失敗することがあります。resize --size=0はコマンドが返った時点ではまだVMを削除し終えておらず[37]、GPUを1枚しか持てない枠がまだ埋まっているためです。gcloud compute instances listが空になってから作ります。
6. 費用と後始末
単価
Cloud Billing Catalog API[38]から2026年8月30日に取得した、us-east5の定価は次のとおりです。単価は変わることがあるため、最新の値は料金ページで確かめてください。
| 構成 | 単価 |
|---|---|
A100 80GB Spot(a2-ultragpu-1g:GPU $1.3093/h + 12 vCPU + 170GB) |
$1.87/h |
L4 Spot(g2-standard-8) |
$0.51/h |
L4オンデマンド(g2-standard-8) |
$0.85/h |
イメージ焼成用のCPU VM(e2-standard-8、オンデマンド) |
$0.27/h |
| カスタムイメージの保管(75.7GB) | 約$3.8/月[39] |
内訳
VMごとの稼働時間はgcloud compute operations listの作成・起動・停止・削除の記録から積み上げました。
| VM | 用途 | 構成 | 稼働 | 費用 |
|---|---|---|---|---|
| scout-dryrun | L4ドライラン | L4 Spot /オンデマンド | 3.09h(Spot 1.15h + オンデマンド 1.94h) | $2.25 |
| image-builder | イメージ焼成 | e2-standard-8 | 0.20h | $0.05 |
| scout-phase0 | Stage A | A100 Spot | 2.07h | $3.86 |
| scout-smoke | スモークテスト | A100 Spot | 8.33h | $15.53 |
| scout-qlora-mig-hb6v | 自動復帰の検証 | A100 Spot | 0.68h | $1.27 |
| scout-eval | ベースライン評価 | A100 Spot | 0.88h | $1.63 |
| scout-qlora-mig-g4v0 | 本学習 | A100 Spot | 2.12h | $3.96 |
| scout-eval | 学習後評価 | A100 Spot | 0.83h | $1.55 |
| 合計 | 18.20h | $30.10 |
VMの分は合計$30です(各行は小数第2位で丸めてあり、丸める前の合計は$30.11です)。NAT、イメージの保管料、消費税を足した見積もりは5,600〜6,600円で、実際の請求額(約6,500円)とおおむね一致しました。
最大の項目は、本学習ではなくスモークテストの8.3時間で、全体の約半分を占めます。スモークテストの中でstartup-scriptが落ちる不具合を見つけ、その調査と修正の間、VMを止めずに残していたためです。記録上、2回の起動試験の間の約5時間、VMは稼働したままでした。
計算していない時間
本学習の段階(評価2回と学習)について、VMの作成要求から削除完了までを数えたGPU時間は3.91時間でした。上の内訳表の該当3行(計3.83時間)はVMが実際に動いていた時間だけを数えているため、作成待ちの分だけ短くなっています。このうち実際に計算していたのは学習1.59時間と生成1.01時間の計2.60時間で、残りの約3分の1(1.31時間)は計算していない時間です。内訳は次の3つです。
- 評価用VMでの生成以外の作業(約31分):SSHでの準備、採点、結果の送付、削除までの時間です。評価用VMは2台とも手で操作しました。
- 学習後の空回り(26分):学習を終えたあとVMが止まらずに空回りした時間です。本学習のテンプレートに
expect=trainが無く(節5)、配線確認用の基準で判定された健全な学習が失敗扱いになり、VMが調査用に残されました。 - 起動とモデルのロード(約21分):1回あたり約7分で、VMを3回立てる構成ではその分が3回かかります。
後始末
作業が終わったら、次の4つの一覧がすべて空であることを確かめます。
gcloud compute instances list
gcloud compute disks list
gcloud compute routers list
gcloud compute addresses list
サイズ0のMIGとインスタンステンプレートは、存在するだけでは課金されません。一方、カスタムイメージはVMと違って「止める」ことができず、削除するまで保管料がかかり続けます[40]。使い終わったら削除します。
gcloud compute images delete scout-qlora-1da88d94
課金レポートへの反映には遅れがあるため、消し忘れの確認にはなりません。稼働の事実はgcloud compute operations listで後からでも追えます[41]。
7. まとめ
外部IPが付けられず、A100はSpotしか使えないという条件の下で、次の形に落ち着きました。
- VMは使い捨てにし、データ・コード・チェックポイントはすべてGCSに配置
- 外への通信は、環境構築時のみNATを張る。学習と評価はNATなしで動作
- 依存と重みはイメージに焼き、学習はMIGに任せ、終わったらVM自身でMIGを0へ変更
- 例外の発出やVMのRUNNINGではなく、GCSに出た成果物とログの実数で実行の成否を確認
学習データの作り方、評価指標の定義、判定の方法は本記事の範囲外です。
注
- Google Cloud「Spot VMs」:Compute Engineは容量を取り戻すため、Spot VMをいつでも回収(preempt)しうる。https://docs.cloud.google.com/compute/docs/instances/spot ↩
- LoRA論文(Hu et al., 2021)§4.1。重みの更新をW + (α/r)·BAの形で持ち、Bを0で初期化する。 ↩
config.jsonのtext_config:hidden_size5120、num_attention_heads40、num_key_value_heads8、head_dim128、num_hidden_layers48。https://huggingface.co/bnb-community/Llama-4-Scout-17B-16E-Instruct-bnb-4bit/blob/main/config.json ↩- LoRA論文§7.1(Table 5):同じパラメータ予算ならq/vの両方に当てるのがよい。§7.2(Table 6):rは1〜8でも十分な場合がある。 ↩
- 1層あたり16 × (5120 × 16384 + 8192 × 5120) ≒ 20.1億、48層で約966億。総パラメータ約1,087億(節4)の89%。 ↩
config.jsonのnum_experts_per_tok1。transformersのLlama4TextMoeは、これに加えて全トークンが通るshared_expertを持つ。https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama4/modeling_llama4.py ↩- PEFT「LoRA」の「Targeting
nn.Parameterdirectly」:MoE層の多くはnn.Linearではなくnn.Parameterで重みを持つためtarget_modulesでは当てられず、target_parametersで指定する。例としてLlama 4のfeed_forward.experts.gate_up_proj/down_projが挙げられている。https://huggingface.co/docs/peft/main/en/developer_guides/lora ↩ - Google Cloud「Free Google Cloud features and trial offer」:無料トライアルの請求先アカウントでは、VMへのGPUの追加とクォータの増加申請ができない。https://docs.cloud.google.com/free/docs/free-cloud-features ↩
- Google Cloud「Create projects」:プロジェクトIDは恒久的で変更できない。https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects ↩
- 現在はGA(General Availability)のGoogle Cloud SDK「gcloud compute instances create」(2026年9月22日更新)にも載っている。https://docs.cloud.google.com/sdk/gcloud/reference/compute/instances/create。120秒の予告そのものはPreviewの機能(Google Cloud「Spot VMs」https://docs.cloud.google.com/compute/docs/instances/spot)。 ↩
- Google Cloud「Create, edit, or delete budgets and budget alerts」:アラートのみの予算は利用額に上限を設けず、しきい値を超えてもサービスの利用や課金を止めない。https://docs.cloud.google.com/billing/docs/how-to/budgets ↩
- Google Cloud「Create, edit, or delete budgets and budget alerts」:リソースの利用からCloud Billingへの反映までには遅れがある。https://docs.cloud.google.com/billing/docs/how-to/budgets ↩
- Google Cloud「Spot VMs」:Spot VMは、プリエンプティブ枠が付与されていないリージョンでは通常枠を消費する。付与された後は、そのリージョンではプリエンプティブ枠しか使えない。https://docs.cloud.google.com/compute/docs/instances/spot ↩
- Google Cloud「Allocation quotas」の「Commitment and committed resource quotas」。https://docs.cloud.google.com/compute/resource-usage ↩
- Google Cloud SDK「gcloud quotas preferences list」。https://docs.cloud.google.com/sdk/gcloud/reference/quotas/preferences/list ↩
- Google Cloud「Bucket locations」:性能のため、データはそれを読む計算資源と同じ場所に置くよう勧めている。同じロケーション内の転送は無料で、リージョンをまたぐと転送料がかかる(Google Cloud「Cloud Storage pricing」https://cloud.google.com/storage/pricing)。https://docs.cloud.google.com/storage/docs/locations ↩
- Google Cloud「How Application Default Credentials works」:VMに付けたSAの認証情報はメタデータサーバから取得され、この方法が推奨されている。https://docs.cloud.google.com/docs/authentication/application-default-credentials ↩
- MIGのサイズ変更に要る権限は
compute.instanceGroupManagers.update(Google Cloud「Method: instanceGroupManagers.resize」https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceGroupManagers/resize)。roles/compute.instanceAdmin.v1はこれに加えてcompute.instances.create/compute.instances.deleteを含む(Google Cloud「Compute Engine roles and permissions」https://docs.cloud.google.com/iam/docs/roles-permissions/compute)。 ↩ - Google Cloud「Configure static external IP addresses」:制約
constraints/compute.vmExternalIpAccessで、外部IPを持てるVMを制限できる。https://docs.cloud.google.com/compute/docs/ip-addresses/configure-static-external-ip-address ↩ - Google Cloud「Use IAP for TCP forwarding」。https://docs.cloud.google.com/iap/docs/using-tcp-forwarding ↩
- Google Cloud「VPC firewall rules」:defaultネットワークにはあらかじめ
default-allow-sshなどの規則が入っている。https://docs.cloud.google.com/firewall/docs/firewalls ↩ - Google Cloud「Private Google Access」:内部IPしか持たないVMから、GoogleのAPIとサービスの外部IPに到達できる。https://docs.cloud.google.com/vpc/docs/private-google-access ↩
- Google Cloud「Network pricing」のCloud NAT。https://cloud.google.com/vpc/network-pricing ↩
- Google Cloud「Cloud NAT overview」:Cloud NATゲートウェイは、NATを必要とするサブネットのリージョンに作る。https://docs.cloud.google.com/nat/docs/overview ↩
a2-ultragpu-1gの構成(A100 80GB × 1、12 vCPU、メモリ170GB)はGoogle Cloud「GPU machines in accelerator-optimized machine family」https://docs.cloud.google.com/compute/docs/accelerator-optimized-machines。ゾーンごとに提供されるマシンシリーズは「GPU locations」https://docs.cloud.google.com/compute/docs/regions-zones/gpu-regions-zonesにあり、us-central1-bの一覧にA2 Ultraは無い(2026年9月28日時点)。 ↩- Google Cloud「Troubleshooting resource availability errors」:このエラーはクォータとは無関係で、別のゾーンで試すか、時間を置いて再試行するよう案内している。https://docs.cloud.google.com/compute/docs/troubleshooting/troubleshooting-resource-availability ↩
- Google Cloud「Choose an image」:
common-cu129-ubuntu-2204-nvidia-580はフレームワーク欄がBaseのイメージファミリー。https://docs.cloud.google.com/deep-learning-vm/docs/images。メタデータinstall-nvidia-driver=TrueはGoogle Cloud「Quickstart: Create a Deep Learning VM instance by using the gcloud CLI」https://docs.cloud.google.com/deep-learning-vm/docs/create-vm-instance-gcloud。 ↩ - Google Cloud「Spot VMs」:終了時の動作が
STOPならVMは停止(TERMINATED)し、付いていた永続ディスクは残る(保管料はかかる)。https://docs.cloud.google.com/compute/docs/instances/spot ↩ - Google Cloud「Handle GPU host maintenance events」:GPU付きのVMはライブマイグレーションできないため、ホストメンテナンス時に停止する設定にする必要がある。https://docs.cloud.google.com/compute/docs/gpus/gpu-host-maintenance ↩
- Google Cloud「Create custom images」:ディスクへの書き込みを止めるため、イメージを作る前にVMを停止するよう勧めている。https://docs.cloud.google.com/compute/docs/images/create-custom ↩
- 実際のエラー:
Spot virtual machines with termination action set to DELETE cannot be used with Managed Instance Groups.(2026年8月29日)。Google Cloud「Create a MIG that uses Spot VMs」はSTOPとDELETEのどちらも指定できると書いており、この制限は載っていない。https://docs.cloud.google.com/compute/docs/instance-groups/create-mig-with-spot-vms ↩ - Google Cloud「View and query VM metadata」:メタデータサーバへの問い合わせには
Metadata-Flavor: Googleヘッダーが要る。https://docs.cloud.google.com/compute/docs/metadata/querying-metadata ↩ - Google Cloud「Spot VMs」の「Preemption process」。https://docs.cloud.google.com/compute/docs/instances/spot ↩
- Google Cloud「Run shutdown scripts」:VMの停止・削除・Spotの回収のいずれでもshutdown-scriptは起動する。https://docs.cloud.google.com/compute/docs/shutdownscript ↩
--metadata-from-fileは手元のファイルの中身をメタデータとして渡す(Google Cloud「Use startup scripts on Linux VMs」https://docs.cloud.google.com/compute/docs/instances/startup-scripts/linux)。インスタンステンプレートは作成後に更新できない(Google Cloud「Instance templates」https://docs.cloud.google.com/compute/docs/instance-templates)。 ↩- Google Cloud「Compute Engine instance lifecycle」:
RUNNINGは、VMが起動処理中か稼働中であることを示す。https://docs.cloud.google.com/compute/docs/instances/instance-lifecycle ↩ - Google Cloud「Method: instanceGroupManagers.resize」:resizeの操作は、VMの追加・削除を予定した時点で完了(
DONE)になる。実際の削除の進み具合は別に確かめる必要がある。https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceGroupManagers/resize ↩ - Google Cloud「Get Google Cloud pricing information」。https://docs.cloud.google.com/billing/docs/how-to/get-pricing-information-api ↩
- Google Cloud「Disk and image pricing」のImage storage:$0.000068493 / GiB・時(730時間で約$0.05 / GiB・月)。https://cloud.google.com/compute/disks-image-pricing ↩
- Google Cloud「Disk and image pricing」:作成・取り込みしたカスタムイメージには保管料がかかる。https://cloud.google.com/compute/disks-image-pricing ↩
- Google Cloud SDK「gcloud compute operations list」。https://docs.cloud.google.com/sdk/gcloud/reference/compute/operations/list ↩
参考文献
Google Cloud
本文の脚注を含め、URLは2026年9月28日に確認しました。料金ページ以外のdocsはdocs.cloud.google.comに移転しています。
- Spot VM:https://docs.cloud.google.com/compute/docs/instances/spot
- Spot VMの料金:https://cloud.google.com/spot-vms/pricing
- Compute Engineの料金(GPUを含む):https://cloud.google.com/products/compute/pricing
- Cloud Billing Catalog APIによる料金の取得:https://docs.cloud.google.com/billing/docs/how-to/get-pricing-information-api
- IAPによるTCP転送:https://docs.cloud.google.com/iap/docs/using-tcp-forwarding
- Private Google Access:https://docs.cloud.google.com/vpc/docs/private-google-access
- Cloud NAT:https://docs.cloud.google.com/nat/docs/overview
- Deep Learning VMのイメージ:https://docs.cloud.google.com/deep-learning-vm/docs/images
- カスタムイメージの作成:https://docs.cloud.google.com/compute/docs/images/create-custom
- インスタンステンプレート:https://docs.cloud.google.com/compute/docs/instance-templates
- マネージドインスタンスグループ:https://docs.cloud.google.com/compute/docs/instance-groups
- Linux VMのstartup-script:https://docs.cloud.google.com/compute/docs/instances/startup-scripts/linux
- Secret Manager:https://docs.cloud.google.com/secret-manager/docs/overview
モデル・データ・ツール
- Llama 4 Scout(Meta):https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct
- 4bit量子化版(bnb-community):https://huggingface.co/bnb-community/Llama-4-Scout-17B-16E-Instruct-bnb-4bit
- データセット
b-mc2/sql-create-context:https://huggingface.co/datasets/b-mc2/sql-create-context - huggingface_hubの環境変数(
HF_HUB_OFFLINEなど):https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables - uvでのPyTorchの導入(
--torch-backend):https://docs.astral.sh/uv/guides/integration/pytorch/
論文
- Hu et al., “LoRA: Low-Rank Adaptation of Large Language Models”, 2021:https://arxiv.org/abs/2106.09685
- Dettmers et al., “QLoRA: Efficient Finetuning of Quantized LLMs”, 2023:https://arxiv.org/abs/2305.14314