DockerのRootlessモードでNVIDIAのGPUを使用する

## 3行まとめ

* RootlessモードでもGPUは使用できる。
* RootlessモードでGPUを使用するためにはnvidia-container-runtimeの設定を変更する必要がある。
* Rootlessモードと通常モードを共存する方法はまだ未解決。

## 1. はじめに

　機械学習分野では、Docker/Kubernetes上でNVIDIAのGPU（以下、単に「GPU」）を使うということがよく行われます。
複数人で共有するGPUマシンのセキュリティをより高めるため、RootlessモードのDockerでGPUが使用できるかどうか調査してみました。
結果から言えば、簡単な設定変更を行うだけで使用できました。（少なくともPyTorchから認識できました）

## 2. 環境

　今回、調査に使用した環境は以下の通りです。

* OS: Ubuntu 19.10（Eoan Ermine）
* GPU: GeForce GTX1070 8GB x4台（ただし1台のみ電源オン状態）
* NVIDIAドライバ: 440.82
* 通常モードのDocker: 19.03.8
* RootlessモードのDocker: 19.03.8

　より詳細な環境の情報は以下の通りです。

```text
yuya$ cat /etc/os-release
NAME="Ubuntu"
VERSION="19.10 (Eoan Ermine)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 19.10"
VERSION_ID="19.10"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=eoan
UBUNTU_CODENAME=eoan

yuya$ uname -a
Linux ml-1 5.3.0-51-generic #44-Ubuntu SMP Wed Apr 22 21:09:44 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

yuya$ dpkg -l | grep nvidia
ii  libnvidia-cfg1-440:amd64             440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA binary OpenGL/GLX configuration library
ii  libnvidia-compute-440:amd64          440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA libcompute package
ii  libnvidia-container-tools            1.0.7-1                                amd64        NVIDIA container runtime library (command-line tools)
ii  libnvidia-container1:amd64           1.0.7-1                                amd64        NVIDIA container runtime library
ii  nvidia-compute-utils-440             440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA compute utilities
ii  nvidia-container-runtime             3.1.4-1                                amd64        NVIDIA container runtime
ii  nvidia-container-toolkit             1.0.5-1                                amd64        NVIDIA container runtime hook
ii  nvidia-dkms-440                      440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA DKMS package
ii  nvidia-docker2                       2.2.2-1                                all          nvidia-docker CLI wrapper
ii  nvidia-headless-440                  440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA headless metapackage
ii  nvidia-headless-no-dkms-440          440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA headless metapackage - no DKMS
ii  nvidia-kernel-common-440             440.82-0ubuntu0~0.19.10.1              amd64        Shared files used with the kernel module
ii  nvidia-kernel-source-440             440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA kernel source package
ii  nvidia-modprobe                      418.56-1                               amd64        utility to load NVIDIA kernel modules and create device nodes
ii  nvidia-utils-440                     440.82-0ubuntu0~0.19.10.1              amd64        NVIDIA driver support binaries

yuya$ nvidia-smi
Fri May  8 15:31:23 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 1070    Off  | 00000000:01:00.0 Off |                  N/A |
|  0%   36C    P8     7W / 195W |      0MiB /  8119MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
...

yuya$ docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b7f0
 Built:             Wed Mar 11 01:25:55 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b7f0
  Built:            Wed Mar 11 01:24:26 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 nvidia:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

yuya$ docker info
...
 Security Options:
  apparmor
  seccomp
   Profile: default
...

yuya$ cat /etc/docker/daemon.json
{
    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "path": "nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}
```

　`nvidia-docker2`パッケージをインストールし、`default-runtime`を`nvidia`に設定しているのは、Kubernetes上でGPUを使用するためです。
主題のRootlessモードには関係ありません。

　なお、記事中で使用している`yuya`ユーザは`docker`グループに属しており、通常モードのDockerにアクセスできます。

```text
yuya$ id yuya
uid=1000(yuya) gid=1000(yuya) groups=1000(yuya),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lxd),998(docker)
```

## 3. 通常モードでのGPUの使用

　通常モード（`root`権限でDockerデーモンが動作している一般的な状態）では、`nvidia-container-toolkit`パッケージをインストールし、`--gpus all`オプションを付加することで、DockerコンテナでGPUを使用することができます。
`nvidia-smi`の実行結果は以下の通りです。Dockerコンテナ内でGPUを認識していることが確認できます。

```text
yuya$ docker container run --tty --rm --gpus all nvidia/cuda:10.2-base-ubuntu18.04 nvidia-smi
Fri May  8 06:54:54 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 1070    Off  | 00000000:01:00.0 Off |                  N/A |
|  0%   36C    P8     7W / 195W |      0MiB /  8119MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
...
```

## 4. RootlessモードでのGPUの使用

### 4.1. RootlessモードのDockerをセットアップする

　まずは、Rootlessモードの実行に必要な`uidmap`パッケージ、`slirp4netns`パッケージをインストールします。

```text
yuya$ sudo apt update && sudo apt install --yes uidmap slirp4netns
```

　続いて、Rootlessモードの動作確認を行うため、`docker`グループに**属していない**新しいユーザ`rootless`を作成します。

```text
yuya$ sudo adduser rootless
yuya$ id rootless
uid=1001(rootless) gid=1001(rootless) groups=1001(rootless)
```

　準備が整ったら、RootlessモードのDockerをインストールしたいユーザ（今回は`rootless`ユーザ）でログインし、RootlessモードのDockerをインストールします。
この際、`su - rootless`などユーザを切り替えるのではなく、ログインシェルが起動する状態でログインする必要があります。そうしないとユーザ毎のsystemdデーモンが起動しないので注意が必要です。念のため、`systemctl --user daemon-reload`が実行できることを確認しておきましょう。

　インストール自体は`curl -fsSL https://get.docker.com/rootless | sh`で一発です。`sudo`コマンドを使用していないことに気を付けましょう。

```text
rootless$ systemctl --user daemon-reload
rootless$ echo $?
0

rootless$ curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 19.03.8
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 60.7M  100 60.7M    0     0  2995k      0  0:00:20  0:00:20 --:--:-- 2904k
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 17.8M  100 17.8M    0     0  2511k      0  0:00:07  0:00:07 --:--:-- 2559k
# starting systemd service
● docker.service - Docker Application Container Engine (Rootless)
   Loaded: loaded (/home/rootless/.config/systemd/user/docker.service; disabled; vendor preset: enabled)
   Active: active (running) since Fri 2020-05-08 15:39:07 JST; 3ms ago
     Docs: https://docs.docker.com
 Main PID: 14632 (dockerd-rootles)
   CGroup: /user.slice/user-1001.slice/user@1001.service/docker.service
           ├─14632 /bin/sh /home/rootless/bin/dockerd-rootless.sh --experimental --storage-driver=overlay2
           └─14639 [grep]

May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + : auto
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + net=
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + mtu=
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + [ -z ]
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + which slirp4netns
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + slirp4netns --help
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + grep -- --disable-host-loopback
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: --disable-host-loopback  prohibit connecting to 127.0.0.1:* on the host namespace
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + net=slirp4netns
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + [ -z ]
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + mtu=65520
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + [ -z slirp4netns ]
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + [ -z 65520 ]
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + [ -z ]
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + _DOCKERD_ROOTLESS_CHILD=1
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + export _DOCKERD_ROOTLESS_CHILD
May 08 15:39:07 ml-1 dockerd-rootless.sh[14632]: + exec rootlesskit --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/rootless/bin/dockerd-rootless.sh --experimental --storage-driver=overlay2
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b7f0
 Built:             Wed Mar 11 01:22:56 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b7f0
  Built:            Wed Mar 11 01:30:32 2020
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
# Docker binaries are installed in /home/rootless/bin
# WARN: dockerd is not in your current PATH or pointing to /home/rootless/bin/dockerd
# Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/home/rootless/bin:$PATH
export DOCKER_HOST=unix:///run/user/1001/docker.sock

#
# To control docker service run:
# systemctl --user (start|stop|restart) docker
#
```

### 4.2. RootlessモードのDockerの動作を確認する

　インストールが完了したら、関連する環境変数を設定し、`docker info`を実行します。`Security Options:`に`rootless`が含まれていれば、Rootlessモードで動作しています。
また、適宜`.bashrc`などを編集し、環境変数の設定を追加します。

```text
rootless$ export PATH=/home/rootless/bin:$PATH
rootless$ export DOCKER_HOST=unix:///run/user/1001/docker.sock
rootless$ docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b7f0
 Built:             Wed Mar 11 01:22:56 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b7f0
  Built:            Wed Mar 11 01:30:32 2020
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

rootless$ docker info
...
 Security Options:
  seccomp
   Profile: default
  rootless
...
```

### 4.3. `nvidia-container-runtime`の設定を変更する

　RootlessモードのDockerで`nvidia-smi`を実行すると、以下の通りエラーとなります。

```text
rootless$ docker container run --tty --rm --gpus all nvidia/cuda:10.2-base-ubuntu18.04 nvidia-smi
docker: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:449: container init caused \"process_linux.go:432: running prestart hook 0 caused \\\"error running hook: exit status 1, stdout: , stderr: nvidia-container-cli: mount error: open failed: /sys/fs/cgroup/devices/user.slice/devices.allow: permission denied\\\\n\\\"\"": unknown.
```

　`moby`リポジトリのIssue#38729を参考に、`nvidia-container-runtime`の設定を変更します。具体的には`no-cgroups`をデフォルト値の`false`から`true`に変更します。

参考: [nvidia-container-runtime doesn't work with rootless mode · Issue #38729 · moby/moby](https://github.com/moby/moby/issues/38729)

```text
yuya$ sudo cp /etc/nvidia-container-runtime/config.toml /etc/nvidia-container-runtime/config.toml.20200508
yuya$ sudo vim /etc/nvidia-container-runtime/config.toml
yuya$ diff -U 3 /etc/nvidia-container-runtime/config.toml.20200508 /etc/nvidia-container-runtime/config.toml
--- /etc/nvidia-container-runtime/config.toml.20200508	2020-05-08 15:50:44.605107694 +0900
+++ /etc/nvidia-container-runtime/config.toml	2020-05-08 15:50:57.125141605 +0900
@@ -8,7 +8,7 @@
 #debug = "/var/log/nvidia-container-toolkit.log"
 #ldcache = "/etc/ld.so.cache"
 load-kmods = true
-#no-cgroups = false
+no-cgroups = true
 #user = "root:video"
 ldconfig = "@/sbin/ldconfig.real"
```

### 4.4. RootlessモードのDockerでGPUを使用する

　`nvidia-container-runtime`の設定を変更すると、`nvidia-smi`が実行できるようになります。なお、Dockerデーモンの再起動は不要です。

```text
rootless$ docker container run --tty --rm --gpus all nvidia/cuda:10.2-base-ubuntu18.04 nvidia-smi
Fri May  8 06:56:51 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 1070    Off  | 00000000:01:00.0 Off |                  N/A |
|  0%   36C    P8     7W / 195W |      0MiB /  8119MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
...

rootless$ docker container run --interactive --tty --rm --gpus all nvidia/cuda:10.2-base-ubuntu18.04 /bin/bash

root@docker# apt update && apt install --yes python3-pip
root@docker# pip3 install torch
root@docker# python3
Python 3.6.9 (default, Apr 18 2020, 01:56:04)
...
>>> import torch
>>> print(torch.cuda.is_available())
True
>>> torch.cuda.get_device_name(0)
'GeForce GTX 1070'
```

　上記の通り、Dockerコンテナ内のPyTorchからもGPUが認識できています。

## 5. 【未解決】通常モードとRootlessモードの共存

　`/etc/nvidia-container-runtime/config.toml`に`no-cgroups = true`を追加し、RootlessモードのDockerでGPUを使用できるようにすると、逆に通常モードのDockerではGPUを使用できなくなってしまいます。
こちらの問題についてはまだ調査しておらず、未解決です。何か情報があれば、お知らせ頂けると嬉しいです。

```text
yuya$ grep no-cgroups /etc/nvidia-container-runtime/config.toml
no-cgroups = true

yuya$ docker container run --tty --rm --gpus all nvidia/cuda:10.2-base-ubuntu18.04 nvidia-smi
Failed to initialize NVML: Unknown Error
```