Reading and Writing a CDI Specification
한국어 원문으로 표시합니다.
한 줄 요약
CDI 는 "이 장치를 컨테이너에 넣으려면 무엇을 해 줘야 하는가"를 YAML 로 적어 둔 명세다. 런타임 훅이라는 블랙박스를 표준 파일로 바꿨다.
왜 이게 필요했나
예전에 도커에서 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 네 가지가 대표적이다.- 스펙 파일은
/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 쪽 등록
쿠버네티스에서는 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 를 안 쓰는 파드까지 그 런타임을 거치게 된다. RuntimeClass 로 필요한 워크로드만 지정하는 편이 안전하다.
현장에서 만나는 모습
드라이버 업데이트 후 GPU 가 안 잡힌다. 십중팔구 CDI 스펙 재생성(nvidia-ctk cdi generate)을 잊은 것이다. 스펙에는 라이브러리 파일의 버전이 박힌 경로가 들어 있어서, 드라이버가 올라가면 그 경로가 사라진다.
컨테이너 안에서 nvidia-smi 는 되는데 CUDA 프로그램이 실패한다. 필요한 라이브러리 중 일부만 마운트된 경우다. 스펙의 mounts 목록을 확인해야 한다.
다음 실습에서 할 것
/etc/cdi/ 에 CDI 스펙을 직접 작성한다. 실제 GPU 는 없으므로 스펙의 문법과 배치, 그리고 --device 인자 형식을 채점한다. 현장에서 실수가 나는 지점이 정확히 거기다.