Ubuntu14.04.3でnvidia-docker使ってCaffeをインストールしてみた

機械学習ライブラリがいろいろあって目移りしますね。私もUbuntuにCaffe,Chainer,Tensorflowと一通り入れてみたのですが、それ以外にも欲張ってあれもこれもと入れているうちに、ライブラリの依存関係がおかしくなったり、入れ替えると戻らなくなったりして、5回ぐらいインストールし直していました。

さすがに辛くなってきたのでgoogle先生に尋ねたところ、docker内からGPU使えることがわかったので、備忘録を兼ねてご紹介します。[NVIDIA Docker](https://github.com/NVIDIA/nvidia-docker)を使います。

## この記事のゴール
+ NVIDIA Dockerをインストールして
+ Caffe組み込み済みのイメージを作成して
+ コンテナの中に入ってMNIST実行するまで

## NVIDIA Dockerとは何ぞい？

[NVIDIA社の日本語説明ページ](http://www.nvidia.co.jp/object/docker-container-jp.html)が参考になりますが、ホストOS側にGPUのドライバがあって、dockerのコンテナ側にCUDA Toolkitを置いて、dockerの中からでもGPUにアクセスできるようにしているようです。

NVIDIA Dockerを使わずに、docker越しにCUDAを使うという記事（[Chainer開発用のDocker環境を整備する](http://qiita.com/unnonouno/items/928a6b6eeee8421d8b2f)）も参考になりました。


## 導入環境

- OS: Ubuntu Server 14.04.3 LTS
- CPU: Intel(R) Core(TM) i5-4670
- GPU: GeForce GTX 980 Ti 
- Memory: 8Gbyte
- HDD: 1Tbyte

## 準備
フォーマット済みのHDDに新規でubuntuインストールして、GPUのドライバインストールして、dockerインストールするまで。

1. [Ubuntu Server 14.04.3 LTS](http://www.ubuntu.com/download/server)をPCにインストールします
  + Linuxカーネルが3.19.0-43なので、docker使うのに都合がよいです。[参考）Ubuntu 14.04 へ Docker のインストール準備](http://yo1000.hateblo.jp/entry/2015/03/07/223807)

2. CUDA Toolkitをインストールします
  + GPUのドライバをインストールするのが目的です。CUDA Toolkitインストールするとドライバもインストールされます。
  + [CUDA Toolkit 7.5](https://developer.nvidia.com/cuda-toolkit)からubuntu14.04用のバイナリをダウンロードします。
  + 上記リンクの下部に書いてある通りにインストールします
  + インストールが終わったら一度再起動します

3. dockerをインストールします
  + NVIDIA Dockerはdockerのアドインっぽいので、docker自体はそのままインストールします。
  + [docker公式のubuntuのインストール方法を書いているページ](https://docs.docker.com/engine/installation/ubuntulinux/)を参考にします

```
$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
$ sudo echo 'deb https://apt.dockerproject.org/repo ubuntu-trusty main' > /etc/apt/sources.list.d/docker.list
$ sudo apt-get update
$ sudo apt-cache policy docker-engine
$ sudo apt-get install docker-engine
$ sudo service docker start
```

## NVIDIA Dockerインストール

[nvidia-dockerのインストールページ](https://github.com/NVIDIA/nvidia-docker/wiki/Installation)見ると、ソースコードからインストールしろとしか書いていないので、githubからとってきます。

```
$ git clone https://github.com/NVIDIA/nvidia-docker.git
$ cd nvidia-docker
$ sudo make install
$ sudo nvidia-docker volume setup
```

特に何も指定しなければ、`/usr/bin/`ディレクトリに、`nvidia-docker`と`nvidia-docker-plugin`が作成されます。

またdockerのイメージとして、最終的に以下のラベルを持つイメージが作成されます。(同じIDのものは同じイメージです。IDは作成環境によって変わります）

| REPOSITORY | TAG | IMAGE ID |
|:-----------|:------------|:------------|
|cuda|7.5|242f4f109770
|cuda|latest|242f4f109770
|cuda|7.5-devel|242f4f109770
|cuda|devel|242f4f109770
|nvidia/cuda|latest|242f4f109770
|cuda|7.5-runtime|6199b491d4b3

最後の、`sudo nvidia-docker volume setup`の挙動ですが、このコマンド実行後、`/var/lib/docker/volumes`ディレクトリに、`nvidia_driver_352.63`というファイルができているのを確認しました。

## NVIDIA Dockerを使ってみる

```
$ sudo nvidia-docker run nvidia/cuda nvidia-smi
```
でnvidia-dockerを実行すると、dockerの中からGPU情報を見て結果を出力します。

```
+------------------------------------------------------+
| NVIDIA-SMI 352.63     Driver Version: 352.63         |
|-------------------------------+----------------------+----------------------+
| 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 980 Ti  Off  | 0000:01:00.0      On |                  N/A |
|  0%   50C    P8    23W / 275W |     99MiB /  6140MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+
```

コンテナ自体はdockerからも扱えるのですが、runさせた際に`/usr/local/nvidia`ディレクトリが見えないため、nvidia-smiにアクセスできず、こんな結果になります。

```
$ sudo docker run nvidia/cuda nvidia-smi
exec: "nvidia-smi": executable file not found in $PATH
```

いろいろ試してみたところ、一度nvidia-dockerでrunさせた後、後からattachで入る時はdockerでもよいことがわかりました。

```
$ sudo nvidia-docker run -it --name "test1" nvidia/cuda /bin/bash
 (ctrl-p, ctrl-qで抜ける)
$ sudo docker attach test1
$ nvidia-smi
 -> 成功する
```

※ちなみに、nvidia-smiのProcessesについては、同一コンテナ内であってもGPUのプロセスが出てこないです。温度とファンの回り具合はわかります。ホストOS側のnvidia-smiには全て出てきます。

## Caffeを入れてみる
さて、本来の目的のCaffeをdocker上に導入してみます。せっかくですので、Dockerfileを使ってインストールすることにします。

### 1. cuDNNを入れる
cuDNNについては、こちらの記事を参考にしてください。
[GTC 2015 - Deep Learning用のCUDAライブラリ「cuDNN」](http://news.mynavi.jp/articles/2015/04/10/gtc2015_cudnn/)

以下のコマンドで、イメージを作成します。Dockerfileの中を見ると、さきほどの「NVIDIA Dockerインストール」で作成した`cuda:7.5-devel`のイメージを基にしていることがわかります。

`nvidia-docker`ディレクトリからの作業をを想定しています。buildの後の-tオプションはタグ名です。タグ名は手順4で出てくるDockerfileで使います。

```
$ sudo nvidia-docker build -t cuda:7.5_cudnn70 ./ubuntu/cuda/7.5/devel/cudnn3
```

以下2〜4の手順は、便宜的に同じディレクトリで作業する前提とします。

### 2. Anacondaをダウンロードしておく

buildの度に毎回ダウンロードするように、Dockerfileを書いてもいいのですが、やり直す度に毎回ダウンロードするのも時間がかかるので、事前に用意しておきます。

[Anacondのダウンロードページ](https://www.continuum.io/downloads#_unix)から、Linux 64bit, Python2.7を選択してダウンロードします。

コマンドで一気にダウンロードしたい場合は以下になります。

```
$ curl "https://3230d63b5fc54e62148e-c95ac804525aac4b6dba79b00b39d1d3.ssl.cf1.rackcdn.com/Anaconda2-2.4.1-Linux-x86_64.sh" > Anaconda2-2.4.1-Linux-x86_64.sh
```


### 3. CaffeのMakefile.configを先に作っておく

Caffeのmake時に、Makefile.configというファイルが必要なのですが、これは事前に用意しておく必要があります。
雛形を取ってきて編集しましょう。

```
$ curl https://raw.githubusercontent.com/BVLC/caffe/master/Makefile.config.example -O
$ cp Makefile.config.example Makefile.config
```

Makefile.configはコメント行を除外しています。
cuDNN使うよというのと、pythonがAnadonda環境だよという点を修正しています。

```mf:Makefile.config
USE_CUDNN := 1
CUDA_DIR := /usr/local/cuda
CUDA_ARCH := -gencode arch=compute_20,code=sm_20 \
		-gencode arch=compute_20,code=sm_21 \
		-gencode arch=compute_30,code=sm_30 \
		-gencode arch=compute_35,code=sm_35 \
		-gencode arch=compute_50,code=sm_50 \
		-gencode arch=compute_50,code=compute_50
BLAS := atlas
ANACONDA_HOME := /opt/anaconda2
PYTHON_INCLUDE := $(ANACONDA_HOME)/include \
		# $(ANACONDA_HOME)/include/python2.7 \
		# $(ANACONDA_HOME)/lib/python2.7/site-packages/numpy/core/include \
PYTHON_LIB := $(ANACONDA_HOME)/lib

INCLUDE_DIRS := $(PYTHON_INCLUDE) /usr/local/include
LIBRARY_DIRS := $(PYTHON_LIB) /usr/local/lib /usr/lib

BUILD_DIR := build
DISTRIBUTE_DIR := distribute
TEST_GPUID := 0
Q ?= @
```


### 4. Dockerfileを作る

作業1で作ったイメージをベースにして、以下の作業を行うDockerfileを作成します。

+ Anaconda(64bit, Python2.7)のインストール
+ Caffe用パッケージのインストール
+ Caffeをgithubから取得
+ Caffeのmake

あと、build時にエラーが出る部分を修正しています。
AnacondaとCaffeは、/opt以下に作成しています。

以下をコピペして、Dockerfileというファイル名で保存します。

```:cuda7.5_cudnn7.0_anaconda2_caffe/Dockerfile
FROM cuda:7.5_cudnn70

COPY Anaconda2-2.4.1-Linux-x86_64.sh /opt

# caffe makefile:use anaconda2 / use cudnn3
COPY Makefile.config /opt

# install anaconda2
RUN echo 'export PATH=/opt/anaconda2/bin:$PATH' > /etc/profile.d/conda.sh && \
    cd /opt && \
    /bin/bash /opt/Anaconda2-2.4.1-Linux-x86_64.sh -b -p /opt/anaconda2 && \
    rm /opt/Anaconda2-2.4.1-Linux-x86_64.sh

# prepare caffe
RUN apt-get update && apt-get install -y \
    libatlas-base-dev libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libboost-all-dev libhdf5-serial-dev libgflags-dev libgoogle-glog-dev liblmdb-dev protobuf-compiler git wget && \
    rm -rf /var/lib/apt/lists/*

# solution "libdc1394 error: Failed to initialize libdc1394"
# ref) http://stackoverflow.com/questions/12689304/ctypes-error-libdc1394-error-failed-to-initialize-libdc1394
RUN ln -s /dev/null /dev/raw1394

# solution "Error loading shared library libhdf5_hl.so"
# ref) https://github.com/BVLC/caffe/issues/1463
RUN cp /opt/anaconda2/pkgs/hdf5-1.8.15.1-2/lib/libhdf5.so.10.0.1 /lib/x86_64-linux-gnu/ && \
    cp /opt/anaconda2/pkgs/hdf5-1.8.15.1-2/lib/libhdf5_hl.so.10.0.1 /lib/x86_64-linux-gnu/ && \
    ldconfig

# make caffe
RUN cd /opt && \
    git clone https://github.com/BVLC/caffe.git && \
    cd caffe && \
    cp /opt/Makefile.config .  && \
    make all -j4 && \
    make test -j4

# ref) https://hub.docker.com/r/eduwass/face-the-internet-worker/~/dockerfile/
CMD sh -c 'ln -s /dev/null /dev/raw1394'; bash
```

面倒な点がひとつありまして、`ln -s /dev/null /dev/raw1394`という処理が、イメージ作成時だけではなくて、コンテナを作成する度に必要になります。
参考) [How to persist 'ln' in Docker with Ubuntu](http://stackoverflow.com/questions/31768441/how-to-persist-ln-in-docker-with-ubuntu)

そのため、コンテナ起動時に実行するコマンドとして設定しています。

### 5. Let's nvidia-docker build!

2〜4で作ったファイルを全て同じディレクトリ内に入れて確認します。3つのファイルがありますね。

```
$ ls 
 Anaconda2-2.4.1-Linux-x86_64.sh  Dockerfile  Makefile.config
```

そのディレクトリで以下のコマンドを実行します。


```
$ sudo nvidia-docker build -t ml_caffe:7570ana2 .
```
(たぶんこのタイミングでは、`docker build -t ml_caffe:7570ana2 .`でも同じだと思われますが、念のためnvidia-dockerを使っています）

自動的にbuildが始まります。ここではbuild後のイメージタグ名を、`ml_caffe:7570ana2`としています。

### 6. runtestで確認しましょう

makeがうまくいったのか、Caffeの動作環境は問題ないか確認しましょう。一番簡単な確認は、Caffeのmake runtestです。

※　実は、Dockerfileの最後に、`make runtest`って書いたら、GPUが使えなくてエラーが出たので、build時はnvidia-dockerはGPU使えないっぽいです。

以下のコマンドでコンテナ内に入り、make runtestを実行します。

```
$ sudo nvidia-docker run -it --name "runtest" ml_caffe:7570ana2
root@e14ad9a3b2d6:/# cd /opt/caffe; make runtest
```

※ `root@e14ad9a3b2d6:/#`の部分は、コンテナの中に入った後のシェルプロンプトです。`e14ad9a3b2d6`はコンテナIDなので、毎回runする度に変わります。

ちゃんと[RUN][OK]が出力されていたらGPUは認識されています。全てのテストがPASSすることを確認しましょう。最後はこんな感じになります。

```
[----------] Global test environment tear-down
[==========] 1744 tests from 257 test cases ran. (241958 ms total)
[  PASSED  ] 1744 tests.
```

普通のdockerで実行するとどうなるかやってみましょう。

```
$ sudo docker run -it --name "runtest_docker" ml_caffe:7570ana2
root@b25a57ebfe81:/# cd /opt/caffe; make runtest
.build_release/tools/caffe
caffe: command line brew
usage: caffe <command> <args>
 :
 (中略)
 :
[----------] 1 test from HDF5OutputLayerTest/3, where TypeParam = caffe::GPUDevice<double>
[ RUN      ] HDF5OutputLayerTest/3.TestForward
F0118 12:02:51.914427   240 syncedmem.hpp:18] Check failed: error == cudaSuccess (35 vs. 0)  CUDA driver version is insufficient for CUDA runtime version
*** Check failure stack trace: ***
 :
 (後略)
 :
```

一瞬動いたかな？と思うのですが、すぐにCUDAのドライバのバージョンがおかしいというエラーが出て終了します。


### 7. MNIST動かしてみましょう

MNISTは機械学習におけるHello worldにあたります。参考）[MNIST For ML Beginners](https://www.tensorflow.org/versions/master/tutorials/mnist/beginners/index.html)

というわけで、以下のコマンドで試してみましょう。コンテナの中に入って作業します。
参考）[Caffeで手書き数字(MNIST)の認識学習をする](http://qiita.com/t-hiroyoshi/items/2bf473fd06c352d97579)

```
$ sudo nvidia-docker run -it --name "mnist" ml_caffe:7570ana2
root@a99d3a442790:/# cd /opt/caffe/
root@a99d3a442790:/opt/caffe# ./data/mnist/get_mnist.sh
root@a99d3a442790:/opt/caffe# ./examples/mnist/create_mnist.sh
root@a99d3a442790:/opt/caffe# ./examples/mnist/train_lenet.sh
```

やっていることは、シンプルです。

+ ディレクトリ移動して(cd /opt/caffe/)
+ MNIST用データ取得して(./data/mnist/get_mnist.sh)
+ データを学習させやすい形に変換して(./examples/mnist/create_mnist.sh)
+ 学習させます(./examples/mnist/train_lenet.sh)

学習結果として、最後こんな出力が出てきたら完了です。

```
I0118 13:18:48.695695    34 solver.cpp:459] Snapshotting to binary proto file examples/mnist/lenet_iter_10000.caffemodel
I0118 13:18:48.700121    34 sgd_solver.cpp:273] Snapshotting solver state to binary proto file examples/mnist/lenet_iter_10000.solverstate
I0118 13:18:48.702507    34 solver.cpp:321] Iteration 10000, loss = 0.00265015
I0118 13:18:48.702540    34 solver.cpp:341] Iteration 10000, Testing net (#0)
I0118 13:18:48.765329    34 solver.cpp:409]     Test net output #0: accuracy = 0.9905
I0118 13:18:48.765362    34 solver.cpp:409]     Test net output #1: loss = 0.0263541 (* 1 = 0.0263541 loss)
I0118 13:18:48.765379    34 solver.cpp:326] Optimization Done.
I0118 13:18:48.765384    34 caffe.cpp:215] Optimization Done.
```
以上、nvidia-dockerでのCaffeの導入でした！

## はまったところ

nvidia-dockerで1つはまったところがあったので共有しておきます。

コンテナを作った後で`docker rm`なり、`nvidia-docker rm`でコンテナ削除した場合は問題ないのですが、`nvidia-docker run --rm`で起動してコンテナを自動削除した場合、`nvidia-docker volume setup'で作成したボリュームが勝手に消えることがあります。

突然、`Error response from daemon: Error looking up volume plugin nvidia-docker: Plugin not found`というエラーが出てきたので、「何もしていないのにエラーが出た」という言葉が浮かんできました。

気を取り直して条件を探ったところ、コンテナが全く登録されていない時に、`nvidia-docker run --rm　〜`を実行すると消えます。

状況を並べるとこうなります。

```
$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
$ sudo nvidia-docker run -it --rm ml_caffe:7570ana2
root@34302aeabcfa:/# exit
exit
$ sudo nvidia-docker run -it --rm ml_caffe:7570ana2
Error response from daemon: Error looking up volume plugin nvidia-docker: Plugin not found
```

バグなのか仕様なのかよくわかってないので、とりあえずの対策としては、

+ `--rm`オプションは使わないようにする
+ `sudo nvidia-docker volume setup; nvidia-docker run --rm 〜`とコマンドを並べて回避
+ `Error looking up volume plugin nvidia-docker: Plugin not found`が出たら、`sudo nvidia-docker volume setup`を実行する

となりますね。

## まとめ

個人で使う分には、環境をdockerのイメージ毎に隔離させることができるので、非常に使い勝手がいいと思っています。

dockerのイメージが、自分のPCのグラフィックカード依存になっているような気がするので、dockerの趣旨に反しているのかもしれませんが、容量用法を守って正しく使えば大丈夫でしょう。

GPUでも楽しいdockerライフを！