Split View: GPU 장애 진단 플레이북 — 계층을 정해 놓고 내려간다
GPU 장애 진단 플레이북 — 계층을 정해 놓고 내려간다
- 들어가며 — 계층을 정해 놓고 내려간다
- 1층 — 파드가 스케줄되지 않는다
- 2층 — 파드는 떴는데 GPU가 안 보인다
- 3층 — 드라이버와 툴킷 버전 불일치
- 4층 — 메모리 부족과 OOM
- 5층 — 노드에서 GPU가 사라진다
- 마치며 — 순서가 곧 실력이다
- 직접 해보기
- 시리즈
- 참고 자료
들어가며 — 계층을 정해 놓고 내려간다
GPU 장애 대응에서 가장 큰 시간 낭비는 순서 없이 여기저기 찔러 보는 것입니다. 노드에 들어가 nvidia-smi를 치고, 파드 로그를 보고, 다시 데몬셋을 재시작해 보고, 그러다 한 시간이 지납니다.
문제는 계층이 다섯 개인데 증상이 비슷하다는 데 있습니다. 파드가 안 뜨는 것은 스케줄링 문제일 수도, 자원이 없어서일 수도, 드라이버가 죽어서일 수도 있습니다. 그래서 순서를 미리 정해 놓고 위에서 아래로 내려가는 편이 항상 빠릅니다.
메트릭 이름과 설정은 2026-08-12에 공식 문서·저장소에서 확인했습니다. 버전에 따라 다를 수 있으니 사용 중인 버전에서 다시 확인하세요.
1층 — 파드가 스케줄되지 않는다
증상은 Pending입니다. 여기서 확인할 것은 두 가지뿐입니다. 노드가 자원을 광고하고 있는가, 그리고 남아 있는가입니다.
# 파드가 왜 스케줄 안 되는지 이벤트로 확인
kubectl describe pod <파드이름> | tail -20
# 노드의 총량과 할당 가능량 비교
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,CAP:.status.capacity.nvidia\.com/gpu,ALLOC:.status.allocatable.nvidia\.com/gpu'
# 아무 노드도 자원을 광고하지 않는다면 라벨부터
kubectl get nodes -L nvidia.com/gpu.count,nvidia.com/gpu.product,nvidia.com/mig.strategy
여기서 갈립니다. 자원 자체가 아예 없다면 2층으로 내려갑니다. 자원은 있는데 남은 수량이 0이면 그것은 장애가 아니라 용량 문제입니다. 그리고 총량은 8인데 할당 가능량이 7이라면 그것은 강한 단서입니다. 쿠버네티스 문서가 명시하듯 장치가 비정상으로 표시되면 kubelet은 할당 가능량만 줄이고 총량은 그대로 둡니다. 이 비대칭이 보인다면 5층으로 바로 뛰어도 됩니다.
라벨이 하나도 안 붙어 있다면 GPU Feature Discovery나 Node Feature Discovery가 돌지 않는 것이므로 오퍼레이터 쪽 문제입니다.
2층 — 파드는 떴는데 GPU가 안 보인다
컨테이너가 시작은 했는데 nvidia-smi가 실패하거나 CUDA가 장치를 못 찾는 경우입니다. 혹은 아예 컨테이너 생성 단계에서 실패합니다.
공식 트러블슈팅 문서가 지목하는 대표 오류 메시지가 있습니다. no runtime for 'nvidia' is configured입니다. 이것은 NVIDIA Container Toolkit이 컨테이너 엔진에 런타임 핸들러를 등록하지 못했다는 뜻입니다. 드라이버 문제가 아니라 툴킷 문제입니다.
# 오퍼레이터 파드 상태 한눈에
kubectl get pods -n gpu-operator
# 툴킷과 디바이스 플러그인 로그
kubectl logs -n gpu-operator nvidia-container-toolkit-daemonset-<POD-ID>
kubectl logs -n gpu-operator nvidia-device-plugin-daemonset-<POD-ID>
# validator가 어느 단계에서 막혔는지
kubectl describe pod -n gpu-operator -l app=nvidia-operator-validator
nvidia-operator-validator를 먼저 보는 습관이 중요합니다. 이 파드는 앞 단계들이 성공했는지를 순서대로 검증하므로, 어디서 막혔는지를 가장 빨리 알려 줍니다. 오퍼레이터 파드들이 Init 단계에서 멈춰 있다면 대개 드라이버 데몬셋이 아직 준비되지 않은 것이고, 다른 파드를 아무리 봐도 답이 안 나옵니다.
3층 — 드라이버와 툴킷 버전 불일치
이 계층의 증상은 미묘합니다. 어제까지 되던 것이 노드를 재부팅한 뒤 안 되거나, 새로 붙인 노드에서만 안 됩니다.
원인은 대개 셋 중 하나입니다. 커널이 업데이트되어 드라이버 모듈이 안 맞거나, 노드마다 OS 버전이 달라 오퍼레이터가 배포하는 드라이버 이미지가 맞지 않거나, 호스트에 이미 드라이버가 깔려 있는데 오퍼레이터도 드라이버를 배포하려는 상태입니다.
# 드라이버 컨테이너 로그 (커널 모듈 빌드 실패가 여기 찍힌다)
kubectl logs -n gpu-operator nvidia-driver-daemonset-<POD-ID> -c nvidia-driver-ctr
# 노드별 OS와 커널 버전 대조
kubectl get nodes -o wide
# GFD가 붙인 드라이버·런타임 버전 라벨 대조
kubectl get nodes -L nvidia.com/cuda.driver-version.full,nvidia.com/cuda.runtime-version.full
마지막 명령이 특히 유용합니다. GPU Feature Discovery가 붙이는 nvidia.com/cuda.driver-version.full과 nvidia.com/cuda.runtime-version.full 라벨을 노드끼리 비교하면 어느 노드만 다른지 한 줄로 보입니다.
호스트에 이미 드라이버가 있는 환경이라면 Helm 값에서 driver.enabled를 거짓으로 두어야 하고, 툴킷이 이미 구성되어 있다면 toolkit.enabled도 마찬가지입니다. 이 두 값을 잘못 두면 증상이 3층처럼 보이지만 원인은 설정입니다.
NVSwitch가 있는 시스템이라면 별도 조건이 하나 더 있습니다. 공식 문서는 패브릭 관리가 필요한 시스템에서 validator가 시스템이 아직 초기화되지 않았다는 메시지와 함께 실패할 수 있으며, 드라이버와 함께 fabricmanager를 설치해야 한다고 안내합니다.
4층 — 메모리 부족과 OOM
GPU 메모리 부족은 CPU 메모리 부족과 다르게 나타납니다. cgroup이 죽여 주는 것이 아니라 애플리케이션이 할당 실패로 예외를 던지거나, 조용히 성능만 무너집니다.
먼저 구분해야 할 것이 있습니다. 컨테이너의 시스템 메모리가 부족한 것인지, GPU의 프레임버퍼가 부족한 것인지입니다. 전자는 파드가 OOMKilled로 종료되고 이벤트에 남지만, 후자는 파드가 살아 있는 채로 요청만 실패합니다.
# 파드가 OOMKilled인지 확인 (시스템 메모리 쪽)
kubectl get pod <파드이름> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}'
# GPU 쪽은 메트릭으로 본다
kubectl -n gpu-operator port-forward svc/nvidia-dcgm-exporter 9400:9400
curl -s localhost:9400/metrics | grep -E 'DCGM_FI_DEV_FB_(USED|FREE)'
메트릭으로 보면 이렇습니다.
# 프레임버퍼 사용 비율이 천장인 GPU
DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE) > 0.95
# 파드별 프레임버퍼 점유 (kubernetes 매핑이 켜져 있을 때)
sum by (namespace, pod) (DCGM_FI_DEV_FB_USED)
# 추론 서버라면 선점이 답을 준다
sum by (model_name) (rate(vllm:num_preemptions_total[5m])) > 0
vLLM 같은 추론 서버는 시작할 때 GPU 메모리를 미리 잡아 두므로, 프레임버퍼 사용량이 높은 것 자체는 정상입니다. 판단 기준은 사용량이 아니라 선점 발생 여부입니다. 메모리 예약 비율은 vLLM 설정의 gpu_memory_utilization 값이 정하며, 이 값은 캐시 설정 정보 메트릭의 라벨로도 노출됩니다.
같은 카드에 여러 워크로드를 time-slicing으로 올린 상태라면 4층 문제는 필연입니다. 앞서 본 대로 복제본 사이에 메모리 격리가 없기 때문입니다.
5층 — 노드에서 GPU가 사라진다
가장 불쾌한 계층입니다. 어제 8장이던 노드가 오늘 7장입니다.
공식 트러블슈팅 문서가 이 상황을 직접 설명합니다. 디바이스 플러그인이 Xid 오류 때문에 장치를 비정상으로 표시하면 노드가 물리적으로 꽂혀 있는 것보다 적은 수의 GPU를 광고하며, 플러그인 로그에서 장치를 비정상으로 표시한다는 항목으로 확인할 수 있습니다.
# 디바이스 플러그인 로그에서 비정상 표시 확인
kubectl logs -n gpu-operator nvidia-device-plugin-daemonset-<POD-ID> | grep -i unhealthy
# 총량과 할당 가능량의 차이 재확인
kubectl describe node <노드이름> | sed -n '/Capacity/,/Allocated resources/p'
# 진단 자료 일괄 수집
curl -o must-gather.sh -L https://raw.githubusercontent.com/NVIDIA/gpu-operator/main/hack/must-gather.sh
chmod +x must-gather.sh
./must-gather.sh
메트릭 쪽 증거는 DCGM에 있습니다. 기본 CSV에 켜져 있는 DCGM_FI_DEV_XID_ERRORS는 마지막으로 마주친 Xid 오류 값을 담습니다. 다만 이것은 마지막 값이라 이력 추적에는 약합니다. 저장소 README에 따르면 exporter는 이를 감시해 Xid별로 시계열을 나누는 DCGM_EXP_XID_ERRORS_TOTAL을 제공하며, xid 라벨이 붙고 값 0은 오류 없음으로 취급되어 세지 않습니다. 이 필드는 기본 CSV에서 주석 처리되어 있으므로 쓰려면 직접 켜야 합니다.
하드웨어 열화를 보는 지표도 기본으로 켜져 있습니다. DCGM_FI_DEV_UNCORRECTABLE_REMAPPED_ROWS, DCGM_FI_DEV_CORRECTABLE_REMAPPED_ROWS, DCGM_FI_DEV_ROW_REMAP_FAILURE 세 개입니다. 특히 마지막 값이 리맵 실패를 뜻하므로, 여기에 값이 찍히면 소프트웨어로 해결할 단계가 아닙니다. 클럭 이벤트를 추적하려면 DCGM_EXP_CLOCK_EVENTS_TOTAL이 DCGM_FI_DEV_CLOCKS_EVENT_REASONS를 감시하며 clock_event 라벨로 나뉜다는 점을 기억해 두면 좋습니다.
마치며 — 순서가 곧 실력이다
다섯 계층을 한 줄씩으로 압축하면 이렇습니다. 자원이 광고되는가, 컨테이너가 장치를 보는가, 버전이 맞는가, 메모리가 충분한가, 카드가 살아 있는가. 위에서부터 확인하면 대부분의 장애가 두세 번째 명령에서 갈립니다.
한 가지 습관을 덧붙이고 싶습니다. 총량과 할당 가능량을 비교하는 것을 첫 명령으로 두는 것입니다. 이 한 줄로 5층 문제인지 아닌지가 즉시 갈리고, 그 판단만으로도 진단 경로가 절반으로 줄어듭니다.
이것으로 시리즈를 마칩니다. 오퍼레이터로 GPU를 노드에 올리고, 자원으로 광고하고, 필요하면 나눠 쓰고, 메트릭으로 관측하고, 약속을 세우고, 깨졌을 때 순서대로 내려가는 것까지가 한 바퀴였습니다. 각 단계에서 이름 하나를 추측 대신 문서에서 확인하는 습관이, 결국 새벽에 깨어 있는 시간을 줄여 줍니다.
직접 해보기
- kubectl 명령어 찾기 — 위 플레이북의 명령들을 상황별로 다시 찾아보세요.
- K8s 실습 랩 — describe와 로그 확인의 흐름을 직접 반복해 보세요.
- Kubestronaut 퀴즈 — 노드 자원과 파드 상태 진단을 문제로 점검해 보세요.
시리즈
- 이전 글: GPU 서빙 SLO와 알람 설계
- 다음 글: 이 글이 시리즈의 마지막입니다.
참고 자료
- GPU Operator Troubleshooting: https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/troubleshooting.html
- dcgm-exporter 기본 카운터 CSV: https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/default-counters.csv
- dcgm-exporter README: https://github.com/NVIDIA/dcgm-exporter/blob/main/README.md
- Kubernetes Device Plugins: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- gpu-operator 저장소 values.yaml: https://github.com/NVIDIA/gpu-operator/blob/main/deployments/gpu-operator/values.yaml
A GPU Troubleshooting Playbook — Fix the Layers, Then Walk Down
- Introduction — Fix the Layers, Then Walk Down
- Layer 1 — The Pod Will Not Schedule
- Layer 2 — The Pod Runs but Cannot See the GPU
- Layer 3 — Driver and Toolkit Version Mismatch
- Layer 4 — Memory Exhaustion and OOM
- Layer 5 — GPUs Disappearing from a Node
- Closing — The Order Is the Skill
- Try It Yourself
- Series
- References
Introduction — Fix the Layers, Then Walk Down
The biggest time sink in GPU incident response is poking around in no particular order. SSH into the node, run nvidia-smi, read pod logs, restart the DaemonSet, and an hour is gone.
The trouble is that there are five layers and the symptoms resemble each other. A pod not starting could be a scheduling problem, an out-of-capacity problem, or a dead driver. So fixing an order in advance and walking down it is always faster.
Metric names and configuration were verified against the official documentation and repositories on 2026-08-12. They can differ between releases, so check again against the version you are running.
Layer 1 — The Pod Will Not Schedule
The symptom is Pending. Only two things need checking here: is the node advertising the resource, and is any of it left.
# Read the events to see why scheduling failed
kubectl describe pod <pod-name> | tail -20
# Compare capacity against allocatable
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,CAP:.status.capacity.nvidia\.com/gpu,ALLOC:.status.allocatable.nvidia\.com/gpu'
# If no node advertises anything, start with the labels
kubectl get nodes -L nvidia.com/gpu.count,nvidia.com/gpu.product,nvidia.com/mig.strategy
The path forks here. If the resource does not exist at all, drop to layer 2. If the resource exists but none is free, that is a capacity question, not an incident. And if capacity reads 8 while allocatable reads 7, that is a strong clue. As the Kubernetes documentation states, when a device is marked unhealthy the kubelet decreases allocatable only and leaves capacity unchanged. Seeing that asymmetry, you can jump straight to layer 5.
If no labels are present at all, GPU Feature Discovery or Node Feature Discovery is not running, which puts the problem on the operator side.
Layer 2 — The Pod Runs but Cannot See the GPU
The container starts but nvidia-smi fails or CUDA cannot find a device. Or container creation fails outright.
The official troubleshooting documentation names a representative error message: no runtime for 'nvidia' is configured. That means the NVIDIA Container Toolkit failed to register a runtime handler with the container engine. It is a toolkit problem, not a driver problem.
# Operator pod status at a glance
kubectl get pods -n gpu-operator
# Toolkit and device plugin logs
kubectl logs -n gpu-operator nvidia-container-toolkit-daemonset-<POD-ID>
kubectl logs -n gpu-operator nvidia-device-plugin-daemonset-<POD-ID>
# Where the validator got stuck
kubectl describe pod -n gpu-operator -l app=nvidia-operator-validator
The habit of reading nvidia-operator-validator first matters. That pod verifies in order whether the preceding stages succeeded, so it tells you fastest where things stopped. Operator pods stuck at the Init stage usually mean the driver DaemonSet is not ready yet, and staring at any other pod will not produce an answer.
Layer 3 — Driver and Toolkit Version Mismatch
Symptoms at this layer are subtle. Something that worked yesterday stops after a node reboot, or fails only on newly added nodes.
The cause is usually one of three: the kernel was updated and the driver module no longer matches, OS versions differ across nodes so the driver image the operator ships does not fit, or drivers are already installed on the host while the operator is also trying to deploy them.
# Driver container logs (kernel module build failures land here)
kubectl logs -n gpu-operator nvidia-driver-daemonset-<POD-ID> -c nvidia-driver-ctr
# Compare OS and kernel versions across nodes
kubectl get nodes -o wide
# Compare the driver and runtime version labels GFD applied
kubectl get nodes -L nvidia.com/cuda.driver-version.full,nvidia.com/cuda.runtime-version.full
The last command is especially useful. Comparing the nvidia.com/cuda.driver-version.full and nvidia.com/cuda.runtime-version.full labels that GPU Feature Discovery applies shows in one line which node is the odd one out.
In an environment where drivers already exist on the host, driver.enabled must be false in the Helm values, and the same goes for toolkit.enabled if the toolkit is already configured. Getting those two wrong produces symptoms that look like layer 3 while the cause is configuration.
Systems with NVSwitch carry one more condition. The official documentation notes that on systems requiring fabric management the validator can fail with a message about the system not yet being initialized, and that nvidia-fabricmanager must be installed alongside the driver.
Layer 4 — Memory Exhaustion and OOM
GPU memory exhaustion presents differently from CPU memory exhaustion. Nothing gets killed by a cgroup; the application throws an allocation failure, or performance quietly collapses.
Separate two things first. Is the container out of system memory, or is the GPU out of framebuffer? The former terminates the pod as OOMKilled and leaves an event; the latter leaves the pod alive while requests fail.
# Check whether the pod was OOMKilled (system memory side)
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}'
# The GPU side is read from metrics
kubectl -n gpu-operator port-forward svc/nvidia-dcgm-exporter 9400:9400
curl -s localhost:9400/metrics | grep -E 'DCGM_FI_DEV_FB_(USED|FREE)'
As metrics, it looks like this.
# GPUs whose framebuffer usage is at the ceiling
DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE) > 0.95
# Framebuffer held per pod (with Kubernetes mapping enabled)
sum by (namespace, pod) (DCGM_FI_DEV_FB_USED)
# For an inference server, preemption gives the answer
sum by (model_name) (rate(vllm:num_preemptions_total[5m])) > 0
Inference servers such as vLLM reserve GPU memory up front at startup, so high framebuffer usage on its own is normal. The criterion is not usage but whether preemption is occurring. The reservation fraction is set by the vLLM gpu_memory_utilization setting, which is also exposed as a label on the cache configuration info metric.
If several workloads share one card through time-slicing, layer 4 problems are inevitable. As covered earlier, there is no memory isolation between replicas.
Layer 5 — GPUs Disappearing from a Node
The most unpleasant layer. The node had eight cards yesterday and has seven today.
The official troubleshooting documentation describes this situation directly. When the device plugin marks devices unhealthy due to Xid errors, the node advertises fewer GPUs than are physically present, identifiable in the device plugin logs by entries about marking a device as unhealthy.
# Look for unhealthy markings in the device plugin log
kubectl logs -n gpu-operator nvidia-device-plugin-daemonset-<POD-ID> | grep -i unhealthy
# Re-confirm the capacity versus allocatable gap
kubectl describe node <node-name> | sed -n '/Capacity/,/Allocated resources/p'
# Collect diagnostics in bulk
curl -o must-gather.sh -L https://raw.githubusercontent.com/NVIDIA/gpu-operator/main/hack/must-gather.sh
chmod +x must-gather.sh
./must-gather.sh
The metric-side evidence lives in DCGM. Enabled in the default CSV, DCGM_FI_DEV_XID_ERRORS holds the value of the last Xid error encountered. Being a last value, it is weak for history. According to the repository README the exporter also offers DCGM_EXP_XID_ERRORS_TOTAL, which watches that field and emits a separate series per observed xid label, with the value 0 treated as no error and not counted. That field is commented out in the default CSV, so using it requires enabling it yourself.
Metrics for hardware degradation are enabled by default: DCGM_FI_DEV_UNCORRECTABLE_REMAPPED_ROWS, DCGM_FI_DEV_CORRECTABLE_REMAPPED_ROWS, and DCGM_FI_DEV_ROW_REMAP_FAILURE. The last one indicates that remapping of rows has failed, so anything appearing there is past the point where software fixes it. For tracking clock events, it helps to remember that DCGM_EXP_CLOCK_EVENTS_TOTAL watches DCGM_FI_DEV_CLOCKS_EVENT_REASONS and splits by a clock_event label.
Closing — The Order Is the Skill
Compressed to one line each, the five layers are: is the resource advertised, does the container see the device, do the versions match, is there enough memory, is the card alive. Check from the top and most incidents resolve at the second or third command.
One habit is worth adding. Make comparing capacity against allocatable your first command. That single line immediately settles whether this is a layer 5 problem, and that judgment alone halves the diagnostic path.
That closes the series. Getting GPUs onto nodes with the operator, advertising them as resources, sharing them when needed, observing them with metrics, building a promise on top, and walking down in order when it breaks was one full loop. The habit of confirming each name in the documentation rather than guessing it is, in the end, what reduces the hours spent awake at three in the morning.
Try It Yourself
- kubectl Command Finder — look the playbook commands back up by situation.
- K8s Lab — repeat the describe and log-reading flow yourself.
- Kubestronaut Quiz — test your grip on node resources and pod status diagnosis.
Series
- Previous: GPU Serving SLOs and Alert Design
- Next: this is the last post in the series.
References
- GPU Operator Troubleshooting: https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/troubleshooting.html
- dcgm-exporter default counters CSV: https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/default-counters.csv
- dcgm-exporter README: https://github.com/NVIDIA/dcgm-exporter/blob/main/README.md
- Kubernetes Device Plugins: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- gpu-operator repository values.yaml: https://github.com/NVIDIA/gpu-operator/blob/main/deployments/gpu-operator/values.yaml