GPU 노드를 쿠버네티스에 붙여 놓고 나면 다음 질문은 항상 같다. 저 비싼 카드가 지금 얼마나 일하고 있나, 어느 파드가 메모리를 다 먹고 있나, 혹시 조용히 맛이 가고 있는 건 아닌가. CPU야 kubelet이 알아서 metrics를 뱉지만 GPU 내부는 드라이버 안쪽이라 밖에서는 안 보인다. 그래서 NVIDIA가 내놓은 DCGM Exporter를 하나 깔아 두는 게 사실상 표준이 됐다.
결론부터 말하자면, GPU Operator를 Helm으로 올리면 DCGM Exporter DaemonSet이 기본으로 따라오고, 포트 9400 /metrics에서 사용률, 온도, 프레임버퍼 메모리, 전력, XID 에러를 Prometheus 포맷으로 꺼내 쓸 수 있다. 4.8대에서는 파드별 매핑을 담당하는 PodMapper와 fallen-off-bus 같은 GPU 전역 헬스 사건까지 지표로 올라온다.
DCGM과 Exporter가 하는 일
DCGM(Data Center GPU Manager)은 NVIDIA가 만든 데이터센터 GPU 관리 라이브러리다. 드라이버에서 텔레메트리를 읽고, 헬스 체크와 진단을 돌리고, 카드별 상태를 들고 있는다. 사람이 직접 쓰기보다는 다른 도구가 호출하는 밑단 엔진이라고 보면 된다.
DCGM Exporter는 그 DCGM을 물고 앉아서, 읽어 온 값을 Prometheus가 긁어 갈 수 있는 텍스트 포맷으로 바꿔 포트 9400에 올려 주는 작은 프로그램이다. Go로 짜여 있고 컨테이너 이미지로 배포된다. 지표 이름은 DCGM 필드 이름을 거의 그대로 쓴다. 예를 들어 GPU 사용률은 DCGM_FI_DEV_GPU_UTIL, 코어 온도는 DCGM_FI_DEV_GPU_TEMP 식이다.
여기서 핵심 용어 하나. XID는 NVIDIA 드라이버가 뱉는 에러 코드다. ECC 메모리 오류, Xid 79번으로 유명한 fallen-off-bus(카드가 PCIe 버스에서 떨어져 나가 응답이 끊긴 상태), 전력 이상 같은 사건에 번호가 매겨져 있다. GPU가 죽는 전조는 대개 XID로 먼저 나타나기 때문에, 모니터링에서 이 값을 놓치면 안 된다.
GPU Operator로 DaemonSet 올리기
노드마다 드라이버, container toolkit, device plugin, 그리고 DCGM Exporter를 각각 수동으로 맞추는 건 고생길이다. GPU Operator가 이 묶음을 한 번에 깐다. DCGM Exporter는 dcgmExporter.enabled가 기본 true라 따로 켤 필요도 없다.
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia helm repo update helm install gpu-operator -n gpu-operator --create-namespace \ nvidia/gpu-operator
설치가 끝나면 GPU가 달린 노드마다 Exporter 파드가 하나씩 뜬다. DaemonSet이라 노드 수만큼 자동으로 스케줄된다.
$ kubectl -n gpu-operator get pods -l app=nvidia-dcgm-exporter NAME READY STATUS RESTARTS AGE nvidia-dcgm-exporter-7fx9k 1/1 Running 0 3m nvidia-dcgm-exporter-q2llm 1/1 Running 0 3m
값이 제대로 나오는지는 파드 안에서 직접 긁어 보는 게 제일 빠르다. 여기서 한 번 걸리는 지점이 있는데, Service가 아니라 파드 IP 9400으로 때려야 노드별 값이 섞이지 않는다.
$ kubectl -n gpu-operator exec -it nvidia-dcgm-exporter-7fx9k -- \
curl -s localhost:9400/metrics | grep DCGM_FI_DEV_GPU_UTIL
# HELP DCGM_FI_DEV_GPU_UTIL GPU utilization (in %).
# TYPE DCGM_FI_DEV_GPU_UTIL gauge
DCGM_FI_DEV_GPU_UTIL{gpu="0",UUID="GPU-a1b2...",device="nvidia0",modelName="NVIDIA A100-SXM4-40GB",Hostname="gpu-node-1"} 73
버전 번호는 저장소 상태에 따라 다를 수 있다. 4.8대 Exporter를 쓰려면 GPU Operator 값에서 이미지 태그를 명시하거나, GPU Operator 자체를 4.8 Exporter가 포함된 릴리스로 맞춘다.
PodMapper가 지표에 파드 이름을 붙인다
기본 지표에는 gpu="0", UUID 같은 하드웨어 라벨만 달린다. 운영에서 정작 궁금한 건 "이 GPU를 지금 어느 파드가 쓰고 있나"인데, 그 연결을 해 주는 게 PodMapper다.
PodMapper는 kubelet의 pod-resources 소켓(/var/lib/kubelet/pod-resources)을 읽어 GPU UUID와 파드를 짝지은 뒤, 지표에 pod, namespace, container 라벨을 추가한다. GPU Operator로 깔면 이 마운트와 --kubernetes=true 설정이 이미 잡혀 있다. 4.8.2에서는 informer 캐싱이 들어가, 매핑을 매번 API로 조회하지 않고 캐시에서 끌어오도록 바뀌었다. 파드가 많은 노드에서 조회 부하가 줄어든다.
라벨이 붙은 지표는 이렇게 나온다.
DCGM_FI_DEV_FB_USED{gpu="0",UUID="GPU-a1b2...",
pod="llm-serving-0",namespace="inference",container="vllm",
Hostname="gpu-node-1"} 38204
이제 "inference 네임스페이스의 llm-serving 파드가 프레임버퍼 38GB를 쓰고 있다"가 한눈에 읽힌다. PromQL로 네임스페이스별 GPU 메모리 합계를 뽑는 것도 바로 된다.
sum by (namespace) (DCGM_FI_DEV_FB_USED)
Prometheus가 긁어 가게 연결
Prometheus Operator(kube-prometheus-stack)를 쓴다면 ServiceMonitor로 잡는 게 깔끔하다. GPU Operator가 만든 Service에 라벨 셀렉터를 맞춘다.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: dcgm-exporter
namespace: gpu-operator
labels:
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: nvidia-dcgm-exporter
endpoints:
- port: gpu-metrics
interval: 15s
여기서 자주 막히는 부분이 labels.release다. Prometheus Operator는 자기 serviceMonitorSelector에 맞는 ServiceMonitor만 집어 가는데, 이 라벨이 Helm 릴리스 이름과 안 맞으면 타깃에 아예 안 올라온다. 긁히는지는 Prometheus UI의 Status > Targets에서 dcgm-exporter 타깃이 UP인지로 확인한다.
Operator 없이 바닐라 Prometheus라면 kubernetes_sd_configs로 파드를 디스커버리하고 포트 9400만 남기면 된다.
Grafana 대시보드와 XID 헬스 경보
시각화는 바퀴를 새로 만들 필요가 없다. NVIDIA가 올려 둔 공식 대시보드가 Grafana.com에 ID 12239로 있다. Grafana에서 Import에 12239를 넣고 Prometheus 데이터소스를 고르면 사용률, 온도, 전력, 프레임버퍼 패널이 바로 뜬다.
진짜 가치는 경보 쪽에 있다. 4.8대에서 GPU 전역 헬스 사건, 그중에서도 fallen-off-bus 같은 치명적 XID가 지표로 올라오고, 4.8.3부터는 XID 누적 카운트와 헬스 심각도, 카테고리 메타데이터가 추가됐다. 마지막으로 발생한 XID 번호는 DCGM_FI_DEV_XID_ERRORS 게이지로 노출된다.
Xid 79(fallen-off-bus)는 카드가 버스에서 떨어진 상태라 사실상 노드 격리 대상이다. Prometheus 경보 규칙으로 바로 잡는다.
groups:
- name: gpu-health
rules:
- alert: GpuFallenOffBus
expr: DCGM_FI_DEV_XID_ERRORS == 79
for: 1m
labels:
severity: critical
annotations:
summary: "GPU fallen off bus (Xid 79) on {{ $labels.Hostname }}"
- alert: GpuXidError
expr: DCGM_FI_DEV_XID_ERRORS > 0
for: 5m
labels:
severity: warning
annotations:
summary: "XID {{ $value }} on {{ $labels.Hostname }} gpu {{ $labels.gpu }}"
- alert: GpuTempHigh
expr: DCGM_FI_DEV_GPU_TEMP > 85
for: 5m
labels:
severity: warning
온도 임계값은 카드마다 다르다. A100, H100 계열은 85도 부근을 경고선으로 잡고, slowdown 임계는 데이터시트 값을 확인해서 맞춘다. 숫자는 환경에 따라 다르니 자기 카드 사양을 기준으로 조정한다.
경보가 울리기 시작하면 다음 단계는 노드를 어떻게 처리하느냐다. fallen-off-bus는 보통 재부팅이나 하드웨어 교체 전까지 복구가 안 되니, 경보에서 바로 kubectl cordon으로 스케줄을 막고 운영팀에 넘기는 식으로 Alertmanager 라우팅을 짜 두면 손이 덜 간다. 자동 cordon까지 엮을지는 클러스터 정책에 따라 정하면 된다.