Ubuntu Weekly Recipe

第461回DockerでCUDA 8.0を使用する

第456回ではUbuntu 16.04 LTS上にCUDA 8.0をインストールする方法を紹介しました。これをもっと簡単に環境構築できるよう、今回は第458回で紹介したDockerを使ってみましょう。

NVIDIAのDocker

第458回で紹介しているようにDockerはカーネルのコンテナ技術などを利用して、アプリケーションをサンドボックス環境の中で動かす仕組みです。Dockerではカーネルの名前空間機能を用いてプロセスなどのリソースをホストの他のリソースから隔離することで、独立したサンドボックス環境を構築しています。KVMやVirtualBoxといった仮想マシンともっとも異なるのは、ホストとコンテナのカーネルは共通であることでしょう。ホストもコンテナも同じカーネルインスタンスの上で動いています。つまりコンテナの操作によってカーネルがハングアップしてしまったとしたら、当然のことながらホストもハングアップしてしまいます。

カーネルが共通であるということは、カーネルモジュールも同じであるということです。そのためNVIDIAのデバイスをDocker上で使いたければ、ホスト上にNVIDIAのドライバーモジュールをインストールしなくてはなりません[1]⁠。非特権コンテナであれば、コンテナ内部でのデバイスファイルの読み書きは明示的にホスト側で許可しない限りは行えません。

つまりDockerコンテナの中でCUDAのようなデバイスファイルの操作を行いたい場合、次の作業が必要になります

  • Control Groupを用いてコンテナからGPUのデバイスファイルへの読み書きの権限を与える--deviceオプション)
  • Linuxのcapabilities(7)を用いて非特権ユーザーにも一部の特権操作を行えるようにする--cap-addオプション)

CUDAを動かしたいだけであれば、必要なのは/dev/nvidia0などの読み書きのみとなります。よってcapabilitiesの設定は不要です。しかしながら指定しなければならないデバイスファイルが複数存在し、しかもnvidia-uvmのようにCUDAの利用によって動的にロードされうるモジュールも存在するため、生のDockerをそのまま使うのは若干手間がかかります。そこで出てくるのが「NVIDIA Docker」です。

NVIDIA DockerはCUDA入りのDockerイメージを構築・利用するためのツールです。もともとDocker Hubには、CUDAとcuDNN入りのイメージが公開されています。これ自体は普通のDockerツールでも利用はできます。NVIDIA Dockerは上記で説明したようなコンテナの中でCUDAを使うための諸々の作業をラッピングして、普通のDockerコンテナと同じように使えるようにしたツールです[2]⁠。

CUDAをDocker上で使う最大のメリットは、複数のバージョンを同じホストで共存できることでしょう。また一度DockerとNVIDIA Dockerをインストールさえすれば、CUDAのインストールやアップグレードが簡単になります。必要なライブラリはすべてコンテナの中にインストールしてしまうので、ホスト側はNVIDIA製のドライバーを除いて、シンプルに保てるのも人によってはうれしいポイントかもしれません。またcuDNN入りのコンテナも用意されています。本来CUDAを利用したDeep Neural Network LibraryであるcuDNNは、ダウンロードするためにNVIDIAのサイトで開発者登録が必要です。今回紹介するNVIDIAのDockerイメージを使えば、最初からcuDNNがインストールされたイメージをデプロイすることが可能なのです。

このようにメリットの多いNVIDIA Dockerですが、2017年の1月19日に待望の1.0.0がリリースされました。1.0.0でDocker 1.13を、最近リリースされた1.0.1ではDocker 17.03にも対応しています。なお、NVIDIA Dockerをインストールするためには公式のDockerパッケージ(docker-engineパッケージ)が必要です。

NVIDIA Dockerのインストール

あらかじめ第458回に従って、Docker公式のdocker-engineパッケージをインストールしておいてください。

NVIDIA Dockerもこれまでと同様に、インストールにはいくつかの方法が存在します。

  • リリース版のDebファイルをダウンロードしてインストールする
  • リリース版のソースコードをダウンロードしてビルド&インストールする
  • Gitリポジトリの最新版ををcloneしてビルド&インストールする

Ubuntuの場合は、単純にDebファイルをダウンロードしてインストールする方法が簡単です。

$ wget -P /tmp https://github.com/NVIDIA/nvidia-docker/releases/download/v1.0.1/nvidia-docker_1.0.1-1_amd64.deb
$ sudo apt install /tmp/nvidia-docker*.deb && rm /tmp/nvidia-docker*.deb

$ ps -fe | grep nvidia
nvidia-+  1259     1  3 20:06 ?        00:00:00 /usr/bin/nvidia-docker-plugin -s /var/lib/nvidia-docker
root      1293     2  0 20:06 ?        00:00:00 [irq/132-nvidia]
root      1294     2  0 20:06 ?        00:00:00 [nvidia]
shibata   1653 22617  0 20:06 pts/2    00:00:00 grep --color=auto nvidia

$ systemctl is-enabled nvidia-docker
enabled

これでインストールは完了です。ちなみに前述のとおり、NVIDIA DockerはDockerのラッパースクリプトです。Docker HubなどへのアクセスはDocker本体のほうが行います。よってプロキシなどのネットワーク関連の設定もDocker側のみ行っておけば大丈夫でしょう。

CUDAコンテナの起動

次はCUDAコンテナを起動してみましょう。まずはNVIDIA Dockerが正しく動くかどうかの確認がてら、コンテナを検索してみます。

$ nvidia-docker search nvidia/cuda
NAME                                   DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nvidia/cuda                            CUDA and cuDNN images from gitlab.com/nvid...   94
(以下略)

いくつも候補が出てくると思いますが先頭にあるnvidia/cudaがNVIDIAの公式イメージです。Docker Hubのサイトを確認するとCUDAのバージョンごとにタグが割り振られていることがわかります。2017年3月時点でのタグ未指定時に使われる「latest」は、Ubuntu 16.04 LTSのコンテナ上にCUDA 8.0とその開発用のファイル一式が入った「8.0-devel-ubuntu16.04」です。

まずはlatestタグのコンテナでnvidia-smiコマンドを使ってみましょう。

$ nvidia-docker run --rm nvidia/cuda nvidia-smi
Using default tag: latest
latest: Pulling from nvidia/cuda
d54efb8db41d: Pull complete
f8b845f45a87: Pull complete
e8db7bf7c39f: Pull complete
9654c40e9079: Pull complete
6d9ef359eaaa: Pull complete
cdfa70f89c10: Pull complete
3208f69d3a8f: Pull complete
eac0f0483475: Pull complete
4580f9c5bac3: Pull complete
6ee6617c19de: Pull complete
Digest: sha256:2b7443eb37da8c403756fb7d183e0611f97f648ed8c3e346fdf9484433ca32b8
Status: Downloaded newer image for nvidia/cuda:latest
Fri Mar  4 08:26:58 2017
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 375.39                 Driver Version: 375.39                    |
|-------------------------------+----------------------+----------------------+
| 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 105...  Off  | 0000:01:00.0     Off |                  N/A |
|  0%   33C    P8    35W /  72W |      0MiB /  4038MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

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

コンテナの中からでもGPUデバイスにアクセスできていることがわかりますね。

普通のDockerとNVIDIA Dockerの違い

nvidia-dockerコマンドとdockerコマンドを比べてみると、どちらも同じイメージファイルが見えています。

$ nvidia-docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nvidia/cuda         latest              1cce8839a2c5        36 hours ago        1.62 GB
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nvidia/cuda         latest              1cce8839a2c5        36 hours ago        1.62 GB

ではdockerコマンドからnvidia-smiを実行するとどうなるのでしょうか。

$ docker run --rm nvidia/cuda nvidia-smi
container_linux.go:247: starting container process caused "exec: \"nvidia-smi\": executable file not found in $PATH"
docker: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "exec: \"nvidia-smi\": executable file not found in $PATH".

nvidia-smiが見つからなかったようです。nvidia-dockerのほうでコマンドの位置を確認してみましょう。

$ nvidia-docker run --rm nvidia/cuda which nvidia-smi
/usr/local/nvidia/bin/nvidia-smi
$ docker run --rm nvidia/cuda ls /usr/local/nvidia
ls: cannot access '/usr/local/nvidia': No such file or directory

nvidia-dockerコマンド経由だと/usr/local/nvidia/bin以下に存在するにもかかわらず、dockerコマンド経由だとディレクトリ自体が存在しないようです。つまり「nvidia/cuda」コンテナのイメージにはこのディレクトリが存在しないことになります。

実はDockerには「Volume」というホストの一部の領域をコンテナからもアクセスできるようにする仕組みが存在します。/usr/local/nvidiaはこのVolumeとしてマウントされた領域です。実際にコンテナの中でmountコマンドを実行してみると、それがわかります。

$ nvidia-docker run --rm nvidia/cuda mount | grep nvidia
/dev/sda2 on /usr/local/nvidia type btrfs (ro,relatime,ssd,space_cache,subvolid=257,subvol=/@/var/lib/nvidia-docker/volumes/nvidia_driver/375.39)

どうやらホストの/var/lib/nvidia-docker/volumes/nvidia_driver/375.39がコンテナ内部の/usr/local/nvidiaになっているようですね[3]⁠。Volumeの状態はdockervolumeサブコマンドを使うと確認できます。ちなみにこのVolumeはnvidia-docker-pluginが作成しています

$ docker volume ls
DRIVER              VOLUME NAME
nvidia-docker       nvidia_driver_375.39
$ docker volume inspect nvidia_driver_375.39
[
    {
        "Driver": "nvidia-docker",
        "Labels": null,
        "Mountpoint": "/var/lib/nvidia-docker/volumes/nvidia_driver/375.39",
        "Name": "nvidia_driver_375.39",
        "Options": {},
        "Scope": "local"
    }
]
$ ls /var/lib/nvidia-docker/volumes/nvidia_driver/375.39/
bin  lib  lib64
$ ls /var/lib/nvidia-docker/volumes/nvidia_driver/375.39/bin/
nvidia-cuda-mps-control  nvidia-cuda-mps-server  nvidia-debugdump  nvidia-persistenced  nvidia-smi

NVIDIAのカーネルドライバーはたんなるカーネルモジュールだけでなく、カーネルモジュールとユーザーランドとの橋渡しを行うライブラリも提供しています。CUDAは原則としてこのライブラリを経由してGPUを操作するのです。よってこれらのライブラリは、コンテナの中から見える必要があります。ところがこれらのライブラリそのものは、カーネルモジュールのバージョンに紐付いています。つまりカーネルドライバーのバージョンがあがると、これらのライブラリも同じバージョンとして更新する必要があるということです。

もしこれらのライブラリをコンテナイメージの中に同梱してしまうと、ホストのカーネルドライバーの更新のたびにコンテナイメージを再構築する必要がでてきます。これではDockerの有りがたみが薄れてしまいます。そこでNVIDIA Dockerでは、カーネルモジュールに紐付いているライブラリや実行バイナリは、DockerのVolumeを使ってコンテナの中からもアクセスできるようにしているわけです。

なおデバイスファイルについても、前述したとおりnvidia-docker経由でのみアクセスできます。

$ nvidia-docker run --rm nvidia/cuda sh -c 'ls -l /dev/nvidia*'
crw-rw-rw- 1 root root 241,   0 Mar  3 09:54 /dev/nvidia-uvm
crw-rw-rw- 1 root root 241,   1 Mar  3 09:54 /dev/nvidia-uvm-tools
crw-rw-rw- 1 root root 195,   0 Mar  3 09:54 /dev/nvidia0
crw-rw-rw- 1 root root 195, 255 Mar  3 09:54 /dev/nvidiactl
$ docker run --rm nvidia/cuda sh -c 'ls -l /dev/nvidia*'
ls: cannot access '/dev/nvidia*': No such file or directory

サンプルの実行

最後のデモとして、第456回と同様にサンプルプログラムをDocker上でビルド&実行してみましょう。NVIDIA Dockerのリポジトリにはサンプルプログラム用のDockerfileがあるので、これを使ってみます。

最初に必要なパッケージのインストールとNVIDIA Dockerのコードのチェックアウトです。

$ sudo apt install git
$ git clone https://github.com/NVIDIA/nvidia-docker.git
$ cd nvidia-docker/

前回と同じくdeviceQueryプログラムから試してみましょう。

$ docker build -t sample:deviceQuery samples/ubuntu-16.04/deviceQuery
(中略)
Successfully built f771d146a5a1

$ docker images sample:deviceQuery
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
sample              deviceQuery         f771d146a5a1        2 minutes ago       1.93 GB

$ nvidia-docker run --rm sample:deviceQuery
./deviceQuery Starting...

 CUDA Device Query (Runtime API) version (CUDART static linking)

Detected 1 CUDA Capable device(s)

Device 0: "GeForce GTX 1050 Ti"
  CUDA Driver Version / Runtime Version          8.0 / 8.0
  CUDA Capability Major/Minor version number:    6.1
  Total amount of global memory:                 4039 MBytes (4235001856 bytes)
  ( 6) Multiprocessors, (128) CUDA Cores/MP:     768 CUDA Cores
  GPU Max Clock rate:                            1392 MHz (1.39 GHz)
  Memory Clock rate:                             3504 Mhz
  Memory Bus Width:                              128-bit
  L2 Cache Size:                                 1048576 bytes
  Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total number of registers available per block: 65536
  Warp size:                                     32
  Maximum number of threads per multiprocessor:  2048
  Maximum number of threads per block:           1024
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 2 copy engine(s)
  Run time limit on kernels:                     No
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 1 / 0
  Compute Mode:
     < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >

deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 8.0, CUDA Runtime Version = 8.0, NumDevs = 1, Device0 = GeForce GTX 1050 Ti
Result = PASS

次にnbodyも試してみましょう。サンプルのDockerfileは/usr/local/cuda/samples/の個々のサンプルディレクトリをWORKDIRに設定した上でmakeし、CMDフィールドでサンプルを実行しています。よって./サンプルプログラム オプションを渡たすことで、任意の引数でサンプルプログラムを実行可能です。

$ docker build -t sample:nbody samples/ubuntu-16.04/nbody
(中略)
Successfully built 32144285f8f4

$ docker images sample:nbody
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
sample              nbody               32144285f8f4        About a minute ago   1.93 GB

$ nvidia-docker run --rm sample:nbody
(中略)
6144 bodies, total time for 10 iterations: 6.136 ms
= 61.522 billion interactions per second
= 1230.441 single-precision GFLOP/s at 20 flops per interaction

$ nvidia-docker run --rm sample:nbody ./nbody -benchmark -numbodies=8192
(中略)
number of bodies = 8192
8192 bodies, total time for 10 iterations: 12.298 ms
= 54.568 billion interactions per second
= 1091.357 single-precision GFLOP/s at 20 flops per interaction

$ nvidia-docker run --rm sample:nbody ./nbody -benchmark -numbodies=65536
(中略)
number of bodies = 65536
65536 bodies, total time for 10 iterations: 636.416 ms
= 67.487 billion interactions per second
= 1349.736 single-precision GFLOP/s at 20 flops per interaction

$ nvidia-docker run --rm sample:nbody ./nbody -benchmark -numbodies=8192 -cpu
(中略)
> Simulation with CPU
number of bodies = 8192
8192 bodies, total time for 10 iterations: 27903.188 ms
= 0.024 billion interactions per second
= 0.481 single-precision GFLOP/s at 20 flops per interaction

コンテナ上であっても、ホスト上で直接実行した第456回と遜色ない速度が出ていることがわかりますね。

仮想マシンでのGPGPU

コンテナではなくKVMのような仮想マシン上でもGPGPUは利用可能です。

たとえばIOMMU(IntelのVT-dやAMDのAMD-Vi)に対応したチップセットであれば、⁠GPUパススルー」という形で仮想マシン上でGPUを利用できます。この場合、GPUを利用できるのはひとつのゲストだけです。そのゲストがGPUを使っている間は、ホストや他のゲストはそのGPUを利用できません。この点、コンテナであれば複数のインスタンスでGPUを共有できます。

たとえばIntelのGVT-gのように、複数の仮想マシンでひとつのGPUを共有できる仕組みも作られています。ただしUbuntu上でいくつか設定するだけで使えるようになるまでには、まだもう少し時間がかかるでしょう。

PCIデバイスとして考えた場合、SR-IOVに対応したチップセットとPCIデバイスであれば複数の仮想マシンでそのPCIデバイスを共有できます。NICにはSR-IOV対応製品がありますが、GPUの場合はごく一部です。ちなみに『うぶんちゅ! まがじん ざっぱ~ん♪ Vol.5』には、SR-IOVに対応したIntelのNICをUbuntuサーバー上に構築した複数の仮想マシンで共有する記事が載っています。

そもそもCUDA 8.0のリリースノートでは「Unified memory is not currently supported with IOMMU. The workaround is to disable IOMMU in the BIOS.」言及されています。つまり仮想マシン上でGPGPUを使いたい場合、CPU/GPUのメモリをシームレスに利用できるUnified Memory機能が使えないということです。

以上のようにいろいろな制約があることから、少なくとも現時点では、仮想マシンではなくコンテナを使って環境を隔離するほうが現実的と言えるでしょう。

おすすめ記事

記事・ニュース一覧