LabHub
学习 学习路径 课程

GPU Operator 与时间片

GPU 故障分类 — 从同一条消息里分出不同的原因

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

GPU 파드가 막히는 일곱 가지 상태를 진짜 스케줄러 위에 만들어 놓고, 오브젝트만 보고 원인을 한 단어로 답하는 분류기를 만듭니다.

왜 중요한가

"GPU 파드가 안 뜬다" 는 신고 하나에 실제 원인은 예닐곱 가지이고, 조사해야 할 자리가 전부 다릅니다. 게다가 자원 미광고·allocatable 0·자리 소진 세 가지는 스케줄러가 완전히 같은 문장을 냅니다. 그래서 메시지를 읽는 것으로는 끝나지 않고, nodeSelector → 테인트 → capacity → allocatable → 남은 자리 순서로 오브젝트를 대조해야 원인이 하나로 좁혀집니다. 순서가 중요한 이유는 앞 단계가 참이면 뒤 단계는 검사할 대상 자체가 없기 때문입니다 — 순서를 어기면 멀쩡한 테인트를 지우는 식으로 클러스터만 망가집니다. 이 판정을 사람이 매번 하면 기준이 흔들리므로, 마지막에 도구로 굳혀 둡니다.

단계

  1. 작업 디렉터리 /root/gputri/cases /root/gputri/out /root/gputri/bin 을 만들고 네임스페이스 gpu-triage 를 만드세요. lab-node-0status.capacitystatus.allocatable 양쪽에 nvidia.com/gpu"2" 로 넣고, 테인트 nvidia.com/gpu=present:NoSchedule 을 거세요. /root/gputri/cases/case-ok.yaml 에 파드 case-ok 를 쓰세요 — 네임스페이스 gpu-triage, nodeSelectorkubernetes.io/hostname: lab-node-0, 그 테인트를 견디는 톨러레이션, 컨테이너 이름 cuda, 이미지 nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04, limitsnvidia.com/gpu: 1. 적용해 Running 이 되면 /root/gputri/out/01-ok.txt 에 두 줄을 적으세요 — NODE=PHASE=.
  2. lab-node-1 은 아무것도 광고하지 않은 채로 둡니다(device plugin 이 죽은 노드입니다). /root/gputri/cases/case-nogpunode.yaml 에 파드 case-nogpunode 를 쓰세요 — nodeSelectorlab-node-1 에 못 박고, 톨러레이션은 1단계와 같게 두고, nvidia.com/gpu: 1 을 요구합니다. 적용한 뒤 Pending 인 것을 확인하고 PodScheduled 조건의 reasonmessage/root/gputri/out/02-nogpunode.txtREASON=MESSAGE= 두 줄로 저장하세요.
  3. lab-node-2status.capacitynvidia.com/gpu"4" 로, status.allocatable 에는 "0" 으로 넣으세요(드라이버 검증에 실패해 노드가 GPU 를 내놓지 못하는 상태입니다). /root/gputri/cases/case-alloc0.yaml 에 파드 case-alloc0 을 쓰세요 — lab-node-2 에 못 박고 나머지는 2단계와 같습니다. 적용한 뒤 /root/gputri/out/03-alloc0.txt 에 세 줄을 적으세요 — CAPACITY=, ALLOCATABLE=, MESSAGE=. 앞 단계의 메시지와 견주어 보세요.
  4. /root/gputri/cases/case-taint.yaml 에 파드 case-taint 를 쓰세요 — lab-node-0 에 못 박고 nvidia.com/gpu: 1 을 요구하되 톨러레이션은 넣지 않습니다. 나머지는 1단계 파드와 같습니다. 적용한 뒤 /root/gputri/out/04-taint.txt 에 두 줄을 적으세요 — TAINT=nvidia.com/gpu=present:NoScheduleMESSAGE=. 자원은 남아 있는데 왜 막히는지 메시지로 확인하세요.
  5. /root/gputri/cases/case-label.yaml 에 파드 case-label 을 쓰세요 — nodeSelectornvidia.com/gpu.product: NVIDIA-A100-SXM4-40GB 로 두고(이 라벨을 가진 노드는 없습니다), 톨러레이션과 nvidia.com/gpu: 1 요청은 그대로 둡니다. 적용한 뒤 /root/gputri/out/05-label.txt 에 두 줄을 적으세요 — MATCHING_NODES= 에는 그 라벨을 실제로 가진 노드 수를, MESSAGE= 에는 조건 메시지를 넣습니다.
  6. /root/gputri/cases/case-exhaust.yaml 에 파드 case-exhaust 를 쓰세요 — lab-node-0 에 못 박고 톨러레이션을 넣고 nvidia.com/gpu: 2 를 요구합니다. 그 노드는 2장을 광고하지만 1단계의 case-ok 가 이미 한 장을 쥐고 있습니다. 적용한 뒤 /root/gputri/out/06-exhaust.txt 에 세 줄을 적으세요 — ALLOCATABLE=(그 노드가 광고한 양), ALLOCATED=(그 노드에 배치된 파드들의 GPU 요청 합), REQUESTED=2.
  7. 먼저 /root/gputri/cases/case-norequest.yaml 에 파드 case-norequest 를 쓰세요 — lab-node-0 에 못 박고 톨러레이션은 넣되 자원 요청은 아예 넣지 않습니다. 적용하면 뜹니다. 다음으로 /root/gputri/cases/case-rtc.yamlruntimeClassName: nvidia 를 단 파드 case-rtc 를 쓰고 적용해, 표준 오류까지 포함한 출력을 /root/gputri/out/07-runtimeclass.txt 에 저장하세요(RuntimeClass 는 만들지 마세요). 끝으로 네임스페이스 gpu-quota 를 만들고 /root/gputri/cases/quota.yaml 에 ResourceQuota gpu-quota 를 써서 requests.nvidia.com/gpu"1" 로 막은 뒤, /root/gputri/cases/case-quota.yaml 의 파드 case-quota(GPU 3장 요구)를 적용해 출력을 /root/gputri/out/07-quota.txt 에 저장하세요. 마지막으로 /root/gputri/out/07-symptoms.txt 에 세 줄을 적으세요 — NOREQUEST=, RUNTIMECLASS=, QUOTA=. 뒤 두 줄에는 파드 오브젝트가 생겼는지를 created 또는 absent 로 적습니다.
  8. /root/gputri/bin/triage.sh 를 만드세요. bash triage.sh <네임스페이스> <파드> 로 부르면 표준 출력에 한 단어만 내고 끝납니다. 답은 일곱 가지입니다 — no-request ok label-mismatch taint no-gpu-node allocatable-zero exhausted. 파드가 없으면 표준 오류에 안내를 내고 1 로 끝냅니다. 판정은 kubectl get -o json 이 준 오브젝트만으로 하고, 순서는 요청 없음 → 이미 배치됨 → nodeSelector → 테인트 → capacity → allocatable → 남은 자리입니다. 만든 뒤 일곱 파드(case-ok case-nogpunode case-alloc0 case-taint case-label case-exhaust case-norequest)에 모두 물려 /root/gputri/out/triage.txt<파드이름> <원인> 일곱 줄을 적으세요.

참고

정상인 GPU 노드 한 대를 세운다

작업 디렉터리 /root/gputri/cases /root/gputri/out /root/gputri/bin 을 만들고 네임스페이스 gpu-triage 를 만드세요. lab-node-0status.capacitystatus.allocatable 양쪽에 nvidia.com/gpu"2" 로 넣고, 테인트 nvidia.com/gpu=present:NoSchedule 을 거세요. /root/gputri/cases/case-ok.yaml 에 파드 case-ok 를 쓰세요 — 네임스페이스 gpu-triage, nodeSelectorkubernetes.io/hostname: lab-node-0, 그 테인트를 견디는 톨러레이션, 컨테이너 이름 cuda, 이미지 nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04, limitsnvidia.com/gpu: 1. 적용해 Running 이 되면 /root/gputri/out/01-ok.txt 에 두 줄을 적으세요 — NODE=PHASE=.

확장 자원은 kubelet 이 스스로 세지 못합니다. 실제 클러스터에서는 device plugin 이 노드 status 에 적어 주는데, 여기서는 kubectl patch node <이름> --subresource=status --type=merge 로 같은 자리에 같은 값을 직접 적습니다. capacity 만 적으면 스케줄러가 쓸 자리가 생기지 않습니다 — 두 칸을 모두 채우세요. 운영 GPU 노드는 거의 항상 테인트가 걸려 있어 일반 워크로드가 흘러들지 않습니다. 그래서 GPU 파드에는 톨러레이션이 따라붙습니다.

자원 이름 자체가 없는 노드

lab-node-1 은 아무것도 광고하지 않은 채로 둡니다(device plugin 이 죽은 노드입니다). /root/gputri/cases/case-nogpunode.yaml 에 파드 case-nogpunode 를 쓰세요 — nodeSelectorlab-node-1 에 못 박고, 톨러레이션은 1단계와 같게 두고, nvidia.com/gpu: 1 을 요구합니다. 적용한 뒤 Pending 인 것을 확인하고 PodScheduled 조건의 reasonmessage/root/gputri/out/02-nogpunode.txtREASON=MESSAGE= 두 줄로 저장하세요.

조건은 kubectl get pod <이름> -n <ns> -o jsonpath='{.status.conditions[0].reason}' 로 꺼낼 수 있습니다. 메시지에 무엇이 적히는지 눈여겨보세요 — 다음 두 단계에서 원인이 전혀 다른 파드가 같은 문장을 냅니다. 노드에 nvidia.com/gpu 를 적지 않았다는 것은 '카드가 없다' 가 아니라 '스케줄러에게 없다고 보인다' 는 뜻입니다.

capacity 는 있는데 내줄 수 없는 노드

lab-node-2status.capacitynvidia.com/gpu"4" 로, status.allocatable 에는 "0" 으로 넣으세요(드라이버 검증에 실패해 노드가 GPU 를 내놓지 못하는 상태입니다). /root/gputri/cases/case-alloc0.yaml 에 파드 case-alloc0 을 쓰세요 — lab-node-2 에 못 박고 나머지는 2단계와 같습니다. 적용한 뒤 /root/gputri/out/03-alloc0.txt 에 세 줄을 적으세요 — CAPACITY=, ALLOCATABLE=, MESSAGE=. 앞 단계의 메시지와 견주어 보세요.

capacity 는 '이 노드가 가진 양', allocatable 은 '스케줄러에게 내줄 수 있는 양' 입니다. 둘이 갈라지는 것이 정상 동작이고(시스템 예약분이 그 차이입니다), GPU 에서는 이 차이가 0 으로 벌어지는 일이 실제로 일어납니다. 두 값은 같은 kubectl get node -o json 출력의 다른 칸에 있습니다. 메시지를 2단계 파일과 나란히 놓고 보면 이 실습이 왜 필요한지가 한눈에 보입니다.

톨러레이션만 빠진 파드

/root/gputri/cases/case-taint.yaml 에 파드 case-taint 를 쓰세요 — lab-node-0 에 못 박고 nvidia.com/gpu: 1 을 요구하되 톨러레이션은 넣지 않습니다. 나머지는 1단계 파드와 같습니다. 적용한 뒤 /root/gputri/out/04-taint.txt 에 두 줄을 적으세요 — TAINT=nvidia.com/gpu=present:NoScheduleMESSAGE=. 자원은 남아 있는데 왜 막히는지 메시지로 확인하세요.

1단계에서 case-ok 가 같은 노드에 떴는데 이 파드는 못 갑니다. 차이는 톨러레이션 하나뿐입니다. 이 단계의 메시지는 앞 두 단계와 다릅니다 — 테인트는 스케줄러가 원인을 정확히 말해 주는 몇 안 되는 자리입니다. 노드에 걸린 테인트는 kubectl get node lab-node-0 -o jsonpath='{.spec.taints}' 로 볼 수 있습니다.

후보 노드가 아예 없는 파드

/root/gputri/cases/case-label.yaml 에 파드 case-label 을 쓰세요 — nodeSelectornvidia.com/gpu.product: NVIDIA-A100-SXM4-40GB 로 두고(이 라벨을 가진 노드는 없습니다), 톨러레이션과 nvidia.com/gpu: 1 요청은 그대로 둡니다. 적용한 뒤 /root/gputri/out/05-label.txt 에 두 줄을 적으세요 — MATCHING_NODES= 에는 그 라벨을 실제로 가진 노드 수를, MESSAGE= 에는 조건 메시지를 넣습니다.

GPU Feature Discovery 가 붙이는 라벨을 그대로 골라 쓴 매니페스트가 클러스터에 그 라벨이 없으면 이렇게 됩니다. 이 파드는 테인트도 자원도 따져 볼 대상이 없습니다 — 후보 노드가 0개이기 때문입니다. 그래서 판정 순서에서 nodeSelector 가 맨 앞에 옵니다. 라벨을 가진 노드 수는 kubectl get nodes -l <키> --no-headers | wc -l 로 셀 수 있습니다.

광고도 멀쩡한데 자리가 없다

/root/gputri/cases/case-exhaust.yaml 에 파드 case-exhaust 를 쓰세요 — lab-node-0 에 못 박고 톨러레이션을 넣고 nvidia.com/gpu: 2 를 요구합니다. 그 노드는 2장을 광고하지만 1단계의 case-ok 가 이미 한 장을 쥐고 있습니다. 적용한 뒤 /root/gputri/out/06-exhaust.txt 에 세 줄을 적으세요 — ALLOCATABLE=(그 노드가 광고한 양), ALLOCATED=(그 노드에 배치된 파드들의 GPU 요청 합), REQUESTED=2.

이 판정만은 노드 오브젝트 하나로 끝나지 않습니다. 그 노드에 배치된 파드를 모아 요청을 더해야 합니다 — kubectl get pods -A --field-selector spec.nodeName=lab-node-0 -o json 이 출발점입니다. 끝난 파드(Succeeded·Failed)는 자리를 쥐고 있지 않으니 합산에서 빼야 합니다. 메시지는 2·3단계와 또 같습니다 — 세 번째로 같은 문장을 보게 됩니다.

파드가 아예 만들어지지 않는 두 가지

먼저 /root/gputri/cases/case-norequest.yaml 에 파드 case-norequest 를 쓰세요 — lab-node-0 에 못 박고 톨러레이션은 넣되 자원 요청은 아예 넣지 않습니다. 적용하면 뜹니다. 다음으로 /root/gputri/cases/case-rtc.yamlruntimeClassName: nvidia 를 단 파드 case-rtc 를 쓰고 적용해, 표준 오류까지 포함한 출력을 /root/gputri/out/07-runtimeclass.txt 에 저장하세요(RuntimeClass 는 만들지 마세요). 끝으로 네임스페이스 gpu-quota 를 만들고 /root/gputri/cases/quota.yaml 에 ResourceQuota gpu-quota 를 써서 requests.nvidia.com/gpu"1" 로 막은 뒤, /root/gputri/cases/case-quota.yaml 의 파드 case-quota(GPU 3장 요구)를 적용해 출력을 /root/gputri/out/07-quota.txt 에 저장하세요. 마지막으로 /root/gputri/out/07-symptoms.txt 에 세 줄을 적으세요 — NOREQUEST=, RUNTIMECLASS=, QUOTA=. 뒤 두 줄에는 파드 오브젝트가 생겼는지를 created 또는 absent 로 적습니다.

이 단계의 세 가지는 앞 다섯 가지와 증상 갈래가 다릅니다. 첫째는 파드가 멀쩡히 뜨는데 장치가 붙지 않고, 뒤 둘은 파드 오브젝트가 아예 생기지 않습니다. 어드미션에서 거부되면 클러스터에는 아무 흔적이 없고 에러는 적용한 사람의 터미널에만 남습니다 — 그래서 2>&1 로 출력을 파일에 담아 두는 습관이 조사에서 결정적입니다. 파드가 없다는 것은 kubectl get pod <이름> -n <ns> 의 종료 코드로 확인할 수 있습니다.

오브젝트만 보고 원인을 한 단어로 답하는 분류기

/root/gputri/bin/triage.sh 를 만드세요. bash triage.sh <네임스페이스> <파드> 로 부르면 표준 출력에 한 단어만 내고 끝납니다. 답은 일곱 가지입니다 — no-request ok label-mismatch taint no-gpu-node allocatable-zero exhausted. 파드가 없으면 표준 오류에 안내를 내고 1 로 끝냅니다. 판정은 kubectl get -o json 이 준 오브젝트만으로 하고, 순서는 요청 없음 → 이미 배치됨 → nodeSelector → 테인트 → capacity → allocatable → 남은 자리입니다. 만든 뒤 일곱 파드(case-ok case-nogpunode case-alloc0 case-taint case-label case-exhaust case-norequest)에 모두 물려 /root/gputri/out/triage.txt<파드이름> <원인> 일곱 줄을 적으세요.

파드 이름으로 답을 정하면 안 됩니다 — 처음 보는 파드에 물렸을 때 맞아야 쓸모가 있습니다. 필요한 입력은 세 가지입니다: 그 파드, 노드 전체, 그리고 모든 네임스페이스의 파드(자리 계산용). 톨러레이션 판정은 operator: ExistsEqual 두 경우를 모두 다뤄야 하고, effect 가 비어 있으면 모든 effect 를 견딥니다. 확장 자원은 limits 만 봐도 됩니다 — requests 와 limits 가 같아야 한다는 규칙 때문입니다. JSON 을 셸로 파싱하려 애쓰지 말고 python3 에 넘기면 훨씬 짧아집니다.