LabHub
배우기 러닝패스 코스

GPU Operator 와 타임슬라이싱 · GPU 파드가 안 뜬다 — 원인을 순서대로 좁힌다 · 이론

같은 메시지, 다른 원인 — GPU 장애 분류표

LabHub 에서 이어서 보기

한 줄 요약

"GPU 파드가 안 뜬다" 는 신고를 받으면 먼저 파드 오브젝트가 있는가 없는가로 두 갈래를 가르고, 있다면 Pending 인지 Running 인지로 다시 가른 뒤, 노드 status 와 파드 spec 을 대조해 일곱 가지 원인 중 하나로 좁힌다. 스케줄러가 남기는 문장은 세 가지 서로 다른 원인에서 글자 하나 다르지 않게 똑같이 나오므로, 메시지를 읽는 것만으로는 끝나지 않는다.

왜 이게 필요했나

GPU 클러스터 운영에서 가장 자주 오는 문의는 늘 같은 한 문장이다. "제 잡이 안 돌아가요." 그런데 이 한 문장 뒤에 있는 실제 상태는 최소한 이만큼 다르다.

세 가지는 조사해야 할 자리가 전혀 다르다. 첫째는 어드미션(쿼터·RuntimeClass·정책), 둘째는 스케줄러, 셋째는 파드 spec 과 노드의 런타임이다. 그런데 신고자는 이 셋을 구분해 주지 않는다. 그래서 받은 쪽이 첫 3분에 갈래를 정하는 절차를 갖고 있어야 한다. 그 절차가 없으면 device plugin 로그를 한 시간 뒤진 다음에야 "아, 파드가 애초에 안 만들어졌네" 를 알게 된다.

한 걸음 더 나쁜 사정이 있다. 쿠버네티스 스케줄러는 실패 이유를 친절하게 남기는 편인데, 확장 자원에 대해서는 그 친절이 원인을 가리는 쪽으로 작동한다. 아래 세 상황에서 나오는 문장이 똑같다.

세 경우 모두 Insufficient nvidia.com/gpu 다. 원인은 각각 "플러그인을 살려라", "노드를 고쳐라", "자리를 기다리거나 늘려라" 로 완전히 다른데 메시지는 구분을 못 해 준다. 이 한 가지 사실이 이 모듈이 존재하는 이유다.

증상을 가르는 첫 질문 두 개

질문 1 — 파드 오브젝트가 있는가.

kubectl get pod <이름> -n <네임스페이스> -o json

없다면 스케줄러는 이 사건과 아무 상관이 없다. 오브젝트는 API 서버의 어드미션을 통과해야 생기는데, GPU 관련해서 어드미션이 막는 대표적인 둘이 ResourceQuota 초과없는 RuntimeClass 참조다. 둘 다 kubectl apply 를 한 사람의 터미널에만 에러가 찍히고 클러스터에는 아무 흔적도 남지 않는다. 신고자가 그 화면을 닫아 버렸다면 같은 매니페스트를 다시 적용해 에러를 재현하는 것이 가장 빠르다.

질문 2 — spec.nodeName 이 비어 있는가.

kubectl get pod <이름> -n <네임스페이스> -o jsonpath='{.spec.nodeName}'

비어 있으면 스케줄링 문제다. 채워져 있는데 문제가 있다면 그것은 노드 위의 문제(런타임 핸들러, 드라이버, 요청을 안 한 매니페스트)다. kubectl get podSTATUS 칸이 아니라 이 필드를 보는 이유는, Pending 이라는 한 단어가 "노드를 못 골랐다" 와 "노드는 골랐는데 컨테이너를 못 만든다" 를 둘 다 덮기 때문이다.

원인이 어느 칸에서 드러나는가

| 원인 | 결정적 증거가 있는 곳 | 보는 명령 |
| --- | --- | --- |
| 쿼터 초과 | 파드가 없음. 생성 시 에러 문구 | kubectl get resourcequota -n <ns> -o json |
| RuntimeClass 없음 | 파드가 없음. 생성 시 에러 문구 | kubectl get runtimeclass |
| 요청을 안 함 | spec.containers[].resources.limitsnvidia.com/gpu 가 없음 | kubectl get pod -o json |
| 라벨 불일치 | spec.nodeSelector 를 만족하는 노드가 0개 | kubectl get nodes --show-labels |
| 테인트 | 후보 노드의 spec.taints 를 파드의 tolerations 가 못 견딤 | kubectl get node -o json |
| 자원 미광고 | 후보 노드의 status.capacity 에 자원 이름 자체가 없음 | kubectl get node -o json |
| allocatable 0 | capacity 는 양수인데 status.allocatable 이 0 | 같은 출력의 다른 칸 |
| 자리 소진 | allocatable 은 양수인데 그 노드의 파드 요청 합이 그만큼 | kubectl get pods -A -o json 을 합산 |

이 표의 쓸모는 "무엇을 보는가" 가 아니라 "어떤 순서로 보는가" 에 있다. 순서는 위에서 아래다. 앞의 것이 참이면 뒤의 것은 볼 필요가 없고, 순서를 지키지 않으면 엉뚱한 결론이 난다. 예를 들어 nodeSelector 가 아무 노드와도 안 맞는 파드는 테인트도 자원도 검사할 대상이 애초에 없다. 그런데 "테인트를 지웠는데도 안 뜬다" 같은 조치를 먼저 해 버리면 멀쩡한 클러스터 설정만 망가진다.

마지막 줄(자리 소진)이 유일하게 계산을 요구한다. 노드 오브젝트만 봐서는 알 수 없고, 그 노드에 배치된 파드들을 모아 GPU 요청을 더해야 한다. 여기서 한 가지 함정이 있다. SucceededFailed 로 끝난 파드는 자리를 쥐고 있지 않으므로 합산에서 빼야 한다. 빼지 않으면 실제로는 비어 있는 노드를 "꽉 찼다" 고 잘못 판정한다.

확장 자원이라서 생기는 사정

nvidia.com/gpu 는 cpu·memory 와 달리 kubelet 이 스스로 세지 못하는 확장 자원이다. 공식 문서는 확장 자원의 규칙을 둘로 못 박는다. 오버커밋을 지원하지 않고, requestslimits 가 같아야 하며, 값은 정수여야 한다. 그래서 GPU 요청을 읽을 때는 limits 만 봐도 충분하다. 반대로 말하면 반 장이나 1.5장 같은 값은 존재할 수 없고, 모자란 만큼만 받는 식의 동작도 없다. 요청은 통째로 만족되거나 파드가 기다리거나 둘 중 하나다.

그리고 확장 자원은 "장치가 있는가" 와 무관하게 성립한다. 노드 status 에 숫자만 적혀 있으면 스케줄러는 그 노드로 파드를 보낸다. 반대로 카드가 여덟 장 꽂혀 있어도 status 에 숫자가 없으면 스케줄러에게 그 노드는 GPU 가 없는 노드다. 조사할 때 물리적 사실이 아니라 노드 오브젝트가 스케줄러의 유일한 진실이라는 것을 붙잡고 있어야 한다.

현장에서 만나는 모습

첫째, kubectl describe 만 읽고 원인을 단정하는 습관이 가장 비싸다. 앞서 본 대로 세 원인이 같은 문장을 낸다. 특히 야간에 "Insufficient nvidia.com/gpu" 를 보고 자동으로 노드를 늘리는 스크립트를 걸어 둔 조직이 있었는데, 실제 원인은 노드 하나의 allocatable 이 0 으로 떨어진 것이었다. 노드를 늘려도 그 노드는 계속 0 이었고, 비용만 늘었다.

둘째, 사람이 판정하면 순서를 지키지 못한다. 그래서 이 판정은 도구로 굳혀 두는 편이 낫다. 입력은 파드 하나, 출력은 원인 한 단어. 이런 도구가 있으면 1차 대응자가 신고를 받은 자리에서 곧장 "이건 쿼터입니다" 라고 답할 수 있고, 판정 기준이 사람마다 달라지지 않는다. 이 모듈의 실습이 만드는 것이 정확히 그 도구다.

셋째, 출력은 한 단어여야 쓸모가 있다. 문장을 내면 사람이 다시 읽어야 하고, 자동화에 물릴 수 없다. no-gpu-node·allocatable-zero·exhausted 처럼 다음 행동이 하나로 정해지는 어휘를 골라 두면 그대로 알림 라우팅 키가 된다. 원인 어휘를 정하는 일이 실은 운영 절차를 정하는 일이다.

넷째, 이 환경의 정직한 한계. 실습 클러스터에는 GPU 도 device plugin 도 없다. 그래서 "장치 플러그인이 죽었다" 는 상태는 플러그인을 죽여서가 아니라 노드 status 에 자원을 안 적는 것으로 만든다. 이것은 흉내가 아니라 같은 상태다 — 실제 장애에서도 스케줄러가 보는 것은 status 뿐이기 때문이다. 다만 "파드는 Running 인데 컨테이너 안에서 장치가 안 보인다" 의 뒷부분은 컨테이너가 실제로 돌지 않으므로 확인할 수 없다. 그 갈래는 파드 spec 에 자원 요청이 없다는 사실까지만 판정한다.

참고 문서

다음 실습에서 할 것

kwok 이 띄운 진짜 스케줄러 위에 일곱 가지 상태를 하나씩 만들어 놓는다. 광고가 멀쩡한 노드, 자원 이름 자체가 없는 노드, capacity 만 있고 allocatable 이 0 인 노드를 세우고, 거기에 각각 파드를 꽂아 세 원인이 같은 문장을 내는 것을 직접 본다. 테인트만 다른 파드와 nodeSelector 만 다른 파드를 더하고, 어드미션에서 거부되어 오브젝트조차 생기지 않는 두 가지(RuntimeClass·쿼터)를 겪는다. 마지막으로 kubectl get -o json 만 보고 원인을 한 단어로 답하는 분류기를 만들어 일곱 파드 전부에 물려 본다.