LabHub

한국어

시작하기
블로그

블로그

GPU Operator 스택 해부와 트러블슈팅

상태: 초안 (2026-10-05). 공식 문서로 확인한 서술을 바탕으로 쓴 글이고, 필자의 실험 환경에서 직접 잰 실측은 아직 반영되지 않았습니다. 글 끝의 「확인한 버전과 날짜」와 「확인하지 못한 것」을 먼저 읽으십시오. 실측이 들어오면 「실측」 절이 더해지고 이 배너가 바뀝니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.

GPU Operator 스택 해부와 트러블슈팅

요약

GPU Operator 는 GPU 노드마다 손으로 하던 드라이버, 컨테이너 런타임 설정, 디바이스 플러그인, 지표 수집기 설치를 ClusterPolicy 라는 오브젝트 하나로 선언하게 만든 오퍼레이터입니다. 이 글은 구성요소의 역할과 실패하는 모양, 파드가 GPU 를 받기까지 지나는 두 관문, 증상별 진단 순서, 용량 계획의 함정을 순서대로 쌓아 올립니다.

이 글을 읽고 나면 면접에서 설명할 수 있는 것

  1. 오퍼레이터가 띄우는 데몬셋 각각의 역할과 순서 의존, 실패했을 때 드러나는 자리
  2. 스케줄러가 보는 확장 자원과 런타임이 보는 핸들러·CDI라는 두 관문, 그리고 CDI 기본값으로 바뀐 현재 경로
  3. 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장이 일주일 동안 광고되지 않았습니다. 정리하면 이렇습니다.

외부 근거로 확인한 것은 이렇습니다. 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. 용량 계획

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. 면접에서 나올 만한 질문

  1. GPU 파드가 Pending 입니다. 어디부터 봅니까? 이벤트 문장보다 노드 status 의 capacity, allocatable, 배정 합을 대조합니다. 같은 Insufficient 가 플러그인 미등록, unhealthy 장치, 자리 소진에서 모두 나옵니다.
  2. 파드가 GPU 를 받는 경로를 설명하십시오. 플러그인 등록, 노드 status 광고, 스케줄러 숫자 판정, Allocate, CDI 또는 nvidia 런타임 주입, runc 순서입니다. 앞은 스케줄러의 관문, 뒤는 런타임의 관문입니다.
  3. 드라이버를 컨테이너로 두는 것과 호스트에 두는 것의 차이는? 컨테이너는 업그레이드 자동화와 OS 일치 제약, 호스트는 자유도와 직접 관리 부담입니다. 커널 변경 때 드라이버가 따라가야 한다는 점이 공통의 위험입니다.
  4. 타임슬라이싱과 MIG 의 용량은 어떻게 셉니까? 타임슬라이싱은 광고가 N 배가 될 뿐 메모리 보장은 0 입니다. MIG 는 하드웨어 슬라이스라 합계가 물리 용량보다 작을 수 있습니다.
  5. containerd 드롭인 사고에서 무엇을 배웠습니까? 같은 플러그인을 건드린 마지막 파일이 설정 전체를 가져가는 병합 규칙을 알아야 합니다. 판정은 파일이 아니라 containerd config dump 로 하고, 변경은 한 노드씩 적용합니다.

9. 흔한 오해

10. 참고 자료와 확인한 버전

모두 2026-10-05 에 직접 열었습니다.

확인한 버전: 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 파드 상태 기록.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다