NVIDIA Container Toolkit (NVIDIA Docker) は何をしてくれるか

Docker で NVIDIA GPU を利用する場合は、[NVIDIA Container Toolkit](https://github.com/NVIDIA/nvidia-docker) (NVIDIA Docker) が必要になります。この記事では、なぜ NVIDIA Container Toolkit がなぜ必要か、どう動くかについて個人的に調べたことを記載します。2021 年 4 月時点での情報になります。今後のバージョンアップで内容が古くなっている可能性がある点にご注意ください ([#確認した環境](#確認した環境))。

NVIDIA Container Toolkit (NVIDIA Docker) 自体のインストール方法や歴史については、NVIDIA Japan の[NVIDIA Docker って今どうなってるの？ (20.09 版)](https://medium.com/nvidiajapan/nvidia-docker-%E3%81%A3%E3%81%A6%E4%BB%8A%E3%81%A9%E3%81%86%E3%81%AA%E3%81%A3%E3%81%A6%E3%82%8B%E3%81%AE-20-09-%E7%89%88-558fae883f44) という記事が非常によくまとまっているため、そちらをご覧ください。

内容が長いため、「[まとめ](#まとめ)」の章を目次として見ていただければと思います。

## なぜ NVIDIA Container Toolkit が必要か
NVIDIA GPU はホストのデバイスファイル (`/dev/nvidia0` など) として登録されています。また、CUDA ライブラリ(`libcuda.so` など)はホストにインストールされたバージョンと一致したものが必要です。そのためコンテナから GPU を利用する場合、これらをすべてホストからマウントする必要があります。

しかし、デバイス名やライブラリのバージョンは可変であり、設定は煩雑です。マウントしたライブラリの利用前には `ldconfig` を実行して、共有ライブラリの更新をするなどの前処理も必要になります。

NVIDIA Container Toolkit は、コンテナで NVIDIA GPU が使えるように、マウントなどの準備を自動で行ってくれるものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100377/cf841dca-b95a-4cea-3a58-781cc6bac44b.png)

ホスト側の NVIDIA デバイスやライブラリをマウントするため、ホスト側に NVIDIA ドライバがインストールされている必要があります。NVIDIA ドライバのインストールはいくつかの方法がありますが、`cuda-drivers` パッケージでインストールするのが簡単です。

### マウントされる内容を見てみる
Docker の `--gpus` オプションの有無でマウントの差分を比較すると、NVIDIA Container Toolkit によってマウントされた内容が確認できます。マウントされる内容は、指定した GPU オプションによって多少異なります。マウント以外にも共有ライブラリの更新を行う `ldconfig` の実行なども自動で行われています。

```console:マウントの差分を確認するコマンド例
diff -U 0 \
  <(sudo docker run --runtime=runc --entrypoint /bin/mount nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2) \
  <(sudo docker run --runtime=runc --gpus=all --entrypoint /bin/mount nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2)
```

```diff:マウントの差分例
--- /dev/fd/63  2021-04-08 08:18:42.567678185 +0000
+++ /dev/fd/62  2021-04-08 08:18:42.567678185 +0000
@@ -1 +1 @@
-overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/2V65PYVZQEQEQUKYGYKL7HR5FR:/var/lib/docker/overlay2/l/7AIBFJWODCJRWMDLILRWOHB4Q7:/var/lib/docker/overlay2/l/V4OWAINIOOG5CUMPSAKM3VJZ5Q:/var/lib/docker/overlay2/l/LKGX4OMPAYSA47UDWAQDI2X5T3:/var/lib/docker/overlay2/l/NMVYKU22UFTYCR746XX4YXYZAJ:/var/lib/docker/overlay2/l/KH33H3HWPX7J54CSBPGEHVODCH:/var/lib/docker/overlay2/l/NPFWWK26TM64CQQJK3GUYF7RPO:/var/lib/docker/overlay2/l/A76FTIXHIEOSZVT427WCLXYTVP:/var/lib/docker/overlay2/l/SZJWHW7O4WNEYUYJ6S2DZ7M22K,upperdir=/var/lib/docker/overlay2/4e868bfba66474c89e5afe795f9cba4e8c30f03b33e8270bceda5f8f718f1b72/diff,workdir=/var/lib/docker/overlay2/4e868bfba66474c89e5afe795f9cba4e8c30f03b33e8270bceda5f8f718f1b72/work,xino=off)
+overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/GPNKN2XS3ZRFTOLF67IUBLPJOG:/var/lib/docker/overlay2/l/7AIBFJWODCJRWMDLILRWOHB4Q7:/var/lib/docker/overlay2/l/V4OWAINIOOG5CUMPSAKM3VJZ5Q:/var/lib/docker/overlay2/l/LKGX4OMPAYSA47UDWAQDI2X5T3:/var/lib/docker/overlay2/l/NMVYKU22UFTYCR746XX4YXYZAJ:/var/lib/docker/overlay2/l/KH33H3HWPX7J54CSBPGEHVODCH:/var/lib/docker/overlay2/l/NPFWWK26TM64CQQJK3GUYF7RPO:/var/lib/docker/overlay2/l/A76FTIXHIEOSZVT427WCLXYTVP:/var/lib/docker/overlay2/l/SZJWHW7O4WNEYUYJ6S2DZ7M22K,upperdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/diff,workdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/work,xino=off)
@@ -23,0 +24,22 @@
+tmpfs on /proc/driver/nvidia type tmpfs (rw,nosuid,nodev,noexec,relatime,mode=555)
+/dev/vda1 on /usr/bin/nvidia-smi type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/bin/nvidia-debugdump type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/bin/nvidia-persistenced type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/bin/nvidia-cuda-mps-control type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/bin/nvidia-cuda-mps-server type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-cfg.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libcuda.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-opencl.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-allocator.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-compiler.so.460.32.03 type ext4 (ro,nosuid,nodev,relatime)
+overlay on /usr/lib/x86_64-linux-gnu/libcuda.so.440.118.02 type overlay (ro,nosuid,nodev,relatime,lowerdir=/var/lib/docker/overlay2/l/GPNKN2XS3ZRFTOLF67IUBLPJOG:/var/lib/docker/overlay2/l/7AIBFJWODCJRWMDLILRWOHB4Q7:/var/lib/docker/overlay2/l/V4OWAINIOOG5CUMPSAKM3VJZ5Q:/var/lib/docker/overlay2/l/LKGX4OMPAYSA47UDWAQDI2X5T3:/var/lib/docker/overlay2/l/NMVYKU22UFTYCR746XX4YXYZAJ:/var/lib/docker/overlay2/l/KH33H3HWPX7J54CSBPGEHVODCH:/var/lib/docker/overlay2/l/NPFWWK26TM64CQQJK3GUYF7RPO:/var/lib/docker/overlay2/l/A76FTIXHIEOSZVT427WCLXYTVP:/var/lib/docker/overlay2/l/SZJWHW7O4WNEYUYJ6S2DZ7M22K,upperdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/diff,workdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/work,xino=off)
+overlay on /usr/lib/x86_64-linux-gnu/libnvidia-fatbinaryloader.so.440.118.02 type overlay (ro,nosuid,nodev,relatime,lowerdir=/var/lib/docker/overlay2/l/GPNKN2XS3ZRFTOLF67IUBLPJOG:/var/lib/docker/overlay2/l/7AIBFJWODCJRWMDLILRWOHB4Q7:/var/lib/docker/overlay2/l/V4OWAINIOOG5CUMPSAKM3VJZ5Q:/var/lib/docker/overlay2/l/LKGX4OMPAYSA47UDWAQDI2X5T3:/var/lib/docker/overlay2/l/NMVYKU22UFTYCR746XX4YXYZAJ:/var/lib/docker/overlay2/l/KH33H3HWPX7J54CSBPGEHVODCH:/var/lib/docker/overlay2/l/NPFWWK26TM64CQQJK3GUYF7RPO:/var/lib/docker/overlay2/l/A76FTIXHIEOSZVT427WCLXYTVP:/var/lib/docker/overlay2/l/SZJWHW7O4WNEYUYJ6S2DZ7M22K,upperdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/diff,workdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/work,xino=off)
+overlay on /usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.440.118.02 type overlay (ro,nosuid,nodev,relatime,lowerdir=/var/lib/docker/overlay2/l/GPNKN2XS3ZRFTOLF67IUBLPJOG:/var/lib/docker/overlay2/l/7AIBFJWODCJRWMDLILRWOHB4Q7:/var/lib/docker/overlay2/l/V4OWAINIOOG5CUMPSAKM3VJZ5Q:/var/lib/docker/overlay2/l/LKGX4OMPAYSA47UDWAQDI2X5T3:/var/lib/docker/overlay2/l/NMVYKU22UFTYCR746XX4YXYZAJ:/var/lib/docker/overlay2/l/KH33H3HWPX7J54CSBPGEHVODCH:/var/lib/docker/overlay2/l/NPFWWK26TM64CQQJK3GUYF7RPO:/var/lib/docker/overlay2/l/A76FTIXHIEOSZVT427WCLXYTVP:/var/lib/docker/overlay2/l/SZJWHW7O4WNEYUYJ6S2DZ7M22K,upperdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/diff,workdir=/var/lib/docker/overlay2/a4aaa0c474a91c9856d083d06618161c34516d4a3a361032c08f34c44bbab08f/work,xino=off)
+tmpfs on /run/nvidia-persistenced/socket type tmpfs (rw,nosuid,nodev,noexec,relatime,size=11344316k,mode=755)
+udev on /dev/nvidiactl type devtmpfs (ro,nosuid,noexec,relatime,size=56704416k,nr_inodes=14176104,mode=755)
+udev on /dev/nvidia-uvm type devtmpfs (ro,nosuid,noexec,relatime,size=56704416k,nr_inodes=14176104,mode=755)
+udev on /dev/nvidia-uvm-tools type devtmpfs (ro,nosuid,noexec,relatime,size=56704416k,nr_inodes=14176104,mode=755)
+udev on /dev/nvidia0 type devtmpfs (ro,nosuid,noexec,relatime,size=56704416k,nr_inodes=14176104,mode=755)
+proc on /proc/driver/nvidia/gpus/0000:00:05.0 type proc (ro,nosuid,nodev,noexec,relatime)
```

## Docker での使い方
Docker から NVIDIA GPU を使う場合、主に 2 つの方法があります。

1. Docker の `--gpus` オプションを利用する
2. Docker のランタイムで `nvidia` を利用する

`--gpus` オプションの方が、後から実装されたものになります。このあたりの歴史的経緯については、NVIDIA Japan の[NVIDIA Docker って今どうなってるの？ (20.09 版)](https://medium.com/nvidiajapan/nvidia-docker-%E3%81%A3%E3%81%A6%E4%BB%8A%E3%81%A9%E3%81%86%E3%81%AA%E3%81%A3%E3%81%A6%E3%82%8B%E3%81%AE-20-09-%E7%89%88-558fae883f44) が詳しいです。

どちらも最終的には、Docker で使われているコンテナランタイム [`runc`](https://github.com/opencontainers/runc) の起動前に、NVIDIA Container Toolkit の [preStart Hook](https://github.com/opencontainers/runtime-spec/blob/master/config.md#prestart) で実行されるフック `nvidia-container-runtime-hook ` によって GPU の設定がなされます。それぞれの処理の流れは「[NVIDIA Container Toolkit の処理の流れ](#nvidia-container-toolkit-の処理の流れ)」の章で説明します。

後方互換のため `nvidia-docker` というコマンドも残っていますが、内部的には runtime に `nvidia` を指定して Docker を呼出す(2 番目の方法)だけの[ラッパースクリプト](https://github.com/NVIDIA/nvidia-docker/blob/v2.5.0/nvidia-docker)となっています。

### 1. Docker の `--gpus` オプションを利用する
Docker 19.03 からは `--gpus` オプションが使えるため、Docker 単体では `--gpus` オプションを利用するのが良いでしょうです。次のコマンドは Docker で `--gpus` オプションを指定した例です。

```console
docker run --gpus=all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2
```

### 2. Docker のランタイムで `nvidia` を利用する
Kubernetes は、v1.21 時点では Docker の `--gpus` オプションに対応していません。そのため、Kubernetes で NVIDIA GPU を利用する場合は、`nvidia` ランタイムをデフォルトランタイムに設定して利用する必要があります。([NVIDIA/k8s-device-plugin の Prerequisites](https://github.com/NVIDIA/k8s-device-plugin#prerequisites) を参照)

次のコマンドは、Docker で `nvidia` ランタイムを指定する例です。なお、デフォルトランタイムを `nvidia` に指定した場合 `--runtime` 指定は不要になります。[環境変数で GPU の指定が必要](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#environment-variables-oci-spec)です。

```console
docker run --runtime=nvidia nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2
```

#### デフォルトランタイムの指定
`nvidia` ランタイムをデフォルトランタイムに指定する場合は、`/etc/docker/daemon.json` に `"default-runtime": "nvidia"` の設定を追加します。

```json:/etc/docker/daemon.json
{
    "default-runtime": "nvidia",
    # 上記を追加。他の設定はここでは省略
}
```

## GPU デバイスとオプションの指定
`--gpus` オプションと `nvidia` ランタイムでは、NVIDIA GPU のオプションの指定の仕方が異なります。この指定がない場合、GPU デバイスなどはマウントされません。指定方法の詳細は、公式ドキュメントの [Environment variables (OCI spec)](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#environment-variables-oci-spec) をご覧ください。

利用方法 | 指定の仕方
--- | ---
`--gpus` オプション | `--gpus` オプションの引数
`nvidia` ランタイム | `NVIDIA_VISIBLE_DEVICES` などの環境変数で指定

なお、`--gpus` オプション指定時も[内部的には環境変数が設定](https://github.com/moby/moby/blob/v20.10.6/daemon/nvidia_linux.go#L54-L75)されています。

指定できるオプションは次の通りです。使用する GPU の列挙のみ必須です。ベースイメージに [NVIDIA CUDA](https://ngc.nvidia.com/catalog/containers/nvidia:cuda) を指定している場合などは、ベースイメージ側で環境変数 (`NVIDIA_VISIBLE_DEVICES=all`) を指定しているので、意識せず指定している場合があります。

- [使用する GPU の列挙](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#gpu-enumeration)
  -  どの GPU デバイスを利用するか指定する
  -  `all` の場合全て。`0,1,2` のインデックスや UUID でも指定できる
- [ドライバ Capabilities](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#driver-capabilities)
  - マウントするドライバの種類を指定 e.g. `compute, utility`
- [CUDA バージョンなどの制限](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#constraints)
  - 環境変数での設定のみ (`--gpus` オプションでは対応していない)

なお、Kubernetes で利用する場合、GPU 列挙の指定はリソースリクエストの `nvidia.com/gpu` 指定に応じて、[NVIDIA device plugin](https://github.com/NVIDIA/k8s-device-plugin) 側で設定されます。

## アーキテクチャ
公式ドキュメントの [Architecture Overview](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/arch-overview.html) を参考に、NVIDIA Container Toolkit のアーキテクチャを紹介します。

Docker の場合、NVIDIA Container Toolkit は主に次のコンポーネントで構成されています。コンポーネントは上から下の順で依存する形になっています。利用方法によって、本当に依存しているコンポーネントは変わってきます。しかし、公式ドキュメントの [Which package should I use then?](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/arch-overview.html#which-package-should-i-use-then) では、シンプルさと過去の互換性のため、すべてを含む `nvidia-docker2` をトップレベルパッケージとして利用することが推奨されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100377/04c6cf45-2d6c-5731-af32-3c0429546718.png)

細かくコンポーネントが分かれている理由は、関連するコンポーネントを見ると理解しやすいです。例えば最終的に NVIDIA GPU の設定を行う `libnvidia-container` は、上位レイヤの [OCI Spec](https://github.com/opencontainers/runtime-spec) には依存せず、どのコンテナランタイムでも利用できる作りになっています。

### nvidia-docker2
https://github.com/NVIDIA/nvidia-docker

Docker 用のトップレベルパッケージです。このパッケージを入れれば、以下のコンポーネントは依存パッケージとして自動的にインストールされます。NVIDIA ドライバ(e.g. `cuda-drivers`)は別途インストールする必要があります。

このパッケージには、`nvidia-container-runtime` のランタイム `nvidia` を Docker に追加する以下の設定 ([/etc/docker/daemon.json](https://github.com/NVIDIA/nvidia-docker/blob/master/daemon.json)) が含まれています。すでに `daemon.json` の設定ファイルがある場合は、手動での解決が必要になるのでご注意ください。(dpkg の場合は [conffiles](https://www.debian.or.jp/community/devel/debian-policy-ja/policy.ja.html/ap-pkg-conffiles.html) 指定となっている)

```json
{
    "runtimes": {
        "nvidia": {
            "path": "nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}
```

### nvidia-container-runtime
https://github.com/NVIDIA/nvidia-container-runtime

runc をラップした OCI ランタイム準拠の `nvidia` ランタイムが含まれています。Docker に `--runtime=nvidia` をつけたときに、このランタイムが呼ばれます。

この `nvidia` ランタイムは、NVIDIA の設定処理を行うフック `nvidia-container-runtime-hook` を [preStart Hook に設定して](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go#L135-L138)、[runc を実行する](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go#L102)だけのラッパーとなっています。ソースコードも 200 行程度の [main.go](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go) だけとシンプルです。

`nvidia-container-runtime-hook` は次項のコンポーネント `nvidia-container-toolkit` に含まれバイナリです。symlink となっており、実体は `nvidia-container-toolkit` というバイナリです。

```console
$ ls -l /usr/bin/nvidia-container-runtime-hook
lrwxrwxrwx 1 root root 33 Apr 13 09:12 /usr/bin/nvidia-container-runtime-hook -> /usr/bin/nvidia-container-toolkit
```

### nvidia-container-toolkit
https://github.com/NVIDIA/nvidia-container-toolkit

runc が呼び出す OCI の preStart Hook です。このフックは実際の NVIDIA 設定処理を行う `nvidia-container-cli` を[呼出します](https://github.com/NVIDIA/nvidia-container-toolkit/blob/v1.4.2/pkg/main.go#L79)。Docker の`--gpus` オプションを利用する場合、本当に依存しているのは `nvidia-container-toolkit` 以下のコンポーネントだけになります。GPU の設定は、[環境変数](https://github.com/NVIDIA/nvidia-container-toolkit/blob/v1.4.2/pkg/container_config.go#L19-L26)経由で行われます。

OCI の preStart Hook として実装されているため `runc` に限らず、[OCI Runtime の仕様](https://github.com/opencontainers/runtime-spec) に沿ったコンテナランタイムであれば、利用できる仕組みになっています。

### libnvidia-container
https://github.com/NVIDIA/libnvidia-container/

Linux コンテナに対して NVIDIA GPU に必要な設定を実際に行う `nvidia-container-cli` が含まれています。Ubuntu などのパッケージは `libnvidia-container1` (ライブラリ) と `libnvidia-container-tools` (CLI) に分かれています。C で書かれており、namespace など Linux コンテナの機能を直接利用しているため、Linux コンテナの実装にしない仕組みとなっています。GPU デバイスやライブラリのマウントだけでなく、`ldconfig` の実行やカーネルモジュールのロードといった機能も含まれています。

以下 Docker での `nvidia-container-cli` の呼び出される引数を整形した例です。

```bash
nvidia-container-cli \
  --load-kmods \                       # NVIDIA のカーネルモジュールをロードする
  configure \                          # コンテナの設定を行うサブコマンド
    --ldconfig=@/sbin/ldconfig.real \  # ldconfig バイナリの指定
    --device=all \                     # 使用 GPU デバイスを指定
    --utility \                        # utility ドライバを有効にする
    --compute \                        # compute ドライバを有効にする
    --pid=57316 \                      # コンテナの PID を指定
    /var/lib/docker/overlay2/6a683460c03ce2c1017d7b3efdb274c6c281e039da4de6e079b268e71832f21e/merged
    # ↑rootfs の指定
```

また、`list` というサブコマンドを使うと、GPU サポートに必要なファイルのリストを確認できます。

```
$ nvidia-container-cli list
/dev/nvidiactl
/dev/nvidia-uvm
/dev/nvidia-uvm-tools
/dev/nvidia-modeset
/dev/nvidia0
/usr/bin/nvidia-smi
/usr/bin/nvidia-debugdump
/usr/bin/nvidia-persistenced
/usr/bin/nvidia-cuda-mps-control
/usr/bin/nvidia-cuda-mps-server
/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-cfg.so.460.56
/usr/lib/x86_64-linux-gnu/libcuda.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-opencl.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-allocator.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-compiler.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-ngx.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-opticalflow.so.460.56
/usr/lib/x86_64-linux-gnu/libnvcuvid.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-eglcore.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-glcore.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-tls.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-glsi.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-fbc.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-ifr.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-rtcore.so.460.56
/usr/lib/x86_64-linux-gnu/libnvoptix.so.460.56
/usr/lib/x86_64-linux-gnu/libGLX_nvidia.so.460.56
/usr/lib/x86_64-linux-gnu/libEGL_nvidia.so.460.56
/usr/lib/x86_64-linux-gnu/libGLESv2_nvidia.so.460.56
/usr/lib/x86_64-linux-gnu/libGLESv1_CM_nvidia.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-glvkspirv.so.460.56
/usr/lib/x86_64-linux-gnu/libnvidia-cbl.so.460.56
/run/nvidia-persistenced/socket
```

## NVIDIA Container Toolkit の処理の流れ
Docker の `--gpus` オプションを利用した場合と、ランタイムで `nvidia` を利用した場合で若干処理の流れが異なります。どちらも最終的には、`nvidia-container-toolkit` が runc の preStart Hook として設定され、実際の設定を行う `nvidia-container-cli` が呼ばれることになります。


### Docker の復習
`docker run` でコンテナを実行した場合、主に次のようにコンポーネントが呼ばれます。最終的にコンテナを実行するランタイムが runc です。(shim は省略しています)

コンポーネント | 概要 | レポジトリ
--- | --- | ---
docker CLI | Docker の CLI | [docker/cli](https://github.com/docker/cli)
dockerd | Docker Engine | [moby/moby](https://github.com/moby/moby)
containerd | 実際のコンテナやイメージ管理を行う | [containerd/containerd](https://github.com/containerd/containerd/)
 runc | [OCI Runtime Spec](https://github.com/opencontainers/runtime-spec) 準拠のコンテナランタイム | [opencontainers/runc](https://github.com/opencontainers/runc)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100377/b9666aa8-6351-ad6c-505f-13f2334402a6.png)

### `--gpus` オプションを利用した場合
1. Docker CLI
    1. `--gpus` オプションが[指定される](https://github.com/docker/cli/blob/v20.10.6/opts/gpus.go#L28)
    1. `Capabilities` の `gpu` などが [DeviceRequests に設定される](https://github.com/docker/cli/blob/v20.10.6/opts/gpus.go#L35-L91)
    1. DeviceRequests が Docker Engine に設定として渡される
1. Docker Engine
    1. deviceDriver 指定が空のとき、[自動的に対応可能なドライバが選択される](https://github.com/moby/moby/blob/v20.10.6/daemon/devices_linux.go#L26-L31) (20.10.6 時点では gpu は `nvidia` のみ)
    1. deviceDriver として登録された [setNvidiaGPUs()](https://github.com/moby/moby/blob/v20.10.6/daemon/nvidia_linux.go#L48) が呼ばれる
    1. [`nvidia-container-runtime-hook`](https://github.com/NVIDIA/nvidia-container-toolkit/blob/v1.4.2/pkg/main.go) を [preStart Hook に設定する](https://github.com/moby/moby/blob/v20.10.6/daemon/nvidia_linux.go#L86-L93)
1. containerd
    1. runc を呼出す
1. runc
    1. preStart Hook に設定された `nvidia-container-runtime-hook` を呼出す
    1. フック内で [`nvidia-container-cli`](https://github.com/NVIDIA/libnvidia-container/blob/v1.3.3/src/cli/main.c) が[呼び出される](https://github.com/NVIDIA/nvidia-container-toolkit/blob/v1.4.2/pkg/main.go#L97-L153)
    1. GPU が準備された状態でコンテナプロセスが起動する :tada:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100377/aed6fa53-3f7b-2d31-6586-b052063fe0bf.png)


以下は `--gpus=all` を指定したコンテナプロセスを `docker inspect` で見た DeviceRequests の設定です。`Capabilities` に "gpu" が指定されていることがわかります。

```json:
            "DeviceRequests": [
                {
                    "Driver": "",
                    "Count": -1,
                    "DeviceIDs": null,
                    "Capabilities": [
                        [
                            "gpu"
                        ]
                    ],
                    "Options": {}
                }
            ],
```

### `nvidia` ランタイムを利用した場合
1. Docker CLI
    1. `--runtime` オプションで `nvidia` を指定 (デフォルトランタイムの場合は不要)
    1. `Runtime` オプションに `nvidia` が指定される Docker Engine に渡される
1. Docker Engine
    1. containerd のランタイムとして `nvidia` ランタイムを指定
1. containerd
    1. runc の代わりに `nvidia` ランタイムを呼出す
1. [nvidia ランタイム](https://github.com/NVIDIA/nvidia-container-runtime)
    1. [`nvidia-container-runtime-hook`](https://github.com/NVIDIA/nvidia-container-toolkit/blob/v1.4.2/pkg/main.go) を [preStart Hook に設定する](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go#L120-L138)
    1. [runc が呼び出される](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go#L90-L104)
        1. preStart Hook に設定された `nvidia-container-runtime-hook` を呼出す
        1. フック内で [`nvidia-container-cli`](https://github.com/NVIDIA/libnvidia-container/blob/v1.3.3/src/cli/main.c) が[呼び出される](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go#L90-L104)
        1. GPU が準備された状態でコンテナプロセスが起動する :tada:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100377/858b6d95-5e5d-4ab5-579c-1f5c1dce2b95.png)

### 補足: `containerd` 単体でも NVIDIA GPU はサポートされる
containerd 単体でも NVIDIA GPU オプションはサポートされています。containerd の CLI である `ctr` コマンドでも `--gpus` オプションをサポートしています。

```console
# 実行用イメージを pull
$ sudo ctr image pull docker.io/library/debian:buster-slim

# --gpus オプション付きで実行。nvidia-smi で GPU が有効であることを確認できる
$ sudo ctr run --gpus 0 --rm docker.io/library/debian:buster-slim debian1 nvidia-smi
Tue Apr 20 04:45:47 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03    Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
...
```

1. `ctr` コマンド
  1. `--gpus` オプションが[指定される](https://github.com/containerd/containerd/blob/v1.4.4/cmd/ctr/commands/run/run_unix.go#L218-L220)
1. containerd
  1. NVIDIA 用の [WithGPUs()](https://github.com/containerd/containerd/blob/v1.4.4/contrib/nvidia/nvidia.go#L68) が呼ばる
  1. `nvidia-container-cli` が containerd の [oci-hook サブコマンド](https://github.com/containerd/containerd/blob/v1.4.4/cmd/containerd/command/oci-hook.go)経由で [preStart Hook として登録](https://github.com/containerd/containerd/blob/v1.4.4/contrib/nvidia/nvidia.go#L90-L101)される
      1. oci-hook を使うのは動的に引数を渡せるようにするため
1. runc
  1. preStart Hook に設定された oci-hook を呼出す
  1. oci-hook 内で [`nvidia-container-cli`](https://github.com/NVIDIA/libnvidia-container/blob/v1.3.3/src/cli/main.c) が[呼び出される](https://github.com/NVIDIA/nvidia-container-runtime/blob/v3.4.2/src/main.go#L90-L104)
  1. GPU が準備された状態でコンテナプロセスが起動する :tada:

## Tips: nvidia-smi の実行
コンテナ内で NVIDA GPU が使えることを確認する場合、`nvidia-smi` (NVIDIA System Management Interface) コマンドを使うことがよくあります。

[公式ドキュメント](https://docs.nvidia.com/datacenter/cloud-native/index.html)の [Setting up NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/nvidia-docker.html?#setting-up-nvidia-container-toolkit) でも以下のコマンドが確認手順として載っています。

```
sudo docker run --rm --gpus all nvidia/cuda:11.0-base nvidia-smi
```

このとき、`nvidia-smi` コマンドはマウントされたホストのものが使われています。次のように `--gpus` オプションをなしにすると、 `nvidia/cuda:11.0-base` イメージに `nvidia-smi` のバイナリが含まれないことを確認できます。

```
sudo docker run --rm --runtime=runc nvidia/cuda:11.0-base which nvidia-smi
# --gpus オプションなしだと何も表示されない
```

`--gpus` オプションさえ指定されていれば、他のコンテナイメージでも `nvidia-smi` コマンドを実行できます。例えば、以下は `debian:buster-slim` イメージを使って実行した例です。外部のコンテナレジストリにアクセスできないような[エアギャップ環境](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%A2%E3%82%AE%E3%83%A3%E3%83%83%E3%83%97)だと、この方法を知っていると便利です。

```
sudo docker run --rm --gpus all debian:buster-slim nvidia-smi
```
```
Fri Apr  9 00:49:57 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03    Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| 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  Tesla P4            On   | 00000000:00:05.0 Off |                    0 |
| N/A   36C    P8     7W /  75W |      0MiB /  7611MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

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

## まとめ
- [なぜ NVIDIA Container Toolkit が必要か](#なぜ-nvidia-container-toolkit-が必要か)
  - ホストの GPU デバイスや CUDA ライブラリをマウントする必要があるため
  - マウント先は可変であり、設定は煩雑
  - NVIDIA Container Toolkit はマウントなど事前準備を自動で行ってくれるもの
- [Docker での使い方](#docker-での使い方)
  - 「`--gpus` オプションの指定」と「`nvidia` ランタイムの使用」の 2 パターンがある
  - Dokcer 単体は `--gpus` が推奨、Kubernetes で使う場合は `nvidia` ランタイムをデフォルトにする必要がある
- [GPU デバイスとオプションの指定](#gpu-デバイスとオプションの指定)
  - `--gpus` オプションはオプションの引数で指定
  - `nvidia` ランタイムは環境変数で指定
- [アーキテクチャ](#アーキテクチャ)
  - 主に `nvidia-docker2`, `nvidia-container-runtime`, `nvidia-container-toolkit`, `libnvidia-container` の 4 つで構成
  - Docker の場合はトップレベルパッケージの `nvidia-docker2` をインストールするのが推奨される
  - 最終的なコンテナ設定は `nvidia-container-cli` が行う
- [NVIDIA Container Toolkit の処理の流れ](#nvidia-container-toolkit-の処理の流れ)
  - Docker の `--gpus` オプションを利用した場合と、ランタイムで `nvidia` を利用した場合で若干異なる
  - どちらも `nvidia-container-toolkit` が runc の preStart Hook として設定される
  - containerd 単体でも NVIDIA GPU はサポートされる
- [Tips: nvidia-smi の実行](#tips-nvidia-smi-の実行)
  - `nvidia-smi` は `--gpus` オプションを指定すれば、コンテナイメージは任意のもので良い

## 確認した環境
この記事で確認に使った環境です。

- OS: Ubuntu 20.04
- GPU: [NVIDIA T4](https://www.nvidia.com/en-us/data-center/tesla-t4/)
- パッケージ
  - `cuda-drivers`: 460.32.03-1
  - `nvidia-docker2`: 2.5.0-1
  - `nvidia-container-toolkit`: 1.4.2-1
  - `nvidia-container-runtime`: 3.4.2-1
  - `libnvidia-container1`: 1.3.3-1
  - `docker-ce`: 5:20.10.6~3-0~ubuntu-focal (20.10.6)
  - `containerd.io`: 1.4.4-1

## 参考
- Docker Engine の GPU サポート issue, PR
    - [Add DeviceRequests to HostConfig to support NVIDIA GPUs #38828](https://github.com/moby/moby/pull/38828)
    - [dockerd gpu support #37504](https://github.com/moby/moby/pull/37504)
- Docker CLI の GPU サポートの issue, PR
    - [\[RFC\] GPU support in CLI #1200](https://github.com/docker/cli/issues/1200)
    - [container: --gpus support #1714](https://github.com/docker/cli/pull/1714)
- containerd の GPU サポートの PR
    - [Add nvidia gpu support #2330](https://github.com/containerd/containerd/pull/2330)