CDIスペックの読み書き
一言でいうと
CDIは、「このデバイスをコンテナに入れるには、何をしてあげる必要があるか」をYAMLで書いておく仕様です。ランタイムフックというブラックボックスを、標準のファイルに変えました。
なぜ必要なのか
以前は、DockerでGPUを使うには、nvidia-container-runtimeというランタイムフックを差し込む必要がありました。コンテナの起動直前に、そのフックが実行されて、ドライバーのライブラリをマウントし、デバイスノードを追加していました。動作はしましたが、問題がありました。
- 何をしているのか、外からはわかりません: フックの中のロジックが、ブラックボックスです。
- ベンダーごとに、自分のフックを作ります: NVIDIA、AMD、FPGA、InfiniBandが、それぞれ違う方式です。
- ランタイムを入れ替える必要があります: runcの代わりに、nvidia-container-runtimeを使うように、設定を変える必要がありました。
CDIは、これを宣言的な仕様に変えました。/etc/cdi/*.yamlに、「このデバイス名がリクエストされたら、こういうデバイスノードと、こういうマウントと、こういう環境変数を追加せよ」と書いておけば、CDIをサポートするランタイム(podman、containerd、CRI-O)が、そのまま実行します。
どう動くのか
スペックの構造
cdiVersion: "0.6.0"
kind: nvidia.com/gpu
devices:
- name: "0"
containerEdits:
deviceNodes:
- path: /dev/nvidia0
- path: /dev/nvidiactl
- path: /dev/nvidia-uvm
mounts:
- hostPath: /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.550.90.07
containerPath: /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.550.90.07
options: ["ro", "nosuid", "nodev", "bind"]
env:
- NVIDIA_VISIBLE_DEVICES=0
hooks:
- hookName: createContainer
path: /usr/bin/nvidia-ctk
args: ["nvidia-ctk", "hook", "update-ldcache"]
- name: all
containerEdits:
deviceNodes:
- path: /dev/nvidia0
- path: /dev/nvidiactl
核心となるルールです。
kindは<벤더>/<클래스>形式です(プレースホルダーはベンダーとクラスです)。nvidia.com/gpu、amd.com/gpu、intel.com/fpgaなどです。ベンダーの部分は、ドメイン形式である必要があります。- デバイス名は、
kind=nameで参照します。nvidia.com/gpu=0、nvidia.com/gpu=allです。 containerEditsは、そのデバイスを付けるときに、OCIスペックに追加する内容です。deviceNodes、mounts、env、hooksの4つが代表的です。- スペックファイルは、
/etc/cdiまたは/var/run/cdiに置きます。rootlessなら、ユーザーのパスを指定することもできます。
実際の使い方
# 스펙 생성 (실제 GPU 가 있는 호스트에서)
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
# 등록된 장치 확인
nvidia-ctk cdi list
# nvidia.com/gpu=0
# nvidia.com/gpu=all
# 컨테이너에서 사용
podman run --rm --device nvidia.com/gpu=all nvidia/cuda:12.4.1-base nvidia-smi
dockerの--gpus allに対応するのが、--device nvidia.com/gpu=allです。最新のpodmanは、--gpusも互換フラグとして受け付けますが、CDIの表記が正式です。
rootlessでも、GPUが使えます。GPUドライバーはカーネル空間で動作するので、コンテナの権限レベルとは無関係です。システム全体の/etc/cdi/nvidia.yamlを読めるなら、そのまま使い、読めなければ、ユーザー空間にスペックを生成して、そのディレクトリを指定すれば済みます。
containerd側への登録
Kubernetesでは、containerdがCDIを読みます。config.tomlに有効化の設定が必要で、ランタイムを別に登録する古い方式も、今も使われています。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
BinaryName = "/usr/bin/nvidia-container-runtime"
SystemdCgroup = true
そして、そのランタイムを使うワークロードは、RuntimeClassで選択します。
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: nvidia
handler: nvidia
default_runtime_nameをnvidiaに変えないのがコツです。そうすると、GPUを使わないPodまで、そのランタイムを経由することになります。RuntimeClassで、必要なワークロードだけを指定するほうが、安全です。
現場での姿
ドライバーの更新後に、GPUが認識されません。十中八九、CDIスペックの再生成(nvidia-ctk cdi generate)を忘れています。スペックには、ライブラリファイルのバージョンが埋め込まれたパスが入っているので、ドライバーが上がると、そのパスがなくなります。
コンテナ内でnvidia-smiは動くのに、CUDAプログラムが失敗します。必要なライブラリのうち、一部だけがマウントされた場合です。スペックのmountsの一覧を、確認する必要があります。
次のラボですること
/etc/cdi/に、CDIスペックを直接作成します。実際のGPUはないので、スペックの文法と配置、そして--device引数の形式を採点します。現場でミスが出る場所が、まさにそこだからです。