NVIDIAの自動運転強化学習基盤「AlpaGym」を触ってみた

NVIDIAの自動運転強化学習基盤「AlpaGym」を触ってみた

自動運転の強化学習基盤AlpaGymをAWS L40S 2枚で動かし、環境構築からRLサイクルの実行、10ステップの学習までを体験してみました。スモークテストで約10分のRLループが回り、報酬の推移から「衝突すると-9台、無衝突で0.6台」と学習信号が数値に刻まれる様子が見えました。
2026.07.19

はじめに

自動運転シリーズの第3部です。第1部でAlpamayoに録画映像の先を予測させ、第2部でシミュレータAlpaSimの中を実際に走らせました。今回はそのシミュレータを学習ループに組み込み、強化学習(RL)を回します。
今回は公式ドキュメント(ONBOARDING.md)に沿って、環境構築からスモークテスト、10ステップだけの短い学習までを体験してみました。

https://dev.classmethod.jp/articles/nvidia-autonomous-driving-vla-alpamayo-aws/
https://dev.classmethod.jp/articles/nvidia-alpasim/

先に結論を書くと、

  • AlpaGym(AlpaSim+Cosmos-RLによるclosed-loop RL基盤)が、AWSのL40S 2枚で動きました。rollout(試走)→報酬計算→勾配更新→重み同期、のRLサイクル1周がスモークテストで約10分です。
  • 10ステップの学習を完走させ、報酬の推移と最終チェックポイント保存まで確認しました。報酬は無衝突のステップで0.6台、衝突が起きたステップで-9.3〜-9.5。「安全に走れたか」が数値として学習信号に刻まれる様子がそのまま見えました。
  • VRAMは学習側・試走側の2枚とも約45GB。公式要件「40GB以上のGPU×2」はほぼ実測どおりで、48GBのL40Sでギリギリです。

なお、Alpamayoのモデル重みは非商用ライセンス(研究・実験・評価目的)で提供されています。本記事はその範囲での技術評価です。

AlpaGymとは

  • リポジトリ: NVlabs/alpagym(Apache-2.0)
  • 前回使ったシミュレータAlpaSimを「環境」、Cosmos-RL(NVIDIAのRLフレームワーク)を「トレーナー」として組み合わせ、自動運転モデルをclosed-loopで強化学習させるための基盤です。

構成はGPU2枚の分業です。

  • policy側(GPU0): 学習対象のモデルを持ち、rolloutの結果から勾配を計算して重みを更新する
  • rollout側(GPU1): モデルのコピーでAlpaSimの中を実際に走る。走り終えたデータをpolicy側に渡し、更新された重みを受け取りながら走り続ける

つまり毎ステップ、試走・報酬計算・勾配更新・重み同期が一通り回ります(試走と学習はパイプラインで並行して進みます)。前回のAlpaSimは「走らせて採点する」までで終わりでしたが、今回はその採点が重みの更新に使われます。

学習アルゴリズムはGRPOです。同じシーンを複数回走らせて、結果(報酬)の差から「どちらの走りに寄せるか」を決める方式で、LLMの事後学習(RLHF)で使われているものと同じ系統です。ちなみにAlpamayo 1.5の総パラメータは起動ログの実測で約11.1Bですが、実際に更新されるのは約2.28Bで、言語モデル部分は凍結されていました。

報酬: progress_safety

今回使う報酬progress_safetyの定義はリポジトリのconfにあり、中身はシンプルな加重和です。

# packages/host/src/alpagym_host/conf/reward/progress_safety.yaml
terms:
  - kind: metric
    metric_name: progress
    scale: 1.0
  - kind: metric
    metric_name: collision_any
    scale: -10.0
  - kind: metric
    metric_name: offroad
    scale: -5.0
  - kind: distance_to_gt
    scale: -0.01

平たく言うと「ルートを進んだ分を評価し、衝突したら-10、コースアウトしたら-5の減点」です。前回のclosed-loop評価で見たprogressやcollision_anyといった指標が、今度は報酬の材料になっているのが分かります。

環境

項目 内容
インスタンス g6e.12xlarge(NVIDIA L40S 48GB×4、48vCPU、RAM 384GB)
AMI Deep Learning Base OSS Nvidia Driver GPU AMI(Ubuntu 24.04)
ディスク EBS gp3 500GB(300GBで始めて足りず拡張。詰まりどころ④)
使用GPU 4枚中2枚(policy+rollout。残り2枚はアイドル)

公式要件は「40GB以上のVRAMを持つCUDA GPU×2」です。AWSでこれを満たす最小構成を探すと、L40S 4枚箱のg6e.12xlargeになりました。

セットアップ(ONBOARDINGどおり)

手順はリポジトリのdocs/ONBOARDING.mdどおりです。apt依存(CUDAキーリング、cuDNN、バージョン固定のNCCL、redis、git-lfs)を入れ、uv syncで環境を作り、Hugging FaceからAlpamayo 1.5をダウンロードして専用形式に変換します。

git clone https://github.com/NVlabs/alpagym.git
cd alpagym
# apt依存とuv syncはONBOARDING.mdのコマンドをそのまま実行

# モデルの取得と変換
uv run --no-sync python -c "from huggingface_hub import snapshot_download; snapshot_download('nvidia/Alpamayo-1.5-10B', local_dir='./tmp/checkpoints/Alpamayo-1.5-10B')"
uv run --no-sync python packages/policies/alpamayo_r1/scripts/convert_release_to_alpagym_checkpoint.py \
  --input ./tmp/checkpoints/Alpamayo-1.5-10B \
  --output ./tmp/checkpoints/alpamayo-1.5-10B_alpagym_ckpt --overwrite

変換後のディレクトリは17MBしかなく、実体は元のスナップショットへの参照です。ダウンロードした元の21GBは消さないでください(ディスク整理のときにやりかけました)。

詰まりどころ①: uv syncがCUDAバージョン不一致で失敗

uv sync中、依存パッケージgrouped-gemmのソースビルドでこれが出ました。

RuntimeError: The detected CUDA version (13.2) mismatches the version that was
used to compile PyTorch (12.8). Please make sure to use the same CUDA versions.

DLAMIは複数のCUDAツールキットを同居させていて、既定(/usr/local/cuda)が13.2を指している一方、PyTorchはcu12.8ビルドです。ONBOARDINGがNCCLを「+cuda12.8」でバージョン固定しているとおり、この基盤は12.8前提なので、ビルドに使うCUDAを明示して再実行すれば通ります。

export CUDA_HOME=/usr/local/cuda-12.8
export PATH=/usr/local/cuda-12.8/bin:$PATH
uv sync --all-packages

詰まりどころ②: cuDNNの二重インストールでpolicyがクラッシュ

スモーク実行中、モデルの映像エンコーダ(conv3d)でこれが出て落ちました。

RuntimeError: cuDNN error: CUDNN_STATUS_SUBLIBRARY_VERSION_MISMATCH

原因はcuDNNが二重に存在していることでした。ONBOARDINGのapt手順はNCCLをバージョン固定する一方、cuDNNは固定なしでインストールするため、aptの最新(9.24)とuv環境が持つPyTorch用(9.10)が食い違いました。venv側のcuDNNを優先させれば解決します。

export LD_LIBRARY_PATH=$HOME/alpagym/.venv/lib/python3.12/site-packages/nvidia/cudnn/lib:$LD_LIBRARY_PATH

スモークテスト: RLサイクル1周を回す

リポジトリにはローカル2GPU用のスモーク実験が同梱されています。実行は1コマンドです。

uv run --no-sync --all-packages python -m alpagym_host.cli \
  experiment=alpamayo_1_5_local_2gpu_smoke \
  policy.model.path="$(pwd)/tmp/checkpoints/alpamayo-1.5-10B_alpagym_ckpt" \
  reward=progress_safety

詰まりどころ③: 初回はDockerイメージの準備でタイムアウト

初回実行は、AlpaSim側のコンテナイメージ準備(レンダラーのpullとローカルビルドで計20分超)が起動待ち時間を食い潰してTimeoutErrorで落ちました。イメージがキャッシュされた2回目は素直に通るので、初回のタイムアウトは慌てずに再実行してください。前回同様alpasim-base pull access deniedの警告も出ますが無害です(ローカルビルドされるため)。

もう1つ、この手の分散構成では異常終了したプロセスの残骸がGPUメモリをキープしたまま残ることがあります。クラッシュ後に再実行してCUDA out of memoryが出たら、nvidia-smiで使用量を確認し、残骸を止めてから再実行します。

pkill -9 -f alpagym_runtime.cosmos.entrypoint
docker ps -q | xargs -r docker stop

スモークの結果

2回目の実行で、closed-loop RLの1サイクルが最後まで通りました。ログから流れを追うと、

  1. rollout側がAlpaSimでシーンを2回試走(各シミュレーション6.17秒・22制御ステップ、いずれも無衝突)
  2. 報酬0.652を計算(このときの試走のprogressは約0.68。そこからGT距離ぶんをわずかに引いた値)
  3. policy側がGRPOの勾配更新を1ステップ実行(22ミニバッチ)
  4. チェックポイントを保存
  5. 更新済み重み約8.7GBをrollout側へ同期(更新対象の409テンソルだけを転送し、凍結分858テンソルはスキップ)

で全体約10分でした。「走る→採点→学習→重みを配り直す」というRLのサイクルが、そのままログで追えます。なお実行設定はon_policy: Falseで、数ステップ前の重みで作ったrolloutを学習に使うことが許容されています(後述の報酬の見方に関わってきます)。

VRAMはこの間、次のとおりでした。

GPU 使用量
使用中の2枚 44,985MiB / 45,421MiB(policy側とrollout側で1枚ずつ)
残りの2枚 3MiB(未使用)

「40GB以上×2」という公式要件は実測に照らしてかなり正確な数字で、これ未満のGPUに収める余地はなさそうです。

本番: 10ステップ学習して報酬の推移を見る

スモークは1ステップで終わる設定なので、ステップ数だけ増やして報酬の動きを見ます。実験設定に元からあるキーの数値を上書きするだけです(同梱の長時間実験alpamayo_1_5_clrl_test_runも同じキーでステップ数を指定していますが、こちらはSLURMクラスタ前提のため今回は使いません)。

uv run --no-sync --all-packages python -m alpagym_host.cli \
  experiment=alpamayo_1_5_local_2gpu_smoke \
  policy.model.path="$(pwd)/tmp/checkpoints/alpamayo-1.5-10B_alpagym_ckpt" \
  reward=progress_safety \
  cosmos.train.max_num_steps=10 \
  cosmos.train.num_epochs=10 \
  2>&1 | tee b_run_10step.log

詰まりどころ④: ディスクが1ラン100GB超のペースで消えていく

最初の挑戦は300GBのディスクで走らせて、3ステップ目でNo space left on deviceになりました。内訳を調べて分かったのは、

  • チェックポイント保存1回で50GB前後(オプティマイザ状態込みの再開用バンドル。10Bモデルの学習を舐めていました)
  • rolloutの記録(4カメラの映像フレームなど)が1ステップあたり数GB〜10GB

で、実測では10ステップのラン1本が約117GBでした。既存のイメージ・モデル類と合わせると300GBでは構造的に足りず、500GBにオンライン拡張して完走しています(EBS拡張の手順は前回記事と同じ)。

もう1つ落とし穴があって、ランを繰り返すと前のランの記録が積みあがっていきます。私は後日同じ設定をもう1本走らせたのですが、過去ランの残骸で500GBが再び満杯になり、そのランはstep 10の途中で死にました(画面で直接見えたエラーは「空ファイルのJSON読み込み失敗」。回収したログでは、その直前にチェックポイント保存がNo space left on deviceで失敗していました)。連続してランを回すなら、古いランのディレクトリを消してからにしてください。

チェックポイントは既定で「20ステップごと+最終ステップ」の保存なので、10ステップ運転なら保存は最後の1回で済みます。

報酬の推移

完走したランの、controllerログから抜き出した10ステップ分です。

step 報酬 状況(報酬式からの逆算)
1 0.673 無衝突
2 0.655 無衝突
3 0.651 無衝突
4 0.658 無衝突
5 0.643 無衝突
6 0.653 無衝突
7 -9.518 衝突あり
8 -9.499 衝突あり
9 0.671 無衝突
10 0.627 無衝突
  • 0.6台の報酬は、式から見ると「衝突なしのprogressからGT距離の微小項を引いた値」です。実際、このランの衝突しなかった試走のprogressは0.66〜0.71でした。
  • -9台のステップは衝突です。報酬式を思い出すと、ここに届く組み合わせは「progress+衝突ペナルティ-10」しかありません(コースアウトは-5、GT距離項は0.01倍なので、どう足しても-9台にはなりません)。実際、このランのログに残っていた試走の評価を数えると16本中3本がcollision_any: 1.0でした。なおon_policy: Falseの並行実行なので、個々の試走と各stepの報酬の対応はログからは特定できません。表の「衝突あり」は、衝突の報酬が集計に現れたステップという意味です。
  • 同じシーン・同じモデルでも走るたびに衝突したりしなかったりします。先に触れた後日のランでも衝突の報酬が2つのステップ(step 2と6)に現れ、このランのstep 7・8とは別の場所でした(そのランの試走は14本中5本が衝突)。RLの観点ではこのばらつきこそが学習の材料で、衝突した試走としなかった試走の報酬差-10を「衝突しない方向へ」の信号にする、というのがGRPOの原理です。
  • しかしながら、この結果から単純に「学習が進んで運転が上手くなった」とは言えません。シーン1つ・10ステップでは傾向を語るには全然足りず、報酬の平均が上がっていく様子も見えていません。今回はあくまで流れを確認、体験したに留まります。
    学習を重ねてモデルを実際に賢くしていく内容については、また別の記事で検証したいと思います。

数字まわり

項目 実測値
VRAM policy側44,985MiB+rollout側45,421MiB
スモーク(1ステップ)所要 約10分
本番10ステップ 約32分(1ステップあたり約1.5〜3.5分)
重み同期 起動時に全量42GBを約45秒、以後は更新分約8.7GBを毎回3秒弱
ディスク消費 1ラン約117GB(rollout記録+最終チェックポイント)

シミュレーション映像を載せていない理由

前回と同じで、シーンの元になっているNuRecデータセットのライセンスが配布・埋め込みを制限しているためです。rolloutごとの車載カメラ映像(mp4)は生成されており、手元での確認には使えます。見た目のイメージはAlpaSimリポジトリのデモGIFをどうぞ。


Alpamayo-1.5-10Bのモデル重みは非商用ライセンス(研究・実験・評価目的)で提供されています。本記事はその範囲での技術評価です。AlpaGym(NVlabs/alpagym)はApache-2.0です。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事