soobook
KUBERNETES

Volcano

A100, H100 같은 GPU 클러스터를 운영한다고 해보자. 거기에 GPU 8장이 필요한 분산 학습 잡을 던졌는데, 마침 클러스터에 7장만 비어 있다. 그러면 어떻게 될까?

기본 Kubernetes 스케줄러(kube-scheduler)는 비어 있는 GPU 7장에 7개 Pod을 그냥 배치한다. 8번째 Pod은 자원이 없어 Pending 상태로 대기한다. 그 사이 이미 떠 있는 7개 rank는 8번째를 기다리며 GPU 7장을 점유한 채 idle하게 굴러간다.

분산 학습은 모든 rank가 동시에 시작해야 의미가 있는 잡이다. 한 명만 빠져도 나머지가 일을 시작하지 못한다. 그런데 kube-scheduler는 이 묶음을 모르고 Pod 하나하나만 본다. 이 어긋남이 GPU 클러스터에서 실제로 일어나는 데드락의 원인이다.

Volcano는 Kubernetes에서 batch / AI 워크로드를 Pod 단위가 아니라 Job 한 덩어리 단위로 스케줄하기 위해 만들어진 추가 scheduler다. 무엇이 부족했고 Volcano가 어떻게 메우는지 가볍게 정리해보자.

기본 스케줄러

Kubernetes 기본 스케줄러는 online service를 굴리도록 설계됐다. Web server, API gateway, Redis 같은 Pod은 보통 다음 두 성질을 갖는다.

  • 각 Pod이 독립적으로 동작한다. Pod A가 늦게 떠도 Pod B는 자기 일을 한다.
  • 늦게 떠도 손해는 늦게 뜬 그 Pod 하나에 그친다.

kube-scheduler는 이 가정 위에서 동작한다. Pending Pod 큐에서 하나를 꺼내, 노드 후보 중 가장 점수 높은 곳에 배치한다. 그 Pod의 동료가 누군지, 같이 떠야 하는지는 보지 않는다.

이 모델 덕분에 Kubernetes가 그렇게 잘 스케일하는 것이지만, batch 워크로드에서는 가정이 깨진다.

분산 학습

GPU 한 장에 다 안 들어가는 모델을 학습할 때, 여러 GPU에 일을 쪼개 같이 돌리는 게 분산 학습이다. Kubernetes에서는 보통 GPU 1장당 worker Pod 1개를 매핑한다. 8장이 필요한 학습이면 worker Pod 8개를 띄우고, 각 Pod이 노드에 꽂힌 GPU 한 장씩 점유하는 식이다.

8개 worker는 시작 시점에 NCCL 라이브러리를 통해 서로 통신할 group을 만든다. 그 뒤로는 step마다 각자 GPU에서 forward와 backward를 돌리고, gradient를 group 전체에 broadcast(AllReduce)해서 같은 가중치 업데이트를 8장 모두에 적용한다. PyTorch DDP, MPI, Horovod 같은 프레임워크가 이 모양으로 동작한다.

sequenceDiagram
    participant R0 as rank 0
    participant R1 as rank 1
    participant R7 as rank 7
    R0->>R1: AllReduce
    R1->>R7: AllReduce
    R7->>R0: AllReduce
    Note over R0,R7: 한 명이라도 빠지면 group 형성 실패

핵심은 시작 시점에 8개가 동시에 떠 있어야 group이 만들어진다는 점이다. 한 명이라도 빠지면 group 형성이 안 돼 학습 step이 시작조차 못 한다.

도입부의 시나리오로 돌아와서, 8개 중 7개만 떠 있으면 무슨 일이 벌어지는지 보자.

  • 7개 rank는 8번째를 기다리며 GPU 7장을 점유한다 (NCCL init 대기).
  • 8번째 rank는 Pending. 다른 잡이 끝나야 자리가 난다.
  • 다른 잡도 GPU가 부족해 Pending 상태로 내가 점유 중인 7장을 기다린다.
  • 양쪽이 서로의 자원을 기다리는 자원 fragmentation deadlock.

같은 GPU 7장을 효율적으로 쓰려면 전혀 안 띄우고 다른 잡에 양보하는 편이 낫다. 이 판단을 해주는 게 Volcano다.

Volcano

Volcano는 Kubernetes 위에서 동작하는 batch 시스템이다.

핵심 디자인은 단순하다. 기본 kube-scheduler를 대체하지 않고, 추가 scheduler로 클러스터에 같이 띄운다. 일반 service Pod은 그대로 kube-scheduler가 처리하고, batch Pod은 schedulerName: volcano로 명시해서 Volcano scheduler가 받는다.

flowchart LR
    API["kube-apiserver"]

    K["kube-scheduler<br/>(서비스용)"]
    V["volcano-scheduler<br/>(배치용)"]

    API --> K
    API --> V

    K -.->|"schedulerName 비명시"| Service["nginx, redis, ..."]
    V -.->|"schedulerName: volcano"| Batch["분산 학습, Spark, ..."]

Volcano를 설치하면 컴포넌트 4개가 들어간다.

  • Scheduler: action × plugin 조합으로 batch 잡을 스케줄
  • ControllerManager: Job, Queue, PodGroup CRD의 lifecycle 관리
  • Admission: CRD validation
  • vcctl: CLI

쓰는 쪽에서 직접 만질 일이 거의 없는 컴포넌트는 Admission과 ControllerManager다. 보통 사용자는 Job을 쓰고, 운영자는 Queue를 만든다. 그리고 그 둘 사이에 자동으로 끼는 게 PodGroup이다.

Job, Queue, PodGroup

Volcano가 새로 도입하는 CRD는 셋이다.

  • Job: 사용자가 작성. “이 학습 잡은 worker 8개로 굴린다” 같은 잡 정의
  • Queue: 운영자가 만든다. 팀별 자원 quota, 우선순위 같은 정책
  • PodGroup: 자동으로 생성된다. gang scheduling의 원자 단위 — “이 묶음 통째로 떠라”

사용자가 Job을 만들면 Volcano가 안에서 PodGroup을 자동으로 만든다. PodGroup에는 minAvailable이라는 숫자가 박힌다. 이 묶음이 동시에 가용해야 하는 최소 Pod 수다. 분산 학습이면 보통 worker 전체 수와 같다.

scheduler는 이 PodGroup을 단위로 본다. minAvailable만큼 자원이 한 번에 모일 때까지 아무것도 띄우지 않는다. 7장만 잡힌 위 시나리오에서 Volcano는 7개를 띄우는 대신 0개를 띄운다. 모인 자원 7장은 다른 잡이 쓸 수 있게 비워둔다.

이게 Volcano가 부르는 gang scheduling이다.

예제

말로만 하면 감이 안 오니 공식 예제를 분산 학습 시나리오로 줄여보자.

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: pytorch-ddp
spec:
  minAvailable: 8        # 8개가 한 번에 떠야 의미 있음
  schedulerName: volcano # kube-scheduler 대신 Volcano가 받음
  queue: ml-team-a       # 운영자가 만든 queue
  policies:
    - event: PodEvicted
      action: RestartJob # rank 1개라도 죽으면 잡 통째로 restart
  tasks:
    - replicas: 8
      name: worker
      template:
        spec:
          containers:
            - image: my-pytorch-ddp:latest
              resources:
                requests:
                  nvidia.com/gpu: 1
          restartPolicy: OnFailure

기억할 줄은 세 군데다.

  • minAvailable: 8 — 8개 모일 때까지 0개도 안 띄운다. gang scheduling 핵심
  • schedulerName: volcano — 이 줄이 빠지면 그냥 kube-scheduler가 잡아서 부분 배치 데드락이 그대로 재발한다
  • policies — 분산 학습에서 rank 1개 죽으면 group이 깨진다. 하나만 evict돼도 잡 전체를 restart하는 게 일반적

tasks는 N개 정의 가능하다. parameter server가 별도로 있는 학습이라면 parameter-server task와 worker task를 따로 두고, minAvailable은 둘을 합친 합계로 잡는다.

어떨 때 쓰면 좋은가

체크리스트로 줄이면 한 줄이다. 워크로드가 “Pod 한 묶음이 동시에 떠야 의미 있다”에 해당하면 Volcano 후보다.

  • 분산 학습 (PyTorch DDP, DeepSpeed, MPI, Horovod): 가장 직접적인 케이스. rank 하나 빠지면 학습이 시작되지 않는다.
  • LLM 분산 추론: GPU 한 장에 안 들어가는 LLM을 tensor-parallel 또는 pipeline-parallel로 서빙할 때. worker 전부가 떠야 한 요청을 처리할 수 있어 학습과 같은 gang scheduling 요구가 그대로 발생한다. v1.13부터 LLM 추론용 LeaderWorkerSet 통합이 들어갔다.
  • 대규모 데이터 batch와 워크플로우: Spark, Flink 잡은 executor가 일정 수 이상 모여야 시작된다. Argo Workflows의 multi-stage DAG도 각 stage가 gang 잡이라 단계 진입 시 자원 일괄 확보가 필요하다. Ray, Kubeflow, MindSpore 같은 ML 프레임워크도 대부분 Volcano를 native scheduler로 지원한다.
  • 멀티 팀 GPU 클러스터 운영: Queue로 팀별 quota를 강제하고 fair-share, 우선순위, preemption으로 굴린다. iFlytek은 Volcano 도입 후 GPU 활용률이 40% 개선됐다고 보고했다.
  • GPU 비용 최적화: binpack plugin은 Pod을 적은 노드에 빡빡히 모은다. 비어 있는 노드는 cluster autoscaler가 scale down할 수 있어, 시간당 단가가 큰 GPU 노드를 그만큼 덜 띄운다.

반대로 과한 경우도 있다.

  • 일반 web service, stateless API: kube-scheduler로 충분하다.
  • worker 1, 2개짜리 작은 잡: minAvailable이 의미 없다.
  • 잡 단위 격리·우선순위가 필요 없는 단일 팀 GPU 클러스터: 운영 부담만 늘 수 있다.

References