
NVIDIAの自動運転強化学習基盤「AlpaGym」を触ってみた
はじめに
自動運転シリーズの第3部です。第1部でAlpamayoに録画映像の先を予測させ、第2部でシミュレータAlpaSimの中を実際に走らせました。今回はそのシミュレータを学習ループに組み込み、強化学習(RL)を回します。
今回は公式ドキュメント(ONBOARDING.md)に沿って、環境構築からスモークテスト、10ステップだけの短い学習までを体験してみました。
先に結論を書くと、
- 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サイクルが最後まで通りました。ログから流れを追うと、
- rollout側がAlpaSimでシーンを2回試走(各シミュレーション6.17秒・22制御ステップ、いずれも無衝突)
- 報酬0.652を計算(このときの試走のprogressは約0.68。そこからGT距離ぶんをわずかに引いた値)
- policy側がGRPOの勾配更新を1ステップ実行(22ミニバッチ)
- チェックポイントを保存
- 更新済み重み約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です。









