個人ロボット研究環境をAWS Spotに乗せる ― lerobot-aws-infra 構築記

IoT / ロボティクス

この記事について

SO-ARM101というロボットアーム向けに、LeRobot(Hugging Face製の模倣学習フレームワーク)で 方策(ACT/SmolVLAなど)を学習させている。今後は現在発注中のローカルのBTO PC(RTX 5060 Ti 16GB)で 学習できるようになる予定だが、「もっと大きいデータセット・長いステップ数で回したくなったときに、AWSへ スケールアップできる選択肢を持っておきたい」というのが、このリポジトリlerobot-aws-infraの 出発点だった。

個人開発・研究用途なので、運用コストと構築の手間を最小限にしたい。そのための設計判断と、 実際にデプロイしてみて踏んだ地雷の記録を、ブログ記事風にまとめておく。


設計思想:なぜこの形にしたか

IaCはTerraformのみ、構成管理ツールは使わない

AnsibleのようなConfiguration Managementツールは意図的に採用していない。EC2インスタンスの 初回セットアップは、すべてuser_data(cloud-init)経由の1本のbashスクリプトで完結させる。 理由はシンプルで、個人開発規模でAnsibleを足すと学習コストとメンテナンスコストの方が 「構成管理ツールを分離するメリット」を上回ってしまうから。

Immutable Infrastructure:インスタンスは使い捨て

学習のたびにEC2インスタンスを新規作成し、終わったらterraform destroyで消す。パッチを 当てて使い回すという発想を最初から捨てている。これによって「環境がじわじわ汚れていく」 問題が構造的に起きなくなる。

これを実現するには、当然ながらインスタンスを消しても失われて困るものをインスタンスの 外に出しておく必要がある。それが次の設計判断につながる。

永続データはS3、ただし「一時領域」と「恒久領域」を混同しない

当初の設計は「学習の出力ディレクトリを丸ごと定期的にS3へaws s3 syncする」というシンプルな ものだった。10分(sync_interval_sec)ごとに同期しておけば、Spotインスタンスが中断されて 消えても、直近10分未満の進捗しか失わない。次のインスタンスが起動したら、S3から続きを 取ってきて--resume=trueで再開する。

ただし構築を進める中で、ここに一つ認識のズレがあることに気づいた。「学習が終わったら S3はもう要らないのでは?」という疑問が出たときに、コードを見返すと確かにlerobot-train 自体が学習完了時にpolicy_repo_idへモデルをHuggingFace Hubへ自動push (push_to_hubはデフォルトTrue)する仕組みになっていた。つまりS3のチェックポイントは Spot中断からのresume専用の一時領域であり、モデルの恒久的な置き場所はHuggingFace Hub の方だった。

この整理がついたことで、「学習完了を検知したらS3のチェックポイントを消してよい」という 判断がクリアになった。ただし「消す条件」には慎重になる必要がある。次で触れる。

terraform destroyとS3削除は、あえて紐付けない

学習完了を自動検知してapply→destroyを回すオーケストレータースクリプト (train_and_destroy.sh)には、destroy後にS3のチェックポイントを削除する処理を入れた。 ただしこの削除ロジックはterraform destroyコマンド自体には紐付けていない

理由は、terraform destroyという操作単体では「学習が完了したから片付けている」のか 「ハイパラミスに気づいて中断している」のか区別がつかないから。もし中断のケースで S3のデータまで自動的に消えてしまったら、再開に必要な唯一のデータを失うことになる。 削除は「学習完了マーカー(_COMPLETE)がS3上に存在すること」を機械的に確認できた 文脈でのみ行う。手動でdestroyだけした場合はS3にデータが残る仕様にして、片付けたければ cleanup_s3_checkpoint.shという独立したヘルパーを能動的に呼んでもらう形にした。

安全側に倒すなら「消し忘れて少し残る」方が「消してはいけないものを消す」よりずっとマシ、 という判断。

永続データ用バケットは、使い捨てインフラのstateに混ぜない

これは構築の途中で見つかった設計の抜けだった。当初dataset_bucket_nameのS3バケットは 「事前に手動で作っておく前提、リポジトリのコードでは作らない」という扱いだった。

しかし「なぜTerraform管理じゃないのか」と聞かれて考え直すと、理由になっていなかった。 このリポジトリには既に、Terraform state保存用バケットを本体terraform/とは別のTerraform statebootstrap/)で管理するパターンが存在していた。理由は「本体は学習ごとに terraform destroyされる使い捨てインフラなので、永続データ用のリソースを同じstateに 混ぜると誤destroyの事故リスクを抱える」から。この理屈は、データセットバケットにも そのまま当てはまる。そこでbootstrap/にデータセットバケットも移し、prevent_destroy ライフサイクルを付けて保護した。

SSM Session Manager優先、SSHは明示設定時のみ

SSHポートを常時開けておくのはセキュリティ的に避けたい。デフォルトはAWS Systems Manager Session Manager経由の接続とし、SSHはallowed_ssh_cidrを明示的に設定した場合のみ有効化する。 IAMロールにAmazonSSMManagedInstanceCoreを付与しておけば、ポートを一切開けずに ブラウザ/CLIからシェルアクセスできる。


実際にデプロイしてみて踏んだ地雷

設計がどれだけきれいでも、実際にterraform applyを叩くと現実の壁にぶつかる。今回の デプロイで実際に遭遇した問題を時系列で振り返る。この手の記録は、次回同じ轍を踏まないための 一番実用的なドキュメントだと思っている。

1. Spot容量不足(InsufficientInstanceCapacity)

最初に選んだg6.xlarge(L4 GPU)が、狙ったAZでSpot容量不足になり起動できなかった。 aws ec2 get-spot-placement-scoresというAPIで、インスタンスタイプ×AZごとに「今どれくらい 確保しやすいか」のスコア(1〜10)を事前に見られることを知り、これを使ってAZを選び直した。 ただしSpot容量は時間帯で変動するため、一度良かったAZが数時間後にはまた埋まっていた。 最終的には、コストも安く容量にも余裕があるg4dn.xlarge(T4 GPU)に切り替えて解決した。 ACTでの学習であればT4の16GBメモリでも十分だった。

2. Spotのサービスクォータが極端に小さい(MaxSpotInstanceCountExceeded)

AWSアカウントのデフォルトでは、GPU系Spotインスタンスのクォータ(All G and VT Spot Instance Requests)がわずか4vCPU程度しかないことがある。g6.xlargeg4dn.xlargeも ちょうど4vCPUなので、常に「クォータぴったり1台分」の余裕ゼロ運用になる。terraform apply でインスタンスを再作成(destroy→create)すると、AWS側のクォータ会計処理が反映されるまでの 一瞬の間にcreateが走ってしまい、失敗することが何度もあった。対策は「数十秒〜数分待って 再実行」で、自動化ループ側にはsleepを1行足して緩和した。恒久対策としてはクォータの 引き上げ申請が有効。だが個人のアカウントとしては事故を防ぐ意味でもあまり増やしたくはないものだ。

3. IAMインスタンスプロファイルの伝播遅延

IAMロール/インスタンスプロファイルを作成した直後にEC2を起動しようとすると、EC2側への 権限反映が間に合わずエラーになることがある。これはTerraform AWS providerが自動リトライ してくれるため、実害としては「applyが数分Still creating...のまま止まって見える」程度で 実務上は問題にならなかった。

4. Ubuntuのawscliパッケージが解決できない

apt-get install awscliが「no installation candidate」で失敗した。Ubuntuのawscliは universeリポジトリに依存しており、環境によっては解決できない。AWS公式のCLI v2インストーラ (zip配布)に切り替えて解決した。set -euxで書いていたスクリプトだったため、この1行の 失敗でセットアップ全体がその場で停止していた。

5. Anacondaのデフォルトチャンネル利用規約

conda createが「Terms of Serviceに同意していない」というエラーで失敗した。Anacondaの デフォルトチャンネル(pkgs/mainpkgs/r)は最近利用規約への同意が必須になっており、 conda tos acceptを事前に実行する必要があった。

6. Python バージョンの不一致

LeRobotの最新版はpython>=3.12を要求するが、当初condaの環境はpython=3.10で作っていた。 pip install -e ".[training]"が依存解決の途中で「Python 3.10.20は3.12以上という要件に 合わない」と失敗した。

7. training extraの中身を誤解していた

最初lerobot[train,dataset]というextrasを指定していたが、trainというextraは存在しない (pipは存在しないextra名を警告だけで無視して先に進んでしまう)。実際のpyproject.tomlを 確認すると、正しくはtrainingextraで、これがdataset一式・wandbaccelerateを まとめて含んでいた。存在しないextra名を指定しても静かに無視されるだけ、というのは 盲点だった。

8. NVIDIAドライバの反映には再起動が要る

ubuntu-drivers autoinstallでドライバを入れても、nvidia-smiが実際に有効になるには 再起動が必要な場合がある。当初は同じ起動シーケンス内でsystemctl enable --nowして 即座に学習を始めていたが、これだとドライバ未反映のまま学習が始まるリスクがあった。 セットアップ完了後にshutdown -r +1で1分後に自動再起動し、再起動後にsystemdが 学習サービスを自動起動する形に直した。

9. データセットバケットが実在しなかった

dataset_bucket_nameは「事前に存在している前提」でコードには作成処理がなかった。実際に デプロイしてみたら、そのバケット自体が存在しておらず、S3への同期がすべて静かに失敗していた (|| trueでエラーを握りつぶす設計だったため、この失敗はログを注意深く見ないと気づけない)。 これが前述の「bootstrap/にデータセットバケットも含める」という設計見直しのきっかけになった。

10. /home/ubuntuのパーミッションでSSM接続時にPermission denied

SSM Session Managerで接続してtail -f /home/ubuntu/train.logしようとしたら Permission deniedになった。Ubuntuのホームディレクトリはデフォルトで750(他ユーザーは トラバース不可)で、SSMのデフォルトユーザーはそこに入れない。sudoを付ければ解決する、 という単純な話だが、初見だとファイルの権限を疑ってしまい少し時間を使った。


コストの目安

  • g6.xlarge(us-east-1・オンデマンド): 約$1.17/時間、Spotなら概ね40〜70%引き
  • g4dn.xlarge(us-east-1・オンデマンド): 約$0.53/時間、Spot容量にも余裕があることが多い
  • ACTのような比較的軽量なモデルであれば、GPUメモリ16GB(T4)でも十分こなせる

まとめ

設計そのものは「使い捨てインフラ+S3同期+resume」というシンプルな組み合わせで、 アイデアの筋は最初から大きく変わっていない。一方で実際にデプロイしてみると、 Spot容量・サービスクォータ・OSパッケージの可用性・ライブラリのバージョン要件・ ファイルパーミッションといった、設計図だけでは見えない「現実の摩擦」が次々に出てきた。

個人開発でクラウドの使い捨てインフラを組むときは、こういう類の地雷を1つずつ踏みながら 潰していく前提で計画しておくのがよさそうだ、というのが今回の一番の学びだった。

コメント

タイトルとURLをコピーしました