ECSでGPUを扱う nvidia-docker2編

過去にAWSの公式手順を参照する以下の記事を書いたのですが、nvidia-dockerがv2になって進化していたので更新します。

[ECSでGPUを扱うためのAMI作成 by Amazon Linux Deep Learning AMI](https://qiita.com/shogomuranushi/items/8a128b35592ce9a363a7)

# なぜ公式の方法ではなくnvidia-dockerを使いたいのか
それにはまず以下の記事でnvidia-dockerとは何をしているのかを知りましょう。

[nvidia-dockerは何をしているのか](https://qiita.com/toshitanian/items/8aaca6b867099ebd442d)

dockerからGPU扱う時には、volumeを指定してマウントしたり、deviceオプションで適切なdeviceを渡してあげるか、privilegedでホスト上のプロセスとほとんど同じレベルでホストへのアクセスを許可するか（AWS公式に記載されている方法）をする必要があります。

上記記事の通り、nvidia-dockerはGPUの情報を抽象化してdockerに渡してくれます。
自分でGPUに関連する情報を調べてdockerに渡す必要はありません。

# ECSでnvidia-docker(1系)を使う方法
nvidia-docker(1系)は `docker run` ではなく `nvidia-docker run` というコマンドを実行します。

```
# 通常
$ docker run hogehoge

# nvidia-docker(1系)
$ nvidia-docker run hogehoge
```

簡単にいうと `nvidia-docker` コマンドを使うことで `docker` コマンドをwrapしてくれるイメージですね。その際にGPUに関するオプションが引き渡されています。

つまり、 `nvidia-docker` コマンドを叩くことでdockerからGPUを扱えるようになります。

ECSは（恐らく）ecs-agentがdockerコマンドを発行していて、もちろんnvidia-dockerには対応してないので、それを解決する方法が公式のvolumeマウントとprivilegedの付与になります。

ということで、 nvidia-docker(1系)はECSでは使えません。

# ECSでnvidia-docker(2系)を使う方法

## nvidia-docker2のリリース

nvidia-docker2というのが、昨年秋頃に発表されました。
これはdockerのruntimeに対応したものになります。runtimeってなにってのは見てもらった方が早いと思います。こんな感じでdockerのruntimeオプションで指定します。

```
# nvidia-docker(1系)
nvidia-docker run --rm nvidia/cuda nvidia-smi

# nvidia-docker(2系)
docker run --runtime=nvidia --rm nvidia/cuda nvidia-smi
```

## runtimeオプションをECSに渡せるのか

よし、じゃあruntimeオプションをECSのtask definitonに渡してあげれば良いのか！
と思いきや、まだECSではruntimeオプションはサポートしていませんでした

[Feature Request: Add support for --runtime parameter for docker run #1084](https://github.com/aws/amazon-ecs-agent/issues/1084)

そして解決方法もこちらに書いてありました。
`default-runtime` を `nvidia` にしてあげればにしてあげれば良いんですね。

## よし早速nvidia-docker2をECS Optimize AMIにインストールだ！・・・
となりましたが、まだ超えなければならない山がありました。

nvidia-docker2のrepoを入れる際の対象OSがubuntu16.04とCentOS/Redhut7系でした。
https://nvidia.github.io/nvidia-docker/

そして、我らがECS Optimize AMIはAmazonLinux（CentOS6系ベース）です。使えません。
悲しかった。

最近AmazonLinux2が出てCentOS7系をベースにしてるのかもしれませんが、まだプレビュー段階なので、Ubuntu16.04で作ることにしました。

# Ubuntu16.04にnvidia-docker2を入れる
- Ubuntuは2018年2月時点のAWS公式最新版を使います
  - `Ubuntu Server 16.04 LTS (HVM), SSD Volume Type`
- 起動後に以下のコマンドを実行します
- nvidia-smiコマンドはGPUインスタンスでないとエラーが出ます。p2.xlargeとかで起動して実行しましょう

## nvidia-docker2インストール
```
sudo apt-get update

# 最新のdockerを入れる
sudo apt-get remove -y docker docker-engine docker.io

sudo apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt-get update
sudo apt-get install -y docker-ce
sudo docker run hello-world

# nvidia-docker2をインストールする
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | \
  sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/ubuntu16.04/amd64/nvidia-docker.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update

sudo apt-get install -y nvidia-docker2
sudo pkill -SIGHUP dockerd

# runtimeオプションで試す
sudo docker run --runtime=nvidia --rm nvidia/cuda nvidia-smi

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

sudo systemctl restart docker

# runtimeオプション無しで試せたら成功
sudo docker run --rm nvidia/cuda nvidia-smi
```

## ecs-agentのインストール
- 公式手順を参考にecs-agentをインストールします
    - [Amazon Linux ではない EC2 インスタンスに Amazon ECS コンテナエージェントをインストールするには](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-agent-install.html)

```
## Install ecs-agent
### Set iptables rules
sudo echo 'net.ipv4.conf.all.route_localnet = 1' | tee -a /etc/sysctl.conf
sudo sysctl -p /etc/sysctl.conf
sudo iptables -t nat -A PREROUTING -p tcp -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679
sudo iptables -t nat -A OUTPUT -d 169.254.170.2 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 51679

### Write iptables rules to persist after reboot
sudo mkdir -p /etc/iptables/
sudo iptables-save | sudo tee /etc/iptables/rules.v4

### Create directories for ECS agent
sudo mkdir -p /var/log/ecs /var/lib/ecs/data /etc/ecs

### Write ECS config file
sudo tee /etc/ecs/ecs.config <<EOF
ECS_DATADIR=/data
ECS_ENABLE_TASK_IAM_ROLE=true
ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true
ECS_LOGFILE=/log/ecs-agent.log
ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs"]
ECS_LOGLEVEL=info
ECS_CLUSTER=default
EOF

### Write systemd unit file
sudo tee /etc/systemd/system/docker-container@ecs-agent.service <<EOF
[Unit]
Description=Docker Container %I
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStartPre=-/usr/bin/docker rm -f %i
ExecStart=/usr/bin/docker run --name %i \
--restart=on-failure:10 \
--volume=/var/run:/var/run \
--volume=/var/log/ecs/:/log \
--volume=/var/lib/ecs/data:/data \
--volume=/etc/ecs:/etc/ecs \
--net=host \
--env-file=/etc/ecs/ecs.config \
amazon/amazon-ecs-agent:latest
ExecStop=/usr/bin/docker stop %i

[Install]
WantedBy=default.target
EOF

sudo systemctl enable docker-container@ecs-agent.service
sudo systemctl start docker-container@ecs-agent.service
sudo systemctl stop docker-container@ecs-agent.service
sudo rm -rf /var/lib/ecs/data/*
```

# これで完成
これでdockerのruntimeがnvidiaになって、デフォルトでnvidia-docker2経由でdockerが実行され、ECSから通常通りdockerコマンドが起動されてもnvidia-docker2経由になリます。

つまり、ECSからnvidia-docker2が扱えるようになりました！！

公式の方法でも実現出来るのですが、より抽象化されるのは実装が楽になっていきますね。

ただ、この状態だと全部が全部nvidia-docker経由になるため、ECS側でもruntimeオプションを指定出来るとより柔軟な環境が作れるかと思います。
あと、早い段階でCentOS7、Ubuntu16.04ベースのECS Optimize AMIが出ることを期待します！