KubeRay는 Ray 클러스터의 기동·확장·재시작을 Kubernetes 자원 모델에 연결하는 오퍼레이터다. 그러나 여러 팀이 하나의 GPU 풀을 공유할 때 학습이 멈추는 사고는 대부분 KubeRay 자체가 아니라 gang scheduling, placement group, autoscaler의 여유(headroom) 설정이 빠진 자리에서 발생한다. 2026년 기준 공식 문서와 운영 사례를 다시 뜯어본 결과, 이번 회고의 결론은 "GPU는 있었지만 모이지 않았다"는 한 문장으로 요약된다.
분산 학습 잡이 GPU를 쥔 채 멈췄다면, KubeRay 설정이 문제였을까?
대부분 아니다. KubeRay Operator는 워커 Pod의 Kubernetes GPU 제한값을 Ray 스케줄러에 정상적으로 전달하고 --num-gpus를 자동 설정한다. 문제가 생기는 지점은 그 위, 즉 여러 팀·여러 잡이 같은 GPU 풀을 두고 경쟁할 때 gang scheduling·quota·autoscaler headroom이 맞물리는 구간이다.
- GPU 인식(limit→
--num-gpus)은 KubeRay의 책임이지만, 팀별 공정 배분은 Kubernetes queue·quota·priority의 책임이다. 출처 - 분산 학습 워커가 일부만 뜨면 GPU는 점유된 채 유휴 상태가 될 수 있다 — gang scheduling 없이는 이 상태가 반복될 수 있다.
maxReplicas를 필요 워커 수에 딱 맞추면, OOM 이후 교체 워커 요청이 autoscaler 확장 실패로 이어질 수 있다.- fault tolerance는 Pod 재시작만으로 완성되지 않는다 — 체크포인트, FailureConfig, elastic worker 범위, head 복구, GPU 하드웨어 건강성까지 묶어야 한다.
사고 재구성은 이렇다. 한 GPU 풀을 팀 A, 팀 B가 공유하는 RayCluster 환경에서 팀 A가 분산 학습 RayJob을 제출했다. 워커 그룹의 Pod 일부는 스케줄됐지만 전체 워커 수가 모이지 않아 Ray Train의 placement group이 완성되지 않았다. 이 상태에서 autoscaler가 idle로 보이는 워커를 회수하면, 다음 스케줄링 사이클에서 다시 Pod를 올리고 또 일부만 뜨는 패턴이 반복될 수 있다 — 여러 클러스터가 하나의 GPU 풀을 경쟁할 때 보고된 패턴이다. 출처 학습 도중 한 워커가 OOM으로 죽고 교체 워커를 요청했지만, maxReplicas가 필요한 워커 수에 딱 맞춰져 있어 autoscaler가 확장하지 못하고 재시도 과정에서 이전 placement group이 남는 정황도 같은 사례에서 설명된다.
GPU 수는 충분한데 왜 워커가 다 모이지 않았을까?
GPU 총량 부족이 아니라 스케줄링 배치 전략의 문제였다. Ray는 태스크·액터의 논리 자원 요구량 합이 노드의 논리 자원 총량을 넘지 않게 제한하지만, 이는 한 노드 안에서의 동시 실행 통제일 뿐 여러 노드에 걸쳐 "동시에 다 같이 뜨는" 보장은 아니다. 출처 gang scheduling 없이 기본 Kubernetes 스케줄러만 쓰면, 워커 Pod가 하나씩 독립적으로 스케줄되기 때문에 일부는 뜨고 일부는 대기하는 상태가 자연스럽게 생긴다.
KAI Scheduler 문서가 구분하는 bin packing과 spread scheduling의 차이도 여기서 작동한다. bin packing으로 GPU 조각화를 줄이면 활용률은 올라가지만, 분산 학습처럼 전부 모여야 시작되는 워크로드에서는 일부 노드에 빈틈이 없어 나머지 워커가 못 들어가는 상황이 생길 수 있다. 출처 즉 GPU 총 개수는 충분했지만, 배치 전략이 "부분 스케줄"을 허용하는 구조였다는 것이 실제 기여 요인 중 하나였다.
KubeRay Operator의 GPU 인식 로직이 깨진 건 아니었을까?
아니다. KubeRay Operator는 자신의 역할인 GPU 제한값 전달을 정상 수행했다. 워커 Pod에 설정된 Kubernetes GPU limit은 Ray 스케줄러와 autoscaler에 그대로 전달되고, ray start에도 적절한 --num-gpus가 자동 설정된다. 출처 워커 Pod들은 자신의 GPU를 정확히 인식하고 있었고, 문제는 태스크·액터 코드 쪽의 @ray.remote(num_gpus=...) 선언이나 워커 그룹의 GPU 수와 Ray Train이 요구하는 GPU 수의 불일치였다.
워커 그룹별 GPU 수와 잡의 요구량이 맞지 않으면 Kubernetes Pod 수는 늘어나도 필요한 placement group이 완성되지 않아 학습이 시작되지 않을 수 있다는 점은 공식 문서에도 명시돼 있다. 이 가설은 "GPU 인식 레이어"가 아니라 "자원 선언과 워커 그룹 설계의 정합성" 문제로 좁혀지면서 기각됐다.
Head Pod 장애가 전체를 멈춘 것은 아니었을까?
이번 사고에서는 아니었지만, 배제할 수 없는 변수로 남아 재발 방지 항목에 포함됐다. 운영 사례에 따르면 Ray head 노드가 node drain, 이미지 업데이트, 보안 패치, 메모리 압박으로 중단되면 클러스터 전체에 영향을 줄 수 있고, GCS fault tolerance를 쓰더라도 워커가 계속 실행되려면 head가 대략 10분 이내에 복구돼야 한다. 출처
이번 사고의 타임라인을 다시 보면 head Pod는 재시작 없이 유지되고 있었고, 증상은 워커 그룹 쪽에서만 나타났기 때문에 head 장애 가설은 기각됐다. 다만 PCIe·NVLink 오류나 멈춘 NFS 마운트처럼 하드웨어 장애가 있어도 Pod 상태는 Ready로 남을 수 있어 Ray가 계속 해당 노드에 작업을 배치하는 사례도 같은 자료에서 지적된다. 이 점은 이번 사고와 별개로 점검 항목에 추가됐다.
그래서 결국 학습을 멈춘 실제 원인은 무엇이었나?
gang scheduling이 구성되지 않은 상태에서 다중 팀이 같은 GPU 풀을 경쟁했고, 여기에 maxReplicas 여유 부재가 겹친 조합이 원인이었다. 워커가 부분적으로만 스케줄되는 동안 GPU는 점유 상태로 묶여 있었고, autoscaler는 idle로 보이는 워커를 회수했다가 다시 요청하는 패턴을 반복했다.
이 과정에서 OOM으로 죽은 워커의 교체 요청이 들어왔지만 maxReplicas가 정확히 필요 수치로 설정돼 있어 확장 여유가 없었고, 재시도 과정에서 이전 placement group이 정리되지 않고 남는 정황도 같은 운영 사례에서 설명된다. 출처 KubeRay v1.4부터는 Kubernetes scheduler-plugins의 PodGroup API를 이용한 gang scheduling을 RayCluster에 통합해, 워커가 일부만 뜬 채 GPU를 점유하고 대기하는 패턴 자체를 줄이는 방향을 제공한다. 요약하면 원인은 단일 컴포넌트 버그가 아니라, gang scheduling 부재·quota 미설정·autoscaler headroom 부족이라는 세 조건이 겹친 구성 문제였다.
재발을 막기 위해 무엇을 바꿨나?
네 가지를 함께 바꿨다. 첫째, Volcano·YuniKorn·scheduler-plugins·NVIDIA KAI Scheduler 등으로 gang scheduling을 구성해, 워커가 함께 시작되거나 필요한 GPU가 모일 때까지 잡이 시작되지 않도록 했다. 출처
둘째, maxReplicas를 학습에 필요한 정확한 워커 수가 아니라 최소 두 개 이상의 여유를 둔 값으로 재설정해, OOM 이후 교체 워커 요청이 autoscaler 확장 실패로 막히지 않게 했다. 셋째, ScalingConfig의 워커 수를 고정 정수가 아니라 (최소, 최대) 범위로 바꾸고 FailureConfig(max_failures=3)를 적용해, 워커 실패 시 학습 루프가 최대 3회까지 재시도되도록 했다 — Google Cloud의 Ray Train 멀티슬라이스 예제와 같은 패턴이다. 출처
넷째, NVIDIA DCGM Exporter와 GPU Xid 오류 감시를 추가해 PCIe·NVLink 오류나 멈춘 NFS 마운트처럼 Pod 상태만으로는 드러나지 않는 하드웨어 장애를 별도로 잡아내고, 장애 노드를 taint·cordon 처리하도록 했다. 더불어 ephemeral RayJob에도 History Server와 외부 로그 수집기를 붙이고, 장애 조사 기간에는 ttlSecondsAfterFinished를 늘려 shutdownAfterJobFinishes 이후에도 드라이버 오류를 재구성할 수 있게 했다.
이 사고가 다른 GPU 스케줄링 스택에도 주는 교훈은?
활용률 최적화와 분산 학습의 시작 보장은 같은 방향으로 움직이지 않을 수 있다는 것이다. bin packing은 GPU 조각화와 유휴 공간을 줄이는 데 유리하지만, 분산 학습처럼 모든 워커가 동시에 모여야 하는 워크로드에서는 부분 스케줄을 허용해 GPU를 점유한 채 대기시키는 결과로 이어질 수 있다. spread scheduling은 노드 간 부하 분산과 장애 격리에 유리하지만 활용률은 낮아질 수 있다. 출처
이 상충 관계는 Ray·KubeRay에 국한되지 않는다. 멀티테넌시로 희소 GPU를 나누는 모든 스케줄링 스택에서, "최대 활용률"과 "all-or-nothing으로 시작하는 잡의 대기시간"은 서로 다른 축이며, 어느 한쪽만 최적화하면 다른 축에서 비용이 발생한다는 점을 설계 단계에서 먼저 결정해야 한다.
핵심 정리
- KubeRay Operator는 GPU limit→
--num-gpus전달을 정상 수행하며, 이번 사고의 근본 원인이 아니었다. 출처 - 다중 팀이 GPU 풀을 공유할 때 핵심 병목은 gang scheduling·quota·priority·placement group·autoscaler headroom의 조합이다.
maxReplicas는 필요 워커 수에 딱 맞추지 말고 OOM 교체 워커를 고려해 여유를 둬야 한다. 출처- bin packing은 활용률에, gang scheduling·spread는 분산 학습의 시작 보장과 장애 격리에 유리하다 — 둘은 같은 방향으로 움직이지 않는다.
- fault tolerance는 Pod 재시작이 아니라 체크포인트, FailureConfig, elastic worker 범위, head 복구 SLA, GPU 하드웨어 건강성 감시를 함께 구성해야 완성된다.
자주 묻는 질문
KubeRay v1.4 이전에는 gang scheduling을 전혀 쓸 수 없었나?
KubeRay 자체 기능으로는 아니었지만, Volcano나 YuniKorn 같은 외부 Kubernetes 스케줄러를 RayCluster와 연동해 유사한 효과를 낼 수 있었다. v1.4부터는 Kubernetes scheduler-plugins의 PodGroup API가 통합돼 별도 스케줄러 없이도 구성 범위가 넓어졌다.
gang scheduling을 켜면 GPU 활용률이 떨어지지 않나?
워커가 함께 뜨기를 기다리는 동안 일부 GPU가 예약된 채 대기할 수 있어 순간 활용률은 낮아 보일 수 있다. 다만 부분 스케줄로 GPU를 점유한 채 학습이 시작되지 못하는 패턴보다 전체 낭비가 적을 수 있다는 것이 공식 자료의 설명 방향이며, 두 방식을 같은 조건에서 비교한 공개 수치는 확인되지 않는다.
maxReplicas 여유는 몇 개가 적당한가?
공식 문서가 제시하는 보편적 수치는 없다. 운영 사례는 OOM으로 교체 워커가 필요한 상황을 고려해 최소 두 개 이상의 여유를 두는 방식을 권장하지만, 이는 현장 운영 권고이지 벤치마크 기반 수치는 아니다. 출처
Spot GPU 노드에서도 같은 fault tolerance 설계가 통하나?
핵심 패턴(체크포인트, 워커 재편성, 최소 자원 규모)은 전이 가능하지만, 검색된 자료는 TPU Spot VM과 MaxText 8B 모델 조건에서 확인된 것이다. NVIDIA GPU에서 동일 조건으로 측정한 복구 시간이나 학습 손실 수치는 확인되지 않으므로, 적용 전에는 모델이 최소 워커 수 안에서 물리적으로 들어가는지부터 검증해야 한다. 출처
더 알아보기
GPU 스케줄링과 학습 오케스트레이션: 희소 자원을 어떻게 분배할 것인가 — 이 주제의 종합 가이드