Docker(19.03)でgpu有効化してpytorchで訓練するまでやる(Ubuntu18.04)

#はじめに
Dockerの解説記事は多くありますが、情報が結構ばらばらで実際に機械学習できるようになるまでには、ネットの海を漂い続けなければなりませんでした。
またDockerのバージョンが新しめなせいもあってか、以前のバージョン通りではうまく行かないことも多々ありました。
今回、dockerのインストールからcudaイメージ、pytorchを使った機械学習まで通してやっていきます。
参考になれば幸いです。

#環境
ubuntu 18.04
Geforce 1080ti
nvidia driver 430.26

#dockerインストール
まずはdockerのインストールからです。
[docker公式](https://docs.docker.com/install/linux/docker-ce/ubuntu/)に従ってコマンドを打ち込んでいきます。

```
$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ 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 docker-ce docker-ce-cli containerd.io
```
上から順に実行していき、最後にバージョンを確認します。

```
$ docker version
>Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:29:11 2019
 OS/Arch:           linux/amd64
 Experimental:      false
```
最後にdockerコマンドにsudoをつけるのがめんどくさい人は以下を実行して再起動します。
ただしdockerに権限を与えてしまうので自己責任で。

```
$ sudo usermod -aG docker __username__
```

###参考
[Ubuntuにdockerをインストールする](https://qiita.com/tkyonezu/items/0f6da57eb2d823d2611d)

#CUDAイメージをプル

最初からpytorchをいれても動く可能性はありますが、まずcudaイメージをいれて、gpuを認識できるか確認しましょう。
cudaイメージをダウンロードします。(もしかするとこの章自体いらないかもしれないです。次の章のpytorchプルをしちゃってください)

```
$ docker pull nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04
```

`$ docker pull nvidia/cuda`だと最新版が入りますが後のpytorchのベースイメージに合わせました。
そして`nvidia-smi`でgpuを確認します。

```
$ docker run --rm --gpus all nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 430.26       Driver Version: 430.26       CUDA Version: 10.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 00000000:17:00.0 Off |                  N/A |
| 23%   28C    P8     9W / 250W |      2MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
```


ちなみに多くのサイトで書いてある、runオプションの`runtime`は自分の環境ではエラーが出てできませんでした、
[これ](https://github.com/NVIDIA/nvidia-docker/issues/838#issuecomment-558962338)にも書いてあるとおり、その代わりに`--gpus`を使うようです。

###エラーが出た人へ
```
docker: Error response from daemon: linux runtime spec devices: could not select device driver "" with capabilities: [[gpu]].
```
もし上記のようなエラーが出たとき、ありがたいことに解決策書いてる方がいらっしゃったので紹介します。
[New Docker CLI API Support for NVIDIA GPUs under Docker Engine 19.03.0 Pre-Release](https://collabnix.com/introducing-new-docker-cli-api-support-for-nvidia-gpus-under-docker-engine-19-03-0-beta-release/)

場所はどこでもいいのでshファイルを作ります。

```
$ gedit nvidia-container-runtime-script.sh
```
そのshファイルに以下を打ち込み、実行します。

```:nvidia-container-runtime-script.sh
curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | \
  sudo apt-key add -
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list
sudo apt-get update
```

実行

```
$ sh nvidia-container-runtime-script.sh
$ sudo apt-get install nvidia-container-runtime
$ docker run -it --rm --gpus all ubuntu nvidia-smi
```
無事にgpuが認識されていたら成功です!

###参考
[dockerでgpuコンテナの作成](https://qiita.com/uni-3/items/c9480d7e177e29b1316c)

#pytorchをビルド

https://github.com/pytorch/pytorch
まずは上のgitからクローンします。
クローンしたら、pytorchディレクトリに移動してビルドをします。

```
docker build -t pytorch -f docker/pytorch/Dockerfile .
```
結構時間がかかりますが、気長に待ちましょう。
終わったら確認してみます。

```
docker images
REPOSITORY          TAG                             IMAGE ID            CREATED             SIZE
pytorch             latest                          47ea93f4ed87        　　　　　　         6.12GB
nvidia/cuda         10.0-cudnn7-devel-ubuntu16.04   b739d7317c55        　　　　　　         3.12GB
```
pytorchのイメージがあればokです。(作成日時は抜きました。)
###(追記)
別にbuildしなくてもpullするだけでよかった
`docker pull pytorch/pytorch`


#コンテナを作成して起動
これでやっと学習のためのコンテナを作成できます。

```
$ docker run -it --gpus all -v マウントしたい場所:docker側の場所 --shm-size 8G --name ml pytorch
```
コマンドの説明をします。
`docker run`はイメージからコンテナを作成し、起動をします。

`-it`はそのままコンテナ内でbashを起動してくれます。つまり、runをしてそのままコンテナ内で作業することができます。

`-v`はホストpcのディレクトリをdockerにマウントします。
例えばホストPC側で`~/ML/`が機械学習のディレクトリであるとき、`-v ~/ML:/ML`とすることで、マウントをすることができ、好きなエディタで編集して実行する事ができます。

`--shm-size`はコンテナに割り当てるメモリの量です。今回は8GB割り当てています。

`--name`はそのコンテナの名前です。起動やログイン時に使います。

起動が成功するとbashの前に`root@0f5c51d62772`のようなコンテナidが付きます。

#学習
先程の例のように`/ML/`に`train.py`を置いて学習させます。
コードは[Pytorchチュートリアル](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py)を参考にしたものです。

```python:train.py
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim


transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net().cuda()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

for epoch in range(10):
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data
        inputs, labels = inputs.cuda(), labels.cuda()
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        if i % 2000 == 1999:
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')
```
　
`python train.py`で訓練が始まったら成功です!

#おまけ
コンテナに入るときはそのコンテナが起動していないといけません。
コンテナが起動状態を見るには`docker ps -a`で確認できます。
起動は`docker start コンテナの名前`、入るときは`docker attach コンテナの名前`を使います。
コンテナから抜けるときは`ctrl P Q`を同時に押します。