TT Lab
요금제

한국어

시작하기
블로그

블로그

NVIDIA GPU Operator 읽기: ClusterPolicy 하나가 GPU 노드를 준비시키고, dcgm-exporter 가 GPU 의 주인을 찾는 길

상태: 초안 (2026-10-17), 일부 실측. 기준 판 NVIDIA/gpu-operator v26.3.3(커밋 b0a49c0e7b2e), NVIDIA/k8s-device-plugin v0.19.3(570e776d6813), NVIDIA/dcgm-exporter 4.5.3-4.8.2(691c92762eb5). 작은 클러스터가 바로 이 판들을 돌리고 있어서, 실측은 2026-10-17 그 클러스터에서 읽기만 해서 얻었습니다(ClusterPolicy·데몬셋·노드 라벨·파드 구성 읽기, API 서버 프록시로 /metrics 읽기, 실행 중 파드 안 파일 읽기). 2026-10-16 에 같은 현상을 더 길게 잰 기록도 함께 인용합니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.

목차

1. 한 줄 요약: GPU 노드용 「설치 기사 + 점검표」

GPU 를 쿠버네티스에서 쓰려면 노드마다 여러 층이 맞물려야 합니다. 커널 드라이버, 컨테이너가 GPU 장치를 보게 해 주는 컨테이너 툴킷, kubelet 에 nvidia.com/gpu 자원을 알리는 device plugin, GPU 사양을 노드 라벨로 붙이는 GPU Feature Discovery(GFD), 지표를 내보내는 dcgm-exporter. 하나라도 순서가 어긋나면 파드는 Init 에서 멈추거나 GPU 를 못 찾습니다.

GPU Operator 는 이 층들을 설치 기사처럼 차례로 깔고, 점검표처럼 앞 단계가 끝났는지 확인한 뒤 다음 단계로 넘어갑니다. 사용자가 쓰는 것은 ClusterPolicy 라는 CR 하나뿐입니다.

구성요소 하는 일 어디서 도나
gpu-operator(오퍼레이터) ClusterPolicy 를 보고 상태 목록을 차례로 적용 Deployment 1개
NFD(node-feature-discovery) PCI 장치를 보고 feature.node.kubernetes.io/pci-10de.present 등 라벨 마스터 Deployment + 워커 데몬셋(모든 노드)
driver 커널 모듈 설치(컨테이너 드라이버). 호스트에 드라이버가 있으면 끔 GPU 노드 데몬셋
container-toolkit 런타임 설정, CDI spec(management.nvidia.com) 생성 GPU 노드 데몬셋
operator-validator 드라이버·툴킷·CUDA·플러그인이 실제로 되는지 확인하고 「준비 파일」을 남김 GPU 노드 데몬셋
device plugin nvidia.com/gpu 광고·할당, 시분할 복제, CDI spec(k8s.device-plugin.nvidia.com) GPU 노드 데몬셋
GFD GPU 개수·복제 수·공유 방식 등을 노드 라벨로 GPU 노드 데몬셋
dcgm-exporter DCGM 값을 Prometheus 형식으로, 파드 라벨을 붙여 GPU 노드 데몬셋

2. 구조도: 상태 목록과 노드 안 준비 사슬

ClusterPolicy 하나가 상태 목록을 차례로 적용한다 ClusterPolicy 클러스터에 하나 Reconcile → step() 반복 안 된 상태가 있으면 5초 뒤 다시 상태 = assets/ 폴더 하나(매니페스트 묶음) → 변환 → 적용 1 pre-requisites 2 operator-metrics 3 driver 4 container-toolkit 5 operator-validation 6 device-plugin 7 mps-control 8 dcgm 9 dcgm-exporter 10 gpu-feature-disc. 11 mig-manager 12 node-status-exp. 13–20 vGPU·샌드박스·kata·cc 계열(샌드박스 워크로드용) 켜짐 꺼짐(작은 클러스터 설정 — 호스트 드라이버 사용 등) GPU 노드 안: 파일 하나씩으로 잇는 준비 사슬 operator-validator 파드의 initContainer 순서 driver 검증 driver-ready toolkit 검증 toolkit-ready cuda 검증 cuda-ready plugin 검증 plugin-ready 파일은 노드의 /run/nvidia/validations 에 남는다 device-plugin·dcgm-exporter 는 toolkit-ready 를 기다린 뒤 뜬다
그림: ClusterPolicy 가 상태 목록을 차례로 적용하고, 노드 안에서는 검증 파일로 준비 순서를 잇는다

그림 읽는 법:

3. 코드 지도

세 저장소를 함께 봅니다. 최상위에서 중요한 곳만 골랐습니다.

경로 무엇 처음 읽을 파일
gpu-operator api/ ClusterPolicy·NVIDIADriver CRD 의 Go 타입 api/nvidia/v1/clusterpolicy_types.go
gpu-operator controllers/ reconcile 루프, 상태 관리, 오브젝트별 적용·변환 clusterpolicy_controller.go, state_manager.go
gpu-operator assets/ 상태별 매니페스트 묶음(state-device-plugin/0500_daemonset.yaml 등) state-operator-validation/0500_daemonset.yaml
gpu-operator cmd/nvidia-validator/ 노드 안 검증기(드라이버·툴킷·CUDA·플러그인) main.go
k8s-device-plugin internal/plugin/ kubelet device plugin gRPC 서버 server.go
k8s-device-plugin internal/rm/ 장치 목록 만들기, 시분할 복제(::N) device_map.go, devices.go
k8s-device-plugin internal/cdi/ CDI spec 생성·저장 cdi.go
dcgm-exporter internal/pkg/transformation/ 지표에 pod·namespace 라벨 붙이기 kubernetes.go
dcgm-exporter internal/pkg/server/ /metrics 처리, 변환 적용 server.go

controllers/object_controls.go 는 5천 줄이 넘습니다. 처음부터 읽지 말고, 아래 경로에서 이름이 나올 때 그 함수만 찾아 읽는 편이 낫습니다.

4. 핵심 경로 따라가기

4.1 reconcile 루프: 상태를 하나씩 「한 걸음」

  1. 오퍼레이터가 ClusterPolicy 를 받으면 controllers/clusterpolicy_controller.go:93 Reconcile 이 돕니다. ClusterPolicy 는 클러스터에 하나만 인정하고(두 번째부터는 Ignored), 처음이면 init 을 부릅니다.
  2. controllers/state_manager.go:854 init 은 GPU 노드를 찾아 라벨을 붙이고 (state_manager.go:521 labelGPUNodes), 상태 목록을 등록합니다. GPU 노드인지는 NFD 라벨 feature.node.kubernetes.io/pci-10de.present=true(10de 는 NVIDIA 의 PCI 벤더 번호)로 판단하고, 그런 노드에 nvidia.com/gpu.present=true 와 nvidia.com/gpu.deploy.<구성요소>=true 라벨을 붙입니다. 각 데몬셋의 nodeSelector 가 바로 이 라벨입니다.
  3. 등록은 state_manager.go:175 addState 가 폴더 경로마다 controllers/resource_manager.go:102 addResourcesControls 를 불러, YAML 의 kind 를 보고 「이 오브젝트를 적용하는 함수」 목록을 만듭니다. 상태 하나 = 함수 목록 하나입니다.
  4. Reconcile 은 state_manager.go:1051 step 을 마지막 상태까지 반복합니다. step 은 그 상태의 함수들을 모두 돌리고, 하나라도 준비가 안 됐으면 그 상태를 NotReady 로 표시한 채 다음 상태로 넘어갑니다. 끝까지 돈 뒤 준비 안 된 상태가 있으면 5초 뒤에 다시 돕니다.
for _, fs := range n.controls[n.idx] {
    stat, err := fs(*n)
    if err != nil {
        return stat, err
    }
    // successfully deployed resource, now check if its ready
    if stat != gpuv1.Ready {
        result = stat
    }
}
// move to next state
n.idx++

출처: NVIDIA/gpu-operator v26.3.3 controllers/state_manager.go(step 의 일부), Apache-2.0.

여기서 설계의 선택이 보입니다. 앞 상태가 준비되기를 기다리며 막지 않습니다. 데몬셋은 일단 다 만들고, 실제 순서는 노드 안의 준비 파일이 맞춥니다(4.2). 오퍼레이터가 노드마다 기다리게 만들면 노드 하나가 늦을 때 클러스터 전체 적용이 멈추기 때문입니다.

  1. 데몬셋을 적용하는 함수는 controllers/object_controls.go:4621 DaemonSet 입니다. 상태가 꺼져 있으면 오브젝트를 지우고 Disabled 를 돌려줍니다. 켜져 있으면 object_controls.go:728 preProcessDaemonSet 이 데몬셋 이름으로 변환 함수를 고릅니다(nvidia-device-plugin-daemonset → TransformDevicePlugin, nvidia-dcgm-exporter → TransformDCGMExporter …). ClusterPolicy 의 이미지·환경 변수·설정이 YAML 에 들어가는 곳이 이 변환 함수들입니다. 준비 여부는 object_controls.go:3989 isDaemonSetReady 가 봅니다.
  2. 새 GPU 노드가 들어오면 clusterpolicy_controller.go:246 addWatchNewGPUNode 의 노드 감시가 reconcile 을 다시 깨웁니다.

4.2 노드 안 준비 사슬: 검증기와 「-ready」 파일

검증기는 cmd/nvidia-validator/main.go 한 파일에 있습니다. 상태 파일을 두는 곳이 /run/nvidia/validations(146줄 defaultStatusPath)이고, 파일 이름이 161–189줄에 상수로 모여 있습니다 (driver-ready, toolkit-ready, cuda-ready, plugin-ready …).

파일로 잇는 방식은 단순하지만 장점이 큽니다. 노드가 재부팅되면 /run 이 비워지므로 검증이 자연스럽게 처음부터 다시 돌고, 사람이 노드에서 ls /run/nvidia/validations 한 번으로 어디서 멈췄는지 볼 수 있습니다.

4.3 device plugin: 시분할 복제와 CDI spec

device plugin 은 kubelet 의 device plugin API 를 구현한 gRPC 서버입니다. 흐름은 세 함수로 요약됩니다.

  1. internal/plugin/server.go:229 Register — kubelet 소켓에 「nvidia.com/gpu 를 내가 맡는다」고 등록.
  2. server.go:267 ListAndWatch — 장치 목록(건강 상태 포함)을 kubelet 에 흘려보냄.
  3. server.go:306 Allocate — 파드에 장치를 줄 때 컨테이너에 넣을 환경 변수·장치·CDI 이름을 돌려줌.

시분할(time-slicing) 은 2번에서 일어납니다. 설정에 replicas: 5 가 있으면 internal/rm/device_map.go:286 updateDeviceMapWithReplicas 가 GPU 한 장을 GPU-<uuid>::0 … ::4 다섯 개의 「장치」로 늘려 광고합니다. ::N 을 붙이고 떼는 일은 internal/rm/devices.go:243 NewAnnotatedID 부근의 작은 타입이 맡습니다. kubelet 은 이것이 같은 GPU 인 줄 모르고 다섯 파드에 하나씩 나눠 줍니다. 메모리 격리는 없습니다. 이 ::N 이 5절의 문제와 이어집니다.

CDI spec 은 컨테이너 런타임에게 「이 장치를 주려면 이 장치 파일과 이 라이브러리를 넣어라」를 적은 파일입니다. internal/cdi/cdi.go:187 CreateSpecFile 이 /var/run/cdi(41줄 cdiRoot)에 spec 을 저장하고, 이 함수는 플러그인을 시작할 때 한 번 불립니다(cmd/nvidia-device-plugin/plugin-manager.go:84). spec 에는 라이브러리 파일 이름이 드라이버 판까지 붙은 채로 들어갑니다. 그래서 호스트 드라이버만 갱신하고 플러그인을 다시 띄우지 않으면, spec 이 가리키는 라이브러리와 실제 적재된 커널 모듈의 판이 어긋날 수 있습니다. 플러그인은 kubelet 재시작·설정 변경·신호를 받으면 다시 시작하므로 (main.go start 의 restart 고리), 드라이버 갱신 뒤에는 노드 재부팅이나 플러그인 파드 재시작이 사실상 필요합니다.

4.4 dcgm-exporter: 「이 GPU 는 누구 것인가」

dcgm-exporter 는 스크랩마다 「이 GPU 는 누구 것」 을 묻는다 DCGM 값 GPU UUID 별 한 줄 PodMapper.Process() kubelet pod-resources List() List() 응답 — 시분할로 한 GPU 를 세 파드가 나눔 파드 P GPU-uuid::0 파드 Q GPU-uuid::1 파드 R GPU-uuid::2 순서는 kubelet 의 Go 맵 순회 → 부를 때마다 다르다 toDeviceToPod ::N 을 떼고 map[UUID] = 파드 → 마지막 파드가 이긴다 실측: 10초 간격 스크랩 6번의 pod 라벨 1회 P 2회 Q 3회 P 4회 R 5회 Q 6회 R 같은 GPU 의 시계열 하나가 세 파드 사이를 오간다 --kubernetes-virtual-gpus 를 켜면 toDeviceToSharingPods → 파드마다 한 줄씩 복사(일부 필드는 프로세스별 값)
그림: dcgm-exporter 가 스크랩마다 kubelet 에 파드를 묻고, 시분할 GPU 에서는 마지막 파드가 이긴다

dcgm-exporter 는 DCGM 에서 GPU 별 값을 읽어 /metrics 로 내보냅니다. DCGM_EXPORTER_KUBERNETES=true 이면 각 줄에 pod·namespace·container 라벨을 붙이는데, 그 정보를 쿠버네티스 API 가 아니라 kubelet 의 pod-resources gRPC(기본 /var/lib/kubelet/pod-resources/kubelet.sock)에서 얻습니다. 「이 컨테이너가 어떤 장치 ID 를 받았나」를 아는 쪽은 kubelet 이기 때문입니다.

  1. /metrics 요청이 오면 internal/pkg/server/server.go:249 Metrics → render 가 변환 목록을 돌립니다(server.go:309 transformation.Process). 스크랩마다 돕니다.
  2. internal/pkg/transformation/kubernetes.go:296 getMappings 가 소켓에 붙어 kubernetes.go:533 listPods 로 List() 를 부릅니다.
  3. 기본값(--kubernetes-virtual-gpus 꺼짐)에서는 kubernetes.go:712 toDeviceToPod 가 「장치 ID → 파드 하나」 맵을 만듭니다. 시분할 장치 ID GPU-<uuid>::N 은 :: 앞만 떼어 GPU UUID 를 열쇠로 씁니다.
} else if strings.Contains(deviceID, "::") {
    gpuInstanceID := strings.Split(deviceID, "::")[0]
    // (디버그 로그 생략)
    deviceToPodMap[gpuInstanceID] = podInfo
}

출처: NVIDIA/dcgm-exporter 4.5.3-4.8.2 internal/pkg/transformation/kubernetes.go 887–899줄 요약, Apache-2.0.

같은 GPU 를 세 파드가 나눠 쓰면 세 파드가 차례로 같은 열쇠에 덮어씁니다(kubernetes.go:898). 결국 응답에서 마지막에 나온 파드가 이깁니다. 그리고 그 응답의 순서는 kubelet 이 파드 목록을 Go 맵에서 꺼내 만들기 때문에 부를 때마다 달라집니다 (kubelet v1.34 pkg/kubelet/pod/pod_manager.go 의 맵 순회, 소스 기준). 코드 한 줄의 「마지막이 이긴다」와 응답 순서의 무작위가 만나 스크랩마다 라벨이 바뀝니다.

--kubernetes-virtual-gpus(환경 변수 KUBERNETES_VIRTUAL_GPUS)를 켜면 kubernetes.go:662 toDeviceToSharingPods 가 GPU 하나에 파드 목록을 묶고, 장치 지표를 파드마다 한 줄씩 복사합니다(일부 필드는 프로세스별 값으로 나눔). GPU Operator 의 기본 배포는 이 값을 켜지 않습니다.

5. 글쓴이의 기여와 이어지는 곳

6. 실측: 작은 클러스터에서 읽은 것

2026-10-17, 모두 읽기 명령입니다(kubectl get, 실행 중 파드 안 ls·grep, API 서버 프록시 kubectl get --raw …/proxy/metrics). 노드는 역할로, 파드는 P·Q·R 로 가렸습니다.

ClusterPolicy 와 데몬셋.

항목 읽은 값
ClusterPolicy 상태 state: ready, 조건 Reconciled(2026-10-14 부터)
driver.enabled false — 호스트에 설치한 드라이버를 씀(3번 상태 driver 꺼짐)
dcgm.enabled / dcgmExporter.enabled false / true — 별도 DCGM 없이 exporter 안의 엔진을 씀
cdi.enabled true
migManager.enabled, sandboxWorkloads.enabled 둘 다 false
devicePlugin.config 기본 no-sharing, 설정 묶음 이름 time-slicing-config
데몬셋 원하는 수 / 준비 nodeSelector
NFD worker 8 / 8 없음(모든 노드)
container-toolkit 5 / 5 nvidia.com/gpu.deploy.container-toolkit=true
operator-validator 5 / 5 …deploy.operator-validator=true
device-plugin 5 / 5 …deploy.device-plugin=true
mps-control-daemon 0 / 0 위 + nvidia.com/mps.capable=true(해당 노드 없음)
dcgm-exporter 5 / 5 …deploy.dcgm-exporter=true
gpu-feature-discovery 5 / 5 …deploy.gpu-feature-discovery=true

노드 여덟 대 중 GPU 노드는 다섯 대이고, 다섯 대 모두 nvidia.com/gpu.present=true 와 gpu.deploy.* 라벨 여덟 개를 달고 있었습니다(4.1의 2단계).

검증 사슬. operator-validator 파드의 initContainer 는 driver-validation → toolkit-validation → cuda-validation → plugin-validation 순서였고, device plugin 파드는 toolkit-validation → config-manager-init, dcgm-exporter 파드는 toolkit-validation 하나였습니다. 그림 아래쪽 사슬 그대로입니다.

시분할. GPU 노드 한 대(GPU 노드 B)만 nvidia.com/device-plugin.config=shared-5 라벨로 다른 설정을 골라 쓰고 있었습니다. 그 설정은 replicas: 5, failRequestsGreaterThanOne: true(한 파드가 슬롯을 둘 이상 잡지 못하게), renameByDefault: false 였고, 결과는 이렇습니다.

GPU 노드 gpu.sharing-strategy gpu.replicas allocatable nvidia.com/gpu
B time-slicing 5 5
나머지 넷 none 1 1

GPU 한 장(24GB급)이 kubelet 에는 다섯 장으로 보입니다. 라벨 gpu.replicas·gpu.sharing-strategy 는 GFD 가 붙인 것입니다.

CDI spec. device plugin 파드 안에서 /var/run/cdi 를 보면 두 파일이 있었습니다. 플러그인이 만든 k8s.device-plugin.nvidia.com-gpu.json 과 툴킷이 만든 management.nvidia.com-gpu.yaml 입니다. 플러그인 spec 의 수정 시각은 다섯 노드 모두 그 플러그인 파드가 시작한 시각이었고(4.3의 「시작할 때 한 번」), spec 속 libnvidia-ml.so.<판> 의 판과 /proc/driver/nvidia/version 의 커널 모듈 판은 오늘은 다섯 노드 모두 같았습니다. 노드마다 spec 날짜가 9월 초부터 10월 중순까지 흩어져 있어, 드라이버를 갱신한 노드는 그 뒤 플러그인도 다시 떴음을 알 수 있습니다.

dcgm-exporter 의 pod 라벨. GPU 노드 B 의 exporter /metrics 를 10초 간격으로 여섯 번 읽었습니다. 한 번에 15개 지표(설정 파일 dcp-metrics-included.csv)가 GPU 하나에 한 줄씩 나왔고, 그 노드에서 GPU 를 요청한 실행 중 파드는 셋이었습니다.

스크랩 1 2 3 4 5 6
DCGM_FI_DEV_GPU_UTIL 의 pod P Q P R Q R

다섯 번 넘어가는 동안 다섯 번 모두 바뀌었습니다. 2026-10-16 에 같은 노드를 2초 간격 30번 읽었을 때는 세 파드가 19·8·3 번(두 번째 실행 19·10·1, 연속한 두 번 사이 바뀜 12번)이었고, Prometheus(15초 간격)에 쌓인 1시간 치는 한 GPU 가 세 시계열로 쪼개져 129·76·35 표본, 24시간 동안 서로 다른 exported_pod 값이 7개였습니다. 대시보드에서 파드별로 묶으면 선이 듬성듬성 끊기는 까닭입니다.

7. 어디부터 읽을까

  1. controllers/clusterpolicy_controller.go:93 Reconcile — 150줄 안에 전체 흐름이 다 보입니다.
  2. controllers/state_manager.go:854 init 과 1051줄 step — 상태 목록과 한 걸음.
  3. assets/state-device-plugin/ 폴더 — 「상태 하나」가 어떤 YAML 묶음인지 눈으로 확인.
  4. object_controls.go:1520 TransformDevicePlugin — ClusterPolicy 값이 YAML 에 들어가는 실제 예.
  5. internal/plugin/server.go 의 Register·ListAndWatch·Allocate — device plugin API 세 함수.
  6. internal/pkg/transformation/kubernetes.go:296 부터 Process 까지 — 지표에 파드가 붙는 길.

8. 첫 기여하기 좋은 곳

9. 한계

티티 한마디. 탁구대 하나를 다섯 명이 번갈아 쓰는데, 점수판 주인 칸에는 「마지막에 줄 선 사람」 이름이 적혀요. 줄 서는 순서가 매번 바뀌니 주인도 매번 바뀌죠. 이름순으로만 세워도 조용해질 텐데!

참고

열람일은 모두 2026-10-17 입니다.

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

댓글

아직 댓글이 없습니다.

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