趣味でディープラーニングするための GPU 環境を安上がりに作る方法

趣味でディープラーニングで遊ぶために GPU マシンを使いたい。

GPU は本当にすごくて、自分の MacBook Air で 2 時間かかるような学習が GPU を使うと 5 分足らずで終わる。CPU だけでディープラーニングするのは馬鹿馬鹿しいという気持ちになる。

しかし自宅に GPU マシンを組んだとしても四六時中計算し続けるわけでもないからもったいないし、ここはクラウドサービスを活用して安く済ませたい。[^zbox]

[^zbox]: 結局 GPU マシンも組んだ： [機械学習用の GPU マシンを組んだ \- ほとラボ](http://blog.hotolab.net/entry/zbox)

さらに、最近では Docker コンテナ内から GPU を利用することができる NVIDIA Docker という Docker プラグインがあり、これを利用することで GPU マシンの環境を汚すことなく好きなだけ学習環境を立ち上げることができる。

今回は Amazon EC2 の GPU インスタンスと NVIDIA Docker を使って趣味用のディープラーニング環境を作ったので、その作り方について書いていく。

**[2018.09 追記]**
新しめのバージョンでやり直したやつを書いたのでコチラも参考にしていただければ。
[Ubuntu 18.04 に NVIDIA Docker 環境構築 (2018年秋) - Qiita](https://qiita.com/hoto17296/items/80ba9c23416991757ab7)

## 書かないこと
以下の事項は詳しく説明しないので、各自調べて欲しい。

- AWS, Amazon EC2 の使い方
- Linux の基本操作
- Docker の基本操作
- ディープラーニングとその関連ツールについて

## インスタンスのスペック
今回は AWS を使ったが、GCP[^gcp] や Azure[^azure] にも GPU インスタンスはあるので好きなのを使えばいいと思う。

今回利用するインスタンスのスペックは以下の通り。

- AMI: Ubuntu Server 16.04 LTS[^ubuntu]
- インスタンスタイプ: p2.xlarge
    - GPU x 1 (NVIDIA Tesla K80[^k80])
    - $1.542/h (東京リージョンの場合)
- ストレージ: SSD (gp2) x 16GB
    - Docker イメージを扱うので 8GB だとおそらく足りない

AWS の GPU インスタンスには現在 G2 と P2 の二種類があり、G2 はグラフィック用途向け / P2 は汎用計算向け となっている。今回はディープラーニングに使いたいので P2 インスタンスの中で一番安い p2.xlarge を利用する。

[Amazon EC2 P2 インスタンス | AWS](https://aws.amazon.com/jp/ec2/instance-types/p2/)

GPU インスタンスは総じて料金が高く、p2.xlarge を起動しっぱなしにすると一ヶ月で10万円以上にもなってしまうので、使うときだけ起動して使い終わったら停止するのを忘れないように気をつけたほうがよい。

[^gcp]: [GPU | Google Cloud Platform](https://cloud.google.com/gpu/)
[^azure]: [Azure N-Series: General availability on December 1 | Blog | Microsoft Azure](https://azure.microsoft.com/en-us/blog/azure-n-series-general-availability-on-december-1/)
[^ubuntu]: 個人的な印象だが、ディープラーニングでは Ubuntu で動かすのがデフォというツールが多い気がしていて、特に理由がなければ Amazon Linux とかよりも Ubuntu を使ったほうがハマらなさそう
[^k80]: この NVIDIA Tesla K80 という GPU は普通に買うと 70万円以上するのでこの価格帯で利用できるのすごい

### ドライバインストール済み AMI もある、が
GPU ドライバインストール済み AMI が NVIDIA 公式から提供されている[^nvidia-ami]のでこちらを利用してもいいかもしれない。

[^nvidia-ami]: [AWS Marketplace: NVIDIA](https://aws.amazon.com/marketplace/seller-profile?ref=cns_srchrow&id=c568fe05-e33b-411c-b0ab-047218431da9)

しかし現時点では Amazon Linux / Ubuntu 14.04 / Windows Server という選択肢しかなく、今回は Ubuntu 16.04 が使いたかったため普通の Ubuntu AMI にドライバを入れて利用することにした。

## Docker をインストールする
Ubuntu への Docker のインストール方法は公式ドキュメントにまとまっているので割愛する。

[Get Docker for Ubuntu | Docker Documentation](https://docs.docker.com/engine/installation/linux/ubuntu/)

### 作業ユーザを docker グループに追加する
Docker のソケットファイルにアクセスするためには root もしくは docker グループのユーザである必要があるので、作業ユーザを docker グループに追加する。(ログインし直すと反映される)

``` console
# usermod -aG docker $USER
```

### 自動起動設定 & 起動
Systemd のいつものやつ。

``` console
# systemctl enable docker
# systemctl start docker
```

## GPU のドライバをインストールする
NVIDIA の公式サイトからドライバを検索できる。[^nv-driver]

[^nv-driver]: [Download Drivers | NVIDIA](http://www.nvidia.com/Download/index.aspx?lang=en-us)

[NVIDIA DRIVERS Tesla Driver for Ubuntu 16.04](http://www.nvidia.com/download/driverResults.aspx/118962/en-us)

AWS の P2 インスタンスで使われている Tesla K80 の Ubuntu 16.04 用のドライバは 375 というやつっぽい。

そのままこれをダウンロードしてインストールしてもいいが、Ubuntu の場合は apt からでもインストールすることができる。

``` console
# apt install nvidia-375
```

### インストールできたかどうか確認する
`nvidia-smi` コマンド[^nvidia-smi]を使って確認できる。

``` console
$ nvidia-smi
Wed Jun  7 01:35:15 2017
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 375.66                 Driver Version: 375.66                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla K80           Off  | 0000:00:1E.0     Off |                    0 |
| N/A   33C    P8    32W / 149W |      0MiB / 11439MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```

`Tesla K80` がひとつ認識できているので、無事ドライバをインストールできたと思われる。

[^nvidia-smi]: `nvidia-smi` コマンドは NVIDIA デバイスの管理インタフェースとのこと： [nvidia-smiコマンドの詳細ついて - Qiita](http://qiita.com/miyamotok0105/items/1b34e548f96ef7d40370)

## NVIDIA Docker をインストールする
NVIDIA Docker の概要については、リポジトリの README にある画像を見るとなんとなくわかる。

[NVIDIA/nvidia-docker: Build and run Docker containers leveraging NVIDIA GPUs](https://github.com/NVIDIA/nvidia-docker)

_(2018/03/12) いつの間にか v2.0 が出て v1.0 が Deprecated になっていたので v2.0 のインストール方法に修正した。_

[公式の Installation](https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(version-2.0)) に従ってインストールする。
※ 既に NVIDIA Docker v1.0 が入っている場合は削除する必要がある。

``` console
$ 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
# apt update
# apt install nvidia-docker2
# pkill -SIGHUP dockerd
```

## コンテナ内から GPU を使ってディープラーニングしてみる
以上までで GPU インスタンスへの NVIDIA Docker の導入が完了したので、ここからは実際にコンテナを立てて利用してみる。

今回は Keras (バックエンドは TensorFlow) が動かせるイメージを作成し、そのイメージのコンテナ内で Keras のサンプルコードを実行する。

### Keras が使えるイメージをビルドする
以下の Dockerfile を作成してビルドする。

``` Dockerfile:Dockerfile
FROM nvidia/cuda:8.0-cudnn6-runtime

RUN apt update && apt install -y python3-pip
RUN pip3 install tensorflow-gpu keras
```

``` console
$ docker build . -t keras-tensorflow-gpu
```

<del>注意点として、cuDNN 6 が既にリリースされているが2017年6月時点ではまだ TensorFlow が cuDNN 6 に対応していないため、cuDNN 5 のイメージをベースとして利用している。</del>

_(2017.10.03 追記) [TensorFlow 1.3 では cuDNN 6 に対応した](https://github.com/tensorflow/tensorflow/releases/tag/v1.3.0)ようなので Dockerfile も 8.0-cudnn6-runtime を使うように修正した。_

ちなみに `nvidia/cuda` イメージを使わずに自前で cuDNN をインストールしようとすると、パッケージをダウンロードするために NVIDIA Developer Program のメンバー登録 (無料) が必要になる[^cudnn-download]ので、cuDNN 入りの `nvidia/cuda` イメージを使うと少しだけ楽。

[^cudnn-download]: [NVIDIA cuDNN | NVIDIA Developer](https://developer.nvidia.com/cudnn)

### ビルドしたイメージでコンテナを起動する
通常の `docker run` コマンドに `--runtime=nvidia` オプションを付けて起動する。[^nv-docker-detail]

[^nv-docker-detail]: NVIDIA Docker がやっている事としては、ホスト OS の GPU 情報を収集して、ホストのドライバをコンテナ側にマウントするようなオプションを追加して `docker run` コマンドに流している、ということらしい： [nvidia-dockerは何をしているのか - Qiita](http://qiita.com/toshitanian/items/8aaca6b867099ebd442d)

``` console
$ docker run --runtime=nvidia -it keras-tensorflow-gpu
```

実行コマンドを省略しているが、省略すると(このイメージの場合は) bash が実行されるので、Bash シェルでコンテナにログインすることができる。

以下、`(container)#` はコンテナにログインした状態で実行したコマンドという意味で使う。

``` console
(container)# nvidia-smi
```

を実行すると、ホストと同じようにコンテナ内でも GPU が認識されていることがわかる。

### Keras のサンプルコードを実行する
MNIST データセットを CNN で学習させる Keras のサンプルコード[^keras-mnist]を wget で落としてきて実行してみる。

[^keras-mnist]: [keras/mnist_cnn.py at master · fchollet/keras](https://github.com/fchollet/keras/blob/master/examples/mnist_cnn.py)

``` console
(container)# apt install wget
(container)# wget https://raw.githubusercontent.com/fchollet/keras/master/examples/mnist_cnn.py
(container)# python3 mnist_cnn.py
```

``` console:実行結果
Using TensorFlow backend.
Downloading data from https://s3.amazonaws.com/img-datasets/mnist.npz
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
Train on 60000 samples, validate on 10000 samples
Epoch 1/12
2017-06-07 08:21:24.737577: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.
2017-06-07 08:21:24.737613: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
2017-06-07 08:21:24.737627: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX instructions, but these are available on your machine and could speed up CPU computations.
2017-06-07 08:21:24.737635: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use AVX2 instructions, but these are available on your machine and could speed up CPU computations.
2017-06-07 08:21:24.737648: W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use FMA instructions, but these are available on your machine and could speed up CPU computations.
2017-06-07 08:21:24.923105: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:901] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2017-06-07 08:21:24.923615: I tensorflow/core/common_runtime/gpu/gpu_device.cc:887] Found device 0 with properties:
name: Tesla K80
major: 3 minor: 7 memoryClockRate (GHz) 0.8235
pciBusID 0000:00:1e.0
Total memory: 11.17GiB
Free memory: 11.11GiB
2017-06-07 08:21:24.923646: I tensorflow/core/common_runtime/gpu/gpu_device.cc:908] DMA: 0
2017-06-07 08:21:24.923660: I tensorflow/core/common_runtime/gpu/gpu_device.cc:918] 0:   Y
2017-06-07 08:21:24.923677: I tensorflow/core/common_runtime/gpu/gpu_device.cc:977] Creating TensorFlow device (/gpu:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0)
60000/60000 [==============================] - 11s - loss: 0.3182 - acc: 0.9025 - val_loss: 0.0727 - val_acc: 0.9770
Epoch 2/12
60000/60000 [==============================] - 9s - loss: 0.1139 - acc: 0.9662 - val_loss: 0.0516 - val_acc: 0.9830
Epoch 3/12
60000/60000 [==============================] - 9s - loss: 0.0877 - acc: 0.9743 - val_loss: 0.0474 - val_acc: 0.9849
Epoch 4/12
60000/60000 [==============================] - 9s - loss: 0.0717 - acc: 0.9784 - val_loss: 0.0424 - val_acc: 0.9852
Epoch 5/12
60000/60000 [==============================] - 9s - loss: 0.0625 - acc: 0.9814 - val_loss: 0.0368 - val_acc: 0.9875
Epoch 6/12
60000/60000 [==============================] - 9s - loss: 0.0572 - acc: 0.9825 - val_loss: 0.0347 - val_acc: 0.9873
Epoch 7/12
60000/60000 [==============================] - 9s - loss: 0.0511 - acc: 0.9850 - val_loss: 0.0340 - val_acc: 0.9876
Epoch 8/12
60000/60000 [==============================] - 9s - loss: 0.0473 - acc: 0.9860 - val_loss: 0.0312 - val_acc: 0.9881
Epoch 9/12
60000/60000 [==============================] - 9s - loss: 0.0441 - acc: 0.9871 - val_loss: 0.0324 - val_acc: 0.9889
Epoch 10/12
60000/60000 [==============================] - 9s - loss: 0.0425 - acc: 0.9872 - val_loss: 0.0302 - val_acc: 0.9890
Epoch 11/12
60000/60000 [==============================] - 9s - loss: 0.0382 - acc: 0.9886 - val_loss: 0.0282 - val_acc: 0.9891
Epoch 12/12
60000/60000 [==============================] - 9s - loss: 0.0376 - acc: 0.9884 - val_loss: 0.0269 - val_acc: 0.9906
Test loss: 0.0268516085245
Test accuracy: 0.9906
```

手元の Mac では 1 epoch あたり 500 秒以上かかっていたのが 9 秒で終わっていて GPU の威力を感じる。

## コスト感
土日のどちらかをディープラーニングで遊んだとして、

```
8時間 × 4日 × $1.542/h = $49.344/mon
```

p2.xlarge インスタンスの料金がこれくらいで、加えて EBS (SSD) 16GB が約 \$2 かかるので、ひと月で $50 くらい。

まぁ趣味としては全然高くないんじゃないだろうか。

GPU を使ってもなお計算に一週間かかる、みたいなことをやるんだったら自前で GPU マシンを組んだほうが良いかもしれない。

## まとめ
- GPU があると快適にディープラーニングで遊べる
- GPU 付きのクラウドサーバを必要なときだけ立ち上げるようにすれば低コストで遊べる
- NVIDIA Docker を使えば環境が汚れるのを気にせずに好き放題遊べる
- **遊んだあとはインスタンス止めるの忘れないで！！！**