GPU Operator 와 타임슬라이싱 · 사고를 처음부터 다시 밟는다 · 실습
GPU 사고 재현 — 설정 병합부터 롤아웃 정지까지
목표
GPU 클러스터를 한 번에 못 쓰게 만들었던 사고를 여덟 단계로 다시 밟습니다. containerd 설정이 합쳐지는 자리, 로드된 것을 확인하는 습관, 자원 광고와 런타임 핸들러라는 두 관문, 타임슬라이싱의 산술, 그리고 롤아웃이 멈춘 클러스터의 모양까지를 손으로 만듭니다.
왜 중요한가
GPU 운영에서 사람을 잡는 것은 드라이버가 아니라 설정이 반영됐다고 믿는 순간입니다. 드롭인 파일에 nvidia 런타임이 멀쩡히 적혀 있어도 로드된 런타임은 runc 하나뿐일 수 있고, toolkit 파드가 Ready 여도 containerd 를 재시작하지 않았으면 핸들러는 등록되지 않았으며, 이미 돌고 있는 파드는 이 모든 것이 깨져도 아무 말을 하지 않습니다. 고장이 잠복하다 노드 재부팅 때 한꺼번에 터지는 이유가 여기 있습니다. 그래서 이 실습은 "무엇을 썼는가" 가 아니라 "무엇이 실제로 로드됐고 무엇이 실제로 스케줄됐는가" 만 보게 만듭니다. 그 습관 하나가 몇 시간짜리 원인 규명을 3초로 줄입니다.
이 환경에는 진짜 GPU 도 containerd 도 GPU Operator 도 없습니다. 1~4단계는 설정 파일과 TOML 파서로 다루고, 5~8단계는 kwok 이 띄운 진짜 컨트롤 플레인과 가짜 노드 3대(lab-node-0/1/2) 위에서 다룹니다. 스케줄링·자원 광고·데몬셋 롤아웃은 진짜 컨트롤러가 처리하므로 그대로 재현되지만, 컨테이너는 실제로 실행되지 않고 노드에 꽂힌 GPU 는 노드 오브젝트의 숫자일 뿐입니다.
단계
1. /root/gpuop/etc/containerd/config.toml 을 만드세요. 최상위에 version = 2 와 imports 배열이 있어야 하고 imports 의 한 항목은 conf.d/*.toml 로 끝나야 합니다. plugins."io.containerd.grpc.v1.cri".containerd 아래에 default_runtime_name = "runc" 를 두고, plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc 테이블에 runtime_type = "io.containerd.runc.v2" 와 options.SystemdCgroup = true 를 넣으세요.
2. /root/gpuop/etc/containerd/conf.d/99-nvidia.toml 을 만드세요. plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia 테이블에 runtime_type = "io.containerd.runc.v2" 를, 그 아래 options 에 BinaryName = "/usr/bin/nvidia-container-runtime" 과 SystemdCgroup = true 를 넣으세요.
3. 두 파일을 tomllib 로 읽어 판정한 결과를 /root/gpuop/out/merge.txt 에 다섯 줄로 적으세요. MAIN_RUNTIMES= 는 주 설정이 직접 정의한 런타임 이름, DROPIN_RUNTIMES= 는 드롭인이 정의한 이름, CONFLICT= 는 양쪽이 같은 테이블을 정의하면 yes, CONFLICT_TABLE= 는 부딪히는 테이블 경로(plugins."io.containerd.grpc.v1.cri".containerd.runtimes), 그리고 마지막 LOADED_RUNTIMES= 입니다. 마지막 줄만은 계산값이 아니라 관측값입니다 — 사고 당일 그 노드에서 containerd config dump 로 확인한 로드된 런타임 핸들러는 runc 하나뿐이었습니다.
4. /root/gpuop/bin/check-runtime.sh 를 만드세요. containerd config dump 를 절대 경로 없이 호출해 그 출력에서 containerd.runtimes.nvidia 를 찾고, 있으면 0, 없으면 1 로 끝나야 합니다. /etc/containerd/config.toml 경로를 읽으면 안 됩니다 — 채점기가 그것을 오답으로 잡습니다.
5. /root/gpuop/k8s/runtimeclass.yaml 에 이름과 handler 가 모두 nvidia 인 RuntimeClass 를 쓰고, /root/gpuop/k8s/gpu-pod.yaml 에 네임스페이스 gpu-lab, 이름 cuda-probe, runtimeClassName: nvidia, 컨테이너 자원 limits 에 nvidia.com/gpu: 1 인 파드를 쓰세요. 네임스페이스를 먼저 만들고 둘 다 적용하세요. 파드는 뜨지 않습니다 — PodScheduled 조건의 reason 과 message 를 두 줄로 /root/gpuop/out/unscheduled.txt 에 저장하세요.
6. kubectl patch node lab-node-0 --subresource=status 로 capacity 와 allocatable 양쪽에 nvidia.com/gpu 를 "4" 로 넣으세요. 파드가 Running 이 되면 세 노드의 이름과 광고량을 /root/gpuop/out/node-gpu.txt 에 두 칸짜리 표로 저장하세요.
7. /root/gpuop/k8s/time-slicing.yaml 에 네임스페이스 gpu-operator, 이름 time-slicing-config 인 ConfigMap 을 쓰세요. data.any 는 YAML 문자열이고 그 안에 sharing.timeSlicing.failRequestsGreaterThanOne: true 와 resources 배열 첫 항목이 name: nvidia.com/gpu, replicas: 5 여야 합니다. 적용한 뒤 lab-node-1 의 광고량을 "20" 으로 올리고, /root/gpuop/out/slots.txt 에 일곱 줄을 적으세요 — PHYSICAL_GPUS REPLICAS ADVERTISED VRAM_PER_GPU_GIB VRAM_GUARANTEED_PER_SLOT_GIB VRAM_SHARED_PER_GPU_GIB ISOLATION. 장비는 40GiB A100 4장 기준입니다.
8. /root/gpuop/k8s/toolkit-ds.yaml 에 네임스페이스 gpu-operator, 이름 nvidia-container-toolkit, updateStrategy.rollingUpdate.maxUnavailable: 1, 이미지 nvcr.io/nvidia/k8s/container-toolkit:v1.16.2, 메모리 요청 128Mi 인 데몬셋을 쓰고 적용하세요. 세 파드가 모두 Ready 가 되면 이미지를 v1.17.0 으로 올리면서 메모리 요청을 64Gi 로 함께 바꾸세요. 롤아웃이 멈추면 /root/gpuop/out/rollout.txt 에 여섯 줄을 적으세요 — DESIRED UPDATED READY OLD_IMAGE_PODS MAX_UNAVAILABLE, 그리고 BLOCKER=insufficient-memory.
참고
- 4단계 채점은 정적 검사에서 끝나지 않습니다. 가짜
containerd를 PATH 앞에 세워 두고 여러분의 스크립트를 실제로 두 번 돌려 봅니다 — nvidia 가 없는 dump 를 줬을 때 실패로, 있는 dump 를 줬을 때 성공으로 끝나야 통과합니다. kubectl patch --subresource=status는 kubectl 1.24 이상에서 동작합니다. 노드 용량은 세 대 모두 CPU 8, 메모리 32Gi 입니다.- 흔한 실수 1: TOML 테이블 이름을
[plugins.io.containerd.grpc.v1.cri...]처럼 따옴표 없이 적는 것. 점이 들어간 키는 큰따옴표로 묶어야 한 덩어리로 읽히고, 그러지 않으면 파서가 전혀 다른 계층을 만듭니다. - 흔한 실수 2: 7단계에서
data.any를 YAML 매핑으로 적는 것. ConfigMap 의data값은 문자열이어야 하므로|-블록 스칼라로 넣어야 합니다. - 흔한 실수 3: 8단계에서 세 파드가 Ready 가 되기 전에 새 스펙을 미는 것. 처음부터 준비되지 않은 상태에서 밀면 무엇 때문에 멈춘 것인지 구분할 수 없습니다.
단계 8개
- 사고 노드의 주 containerd 설정 재현
- toolkit 이 떨어뜨리는 드롭인 재현
- 두 파일이 같은 테이블을 두고 부딪히는지 판정
- 로드된 설정을 보는 점검 스크립트
- RuntimeClass 와 GPU 파드, 그리고 Pending
- device plugin 이 하는 일을 노드 status 로
- 타임슬라이싱 설정을 읽고 슬롯과 메모리 계산
- 롤아웃을 멈춰 세우고 갈라진 상태를 보고