AWSDeepRacerのローカルトレーニング 2021版(DeepRacer For Cloud) を最速で走らせてみる

2021.02.20

AWS事業本部梶原@福岡オフィスです。AWS DeepRacerではMentaiko-DevelopersIOで参加しています。

AWS DeepRacerには昨年のre:Inventはオンライン大会の決勝にでたんですが、残念ながら初戦敗退してしまいました。 しかしながら先月の1月の2021 Pre-seasonでは、裏技テレポートして3位表彰台に上がってしまいました。(本選ではダメだよ 裏技まで使ったのにまたしても目の前にはぐぬぬぬ

気を取り直して、今年も本シーズンに向けてすでに予選が始まってます!
そして、今月上位10%以内に入るとPROリーグ選出、なんとAWSDeepRacerオリジナルジャケットが貰えるそうです!

公式通知 https://d1.awsstatic.com/deepracer/2021AWSDeepRacerPreSeasonTermsandConditions.pdf

WINNER DETERMINATION AND PRIZES: The top 10% of racers with the lowest total time will be announced on March 5, 2021 and will be promoted to the Pro division of the AWS DeepRacer League, described further below, and will win a DeepRacer racing jacket (the “Prize”).

ということで?、AWS DeepRacerを一番速く走らせれるのは僕なんだ!といきたいところですが
ちょっと現状世界の壁が厚いので

その前に、AWS上でAWS DeepRacerのローカルトレーニングを一番速く立ち上げてみたいと思います。

待望の?スポットインスタンスで CloudFormation一撃化 S3バケットもRoleもつけといた!
cloud-configで、AWS DeepRacer for Cloud のリポジトリももってきて、初期化処理までおまけだ!、もってけ!

しています。もうエンジンがかからないなんて言わせない。

「Developers, start your engines. 」

※※※ 注意 ※※※

去年に続きお約束です。

無料利用枠の対象外のリソースを作成しますので課金が発生します。トレーニングも通常長い時間がかかりますので、実際にトレーニングを行う場合はスポットインスタンスのご利用を強くお勧めします。

今回はスポットインスタンス起動込みのCloudFormationテンプレートを提供しています

また、今回ご紹介する、EC2でのAWS DeepRacer のローカルトレーニング方法はAWS ではサポートされていませんのでAWSへの問合せなどはお控えください。

https://aws.amazon.com/jp/deepracer/faqs/?nc1=h_ls

Q: AWS Cloud ではなく自分のコンピューターで、モデルをローカルにトレーニングすることはできますか?
現在のところ AWS DeepRacer ではローカルトレーニングはサポートされていません。

困った場合は AWS DeepRacer Community: https://deepracing.io/ などで(自分も参加しています)助けてもらえる事があるかもしれませんが、あくまで自己責任で実施ください。

また、2月は特に問題はないように見えますが、今後も含めて、ローカルトレーニングで作成したモデルで今後も参加できることは保証できません。

こんな状況なのですが、AWS DeepRacer Community の有志の方々でローカルトレーニングは日々、修正メンテされており、ほんと頭が上がりません、AWS DeepRacer Communityの皆さんにこの場を借りてお礼申し上げます。コロナ過が落ち着いたらラスベガスにお土産持っていきます。

AWS DeepRacerとは

おそらく、このページが必要になってる方は説明不要だと思うので詳細割愛します。

私見ですが、

車が好きでレースに参加していたら、気が付くと機械学習に詳しくなってラスベガスに行けてしまう?そんなサービスです。

Developsers.IOの特集カテゴリもございますので、ご参考ください

AWS DeepRacer – 特集カテゴリー –
https://dev.classmethod.jp/referencecat/aws-deepracer/

ローカルトレーニングとは

通常 AWS DeepRacerはAWSのコンソール上から、お手軽にモデルのトレーニングを開始、レースに参加できます。(無料利用枠もあります)

AWS DeepRacerは、AWSのマネージドサービスSageMaker, RoboMaker, S3, Kinesis等を組み合わせてAWSのコンソール上で実施できるようにサービスが提供されています。 AWS DeepRacerの構築内容などはAWSから公開されています。それを単独の環境(EC2やローカルのPCなど)でモデルのトレーニングを実行できるように有志によって作成されたのがローカルトレーニングになります。

主なモチベーションはコストメリットになりますが、やはりマネージドサービスを使わないことによるトレードオフが発生します。具体的にはメモリ不足、ストレージの空き容量などのハード面の管理、個人的に一番大きいと思っているのがモデルの履歴管理を自分でしないといけません。これがSageMakerやAWS DeepRacerのコンソールはよくできており、ローカルトレーニングをやっているとすぐモデルやハイパーパラメータが迷子になってしまいます。

とはいえ、ローカルトレーニング環境を触っていると、各サービスの連携やSageMaker,RoboMakerの実装方法などに詳しくなれます。そして自由にカスタマイズ(壊すこともよくありますが)もできます。

EC2でのAWS DeepRacerのローカルトレーニング環境構築

環境作成にあたり

AWS DeepRacer Community のローカルトレーニングdeepracer-for-cloud

https://aws-deepracer-community.github.io/deepracer-for-cloud/

の情報をもとに構築を行っています。

使用するリポジトリ

AWS DeepRacerのローカルトレーニングのリポジトリはいくつかありますが、以下のリポジトリを使用しています

https://github.com/aws-deepracer-community/deepracer-for-cloud

作成するリソース

S3バケット

項目 備考
バケット名 deepracer-for-cloud-${UniqueId}

ここに、モデル等がアップロードされます また、トレーニング中も使用されます

セキュリティグループ

項目 備考
名前 deepracer-for-cloud-sg
SSH 22 入力された接続元のみで制限
ビューア用 8100 入力された接続元のみで制限
ログ解析用 8888 入力された接続元のみで制限

EC2用のRole

項目 備考
S3 FullAccess 作成したS3バケットへの読み書き
AmazonKinesisVideoStreams FullAccess KinesisVideoStreamsへの読み書き
SSM AmazonSSMManagedInstanceCore 基本不要ですがメンテナンス用です

EC2インスタンス

項目 備考
AMI AWS Deep Learning AMI (Ubuntu 18.04) Version 40.0 (ami-0423232d1433f88e6) 米国東部 (バージニア北部)us-east-1 以外は別の値を指定してください
ディスク容量 150GB 必要に応じて拡張してください
EIP aaa.bbb.ccc.ddd EIPアドレスを割り当てています
インスタンスタイプ c5.2xlarge GPU使用したい場合はG系のインスタンスを選択ください
Role S3, Kinesisへの権限をつけたRoleを付与しています 作成したモデルをアップロードするために必要です。

スポットインスタンスで起動します。デフォルト設定ではオンデマンド価格設定のため、スポット価格の高騰にはご注意ください

環境構築

CloudFormatin 一撃

下記のリンクをポチっとして、スタックをデプロイするだけで、米国東部 (バージニア北部)us-east-1 で DeepRacer for cloud の初期設定まで終わったスポットインスタンスが起動します 初期化に少々時間がかかるので10分程度はお待ちください

クイックスタック起動

テンプレートはこちら 各パラメータは下記です。

項目 備考
ImageId ami-072519eedc1730252 Ubuntu 18.04 米国東部 (バージニア北部)以外は別の値を指定してください
InstanceType c5.2xlarge gpuでしたいときはg4dn.2xlarge
KeyName キーペアを指定してください
SpotPrice スポットインスタンスの価格を設定してください 指定なしはオンデマンド価格です
VolumeSize 150 S3にモデルは置かれるので十分だとおもいますが必要に応じて拡張を
SourceIp xx.xx.xx.xx/xx アクセス制限もとのIPアドレスを指定してください

環境初期設定

SSHで対象のインスタンスに接続して、以下コマンドを実行していきます。 AWS Deep Learning AMI (Ubuntu 18.04) を使用しているので ユーザ名は 'ubuntu'です。

作成したキーペアでSSHでアクセスできることを確認してください

なお、DeepRacer for Cloud で案内されているInstall時のコマンドはcloud-initで実施しており そのなかで、Dockerやらドライバやらやらすべて実施されます

git clone https://github.com/aws-deepracer-community/deepracer-for-cloud.git
cd deepracer-for-cloud && ./bin/prepare.sh

そう、今回、環境構築はありません。ないんです! AWS DeepRacer Community の皆さんのおかげです。

なお、S3バケットを作ったり、Roleを作ったり、SGを作ったり、スポットインスタンスを作ったり、
cloud-configで初期化したりするCloudFormationの部分はちょっぴりほめてくれる方がいれば嬉しいです。

トレーニングの開始

てことで 一旦、報酬関数やハイパーパラメータ、トラックなどは変更せずに動作確認をおこなってみてください。

初回は起動は時間がかかる、また起動に失敗することもありますので、シミュレーション画面が見れなかった場合や、トレーニングが開始してない場合は一度トレーニングを終了し再度開始してください。

特にエラーが発生せず、ログが進みだして、シミュレーションが開始されれば成功です。

カスタムファイルのアップロード

$ cd deepracer-for-cloud/
$ dr-upload-custom-files 
Uploading files to s3://deepracer-for-cloud-XXXXXXXX/custom_files/
upload: custom_files/hyperparameters.json to s3://deepracer-for-cloud-XXXXXXXX/custom_files/hyperparameters.json
upload: custom_files/model_metadata.json to s3://deepracer-for-cloud-XXXXXXXX/custom_files/model_metadata.json
upload: custom_files/reward_function.py to s3://deepracer-for-cloud-XXXXXXXX/custom_files/reward_function.py

なお、dr-xxxコマンドが出ない場合は

$ source bin/activate.sh

で、読み込まれます

トレーニング開始

$ dr-start-training

省略
## Stop physics after creating graph
## Creating session
Creating regular session
Checkpoint> Saving in path=['./checkpoint/agent/0_Step-0.ckpt']

ビューアー表示

$ dr-start-viewer 
Creating service deepracer-0-viewer_proxy

シミュレーション画面の確認

ブラウザでインスタンスに割り当てたパブリックIPにアクセスしてみます

http://aaa.bbb.ccc.ddd:8100

にアクセスしてみてください。

トレーニングの終了

以下コマンドを実行するとトレーニングが終了します

$ dr-stop-training

各種パラメータの設定

ここまでいけば、アクション、報酬関数、ハイパーパラメータ、トラックなどを変更してトレーニングを実施してみてください

詳しくは https://aws-deepracer-community.github.io/deepracer-for-cloud/reference.html

をご参照ください

作成したモデルのインポート

トレーニング中、今回作成したS3バケットを使用しているので、インポート時はそのバケットを指定します 特に変更しなければ、最終トレーニングしたものは

s3://deepracer-for-cloud-XXXXXXXX/rl-deepracer-sagemaker/

に保存されます。 ベストモデルなどをアップロードしたい場合は内容を変更してください

まとめ

ちょっと違う意味の最速でしたがいかがでしょうか?いい感じに活用してください。 なお、まだプレシーズンなので、ローカルトレーニングもこのまま無事動いて完走できることはないと思っています。
困った場合はAWS DeepRacerのコミュニティで声をかけてください。日本語チャンネルもあります。 では、仮想サーキットでお会いできるのを楽しみにしております。

おまけ

実は自宅PCでも以下Specで24H耐久レースでトレーニングをしています。今年はCPUを変更したいです。

項目 備考
OS Ubuntu 18.04
CPU Intel CPU Core i7-6700
Memory 48GB
GPU Nvidia GPU RTX2060 Super

参考情報

AWS DeepRacer Community Wiki - Local Training https://wiki.deepracing.io/Local_Training

AWS DeepRacer Community Wiki https://wiki.deepracing.io/Main_Page

deepracer-for-cloud https://aws-deepracer-community.github.io/deepracer-for-cloud/

テンプレートファイル

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  ImageId:
    Description: Enter the AWS Deep Learning AMI (Ubuntu 18.04) AMI for the us-east-1 region.
    Type: String
    Default: ami-072519eedc1730252

  InstanceType:
    Description: EC2 instance of type G3, G4, P2 or P3 - recommendation is g4dn.2xlarge - for GPU enabled training. C5 or M6 types - recommendation is c5.2xlarge - for CPU training.
    Type: String
    AllowedValues:
      - g3s.xlarge
      - g3.4xlarge
      - g4dn.xlarge
      - g4dn.2xlarge
      - g4dn.4xlarge
      - p2.xlarge
      - p3.2xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
    Default: c5.2xlarge

  KeyName:
    Description: The name of the key pair.
    Type: AWS::EC2::KeyPair::KeyName
    Default: drfc-ec2-keypair

  SpotPrice:
    Description: The maximum price per unit hour that you are willing to pay for a Spot Instance. The default is the On-Demand price.
    Type: String
    Default: ""

  VolumeSize:
    Description: The size of the volume, in GiBs.
    Type: Number
    Default: 150
    MinValue: 1
    MaxValue: 16384

  SourceIp:
    Description: Specify the CIDR block of the access source.
    Type: String
    Default: 192.168.1.1/32
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})

Conditions:
  SpotPrice: !Not [!Equals [!Ref SpotPrice, ""]]

Resources:
  DRfCS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub
        - deepracer-for-cloud-${UniqueId}
        - UniqueId: !Select [0, !Split ['-', !Select [2, !Split [/, !Ref 'AWS::StackId']]]]

  DRfCSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: deepracer-for-cloud-sg
      GroupName: deepracer-for-cloud-sg
      SecurityGroupIngress:
        - Description: access to ssh
          CidrIp: !Ref SourceIp
          FromPort: 22
          IpProtocol: tcp
          ToPort: 22
        - Description: access to viewer
          CidrIp: !Ref SourceIp
          FromPort: 8100
          IpProtocol: tcp
          ToPort: 8100
        - Description: access to log-analysis
          CidrIp: !Ref SourceIp
          FromPort: 8888
          IpProtocol: tcp
          ToPort: 8888

  DRfCInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      Policies:
        - PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - s3:*
                Resource:
                  - !Sub 'arn:${AWS::Partition}:s3:::${DRfCS3Bucket}'
                  - !Sub 'arn:${AWS::Partition}:s3:::${DRfCS3Bucket}/*'
                Effect: Allow
          PolicyName: s3-deepracer-bucket-policy
      Path: /
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore'
        # - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonVPCReadOnlyAccess'
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonKinesisVideoStreamsFullAccess'
        # - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchFullAccess '
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action: sts:AssumeRole

  DRfCInstanceRoleProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref DRfCInstanceRole

  DRfCLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        InstanceMarketOptions:
          MarketType: spot
          SpotOptions:
            InstanceInterruptionBehavior: stop
            MaxPrice: !If [SpotPrice, !Ref SpotPrice, !Ref 'AWS::NoValue']
            SpotInstanceType: persistent
      LaunchTemplateName: DRfCLaunchTemplate

  DRfCInstance:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: !Ref VolumeSize
            VolumeType: gp3
      IamInstanceProfile: !Ref DRfCInstanceRoleProfile
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      KeyName: !Ref KeyName
      LaunchTemplate:
        LaunchTemplateId: !Ref DRfCLaunchTemplate
        Version: !GetAtt DRfCLaunchTemplate.LatestVersionNumber
      SecurityGroupIds:
        - !GetAtt DRfCSecurityGroup.GroupId
      Tags:
        - Key: Name
          Value: DRfC
      UserData: !Base64
        Fn::Sub: |
          #cloud-config
          repo_update: true
          repo_upgrade: all
          runcmd:
            - su - ubuntu -c "cd /home/ubuntu; git clone https://github.com/aws-deepracer-community/deepracer-for-cloud.git"
            - su - ubuntu -c "sed -i -e 's/^\DR_LOCAL_S3_BUCKET=bucket/DR_LOCAL_S3_BUCKET=${DRfCS3Bucket}/' /home/ubuntu/deepracer-for-cloud/defaults/template-system.env"
            - su - ubuntu -c "cd /home/ubuntu/deepracer-for-cloud; ./bin/prepare.sh"
          output : { all : '| tee -a /var/log/cloud-init-output.log' }

  DRfCEIP:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref DRfCInstance
      Tags:
        - Key: Name
          Value: DRfC

  CancelSpotInstanceRequestsLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse
          def handler(event, context):
            try:
              print(event)
              params = dict([(k, v) for k, v in event['ResourceProperties'].items() if k != 'ServiceToken'])
              client = boto3.client('ec2')
              if event['RequestType'] == 'Create':
                response = client.describe_instances(**params)
                print(response)

                SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId']
                responseData = {}
                responseData['SpotInstanceRequestId'] = SpotInstanceRequestId
                cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, SpotInstanceRequestId)

              if event['RequestType'] == 'Delete':
                response = client.describe_instances(**params)
                print(response)

                SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId']
                response = client.cancel_spot_instance_requests(
                    SpotInstanceRequestIds=[
                        SpotInstanceRequestId,
                    ],
                )
                print(response)

                responseData = {}
                responseData['SpotInstanceRequestId'] = SpotInstanceRequestId
                cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, SpotInstanceRequestId)

              if event['RequestType'] == 'Update':
                old_params = dict([(k, v) for k, v in event['OldResourceProperties'].items() if k != 'ServiceToken'])
                response = client.describe_instances(**old_params)
                print(response)

                SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId']
                response = client.cancel_spot_instance_requests(
                    SpotInstanceRequestIds=[
                        SpotInstanceRequestId,
                    ],
                )
                print(response)

                response = client.describe_instances(**params)
                print(response)
                
                SpotInstanceRequestId = response['Reservations'][0]['Instances'][0]['SpotInstanceRequestId']
                responseData = {}
                responseData['SpotInstanceRequestId'] = SpotInstanceRequestId
                cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, SpotInstanceRequestId)

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, {})
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt AmazonEC2FullAccessRoleforLambda.Arn
      Runtime: python3.6
      Timeout: 120

  AmazonEC2FullAccessRoleforLambda:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/AmazonEC2FullAccess"

  CancelSpotInstanceRequests:
    Type: Custom::CancelSpotInstanceRequests
    Properties:
      ServiceToken: !GetAtt CancelSpotInstanceRequestsLambda.Arn
      InstanceIds: 
        - !Ref DRfCInstance

Outputs:
  DRfCS3BucketConsole:
    Value: !Sub "https://console.aws.amazon.com/s3/buckets/${DRfCS3Bucket}"
  DRfCEIP:
    Value: !Ref DRfCEIP
  DRfCViewerURL:
    Value: !Sub "http://${DRfCEIP}:8100/"
  DRfCLogAnalysisURL:
    Value: !Sub "http://${DRfCEIP}:8888/"