KubernetesからGPUを使ってTensorflowを動かす

# KubernetesからGPUを使ってTensorflowを動かす
Kubernetes 1.8ではα版（実験的）機能としてPodからGPUを使うことが可能になっています。
KubernetesからGPUを使うメリットは、ディープラーニングとコンテナ・オーケストレーションを組み合わせられることです。
ディープラーニングのトレーニングフェーズではGPUを使うことでスピードアップするのが一般的です。
NvidiaもDockerからGPUとCUDAを使うための[Nvidia Docker](https://github.com/NVIDIA/nvidia-docker)を提供しており、コンテナ上でGPUを使うのは有効な手段として広まっていっていると感じます。
KubernetesでもGPU使用が要望されており、現在はα版として使用可能になっています。


## KubernetesからGPUを使う方法
詳しくは以下で書かれていますが、ホストサーバにインストールしたNvidia CUDAやCudnnのライブラリをPodにマウントして呼び出す仕組みになっています。
https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/

なお、Kubernetes on GPUをKubernetes1.6で試している例もありますので、こちらも参考になります。
https://github.com/Langhalsdino/Kubernetes-GPU-Guide

## 環境
インフラ、ソフトウェアは以下を使います。
- 基盤：Google Cloud Platform（GCP）のCloud Engine（仮想マシン）1台
- スペック：4CPU, 15GB RAM, 100GB SSD, 1GPU(Nvidia K80)
- OS：Ubuntu16.04
- Docker CE 17.03
- Kubernetes 1.8
- Nvidia CUDA 8.0
- Nvidia Cudnn 6.0
- Podのコンテナイメージ：gcr.io/tensorflow/tensorflow:latest-gpu（現時点ではPython2.7、Tensorflow1.4）

![0.png](https://qiita-image-store.s3.amazonaws.com/0/55384/9357aa6e-64be-ffa1-777e-ed7f92fa69d5.png)


各ソフトウェアのバージョンは、現時点（2017年11月）でいろいろ試した結果、うまくいった組み合わせです。
バージョン合わせるのって大変ですよね。

GCPを使ったのは仮想マシンの配備が速い＆分単位課金だからです。
ソフトウェアのバージョンを変えつつ、安定して動かすのに何回も作って壊してを繰り返したので、すぐ作り直せてお金も無駄にならないことが必要でした。

・・・なんて思っていたら、AWS EC2は秒単位課金になっていたんですね（驚き）。
http://www.itmedia.co.jp/news/articles/1709/19/news049.html

まあ今回使うKubernetesもTensorflowもGoogle発ですし、クラウドもGoogleで揃えて良いでしょう。

## 構築
それでは早速構築していきます。
GCPのCloud EngineでUbuntu16.04仮想マシンを1台配備したら、まずはアップデートしてDocker CEをインストールして諸々設定します。
Docker CEのインストール方法は以下をご参照ください。
https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/
https://kubernetes.io/docs/setup/independent/install-kubeadm/

```bash
# update Linux and install docker
sudo su -
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
apt-get -y update && apt-get -y upgrade

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/docker.list
deb https://download.docker.com/linux/$(lsb_release -si | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable
EOF
apt-get update && apt-get install -y curl apt-transport-https git vim jq docker-ce=$(apt-cache madison docker-ce | grep 17.03 | head -1 | awk '{print $3}')

# start docker
systemctl start docker && systemctl enable docker

# set network
cat << EOF > /tmp/host
localhost
10.10.0.2
EOF
mkdir -p ~/.ssh/
for i in $(cat /tmp/host); do ssh-keyscan -H $i >> ~/.ssh/known_hosts ; done
```

続いてCUDAとCudnnのインストールです。
KubernetesからGPUを使うときはホストサーバのライブラリを読み込むので、ホストサーバ側にCUDAとCudnnが必要になります。
CUDAもCudnnもバージョン指定でインストールします。
https://developer.nvidia.com/cuda-toolkit

```bash
## find nvidia GPU
lspci | grep -i nvidia

# install requirements for cuda and cndnn
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
dpkg -i cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
apt-get -y update
apt-get -y install --allow-unauthenticated cuda-8-0

echo 'export CUDA_HOME=/usr/local/cuda' >> /etc/profile
echo 'export PATH=/usr/local/cuda-8.0/bin:${PATH}' >> /etc/profile
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-8.0/lib64:/usr/lib/nvidia-384:/usr/lib/nvidia:${CUDA_HOME}:${LD_LIBRARY_PATH}' >> /etc/profile
source /etc/profile
```

CUDAはwgetでインストーラを入手できますが、Cudnnはブラウザからダウンロードしたものを仮想マシンにアップロードする必要があります。
以下のコマンドの前に、Cudnnをダウンロードして仮想マシンにアップロードしておいてください。
CudnnのダウンロードにはNvidiaへの登録とアンケート回答が必要になります。
https://developer.nvidia.com/rdp/cudnn-download

```bash
# prerequisite: upload cudnn to the server
tar zxvf cudnn-8.0-linux-x64-v6.0.tgz
cp cuda/include/cudnn.h /usr/local/cuda/include
cp cuda/lib64/libcudnn* /usr/local/cuda/lib64
chmod a+r /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*

nvcc --version
nvidia-smi
```

以下のようにGPUが認識されていることを確認してください。

![1.PNG](https://qiita-image-store.s3.amazonaws.com/0/55384/9c073292-3719-8c93-ef47-ac685f27ef30.png)


続いてKubernetesをインストールします。
Kubeadmで構築しますので、まずはKubeadmをインストールします。
https://kubernetes.io/docs/setup/independent/install-kubeadm/

```bash
# install kube tools
apt-get update && apt-get install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF

apt-get -y update
apt-get install -y --allow-unauthenticated kubelet kubeadm kubectl kubernetes-cni
```

KubernetesでGPUを使うためには、Kubeletの起動コマンドに`--feature-gates=Accelerators=true`オプションを追加する必要があります。
このオプションは`/etc/systemd/system/kubelet.service.d/`配下の*-kubeadm.confファイルに追記することで追加可能です。

```bash
FILE_NAME=$(ls /etc/systemd/system/kubelet.service.d/*-kubeadm.conf)
sed -i '/^ExecStart=\/usr\/bin\/kubelet/ s/$/ --feature-gates=Accelerators=true/' ${FILE_NAME}

systemctl daemon-reload
systemctl restart kubelet
```

KubeadmからKubernetesクラスターを構築します。
https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/

```bash
# initialize kubeadm and kube cluster
kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=$(hostname -i)
systemctl enable kubelet && systemctl start kubelet
systemctl status kubelet -l

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

kubectl cluster-info

# install flannel
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.0/Documentation/kube-flannel.yml
kubectl get pods --all-namespaces
```

今回はマスター1台のみの構成にするので、マスターへのデプロイを有効化します。

```bash
# allow deployment to Kubernetes master
kubectl taint nodes --all node-role.kubernetes.io/master-
```

稼働確認として靴下ショップをデプロイします。

```bash
# deploy sample sock-shop app
kubectl create namespace sock-shop
kubectl apply -n sock-shop -f "https://github.com/microservices-demo/microservices-demo/blob/master/deploy/kubernetes/complete-demo.yaml?raw=true"
kubectl -n sock-shop get svc front-end
kubectl get pods -n sock-shop
```

無事デプロイできたら消します。

```bash
# delete after deployment
kubectl delete namespace sock-shop
```

## KubernetesからGPUを使ってTensorflowを動かす
これで構築完了です。
それではKubernetesのPodからGPUを使ってTensorlfowを動かします。

Podの定義ymlは以下です。
環境変数にCUDAやCudnn等Nvidia GPUライブラリへのパスを設定します。
また、Kubernetesはホストサーバのライブラリを使ってGPUを動かすので、ホストサーバのCUDAやCudnn等Nvidia GPU系のディレクトリをコンテナにマウントします。
（いろいろマウントしまくっていますが、もしかしたら不要なものもあるかもしれません・・・）

```bash
cat <<- EOF > kubegpu.yml
kind: Pod
apiVersion: v1
metadata:
  name: gpu-pod
spec:
  containers:
  - name: gpu-container
    image: gcr.io/tensorflow/tensorflow:latest-gpu
    resources:
      limits:
        alpha.kubernetes.io/nvidia-gpu: 1
    env:
    - name: CUDA_HOME
      value: '/usr/local/cuda'
    - name: PATH
      value: '/usr/local/cuda-8.0/bin:${PATH}'
    - name: LD_LIBRARY_PATH
      value: '/usr/local/cuda-8.0/lib64:/usr/lib/nvidia-384:/usr/lib/nvidia:${CUDA_HOME}:${LD_LIBRARY_PATH}'
    volumeMounts:
    - mountPath: /usr/lib/nvidia-384/bin
      name: bin
    - mountPath: /usr/lib/nvidia-384
      name: lib
    - mountPath: /usr/local/cuda-8.0
      name: cuda
    - mountPath: /usr/lib/x86_64-linux-gnu
      name: gnu
    - mountPath: /usr/bin/nvidia-smi
      name: nvs
    - mountPath: /usr/local/cuda/lib64
      name: clib64
    - mountPath: /usr/local/cuda/include
      name: include
  volumes:
  - hostPath:
      path: /usr/lib/nvidia-384/bin
    name: bin
  - hostPath:
      path: /usr/lib/nvidia-384
    name: lib
  - hostPath:
      path: /usr/local/cuda-8.0
    name: cuda
  - hostPath:
      path: /usr/lib/x86_64-linux-gnu
    name: gnu
  - hostPath:
      path: /usr/bin/nvidia-smi
    name: nvs
  - hostPath:
      path: /usr/local/cuda/lib64
    name: clib64
  - hostPath:
      path: /usr/local/cuda/include
    name: include
EOF
```

動かしてみましょう。

```bash
# create pod
kubectl create -f kubegpu.yml

# see if it is running
kubectl get pods
```

![2.PNG](https://qiita-image-store.s3.amazonaws.com/0/55384/af23997d-cb34-bf57-3170-0961672f4764.png)


起動（Running）になったらpodにログインし、GPUを認識しているか確認してみましょう。

```bash
kubectl exec -it gpu-pod /bin/bash
```

```bash
nvidia-smi
```

GPUを認識できていたら、以下のように表示されます。

![3.PNG](https://qiita-image-store.s3.amazonaws.com/0/55384/9be8d4d1-6922-f816-1363-4231ac5308d7.png)


最後にTensorflowからGPUを使えるか試してみましょう。
プログラムは以下のものを使用しています。
https://www.tensorflow.org/tutorials/using_gpu

```bash
cat << EOF > ten.py
import tensorflow as tf
# Creates a graph.
a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
# Creates a session with log_device_placement set to True.
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Runs the op.
print(sess.run(c))
EOF

python ten.py
```

試しに上記のような簡単な計算を動かしていますが、無事GPUを認識して利用しているようです。

![4.PNG](https://qiita-image-store.s3.amazonaws.com/0/55384/18bc3dd0-482f-f568-e2d5-27da92458207.png)


## 最後に
KubernetesからGPUを使ってTensorflowを動かすことができました。
ディープラーニングのトレーニングは一時的な稼働になることが多く、都度コンテナで環境を用意するのがちょうど良いと思います。
ディープラーニングを活用し始めると、違うモデル作成のために多数のコンテナを適時デプロイしてトレーニングを実行することになると思います。
そのためにはKubernetesのようなコンテナ・オーケストレーションが便利です。

コンテナでディープラーニングを行うためにNvidia-dockerでGPUを使う方法が有効ですが、多数のコンテナを管理しようとなると、KubernetesでGPUを使う必要が出てきます。

今回はこの両方のニーズを実現する方法を紹介しました。
KubernetesからGPUを使うのはまだ実験的なα版機能ですが、はやくGAしてもっと手軽に使えるようになってほしいものです。