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つあります。以下の通りです:

  1. 組織ポリシーでVMに外部IPを付けられない
  2. 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を立て、学習前と学習後のモデルを同じ条件で評価

全体の構成。IAP = Identity-Aware Proxy、PGA = Private Google Access(いずれも節3で説明)

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に出た成果物とログの実数で実行の成否を確認

学習データの作り方、評価指標の定義、判定の方法は本記事の範囲外です。

注

  1. Google Cloud「Spot VMs」:Compute Engineは容量を取り戻すため、Spot VMをいつでも回収(preempt)しうる。https://docs.cloud.google.com/compute/docs/instances/spot ↩
  2. LoRA論文(Hu et al., 2021)§4.1。重みの更新をW + (α/r)·BAの形で持ち、Bを0で初期化する。 ↩
  3. config.jsonのtext_config:hidden_size 5120、num_attention_heads 40、num_key_value_heads 8、head_dim 128、num_hidden_layers 48。https://huggingface.co/bnb-community/Llama-4-Scout-17B-16E-Instruct-bnb-4bit/blob/main/config.json ↩
  4. LoRA論文§7.1(Table 5):同じパラメータ予算ならq/vの両方に当てるのがよい。§7.2(Table 6):rは1〜8でも十分な場合がある。 ↩
  5. 1層あたり16 × (5120 × 16384 + 8192 × 5120) ≒ 20.1億、48層で約966億。総パラメータ約1,087億(節4)の89%。 ↩
  6. config.jsonのnum_experts_per_tok 1。transformersのLlama4TextMoeは、これに加えて全トークンが通るshared_expertを持つ。https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama4/modeling_llama4.py ↩
  7. PEFT「LoRA」の「Targeting nn.Parameter directly」: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 ↩
  8. Google Cloud「Free Google Cloud features and trial offer」:無料トライアルの請求先アカウントでは、VMへのGPUの追加とクォータの増加申請ができない。https://docs.cloud.google.com/free/docs/free-cloud-features ↩
  9. Google Cloud「Create projects」:プロジェクトIDは恒久的で変更できない。https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects ↩
  10. 現在は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)。 ↩
  11. Google Cloud「Create, edit, or delete budgets and budget alerts」:アラートのみの予算は利用額に上限を設けず、しきい値を超えてもサービスの利用や課金を止めない。https://docs.cloud.google.com/billing/docs/how-to/budgets ↩
  12. Google Cloud「Create, edit, or delete budgets and budget alerts」:リソースの利用からCloud Billingへの反映までには遅れがある。https://docs.cloud.google.com/billing/docs/how-to/budgets ↩
  13. Google Cloud「Spot VMs」:Spot VMは、プリエンプティブ枠が付与されていないリージョンでは通常枠を消費する。付与された後は、そのリージョンではプリエンプティブ枠しか使えない。https://docs.cloud.google.com/compute/docs/instances/spot ↩
  14. Google Cloud「Allocation quotas」の「Commitment and committed resource quotas」。https://docs.cloud.google.com/compute/resource-usage ↩
  15. Google Cloud SDK「gcloud quotas preferences list」。https://docs.cloud.google.com/sdk/gcloud/reference/quotas/preferences/list ↩
  16. Google Cloud「Bucket locations」:性能のため、データはそれを読む計算資源と同じ場所に置くよう勧めている。同じロケーション内の転送は無料で、リージョンをまたぐと転送料がかかる(Google Cloud「Cloud Storage pricing」https://cloud.google.com/storage/pricing)。https://docs.cloud.google.com/storage/docs/locations ↩
  17. Google Cloud「How Application Default Credentials works」:VMに付けたSAの認証情報はメタデータサーバから取得され、この方法が推奨されている。https://docs.cloud.google.com/docs/authentication/application-default-credentials ↩
  18. 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)。 ↩
  19. 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 ↩
  20. Google Cloud「Use IAP for TCP forwarding」。https://docs.cloud.google.com/iap/docs/using-tcp-forwarding ↩
  21. Google Cloud「VPC firewall rules」:defaultネットワークにはあらかじめdefault-allow-sshなどの規則が入っている。https://docs.cloud.google.com/firewall/docs/firewalls ↩
  22. Google Cloud「Private Google Access」:内部IPしか持たないVMから、GoogleのAPIとサービスの外部IPに到達できる。https://docs.cloud.google.com/vpc/docs/private-google-access ↩
  23. Google Cloud「Network pricing」のCloud NAT。https://cloud.google.com/vpc/network-pricing ↩
  24. Google Cloud「Cloud NAT overview」:Cloud NATゲートウェイは、NATを必要とするサブネットのリージョンに作る。https://docs.cloud.google.com/nat/docs/overview ↩
  25. 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日時点)。 ↩
  26. Google Cloud「Troubleshooting resource availability errors」:このエラーはクォータとは無関係で、別のゾーンで試すか、時間を置いて再試行するよう案内している。https://docs.cloud.google.com/compute/docs/troubleshooting/troubleshooting-resource-availability ↩
  27. 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。 ↩
  28. Google Cloud「Spot VMs」:終了時の動作がSTOPならVMは停止(TERMINATED)し、付いていた永続ディスクは残る(保管料はかかる)。https://docs.cloud.google.com/compute/docs/instances/spot ↩
  29. Google Cloud「Handle GPU host maintenance events」:GPU付きのVMはライブマイグレーションできないため、ホストメンテナンス時に停止する設定にする必要がある。https://docs.cloud.google.com/compute/docs/gpus/gpu-host-maintenance ↩
  30. Google Cloud「Create custom images」:ディスクへの書き込みを止めるため、イメージを作る前にVMを停止するよう勧めている。https://docs.cloud.google.com/compute/docs/images/create-custom ↩
  31. 実際のエラー: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 ↩
  32. Google Cloud「View and query VM metadata」:メタデータサーバへの問い合わせにはMetadata-Flavor: Googleヘッダーが要る。https://docs.cloud.google.com/compute/docs/metadata/querying-metadata ↩
  33. Google Cloud「Spot VMs」の「Preemption process」。https://docs.cloud.google.com/compute/docs/instances/spot ↩
  34. Google Cloud「Run shutdown scripts」:VMの停止・削除・Spotの回収のいずれでもshutdown-scriptは起動する。https://docs.cloud.google.com/compute/docs/shutdownscript ↩
  35. --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)。 ↩
  36. Google Cloud「Compute Engine instance lifecycle」:RUNNINGは、VMが起動処理中か稼働中であることを示す。https://docs.cloud.google.com/compute/docs/instances/instance-lifecycle ↩
  37. Google Cloud「Method: instanceGroupManagers.resize」:resizeの操作は、VMの追加・削除を予定した時点で完了(DONE)になる。実際の削除の進み具合は別に確かめる必要がある。https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceGroupManagers/resize ↩
  38. Google Cloud「Get Google Cloud pricing information」。https://docs.cloud.google.com/billing/docs/how-to/get-pricing-information-api ↩
  39. Google Cloud「Disk and image pricing」のImage storage:$0.000068493 / GiB・時(730時間で約$0.05 / GiB・月)。https://cloud.google.com/compute/disks-image-pricing ↩
  40. Google Cloud「Disk and image pricing」:作成・取り込みしたカスタムイメージには保管料がかかる。https://cloud.google.com/compute/disks-image-pricing ↩
  41. 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に移転しています。

モデル・データ・ツール

論文