Kubernetesの物理GPUノードでGPUコンテナを試す！

物理GPUノードの計算能力を、メンバと共用で利用したい！
そんな思いに、KubernetesのGPUノードはぴったりです。

## KubernetesのGPUノードで何ができるの？
物理サーバと違うのは、GPUを利用した計算のスケジューリングを、人間同士の調整不要で行える点です。
例えば、実行中のGPUコンテナが終了するまで、次のGPUコンテナが起動しません。
これは「ずっと見張っていて、GPUが空いたら実行！」とする必要は無く、何も意識せずにGPUコンテナを実行することで、計算リソースが空くのを勝手に待ってくれます。
## やること
今回は構築支援ツールkubeadmを利用して作成したクラスタに、物理のGPUノードを追加しました。
その後Tensorflow GPUコンテナでGPU利用した計算を試してみます！

## 試した環境

Kubernetesクラスタ環境マシン構成は以下です。

|ホスト名|cpu|メモリ|GPU|OS|仮想/物理|
|----|----|----|----|----|----|
|controller-00|2コア|2GB|なし|Ubuntu18.04|仮想|
|controller-01|2コア|2GB|なし|Ubuntu18.04|仮想|
|controller-02|2コア|2GB|なし|Ubuntu18.04|仮想|
|worker-00|1コア|2GB|なし|Ubuntu18.04|仮想|
|worker-01|1コア|2GB|なし|Ubuntu18.04|仮想|
　　
Kubernetesコンポーネントのバージョンは以下です。
ネットワークはflannelで構成しています。

|コンポーネント|バージョン|
|----|----|
|Kubernetes|v1.18.2|
|Docker|19.03.8|
|flannel|v0.12.0-amd64|
|nvidia-docker2|1.0.0-rc10|
|NVIDIA driver|440.82|
|NVIDIA device-plugin for Kubernets|1.0.0-beta6|


## 追加するノード
追加するノードは以下です。

|ホスト名|cpu|メモリ|GPU|OS|仮想/物理|
|----|----|----|----|----|----|
|worker-gpu|8コア x2|128GB|NVIDIA Geforce GTX1060|Ubuntu18.04|物理|

## GPUノードを準備する
基本的にはkubeadm joinで参加させる時と同様に、ノードの準備を行いますが、以下が違います。

- GPUのドライバをインストールする
- ランタイムをnvidia-docker2に変更する

そして参加した後

- NVIDIA device-plugin for Kubernetesを導入する

#### 1. GPUノードの前提作業
Ubuntu 18.04をインストールして、既存のクラスタと疎通をとれるようにしておきます。
**flannelをデフォルトで利用している場合は、1枚目のNICでクラスタと通信できるようにしてください。**
flannelはデフォルトだとOSの1枚目のNICでポッドネットワークを作成するため、2枚目以降のNICで構築したい場合は、flannelのマニフェストを書き換える必要があります。

#### 2. GPUノードの準備
この項はkubeadmのノード追加では恒例の作業なのでご存知の方は読み飛ばしてください。
  
まずカーネルパラメータを設定します。

```
# cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sudo sysctl --system
```

kubeletを起動するためにswapを無効化しておきます。ノードの再起動が発生する場合は/etc/fstabのswap行をコメントアウトします。

```
# swapoff -a
# swapon --show
# 
```
swapon --showコマンドを実行して何も表示されなければ、swapは無効になっています。

  
**Dockerをインストールします。**
まず前提パッケージのインストールします。

```
# apt-get update && apt-get install -y \
> apt-transport-https ca-certificates curl software-properties-common gnupg2
```

Dockerリポジトリを追加します。

```
# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
# add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
```

Dockerをインストールします。

```
# apt-get update && apt-get install -y \
> containerd.io=1.2.13-1 \
> docker-ce=5:19.03.8~3-0~ubuntu-$(lsb_release -cs) \
> docker-ce-cli=5:19.03.8~3-0~ubuntu-$(lsb_release -cs)
```

Docker設定ファイルを作成します。

```json
# cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF
```

Dockerサービス用のディレクトリを作成します。

```
# mkdir -p /etc/systemd/system/docker.service.d
```

#### 3.NVIDIAドライバのインストール
GPUノードに適切なGPUドライバを確認します。

```
# apt install ubuntu-drivers-common
～～～～～～
# ubuntu-drivers devices
== /sys/devices/pci0000:40/0000:40:02.0/0000:41:00.0 ==
modalias : pci:v000010DEd00001C03sv000010DEsd00001C03bc03sc00i00
vendor   : NVIDIA Corporation
model    : GP106 [GeForce GTX 1060 6GB]
manual_install: True
driver   : nvidia-driver-435 - distro non-free
driver   : nvidia-driver-390 - distro non-free
driver   : nvidia-driver-440 - distro non-free recommended   ←
driver   : xserver-xorg-video-nouveau - distro free builtin

#
```
　
nvidia-driver-440が最適であることが分かりました。
NVIDIA公式からダウンロードしてインストールする方法もありますが、パッケージ管理ツール以外でインストールしてしまうと、カーネルモジュールの更新によって動作しなくなることがあるので、アップデート時も依存関係を考慮してくれるaptでインストールします。

```
# apt install nvidia-driver-440
```
　
GPUが認識されているか確認してみます。

```
# nvidia-smi
Fri May 15 14:40:29 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.82       Driver Version: 440.82       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 106...  Off  | 00000000:41:00.0 Off |                  N/A |
| 25%   33C    P8     9W / 120W |      0MiB /  6070MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
#
```
ちゃんと認識されています。
これでドライバをインストールすることが出来ました。

#### 4. nvidia-docker2のインストール
GPUを利用するためのランタイムをインスト―ルします。

```
# distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
# curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
OK
# curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
deb https://nvidia.github.io/libnvidia-container/ubuntu18.04/$(ARCH) /
deb https://nvidia.github.io/nvidia-container-runtime/ubuntu18.04/$(ARCH) /
deb https://nvidia.github.io/nvidia-docker/ubuntu18.04/$(ARCH) /
# apt-get update && sudo apt-get install -y nvidia-docker2
～～～～～～～～
#
```
　
Dockerが利用するランタイムを変更します。

```
# vi /etc/docker/daemon.json
## 内容
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2",
  "default-runtime": "nvidia",
  "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}
```
　
Dockerを再起動＆ enableにしておきます。

```
# systemctl restart docker && systemctl enable docker 
Synchronizing state of docker.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable docker
```
　
Dockerのランタイムがnvidiaに変更されていることを確認します。

```
# docker info | grep Runtime
 Runtimes: nvidia runc
 Default Runtime: nvidia
WARNING: No swap limit support
#
```
**これでGPUノードをクラスタに追加する準備が出来ました！**

## GPUノードをクラスタに追加する

おなじみのあのコマンドを実行するだけです！
と、そのまえにトークンの有効期限は24時間という事なので、期限が切れている場合はマスターノードでトークンを発行しましょう！

```
# kubeadm token create
p4mn00.l3zru21qawhp4m60
```
初回join時のハッシュも紛失してしまった場合はハッシュも再取得します。

```
# openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt \
>  | openssl rsa -pubin -outform der 2>/dev/null \
>  | openssl dgst -sha256 -hex | sed 's/^.* //'
6170c56742029031fb2845088b8dec4f73d7d9e90459dd9e07a1ffc8eb619f9e
```
　
さあお待ちかねのあのコマンドを実行しましょう！

```
# kubeadm join 192.168.184.213 --discovery-token p4mn00.l3zru21qawhp4m60 --discovery-token-ca-cert-hash sha256:6170c56742029031fb2845088b8dec4f73d7d9e90459dd9e07a1ffc8eb619f9e
～～～～～～～～
This node has joined the cluster:
～～～～～～～～
```
　
参加できているか確認してみましょう。

```
# kubectl get nodes
NAME            STATUS   ROLES    AGE   VERSION
controller-00   Ready    master   26h   v1.18.2
controller-01   Ready    master   25h   v1.18.2
controller-02   Ready    master   25h   v1.18.2
worker-00       Ready    <none>   25h   v1.18.2
worker-01       Ready    <none>   25h   v1.18.2
worker-gpu      Ready    <none>   44s   v1.18.2
#
```
うまく参加できたようです！


## NVIDIA device pluginを実行する
参加したGPUノードのリソースを見てみましょう。

```
# kubectl describe node worker-gpu
Name:               worker-gpu
Roles:              <none>
～～～～～
Addresses:
  InternalIP:  192.168.207.201
  Hostname:    worker-gpu
Capacity:
  cpu:                32
  ephemeral-storage:  1951507452Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             131948100Ki
  pods:               110
Allocatable:
  cpu:                32
  ephemeral-storage:  1798509264786
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             131845700Ki
  pods:               110
～～～～～
PodCIDR:                      10.200.5.0/24
PodCIDRs:                     10.200.5.0/24
Non-terminated Pods:          (2 in total)
  Namespace                   Name                           CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                   ----                           ------------  ----------  ---------------  -------------  ---
  kube-system                 kube-flannel-ds-amd64-v4zsx    100m (0%)     100m (0%)   50Mi (0%)        50Mi (0%)      45s
  kube-system                 kube-proxy-vv9kh               0 (0%)        0 (0%)      0 (0%)           0 (0%)         45s
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests   Limits
  --------           --------   ------
  cpu                100m (0%)  100m (0%)
  memory             50Mi (0%)  50Mi (0%)
  ephemeral-storage  0 (0%)     0 (0%)
  hugepages-1Gi      0 (0%)     0 (0%)
  hugepages-2Mi      0 (0%)     0 (0%)
～～～～～
#
```
　
まだGPUに関わる情報が見えません。

NVIDIA device-pluginを適用してみます。

```
# wget https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/1.0.0-beta6/nvidia-device-plugin.yml
# kubectl -f apply nvidia-device-plugin.yml
daemonset.apps/nvidia-device-plugin-daemonset created
# 
```
デーモンセットで各ノードにGPUを参照するポッドを配置しているようです。

もう一度GPUノードを見てみます。

```
# kubectl describe node worker-gpu
Name:               worker-gpu
Roles:              <none>
～～～～～～
Addresses:
  InternalIP:  192.168.207.201
  Hostname:    worker-gpu
Capacity:
  cpu:                32
  ephemeral-storage:  1951507452Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             131948100Ki
  nvidia.com/gpu:     1
  pods:               110
Allocatable:
  cpu:                32
  ephemeral-storage:  1798509264786
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             131845700Ki
  nvidia.com/gpu:     1
  pods:               110
～～～～～～～～
PodCIDR:                      10.200.5.0/24
PodCIDRs:                     10.200.5.0/24
Non-terminated Pods:          (3 in total)
  Namespace                   Name                                    CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                   ----                                    ------------  ----------  ---------------  -------------  ---
  kube-system                 kube-flannel-ds-amd64-v4zsx             100m (0%)     100m (0%)   50Mi (0%)        50Mi (0%)      64m
  kube-system                 kube-proxy-vv9kh                        0 (0%)        0 (0%)      0 (0%)           0 (0%)         64m
  kube-system                 nvidia-device-plugin-daemonset-6ztxk    0 (0%)        0 (0%)      0 (0%)           0 (0%)         63m
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests   Limits
  --------           --------   ------
  cpu                100m (0%)  100m (0%)
  memory             50Mi (0%)  50Mi (0%)
  ephemeral-storage  0 (0%)     0 (0%)
  hugepages-1Gi      0 (0%)     0 (0%)
  hugepages-2Mi      0 (0%)     0 (0%)
  nvidia.com/gpu     0          0
～～～～～～
#
```

項目 nvidia.com/gpu: が追加されました。
これでなんかリソース指定でGPUを使えそうですね！


## GPUポッドを起動してみる
ポッド上からGPUが利用出来るか確認します。
以下のマニフェストを作成して

```yaml

# tens-pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: tens-pod
spec:
  containers:
    - name: tens-container
      image: tensorflow/tensorflow:latest-gpu-py3
      command: ["/bin/sleep"]
      args: ["3600"]
      resources:
        limits:
          nvidia.com/gpu: 1   ←これでGPUを要求しています。
```
適用します。

```
# kubectl apply -f tens-pod.yml
pod/tens-pod created
# k describe po | grep -e Name -e Node
Name:         tens-pod
Namespace:    default
Node:         worker-gpu/192.168.207.201
    SecretName:  default-token-8c9dw
Node-Selectors:  <none>
# 
```
ちゃんとGPUノードで起動していますね！
エライ！！

ポッドに接続してGPUが見えているか確認してみましょう！

```
# kubectl exec -it tens-pod -- /bin/bash

________                               _______________
___  __/__________________________________  ____/__  /________      __
__  /  _  _ \_  __ \_  ___/  __ \_  ___/_  /_   __  /_  __ \_ | /| / /
_  /   /  __/  / / /(__  )/ /_/ /  /   _  __/   _  / / /_/ /_ |/ |/ /
/_/    \___//_/ /_//____/ \____//_/    /_/      /_/  \____/____/|__/


WARNING: You are running this container as root, which can cause new files in
mounted volumes to be created as the root user on your host machine.

To avoid this, run the container by specifying your user's userid:

$ docker run -u $(id -u):$(id -g) args...
# nvidia-smi
Thu May 14 16:51:10 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.82       Driver Version: 440.82       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 106...  Off  | 00000000:41:00.0 Off |                  N/A |
| 25%   34C    P8     9W / 120W |      0MiB /  6070MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
#
```
ちゃんとGPUが認識されていますね！


Tensorflowの簡単なものを実行して、GPUが動作しているか確認してみましょう。

```
# python
## 以下をコピペして実行

from tensorflow.python.client import device_lib
device_lib.list_local_devices()

import tensorflow as tf
mnist = tf.keras.datasets.mnist
 
(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
 
model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(input_shape=(28, 28)),
  tf.keras.layers.Dense(512, activation=tf.nn.relu),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
 
model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

```
　
実行中にGPUの負荷を見てみます。

```
# nvidia-smi
Thu May 14 17:16:14 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.82       Driver Version: 440.82       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 106...  Off  | 00000000:41:00.0 Off |                  N/A |
| 25%   44C    P2    25W / 120W |   5905MiB /  6070MiB |      9%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0     13584      C   python                                      5895MiB |
+-----------------------------------------------------------------------------+
#
```

負荷と温度がほんのり上がっているのが分かります。
また、ProssesにPythonが表示されています。

**これでGPUポッドが実行できるようになりました！！**


## おわりに
それぞれの用途に合わせて、マニフェストやコンテナイメージにPythonなどのコードを仕込んで、
ジョブコントローラで実行してもいいですね！
コンテナのジョブとしての実行も、Kubernetesの魅力のひとつですよね！

くれぐれも、GPUを占有しないように、計算が完了したらポッドが終了するような設定を行ってください！

 
[弊社ブログ](http://c.itdo.jp/category/technical-information/docker-kubernetes/)ではKubernetes,AWS, Ansibleをはじめとしたさまざまな技術記事を紹介しています。
是非ご覧ください！