ローカル環境のDockerコンテナからSageMakerの学習ジョブをlocalモードで実行する

## はじめに
* ローカル環境のDockerコンテナからSageMakerの学習ジョブをlocalモードで実行したときのメモ。

## 動機
* ローカルにGPUを積んでいるマシンが一応あるので、最初はローカルで学習を回して様子を見つつ、いける！となったら並行に学習ジョブを回すため&学習結果を管理するためにSageMakerを使いたかった。

## 前提
* SageMakerのトレーニングジョブをlocalモードで実行する際には、docker-composeによって作成されたdockerコンテナ上で実行される。

## 構築手順
* requirements.txt準備

```bash:requirements.txt
jupyter==1.0.0
opencv-python==4.0.1.24
pillow==6.2.0
docker-compose==1.24.1
awscli==1.16.279
boto3==1.10.15
sagemaker==1.43.4.post1
papermill==1.2.1
```

* Dockerfile準備

```dockerfile:Dockerfile
FROM 520713654638.dkr.ecr.ap-northeast-1.amazonaws.com/sagemaker-pytorch:1.1.0-cpu-py3

ARG PYTHON_PASS=/usr/bin/python3.6
ENV GIT_SSL_NO_VERIFY=1

# apt install
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
         curl \
         git \
         vim \
         groff-base # awscli

# pip install
COPY requirements.txt /tmp/
RUN $PYTHON_PASS -m pip install --upgrade pip && \
    $PYTHON_PASS -m pip install -r /tmp/requirements.txt

# install docker
RUN curl https://get.docker.com | sh

# make dir
RUN mkdir -p /opt/config \
             /opt/data/raw \
             /opt/data/interim \
             /opt/data/processed \
             /opt/data/external \
             /opt/features \
             /opt/logs \
             /opt/models \
             /opt/notebooks \
             /opt/reports \
             /opt/scripts \
             /opt/utils

# cd
WORKDIR /opt

# expose port
EXPOSE 8888
```

* コンテナ起動

```bash:run.sh
#!/bin/bash

# usage
SCRIPT_FILE=`basename $0`
function usage()
{
  echo "usage: ${SCRIPT_FILE} docker/nvidia-docker" 1>&2
}

# arguments
if [ $# -ne 1 ]; then
  usage
  exit 1
fi
DOCKER_CMD=$1

# constant
SCRIPT_DIR=$(cd $(dirname $0); pwd)
LOG_FILE=${SCRIPT_DIR}/../logs/docker.log
CONTAINER_NAME=sagemaker_pytorch_1_1_0_local
CONTAINER_IMAGE=sagemaker_pytorch_1_1_0_local
HOST_PORT=28888
CONTAINER_PORT=8888

# log
function log() {
  local fname=${BASH_SOURCE[1]##*/}
  echo "$(date '+%Y-%m-%dT%H:%M:%S') ${PROCNAME} (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $@" | tee -a ${LOG_FILE}
}

# main
log "info start script"
log "info CONTAINER_NAME: ${CONTAINER_NAME}, CONTAINER_IMAGE: ${CONTAINER_IMAGE}, HOST_PORT: ${HOST_PORT}, CONTAINER_PORT: ${CONTAINER_PORT}"

${DOCKER_CMD} run --name ${CONTAINER_NAME} \
  --privileged \
  --entrypoint bash \
  -v $(pwd)/../config:/opt/config \
  -v $(pwd)/../data:/opt/data \
  -v $(pwd)/../features:/opt/features \
  -v $(pwd)/../logs:/opt/logs \
  -v $(pwd)/../models:/opt/models \
  -v $(pwd)/../notebooks:/opt/notebooks \
  -v $(pwd)/../reports:/opt/reports \
  -v $(pwd)/../scripts:/opt/scripts \
  -v $(pwd)/../utils:/opt/utils \
  -p ${HOST_PORT}:${CONTAINER_PORT} \
  -it ${CONTAINER_IMAGE}
if [ $? -gt 0 ]; then
  log "error docker run"
fi
```

```bash:コンテナ起動
$ docker build ./ -t sagemaker_pytorch_1_1_0_local
$ run.sh nvidia-docker
```

* 学習ジョブ実行

```python:pytorch_tutorial_cifar10_sagemaker.ipynb
# データ準備
import sagemaker

sagemaker_session = sagemaker.Session()

bucket = sagemaker_session.default_bucket() # ex. 'sagemaker-us-east-2-xxxxxxxxxxxx'

role = sagemaker.get_execution_role() # ex. arn:aws:iam::xxxxxxxxxxxx:role/service-role/...

# We use the sagemaker.Session.upload_data function to upload our datasets to an S3 location. 
# The return value inputs identifies the location -- we will use this later when we start the training job.
inputs = sagemaker_session.upload_data(path='../data/raw', bucket=bucket, key_prefix='data/raw/cifar10') # ex. s3://sagemaker-us-east-2-xxxxxxxxxxxx/data/raw/cifar10

# インスタンスタイプ設定
import subprocess

instance_type = 'local'
# instance_type = 'ml.m5.large'

if subprocess.call('nvidia-smi') == 0:
    ## Set type to GPU if one is present
    instance_type = 'local_gpu'
    
print("Instance type = " + instance_type)

# 学習コンテナ起動
from sagemaker.pytorch import PyTorch

# estimator
hyper_param = {
    'workers': 2,
    'epochs':2,
    'batch_size': 4,
    'lr': 0.001,
    'momentum': 0.9,
}

cifar10_estimator = PyTorch(entry_point='../models/cifar10_sagemaker.py',
                            hyperparameters=hyper_param,
                            role=role,
                            framework_version='1.1.0',
                            train_instance_count=1,
                            train_instance_type=instance_type)

cifar10_estimator.fit(
    inputs=inputs,
)

```


## ハマったこと
はじめは、 docker run 時に -v /var/run/docker.sock:/var/run/docker.sock によってソケットをマウントし、ホストの Docker エンジンを用いて Docker コンテナを起動しようとした。が、失敗した。（下記エラー）

```
No such file or directory: '/opt/ml/input/config/resourceconfig.json'
```

どうやら、トレーニングジョブの実行に必要な /opt/ml/input/config/resourceconfig.json は、トレーニングジョブ開始を指示するコンテナ内で作成されるようで、localモードにて開始するコンテナでは、 /opt/ml/input/ に /tmp 配下の一時ディレクトリをマウントするが、ホストのDockerエンジンを用いてDockerコンテナを起動するために、ホストの /tmp 配下の一時ディレクトリをマウントすることとなり、結果として、トレーニングジョブ開始を指示するコンテナ内に作成された resourceconfig.jsonを取得できず、エラーが発生しているらしい。
よって、今回は上記のようにDockerコンテナ内で実行されるDockerエンジンを利用した。