Kubernetes: NVIDIA GPU Operatorを使ったMIG設定の動作検証

# はじめに

NVIDIA Ampere アーキテクチャのGPUより[MIG(Multi-Instance GPU)](https://docs.nvidia.com/datacenter/tesla/mig-user-guide/)というGPUを分割する機能が登場しました。このMIGにより分割されたGPUをKubernetesから利用する際、GPU Instanceを作成しておく必要があります。
2021/12時点では、GPU Instanceを作成する方法としては大きく2つの方法があります。

1. Kubernetesのノードのsystemdなどからスクリプト(or コマンド)を呼び出しGPU Instanceを作成
    - 例
        - [mig-parted: The nvidia-mig-manager.service](https://github.com/NVIDIA/mig-parted/tree/master/deployments/systemd) 
        - [Utilizing NVIDIA Multi-Instance GPU (MIG) in Amazon EC2 P4d Instances on Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/jp/blogs/containers/utilizing-nvidia-multi-instance-gpu-mig-in-amazon-ec2-p4d-instances-on-amazon-elastic-kubernetes-service-eks/)
 
2. [NVIDIA/gpu-operator](https://github.com/NVIDIA/gpu-operator)を使いKubernetes上のPodからGPU Instanceを作成

本検証では、上記の2について動作検証を行います。

# 検証環境

- Kubernetes v1.22.0
- GPU: NVIDIA A100
- nvidia/k8s-device-plugin:v0.9.0

# 検証

本検証では、MIGのGPU Instance作成向けの便利ツールmig-partedのGitHubリポジトリで提供されているgpu-operatorのManifestを使い検証を行います。

- [NVIDIA/mig-parted: gpu-operator](https://github.com/NVIDIA/mig-parted/tree/master/deployments/gpu-operator)

## GPU Instanceの作成

まず、MIGを設定するためには、GPUが利用されていない状態にする必要があります。
**利用されていない状態**とは、GPUで実行されているプロセスの有無だけでなく、`/dev/nvidia{N}`のデバイスがnvidia-persistencedのデーモンプロセス以外から使われていない状態を指します。
GPUを利用するアプリケーションを実行していなくても、Kubernetes上でNVIDIA GPU向けにdcgm exporterなどをデプロイしている場合などは、`/dev/nvidia{N}`のデバイスを利用していますので、これらを事前に削除する必要があります。
不安な場合は、A100を備えたノードに入り、lsofコマンドなどで確認してください。

```
$ sudo lsof /dev/nvidia*
COMMAND    PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
nvidia-pe 1406 root    3u   CHR 195,255      0t0  576 /dev/nvidiactl
nvidia-pe 1406 root    4u   CHR   195,0      0t0  579 /dev/nvidia0
nvidia-pe 1406 root    5u   CHR   195,0      0t0  579 /dev/nvidia0
nvidia-pe 1406 root    6u   CHR   195,0      0t0  579 /dev/nvidia0
nvidia-pe 1406 root    7u   CHR   195,0      0t0  579 /dev/nvidia0
```

ここから、実際にGPU Instanceを作成していきます。
まず、A100を搭載したノードに入りMIGを有効化します。

```
$ sudo nvidia-smi -i 0 -mig 1
Enabled MIG Mode for GPU 00000000:00:05.0
All done.
```

次に、[nvidia/k8s-device-plugin](https://github.com/NVIDIA/k8s-device-plugin)のManifest(`nvidia-device-plugin.yml`)を編集します。

```
$ wget https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.9.0/nvidia-device-plugin.yml
```

編集箇所は以下です。`--mig-strategy`オプションを付与しています。

```diff
--- nvidia-device-plugin.yml.org	2021-12-17 19:43:12.000000000 +0900
+++ nvidia-device-plugin.yml	2021-12-17 19:44:11.000000000 +0900
@@ -48,7 +48,7 @@
       containers:
       - image: nvcr.io/nvidia/k8s-device-plugin:v0.9.0
         name: nvidia-device-plugin-ctr
-        args: ["--fail-on-init-error=false"]
+        args: ["--fail-on-init-error=false", "--mig-strategy=mixed"]
         securityContext:
           allowPrivilegeEscalation: false
           capabilities:
```

編集したManifest(`nvidia-device-plugin.yml`)をデプロイします。

```
$ kubectl apply -f nvidia-device-plugin.yml
```

なお、この状態は、MIGを有効にしただけでGPU Instanceがないため、DaemonSet(`nvidia-device-plugin-daemonset`)のPod起動に失敗します(意図通り)。

```
$ kubectl get pod -n kube-system nvidia-device-plugin-daemonset-q2f7p 
NAME                                   READY   STATUS             RESTARTS      AGE
nvidia-device-plugin-daemonset-q2f7p   0/1     CrashLoopBackOff   3 (30s ago)   74s

$ kubectl logs nvidia-device-plugin-daemonset-q2f7p 
2021/11/15 07:12:36 Loading NVML
2021/11/15 07:12:36 Starting FS watcher.
2021/11/15 07:12:36 Starting OS watcher.
2021/11/15 07:12:36 Retreiving plugins.
2021/11/15 07:12:36 Shutdown of NVML returned: <nil>
panic: At least one device with migEnabled=true was not configured correctly: No MIG devices associated with /dev/nvidia0: GPU-2bacbad4-83e9-78f1-b44f-d10a754a60b8

goroutine 1 [running]:
main.(*migStrategyMixed).GetPlugins(0x1042638, 0x5, 0xae1140, 0x1042638)
        /go/src/nvidia-device-plugin/mig-strategy.go:171 +0xa41
main.start(0xc420100ec0, 0x0, 0x0)
        /go/src/nvidia-device-plugin/main.go:146 +0x54c
nvidia-device-plugin/vendor/github.com/urfave/cli/v2.(*App).RunContext(0xc420268000, 0xae5a40, 0xc42002c018, 0xc42001e090, 0x3, 0x3, 0x0, 0x0)
        /go/src/nvidia-device-plugin/vendor/github.com/urfave/cli/v2/app.go:315 +0x6c8
nvidia-device-plugin/vendor/github.com/urfave/cli/v2.(*App).Run(0xc420268000, 0xc42001e090, 0x3, 0x3, 0x4567e0, 0xc42018df50)
        /go/src/nvidia-device-plugin/vendor/github.com/urfave/cli/v2/app.go:215 +0x61
main.main()
        /go/src/nvidia-device-plugin/main.go:88 +0x751
```

`nvidia-device-plugin-daemonset`のPodが起動しないのは意図通りのため、ここでは一旦無視します。
次に、mig-partedのgpu-operatorのサンプルのManifest(`nvidia-mig-manager-example.yaml`)をダウンロードします。

```
$ wget https://raw.githubusercontent.com/NVIDIA/mig-parted/master/deployments/gpu-operator/nvidia-mig-manager-example.yaml
`nvidia-mig-manager-example.yaml' に保存中
...
$ cp nvidia-mig-manager-example.yaml nvidia-mig-manager.yaml
```

`nvidia-mig-manager-example.yaml`をコピーした `nvidia-mig-manager.yaml`を編集します。
編集内容は、ServiceAccount追加とデプロイするNamespaceの変更を行なっています。
環境によっては変更しなくても良いので、その場合はSkipしてください。

```diff
--- nvidia-mig-manager-example.yaml	2021-11-15 15:31:46.000000000 +0900
+++ nvidia-mig-manager.yaml	2021-11-15 16:29:42.000000000 +0900
@@ -1,9 +1,31 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: gpu-operator
+  labels:
+    name: gpu-operator
+  namespace: kube-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: kube-system:gpu-operator
+  labels:
+    name: gpu-operator
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: cluster-admin
+subjects:
+- kind: ServiceAccount
+  name: gpu-operator
+  namespace: kube-system
 ---
 apiVersion: apps/v1
 kind: DaemonSet
 metadata:
   name: nvidia-mig-manager
-  namespace: default
+  namespace: kube-system
   labels:
     app: nvidia-mig-manager
 spec:
@@ -43,7 +71,7 @@
         - name: HOST_MIG_MANAGER_STATE_FILE
           value: "/etc/systemd/system/nvidia-mig-manager.service.d/override.conf"
         - name: OPERATOR_NAMESPACE
-          value: "gpu-operator-resources"
+          value: "kube-system"
         - name: WITH_SHUTDOWN_HOST_GPU_CLIENTS
           value: "true"
         - name: WITH_REBOOT
@@ -79,7 +107,7 @@
 kind: ConfigMap
 metadata:
   name: gpu-clients
-  namespace: default
+  namespace: kube-system
 data:
   clients.yaml: |
     version: v1
@@ -98,7 +126,7 @@
 kind: ConfigMap
 metadata:
   name: mig-parted-config
-  namespace: default
+  namespace: kube-system
 data:
   config.yaml: |
     version: v1
```

Manifest(`nvidia-mig-manager.yaml`)をデプロイし、DaemonSet(`nvidia-mig-manager`)のPodが起動されていることを確認します。

```
$ kubectl apply -f nvidia-mig-manager.yaml 
serviceaccount/gpu-operator created
clusterrolebinding.rbac.authorization.k8s.io/kube-system:gpu-operator created
daemonset.apps/nvidia-mig-manager created
configmap/gpu-clients created
configmap/mig-parted-config created

$ kubectl get pod -n kube-system |grep nvidia-mig
nvidia-mig-manager-2hccw                                  1/1     Running            0               80s
```

次に、A100を備えたノードにラベル(`nvidia.com/mig.config`)を付与します。
ここで指定する値は、上記でデプロイした[configmap/mig-parted-config](https://github.com/NVIDIA/mig-parted/blob/master/deployments/gpu-operator/nvidia-mig-manager-example.yaml#L103) の値です。

```
$ MIG_CONFIGURATION=all-1g.5gb
$ kubectl label nodes ysaka-mig-w-gpu-db500b78-sg2wl nvidia.com/mig.config=$MIG_CONFIGURATION --overwrite
```

ノードにラベル(`nvidia.com/mig.config`)が付与されると、GPU Instanceが作られます。
Pod(`nvidia-mig-manager-2hccw`)のログを確認します。

```
$ kubectl logs nvidia-mig-manager-2hccw 
...
time="2021-11-15T07:33:21Z" level=debug msg="Parsing config file..."
time="2021-11-15T07:33:21Z" level=debug msg="Selecting specific MIG config..."
time="2021-11-15T07:33:21Z" level=debug msg="Running apply-start hook"
time="2021-11-15T07:33:21Z" level=debug msg="Checking current MIG mode..."
time="2021-11-15T07:33:21Z" level=debug msg="Walking MigConfig for (devices=all)"
time="2021-11-15T07:33:21Z" level=debug msg="  GPU 0: 0x20F110DE"
time="2021-11-15T07:33:21Z" level=debug msg="    Asserting MIG mode: Enabled"
time="2021-11-15T07:33:21Z" level=debug msg="    MIG capable: true\n"
time="2021-11-15T07:33:21Z" level=debug msg="    Current MIG mode: Enabled"
time="2021-11-15T07:33:21Z" level=debug msg="Running apply-exit hook"
MIG configuration applied successfully
...
time="2021-11-15T07:33:22Z" level=info msg="Successfuly updated to MIG config: all-1g.5gb"
time="2021-11-15T07:33:22Z" level=info msg="Waiting for change to 'nvidia.com/mig.config' label"
```

ログに`Successfuly updated to MIG config: all-1g.5gb`が出力され、MIGの設定に成功したことがわかります。
念のため、A100を備えたノードにkubectl execコマンドで入り、`nvidia-smi`コマンドを使って、GPU Instanceが作られているかを確認します。

```
$ nvidia-smi
Mon Nov 15 07:35:11 2021       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 495.29.05    Driver Version: 495.29.05    CUDA Version: 11.5     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA A100-PCI...  On   | 00000000:00:05.0 Off |                   On |
| N/A   33C    P0    36W / 250W |     24MiB / 40536MiB |     N/A      Default |
|                               |                      |              Enabled |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| MIG devices:                                                                |
+------------------+----------------------+-----------+-----------------------+
| GPU  GI  CI  MIG |         Memory-Usage |        Vol|         Shared        |
|      ID  ID  Dev |           BAR1-Usage | SM     Unc| CE  ENC  DEC  OFA  JPG|
|                  |                      |        ECC|                       |
|==================+======================+===========+=======================|
|  0    7   0   0  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
|  0    8   0   1  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
|  0    9   0   2  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
|  0   11   0   3  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
|  0   12   0   4  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
|  0   13   0   5  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
|  0   14   0   6  |      3MiB /  4864MiB | 14      0 |  1   0    0    0    0 |
|                  |      0MiB /  8191MiB |           |                       |
+------------------+----------------------+-----------+-----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```

`MIG devices`の項目に作成されたGPU Instanceが並んでいます。
また、GPU Instanceが作られたことで、エラーになっていたPod(`nvidia-device-plugin-daemonset-q2f7p`)が`Running`になりました。

```
$ kubectl get pod -n kube-system |grep nvidia
nvidia-device-plugin-daemonset-q2f7p                      1/1     Running   10 (6m20s ago)   27m
nvidia-mig-manager-2hccw                                  1/1     Running   0                5m
```

kubectl describeコマンドを使いノードの利用可能なリソースとしてGPUが認識されているかを確認します。

```
$ kubectl describe node ysaka-mig-w-gpu-db500b78-sg2wl 
...
Allocatable:
  cpu:                    62
  ephemeral-storage:      224612683405
  hugepages-1Gi:          0
  hugepages-2Mi:          0
  memory:                 241127988Ki
  nvidia.com/gpu:         0
  nvidia.com/mig-1g.5gb:  7
  pods:                   110
...
```

`nvidia.com/mig-1g.5gb`が7個、利用可能なリソースとして追加されました。
以上で、設定は完了です。

## `mig-1g.5gb`のGPUの動作確認

上記で作成したGPU Instance(`mig-1g.5gb`)を使いcuda-vector-addを実行してみます。
Manifest(`cuda-vector-add.yaml`)は以下です。

- cuda-vector-add.yaml

```YAML
apiVersion: v1
kind: Pod
metadata:
  name: cuda-vector-add
spec:
  restartPolicy: OnFailure
  containers:
    - name: cuda-vector-add
      image: "k8s.gcr.io/cuda-vector-add:v0.1"
      resources:
        limits:
          nvidia.com/mig-1g.5gb: 1
```

Manifest(`cuda-vector-add.yaml`)をデプロイし、実行を確認します。

```
$ kubectl apply -f cuda-vector-add.yaml 
pod/cuda-vector-add created

$ kubectl get pod 
NAME              READY   STATUS      RESTARTS   AGE
cuda-vector-add   0/1     Completed   0          23s

$ kubectl logs cuda-vector-add 
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
Test PASSED
Done
```

問題なく実行されました。

# 感想

　今回の動作検証では、NVIDIA GPUのMIGをKubernetesで設定するgpu-operator(mig-parted)の動作検証を行いました。
　MIGに関しては、MIGの有効/無効の状態は不揮発メモリに保存されるため、ノードがRebootされても設定は残りますが、GPU Instanceの設定はRebootなどノードが再起動されると削除されてしまいます。そのため、GPU Instanceを設定し運用するためには「いつ」「誰が」「どのタイミング」で設定するのかが重要になってきます。
もし、決まった構成のみでGPU Instanceをインフラ管理者から提供する場合は、systemdで設定するのが安全かつシンプルな方法な方法のひとつです。一方、今回検証したgpu-operatorで設定する方法は、ノードに入らずkubectlコマンドを使いKubernetesの操作で設定できる嬉しさがあります。GPUをインフラ管理者が提供し始める時点ではGPU Instanceの構成が定まらず、Kubernetesの利用者側で、GPUを利用するアプリケーションに応じてGPU Instanceの構成を柔軟に変更できるメリットがあります。
ただし、幾つか注意点があります。

- GPUを利用していない状態にする必要があります。kubectl drain コマンドなどでノードからPodをevictする場合は、DaemonSetのPodも対象となるよう`--ignore-daemonsets=true` などの指定を行ってください。
  - 上記でノードがGPUを利用していない環境を作り出すのが不安な場合は、ノードをkubectl drainやcordonなどでスケジューリング対象から外したのち、ノードをRebootしGPUを利用していない状態を担保するのも手です。
- gpu-operatorではノードのラベルしかチェックしておらず、ノード自身のイベントを拾いRecocilation Loopで処理などは行っていません。そのため、ノードが障害などで再起動された場合は、GPU Instanceの設定が消去し、MIGが有効化された状態となります(動作検証で示したようにDaemonSet(`nvidia-device-plugin-daemonse`)のPodもRunningにならない状態になります)。このような状態に陥りGPUが利用不可とならないようにアラートなど含めて運用を作り上げる必要があります。

上記のように幾つか注意点があるものの、組織によっては、Kubernetesの操作でより柔軟なGPU Instanceの運用を行いたい場合や、ノードへ入る権限を持ち合わせていない場合などgpu-operatorを使う場合も出てくると推察します。このような場合に備えて、本方法も手段の一つとして知っておくと、自身の組織に適した構成や運用を作れるのではないでしょうか。