상태: 초안 (2026-10-05). 공식 문서로 확인한 서술을 바탕으로 쓴 글이고, 필자의 실험 환경에서 직접 잰 실측은 아직 반영되지 않았습니다. 글 끝의 「확인한 버전과 날짜」와 「확인하지 못한 것」을 먼저 읽으십시오. 실측이 들어오면 「실측」 절이 더해지고 이 배너가 바뀝니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.
GPU Operator 스택 해부와 트러블슈팅
요약
GPU Operator 는 GPU 노드마다 손으로 하던 드라이버, 컨테이너 런타임 설정, 디바이스 플러그인, 지표 수집기 설치를 ClusterPolicy 라는 오브젝트 하나로 선언하게 만든 오퍼레이터입니다. 이 글은 구성요소의 역할과 실패하는 모양, 파드가 GPU 를 받기까지 지나는 두 관문, 증상별 진단 순서, 용량 계획의 함정을 순서대로 쌓아 올립니다.
이 글을 읽고 나면 면접에서 설명할 수 있는 것
- 오퍼레이터가 띄우는 데몬셋 각각의 역할과 순서 의존, 실패했을 때 드러나는 자리
- 스케줄러가 보는 확장 자원과 런타임이 보는 핸들러·CDI라는 두 관문, 그리고 CDI 기본값으로 바뀐 현재 경로
Insufficient nvidia.com/gpu가 서로 다른 원인에서 같은 문장으로 나오는 이유와 가르는 순서
1. 구성요소: 무엇이 무엇을 하는가
오퍼레이터는 노드 라벨로 어느 노드에 무엇을 띄울지 정합니다. NFD(Node Feature Discovery)가 PCI 벤더 라벨(feature.node.kubernetes.io/pci-10de.present 등)을 붙이면 오퍼레이터가 GPU 노드로 판단해 nvidia.com/gpu.present 와 nvidia.com/gpu.deploy.<이름>=true 를 붙이고, 각 데몬셋은 그 라벨을 nodeSelector 로 읽습니다(오퍼레이터 소스 확인). 순서 의존은 init 컨테이너가 맡습니다. 툴킷은 driver-validation, 디바이스 플러그인은 toolkit-validation 을 먼저 통과해야 뜹니다.
| 구성요소 | 하는 일 | 실패하는 모양 |
|---|---|---|
| NFD, GFD | 하드웨어 사실을 라벨로 만듦. GFD 는 gpu.product·gpu.count 등 |
라벨이 없거나 테인트를 못 견디면 오류 없이 데몬셋 DESIRED 가 0 |
| 드라이버 | 커널 모듈. 컨테이너 또는 호스트 사전 설치 | nvidia-smi 실패, 다른 파드가 Init 에서 대기(nouveau 충돌 포함) |
| Container Toolkit | nvidia 런타임 핸들러를 containerd 설정에 등록 |
no runtime for "nvidia" is configured |
| Device Plugin | kubelet 에 등록, 장치 목록 광고, Allocate, XID 감시 |
nvidia.com/gpu 없음 또는 감소 |
| DCGM Exporter | 지표 노출 | 분리된 DCGM 에 연결 못 하면 CrashLoopBackOff |
| MIG Manager | MIG 프로파일 적용 | MIG 불가 GPU 에서는 무의미 |
| Validator | driver, toolkit, cuda, plugin 순서로 검증 | NVSwitch 노드에서 Fabric Manager 가 없으면 system not yet initialized |
Fabric Manager 는 NVSwitch 시스템(HGX 등)에서 GPU 간 메모리 패브릭을 구성하는 별도 서비스입니다. 오퍼레이터의 드라이버 컨테이너는 NVSwitch 를 감지하면 이를 설치하고, 호스트 드라이버를 쓰면 직접 설치해야 합니다.
2. 파드가 GPU 를 받기까지
flowchart TD
P["디바이스 플러그인<br/>kubelet 소켓에 등록, 장치 목록 전달"] --> N["kubelet<br/>노드 status 의 capacity·allocatable 갱신"]
N --> S["kube-scheduler (NodeResourcesFit)<br/>요청 ≤ allocatable − 이미 배정 ?"]
S --> A["kubelet: Allocate 호출<br/>응답: 장치 노드·환경변수·마운트·CDI 이름"]
A --> R["containerd<br/>CDI 스펙 적용 또는 nvidia 런타임이 OCI 스펙 수정"]
R --> C["runc 가 컨테이너 시작"]
관문 1은 숫자입니다. 스케줄러는 장치가 아니라 노드 status 의 숫자를 봅니다. GPU 는 limits 에만 쓰고(requests 를 쓰면 같은 값이어야 함) 정수입니다. 플러그인과의 연결이 끊기면 kubelet 은 장치를 unhealthy 로 돌려 allocatable 을 0 으로 내리고, 5분의 유예가 지나면 자원 이름을 지웁니다(kubelet 소스). 건강 감시로 장치 하나가 unhealthy 가 되어도 capacity 는 그대로이고 allocatable 만 줄어듭니다(공식 문서).
관문 2는 주입 방식입니다. 과거에는 nvidia-container-runtime 이 runc 를 감싸 prestart 훅으로 nvidia-container-cli 를 부르는 방식이었고, 오퍼레이터 25.10.0 부터는 CDI(Container Device Interface)가 기본입니다. 일반 워크로드는 runtimeClassName 이 필요 없습니다. 다만 오퍼레이터 자신의 operand 파드는 컨트롤러가 runtimeClassName: nvidia 를 붙이므로(NRI 플러그인 사용 시 제외, 소스 확인) 핸들러가 사라지면 플러그인부터 못 뜨고 광고가 끊깁니다.
3. 설치 방식 선택
| 방식 | 장점 | 비용 |
|---|---|---|
| 드라이버 컨테이너 | 오퍼레이터가 업그레이드 상태 기계(cordon, 대기, GPU 파드 삭제, 선택적 drain)로 관리 | 모든 GPU 노드의 OS 판을 맞춰야 함. 커널이 올라가면 드라이버를 다시 맞춰야 함 |
호스트 사전 설치 (driver.enabled=false) |
OS 가 섞여도 됨 | 오퍼레이터는 호스트 드라이버의 수명주기를 관리하지 않음 |
| 사전 컴파일 이미지 | 노드에서 빌드하지 않음 | 태그가 <브랜치>-<커널>-<OS> 형식이라 커널판이 맞는 이미지가 있어야 함 |
드라이버는 커널 모듈이므로 교체 전에 GPU 클라이언트를 모두 멈춰야 합니다. 문서(Ubuntu 22.04 각주)는 unattended-upgrades 의 자동 커널 업그레이드를 끌 것을 권하고, 업그레이드가 멈추면 nvidia.com/gpu-driver-upgrade-state=upgrade-failed 라벨 노드를 찾으라고 합니다. 필자의 실험 환경은 호스트 드라이버(595 계열)와 toolkit.enabled=true 로 v26.3.3 을 씁니다. Secure Boot 노드에서는 DKMS 모듈이 서명 거부로 올라오지 않을 수 있고, 배포판이 서명한 모듈 패키지를 쓰면 그 모듈이 커널판에 묶여 있어 커널 버전을 드라이버에 맞춰야 합니다. 26.3.x 는 지원 상태가 「Deprecated」이고 26.7.x 가 「Supported」입니다. 소비자용 GPU 는 문서의 지원 제품 목록에 없으므로 필자의 환경은 지원 대상 밖으로 읽어야 합니다.
4. 증상별 진단 순서
GPU 파드가 안 된다
├─ 파드 오브젝트가 없다 ........ 어드미션: ResourceQuota, 없는 RuntimeClass (apply 때 에러)
└─ 있다
├─ spec.nodeName 이 비어 있다 (Pending)
│ ├─ 이벤트에 taint/selector ... 톨러레이션, 라벨
│ └─ Insufficient nvidia.com/gpu → 노드별로 capacity / allocatable / 배정 합을 대조
│ ├─ 자원 이름이 없다 ........ 플러그인 미등록 (5분 유예 경과 포함)
│ ├─ capacity > allocatable .. unhealthy 장치 (XID), 또는 플러그인 연결 끊김 직후
│ └─ 배정 합 = allocatable ... 진짜 자리 소진
└─ 노드에 붙었다
├─ no runtime for "nvidia" ..... containerd config dump 의 런타임 목록
├─ operand 가 Init 에서 정지 .... 선행 단계(드라이버, 툴킷) 로그
└─ Running 인데 nvidia-smi 실패 . limits 누락, 드라이버/NVML, 장치 접근 상실
(노드에서 dmesg | grep -iE 'NVRM|Xid')
스케줄러는 요청 > allocatable − 이미 배정 이면 원인을 가리지 않고 Insufficient <자원 이름> 이라고 적습니다(kube-scheduler 소스). 그래서 이벤트 한 줄로 원인을 단정하면 안 됩니다.
5. 흔히 겪는 함정: 드롭인이 CRI 설정을 통째로 갈았다
필자의 실험 환경에서 GPU 4장이 일주일 동안 광고되지 않았습니다. 정리하면 이렇습니다.
- HTTP 로 접근하는 사설 레지스트리 때문에
conf.d에 레지스트리 설정 드롭인을 하나 넣었습니다. 내용은 CRI 플러그인의registry.config_path세 줄뿐이었습니다. - 같은 CRI 플러그인을 건드린 마지막 파일이 설정 전체를 가져갑니다. containerd 1.7.27 실측은
99-nvidia.toml단독 6개, 레지스트리 드롭인 단독 0개, 둘 다 0개였습니다. cat 99-nvidia.toml은 멀쩡하고config dump에도runc와 그럴듯한 기본값이 보여 두 번 오진했습니다. 드롭인을 하나씩 빼며 dump 를 다시 뜨자 원인이 드러났습니다.- 기록에 따르면 타임슬라이싱을 네 노드에 한꺼번에 걸자 툴킷이 다시 돌며 설정이 효력을 얻었습니다. 이후에는 한 노드씩 적용하는 절차로 바꿨습니다.
외부 근거로 확인한 것은 이렇습니다. containerd 1.7 소스의 mergeConfig 는 주석대로 「Plugins 의 값을 통째로 교체」하며, 공식 man 페이지의 「map 은 append」 문장은 이 동작을 설명하지 못합니다. NVIDIA 툴킷 소스에는 1.7 같은 옛 버전에서는 플러그인 섹션이 키 단위로만 병합되므로 드롭인에 CRI 설정 전체를 복제해 넣는다는 주석이 있습니다. 2.x 소스에는 이 교체 코드가 보이지 않지만 같은 재현이 되는지는 확인하지 못했습니다. 툴킷은 종료 시 자신의 드롭인 파일을 지웁니다. 필자가 본 한 설명은 「SIGHUP 으로는 핸들러가 등록되지 않는다」고 쓰지만, containerd 소스의 처리 신호 목록(1.7, main)에 SIGHUP 이 없고 툴킷의 기본 재시작 방식이 SIGHUP 이라, 제 해석으로는 프로세스가 종료되고 systemd 가 되살리는 경로입니다(업스트림 유닛은 Restart=always). 이 경로를 일회용 VM 에서 직접 확인했습니다. 우분투 패키지의 containerd 2.2.1 을 별도 서비스로 띄우고 systemctl kill -s HUP 으로 SIGHUP 을 세 번 보내자, 세 번 모두 프로세스가 종료되고 새 프로세스로 바뀌었습니다(PID 변경, 상태가 activating 을 거쳐 active). 저널에는 Sent signal SIGHUP to main process 다음에 Scheduled restart job, Started containerd.service 가 찍혔고, 유닛은 Restart=always, 재시작 대기 5초였습니다. 로그 수준을 debug 로 바꾼 설정 파일을 두고 SIGHUP 을 보냈을 때도 새 프로세스가 올라와 새 설정을 읽었으니, 이것은 설정 재적재가 아니라 재시작을 통한 반영입니다. 필자 환경의 노드를 읽어 보니 containerd 는 일부가 1.7.27, 나머지가 2.3.5 였고 유닛은 모두 Restart=always, 재시작 대기 5초였습니다(읽기만 했고 SIGHUP 은 보내지 않았습니다). 한계: 시험한 버전은 2.2.1 이고 1.7 에서의 SIGHUP 은 시험하지 않았습니다(소스상 처리 신호 목록에 SIGHUP 이 없다는 것까지 확인).
6. 용량 계획
- capacity 와 allocatable: 건강하지 않은 장치는 allocatable 에서만 빠집니다. 용량은 allocatable 로 셉니다.
- allocatable 과 실제 사용: 스케줄러는 배정을 보고 DCGM 은 사용을 봅니다. 필자의 3시간 실측에서 24GB급 소비자용 GPU 한 장은 사용률 0 인데 프레임버퍼 13.7 GiB 를 잡고 21 W 였습니다. 놀지만 비어 있지는 않은 카드입니다.
- 타임슬라이싱:
replicas: N은 같은 장치를 N 번 등록하는 일이고 메모리와 오류는 격리되지 않습니다. 예를 들어 24 GB GPU 를 5로 광고하면 슬롯당 평균 4.8 GB 는 설명용 산술일 뿐 보장값은 0 입니다.failRequestsGreaterThanOne: true는 2 이상의 요청을 거부합니다. 오퍼레이터는 설정 ConfigMap 변경을 감시하지 않으며 시분할 중에는 DCGM 이 컨테이너 귀속을 못 합니다(공식 문서). - MIG: A100 40GB 는 5 GB 메모리 슬라이스 8개와 SM 슬라이스 7개이므로
1g.5gb는 최대 7개이고 5 GB 가 남습니다(문서 구조에서 계산). 지원 목록에 소비자용 GPU 가 없어 필자의 실험 환경에서는 쓸 수 없습니다.
7. 직접 해 보기 (읽기 전용)
# 노드별 광고량
kubectl get nodes -o custom-columns=N:.metadata.name,CAP:.status.capacity.nvidia\.com/gpu,ALLOC:.status.allocatable.nvidia\.com/gpu
# operand 현황과 NFD 라벨
kubectl -n gpu-operator get ds,po -o wide
kubectl get nodes -L nvidia.com/gpu.present,nvidia.com/device-plugin.config
# 각 GPU 노드에서: 파일이 아니라 로드된 것을 본다 (0 이면 사고)
containerd config dump | grep -c runtimes.nvidia; ls /etc/containerd/conf.d
# 툴킷이 SIGHUP 을 보낸 시각에 containerd 가 어떻게 되었나
sudo journalctl -u containerd --since "-7d" | grep -E "Started|Stopped|signal" | tail
임시 파드로 끝내는 확인은 nvidia-smi -L 한 줄입니다(kubectl -n gpu-operator run gputest --rm -it --restart=Never ... --limits=nvidia.com/gpu=1 -- nvidia-smi -L). 이 파드는 슬롯 하나를 잠시 쓰고 --rm 으로 정리됩니다. 그 밖의 변경 명령은 쓰지 않았습니다.
8. 면접에서 나올 만한 질문
- GPU 파드가 Pending 입니다. 어디부터 봅니까? 이벤트 문장보다 노드 status 의 capacity, allocatable, 배정 합을 대조합니다. 같은
Insufficient가 플러그인 미등록, unhealthy 장치, 자리 소진에서 모두 나옵니다. - 파드가 GPU 를 받는 경로를 설명하십시오. 플러그인 등록, 노드 status 광고, 스케줄러 숫자 판정,
Allocate, CDI 또는 nvidia 런타임 주입, runc 순서입니다. 앞은 스케줄러의 관문, 뒤는 런타임의 관문입니다. - 드라이버를 컨테이너로 두는 것과 호스트에 두는 것의 차이는? 컨테이너는 업그레이드 자동화와 OS 일치 제약, 호스트는 자유도와 직접 관리 부담입니다. 커널 변경 때 드라이버가 따라가야 한다는 점이 공통의 위험입니다.
- 타임슬라이싱과 MIG 의 용량은 어떻게 셉니까? 타임슬라이싱은 광고가 N 배가 될 뿐 메모리 보장은 0 입니다. MIG 는 하드웨어 슬라이스라 합계가 물리 용량보다 작을 수 있습니다.
- containerd 드롭인 사고에서 무엇을 배웠습니까? 같은 플러그인을 건드린 마지막 파일이 설정 전체를 가져가는 병합 규칙을 알아야 합니다. 판정은 파일이 아니라
containerd config dump로 하고, 변경은 한 노드씩 적용합니다.
9. 흔한 오해
- 「
nvidia-smi가 호스트에서 되면 파드에서도 된다」: 런타임 핸들러와 광고는 별개입니다. - 「
requests에 0.5 를 쓰면 반 장」: GPU 는 정수 확장 자원입니다. - 「
replicas: 5는 메모리를 5등분한다」: 같은 VRAM 을 공유합니다. - 「오퍼레이터를 올리면 containerd 설정은 안전하다」: 툴킷이 호스트 파일을 씁니다.
10. 참고 자료와 확인한 버전
모두 2026-10-05 에 직접 열었습니다.
- https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/ 의 release-notes, platform-support, getting-started, troubleshooting, cdi, gpu-driver-upgrades, gpu-sharing, gpu-operator-mig 페이지
- https://github.com/NVIDIA/gpu-operator (controllers/object_controls.go, state_manager.go, assets/)
- https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/ (install-guide, arch-overview, cdi-support)
- https://github.com/NVIDIA/nvidia-container-toolkit (cmd/nvidia-ctk-installer, pkg/config/engine/containerd)
- https://github.com/NVIDIA/k8s-device-plugin (README.md)
- https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- https://github.com/kubernetes/kubernetes (noderesources/fit.go, pkg/kubelet/cm/devicemanager, master)
- https://github.com/containerd/containerd (config.go 1.7, main_unix.go, docs/man/containerd-config.toml.5.md)
- https://docs.nvidia.com/datacenter/tesla/mig-user-guide/ 의 supported-gpus, concepts 페이지
확인한 버전: GPU Operator 26.7.1(2026-08-21, 툴킷 1.20.1, 디바이스 플러그인 0.20.1, DCGM Exporter 4.6.1-4.8.4), 쿠버네티스 안정판 v1.37.1, containerd 2.4.1 및 1.7.36. 필자의 실험 환경은 GPU Operator v26.3.3.
확인하지 못한 것: 26.7.0 의 containerd 최소 버전이 문서에 「1.8 에서 2.0」으로 적힌 이유(1.8 은 오기로 보임), containerd 1.7 에서의 SIGHUP 관측(2.2.1 에서만 시험), 2.x 에서의 병합 동작, 설치 문서의 「드라이버가 없으면 플러그인이 조용히 0개 보고」(플러그인 README 는 FAIL_ON_INIT_ERROR 기본이 true 라 실패한다고 적음), 사고 당시 operand 파드 상태 기록.