AWS Batch環境でのAPV-MCTSのセルフプレイによる強化学習をGPU化してみた

前回の記事で作成したAPV-MCTSのセルフプレイによる強化学習のAWS Batch環境GPU化して動かしてみました。

今回のコード

下記、タグv0.0.2になります。

github.com

環境の概要

全体構成は下記です。

f:id:nihma:20190209234629p:plain

前回の構成から下記の変更を行いました。

  • nvidia-docker2に対応したAMIの作成を行うCodeBuildを追加
  • セルフプレイの新モデル作成時のトレーニングをGPUに対応
  • AWS Batchのコンピューティング環境で使うAMIとインスタンスを変更

GPU化の実装

具体的な実装内容です。ちなみに環境作成手順はbashからマークダウン形式に変更しdocs/setup_batch.mdに移動しました。

nvidia-docker2に対応したAMIの作成を行うCodeBuildを追加

Packerを使ってDeep Learning AMI(ami-08a7740ff4d3fd90f)をベースにnvidia-docker2に対応したAMIを作成するCodeBuildを追加しました。

CodeBuildでPackerを使ってのAMIの作成方法は下記を参考にしました。

aws.amazon.com

nvidia-docker2に対応したAMIの作成方法は下記を参考にしました。

docs.aws.amazon.com

ただ、Deep Learning AMI/usr/local/cuda/version.txtが複数行になっていたのでconfigure-gpu.shGet CUDA versioncathead -n 1に変更しました。

以下、コードです。

CodeBuildの作成手順は下記です。Packerで必要になるAMI作成場所のサブネットID*1などを環境変数で渡すように指定しています。

setup_batch.md

cat <<EOF > Source.json
{
  "type": "CODECOMMIT",
  "location": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/heta-reversi",
  "buildspec": "buildspec_ami.yml"
}
EOF
cat <<EOF > Artifacts.json
{
  "type": "NO_ARTIFACTS"
}
EOF
cat <<EOF > Environment.json
{
  "type": "LINUX_CONTAINER",
  "image": "aws/codebuild/docker:18.09.0",
  "computeType": "BUILD_GENERAL1_SMALL",
  "environmentVariables": [
    {
      "name": "AWS_REGION",
      "value": "ap-northeast-1",
      "type": "PLAINTEXT"
    },
    {
      "name": "AWS_SUBNET_ID",
      "value": "${SUBNET_ID}",
      "type": "PLAINTEXT"
    }
  ]
}
EOF
aws codebuild create-project --name test-batch-ami \
                               --source file://Source.json \
                               --artifacts file://Artifacts.json \
                               --environment file://Environment.json \
                               --service-role ${ROLE_AMI_BUILD_ARN}
CODEBUILD_ID=$(aws codebuild start-build --project-name test-batch-ami --source-version ${BRANCH} | tr -d "\n" | jq -r '.build.id')
echo "started.. id is ${CODEBUILD_ID}"
while true
do
  sleep 10s
  STATUS=$(aws codebuild batch-get-builds --ids "${CODEBUILD_ID}" | tr -d "\n" | jq -r '.builds[].buildStatus')
  echo "..status is ${STATUS}."
  if [ "${STATUS}" != "IN_PROGRESS" ]; then
    if [ "${STATUS}" != "SUCCEEDED" ]; then
      echo "faild."
    fi
    echo "done."
    break
  fi
done
AMI_IDS=$(aws ec2 describe-images \
    --owner self \
    --filters "Name=tag:USE,Values=heta-reversi" | jq -r ".Images[].ImageId")
AMI_ID="${AMI_IDS[0]}"

CodeBuildで実施するAMI作成処理は下記です。今回はAMIが1つしかない前提でのAMI ID取得*2になっているためAMI作成前に過去に作成済みのAMIを削除するようにしてあります。*3

buildspec_ami.yml

version: 0.2

phases:
  pre_build:
    commands:
      - curl -qL -o packer.zip https://releases.hashicorp.com/packer/1.3.3/packer_1.3.3_linux_amd64.zip && unzip packer.zip
      - ./packer validate amazon-linux_packer-template.json
  build:
    commands:
      - ./delete_amis.sh
      - ./packer build amazon-linux_packer-template.json
  post_build:
    commands:
      - echo "done $(date)"

過去のAMIを削除する処理は下記です。今回の作成AMIにはタグUSEheta-reversiを付与するようにしてあるため削除の目印にしてあります。

delete_amis.sh

#!/bin/bash

aws ec2 describe-images \
    --owner self \
    --filters "Name=tag:USE,Values=heta-reversi" | jq -r ".Images[].ImageId" | while read -r AMI_ID
do
  aws ec2 deregister-image --image-id ${AMI_ID}
done

AMIを作成するPackerのTemplateは下記です。インスタンスタイプはGPUインスタンスp2.xlargeを使うようにしました。

amazon-linux_packer-template.json

{
    "variables": {
        "aws_region": "{{env `AWS_REGION`}}",
        "aws_subnet_id": "{{env `AWS_SUBNET_ID`}}",
        "aws_ami_name": "amazon-linux_heta-reversi"
    },

    "builders": [{
        "type": "amazon-ebs",
        "region": "{{user `aws_region`}}",
        "instance_type": "p2.xlarge",
        "subnet_id": "{{user `aws_subnet_id`}}",
        "ssh_username": "ec2-user",
        "ami_name": "{{user `aws_ami_name`}}",
        "ami_description": "heta-reversi's Amazon Linux",
        "source_ami": "ami-08a7740ff4d3fd90f",
        "ssh_pty": true,
        "tags": {
            "USE": "heta-reversi"
        }
    }],

    "provisioners": [
        {
            "type": "file",
            "source": "configure-gpu.sh",
            "destination": "/home/ec2-user/configure-gpu.sh"
        },
        {
            "type": "shell",
            "inline": [
                "bash ./configure-gpu.sh",
                "sudo rm -rf /var/lib/ecs/data/ecs_agent_data.json"
            ]
        }
    ]
}

PackerのTemplateのprovisionersで実行するnvidia-docker2の設定処理は下記です。dockerのdefault-runtimeにnvidiaを指定してnvidia-docker2が有効になるようにしています。

configure-gpu.sh

#!/bin/bash

# Install ecs-init, start docker, and install nvidia-docker 2
sudo yum install -y ecs-init
sudo service docker start
DOCKER_VERSION=$(docker -v | awk '{ print $3 }' | cut -f1 -d"-")
DISTRIBUTION=$(. /etc/os-release;echo ${ID$VERSION_ID})
curl -s -L https://nvidia.github.io/nvidia-docker/${DISTRIBUTION}/nvidia-docker.repo | \
  sudo tee /etc/yum.repos.d/nvidia-docker.repo
PACKAGES=$(sudo yum search -y --showduplicates nvidia-docker2 nvidia-container-runtime | grep ${DOCKER_VERSION} | awk '{ print $1 }')
sudo yum install -y ${PACKAGES}
sudo pkill -SIGHUP dockerd

# Get CUDA version
CUDA_VERSION=$(head -n 1 /usr/local/cuda/version.txt | awk '{ print $3 }' | cut -f1-2 -d".")

# Run test container to verify installation
sudo docker run --privileged --runtime=nvidia --rm nvidia/cuda:${CUDA_VERSION}-base nvidia-smi

# Update Docker daemon.json to user nvidia-container-runtime by default
sudo tee /etc/docker/daemon.json <<EOF
{
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    },
    "default-runtime": "nvidia"
}
EOF

sudo service docker restart

セルフプレイの新モデル作成時のトレーニングをGPUに対応

現在のモデルをベースにセルフプレイで生成した棋譜データを使って学習している部分をGPUで行うようにしました。変更を行なったらCodeBuildを実行してECRのdocker imageを更新します。

以下、コードです。

まずGPUを使う箇所を局所化するためにgpu_deviceをグローバル定義しました。*4この値を目印にGPU処理にするか判断します。

main.py: L26

gpu_device = -1

学習を行う_create_new_modelGPU化します。学習の実施前後でモデルをto_gpuGPUモードにしてto_cpuでCPUモードに戻しています。

main.py: L549-L573

    def _create_new_model(self, steps_list, epoch_num = None, batch_size = 2048):
        epoch_num = default_params['dual_net_trainer_create_new_model_epoch_num'] if epoch_num is None else epoch_num

        model = DualNet()
        model.load(self.model_filename)

        if gpu_device >= 0:
            cuda.get_device(gpu_device).use()
            model.to_gpu(gpu_device)

        optimizer = chainer.optimizers.Adam()
        optimizer.setup(model)
        for i in range(epoch_num):
            x_train, y_train_policy, y_train_value = self._get_train_batch(steps_list, batch_size)
            y_policy, y_value = model(x_train)
            model.cleargrads()
            loss = F.mean_squared_error(y_policy, y_train_policy) + F.mean_squared_error(y_value, y_train_value)
            loss.backward()
            optimizer.update()
            print("[new nodel] epoch: {} / {}, loss: {}".format(i + 1, epoch_num, loss))

        if gpu_device >= 0:
            model.to_cpu()

        return model

棋譜データから学習データを取り出す_get_train_batchです。GPUモードの場合はnumpyでは無くcuda.cupyを使う必要があります。

main.py: L532-L547

    def _get_train_batch(self, steps_list, batch_size):
        batch_x, batch_y_policy, batch_y_value = [], [], []
        for _ in range(batch_size):
            x, y_policy, y_value = self._get_train_random(steps_list)
            batch_x.append(x)
            batch_y_policy.append(y_policy)
            batch_y_value.append(y_value)

        xp = np
        if gpu_device >= 0:
            xp = cuda.cupy

        x_train        = Variable(xp.array(batch_x)).reshape(-1, 4, 8, 8)
        y_train_policy = Variable(xp.array(batch_y_policy)).reshape(-1, 64)
        y_train_value  = Variable(xp.array(batch_y_value)).reshape(-1, 1)
        return x_train, y_train_policy, y_train_value

最後にcreate-model-batchで動かす場合にGPUモードにしています。

main.py: L729-L745

    elif len(args) > 2 and args[1] == 'create-model-batch':
### ...(省略)...
        gpu_device = 0
### ...(省略)...

AWS Batchのコンピューティング環境で使うAMIとインスタンスを変更

AWS Batchで今回のAMIをGPUインスタンスを使うようにしました。

以下、コードです。

コンピューティング環境の定義のimageIdに今回のAMIをinstanceTypesGPUインスタンスを指定します。今回はp2.xlargeにしました。

setup_batch.md

cat << EOF > compute-environment.spec.json
{
    "computeEnvironmentName": "test-compute-environment",
    "type": "MANAGED",
    "state": "ENABLED",
    "computeResources": {
        "type": "EC2",
        "minvCpus": 0,
        "maxvCpus": 4,
        "desiredvCpus": 0,
        "instanceTypes": [
          "p2.xlarge"
        ],
        "imageId": "${AMI_ID}",
        "subnets": ["${SUBNET_ID}"],
        "securityGroupIds": ["${DEFAULT_SECURITY_GROUP_ID}"],
        "instanceRole": "${INSTANCE_ROLE_ARN}"
    },
    "serviceRole": "${ROLE_SERVICE_ARN}"
}
EOF
COMPUTE_ENV=$(aws batch create-compute-environment --cli-input-json file://compute-environment.spec.json)

動かした結果

結論は前回と同じで4時間かかり学習時間が短縮することはありませんでした。

GPUが有効に動いていないことが原因ではないかと今回のAMI/インスタンス/docker imageでMNISTのサンプルを動かしてみましたがきちんと動作しているようです。GPUは有効だと思われます。

ChainerでGPUを使ってみよう - TIM Labs

chainer/examples/mnist at v5 · chainer/chainer · GitHub

バグがある可能性や今回のケースではあまりGPUの効果がない可能性、バッチサイズを増やしたりパラメータを変更したら効果がある可能性、など疑っていますが調査中です... 🤔

まとめ

今回はAPV-MCTSのセルフプレイによる強化学習のAWS Batch環境GPU化してみました。残念ながら学習時間は前回と同じで短縮できませんでしたがPackerの使い方を知ることができてGPU環境も作ることができました。引き続き調査と流石にそろそろコードが限界なのでリファクタリングをしたいです。

*1:今回はAWS Batchが動くサブネットと同じにしました

*2:${AMI_IDS[0]}

*3:過去のAMIを残すようにする場合はAMIの名前が被らないようにする対応も必要と思います

*4:今回はお試し前提、リファクタあとでやる前提でグローバル変数にしました。。