Skip to main content

학습 작업은 왜 웹 서버처럼 다루면 안 되는가

쿠버네티스를 웹 서비스 중심으로 배운 사람은 보통 이렇게 생각한다. "Pod가 몇 개 뜨면 일단 서비스는 시작되는 것 아닌가?" Deployment라면 맞는 말이다. API 서버 10개 중 7개만 먼저 떠도 로드밸런서는 살아 있는 7개로 트래픽을 보낼 수 있다. 나머지 3개는 나중에 붙으면 된다.

하지만 분산 학습은 다르다. 8개의 GPU Pod가 하나의 학습 작업을 구성하는데 7개만 실행되고 1개가 Pending 상태라면, 그 7개는 대개 아무 일도 하지 못한다. PyTorch DDP, FSDP, DeepSpeed, Megatron 같은 분산 학습 방식은 여러 프로세스가 같은 시점에 통신 그룹을 만들고, 서로의 rank를 알고, gradient를 주고받는 것을 전제로 한다. 한 명이 빠진 축구팀이 경기를 시작하지 못하는 것과 비슷하다. 문제는 이 "기다리는 7개 Pod"가 고가의 GPU를 이미 점유하고 있다는 점이다.

추론 서버는 부분 가용성이 의미가 있다. 4개 replica 중 2개만 떠도 요청을 일부 처리할 수 있다. 반면 학습 작업은 시작 조건이 더 엄격하다. 필요한 Pod와 GPU가 모두 준비되어야 진행된다. 이것을 보통 all-or-nothing 스케줄링 또는 gang scheduling이라고 부른다. 여기서 gang은 나쁜 의미가 아니라 "함께 움직이는 묶음"이라는 뜻에 가깝다.

AI 학습 워크로드가 추론 워크로드와 다른 점은 크게 네 가지다.

  • 학습은 여러 Pod가 동시에 시작되어야 하는 경우가 많다.

  • 한 번 시작하면 수십 분에서 수일 동안 GPU를 오래 점유한다.

  • 중간 실패에 대비해 체크포인트를 저장해야 한다.

  • 여러 팀이 같은 GPU 풀을 공유할 때 공정한 대기열과 쿼터가 필요하다.

이 장의 핵심은 바로 여기에 있다. 쿠버네티스에서 학습 작업을 "그냥 Job으로 실행하는 것"과 "운영 가능한 배치 플랫폼으로 다루는 것"은 완전히 다르다. 우리는 Kueue, JobSet, Kubeflow Trainer, Volcano, KAI Scheduler를 통해 GPU를 공정하게 나누고, 일부 Pod만 떠서 GPU를 낭비하는 상황을 막고, Spot 노드가 사라져도 학습을 이어갈 수 있는 구조를 만들 것이다.

배치 학습 워크로드 운영

기본 Job만으로는 부족한 순간

먼저 가장 단순한 학습 Job부터 보자. 다음 예시는 GPU 1개를 사용하는 단일 PyTorch 학습 작업이다.

apiVersion: batch/v1
kind: Job
metadata:
  name: single-gpu-train
  namespace: team-a
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: trainer
          image: ghcr.io/example/pytorch-trainer:2026.05
          command: ["python", "train.py"]
          env:
            - name: CHECKPOINT_DIR
              value: /checkpoints
          resources:
            requests:
              cpu: "4"
              memory: 32Gi
              nvidia.com/gpu: "1"
            limits:
              cpu: "8"
              memory: 64Gi
              nvidia.com/gpu: "1"
          volumeMounts:
            - name: checkpoint
              mountPath: /checkpoints
      volumes:
        - name: checkpoint
          persistentVolumeClaim:
            claimName: team-a-checkpoints

작은 실험이나 단일 GPU 파인튜닝이라면 이 정도로도 충분하다. backoffLimit은 실패 시 재시도 횟수를 정하고, PVC는 체크포인트 저장 위치를 제공한다. GPU 노드에 taint가 걸려 있다면 tolerations도 필요하다.

문제는 작업이 커지는 순간부터 시작된다. 예를 들어 4개 노드, 노드당 8개 GPU를 사용하는 학습 작업이라면 총 32개의 GPU가 동시에 필요하다. 기본 Job은 parallelism: 4, completions: 4로 여러 Pod를 만들 수 있지만, 기본 스케줄러는 각각의 Pod를 독립적으로 배치한다. 즉, 4개 중 3개만 먼저 뜨고 1개가 Pending이 되는 상황을 막지 못한다.

apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-train-raw-job
  namespace: team-a
spec:
  parallelism: 4
  completions: 4
  completionMode: Indexed
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: ghcr.io/example/pytorch-distributed:2026.05
          command:
            - torchrun
            - --nnodes=4
            - --nproc_per_node=8
            - train.py
          resources:
            limits:
              nvidia.com/gpu: "8"
              cpu: "64"
              memory: 512Gi

이 YAML은 겉으로는 그럴듯하지만 운영 관점에서는 위험하다. 4개 Pod가 모두 실행되어야 의미 있는 작업인데, 쿠버네티스 기본 스케줄러는 "이 Job의 모든 Pod가 동시에 준비될 수 있는가?"를 먼저 확인하지 않는다. 이미 스케줄링된 Pod는 GPU를 잡은 채 대기하고, 남은 Pod는 자리가 날 때까지 Pending으로 남는다.

GPU가 넉넉한 개발 클러스터에서는 이 문제가 잘 드러나지 않는다. 그러나 실제 조직의 공유 클러스터에서는 거의 반드시 터진다. 한 팀의 대형 학습 작업이 GPU 24개를 잡고 멈춰 있는데, 다른 팀의 작은 실험은 GPU 1개가 없어 대기한다. 모두가 화가 나고, 플랫폼 팀은 슬랙에서 "누가 GPU 잡고 있나요?"를 반복하게 된다.

이 문제를 해결하려면 Job을 직접 실행하는 대신, 먼저 대기열에 넣고, 필요한 자원이 모두 준비되었을 때만 시작하게 해야 한다. 여기서 Kueue가 등장한다.

Kueue: 쿠버네티스에 배치 대기열을 붙이다

Kueue는 batch, HPC, AI/ML 워크로드를 위한 쿠버네티스 네이티브 Job 큐잉 시스템이다. 공식 설명에 따르면 Kueue는 여러 팀이 공유하는 클러스터에서 쿼터와 계층 구조를 바탕으로 작업이 언제 기다리고, 언제 실행되어야 하는지를 결정한다1. 중요한 점은 Kueue가 전통적인 의미의 스케줄러가 아니라는 것이다. Kueue는 "지금 이 작업을 시작해도 되는가?"를 결정하고, 실제 Pod 배치는 기본 kube-scheduler가 맡는다.

이 차이를 이해하면 Kueue의 역할이 명확해진다.

일반 Job은 생성되자마자 Pod를 만든다. 반면 Kueue를 사용하는 Job은 보통 suspend: true 상태로 제출된다. Kueue는 Job을 보고 필요한 CPU, 메모리, GPU를 계산한다. 그리고 해당 팀의 쿼터와 클러스터의 가용 자원을 확인한다. 조건이 맞으면 Job을 admission, 즉 승인하고 suspend를 해제한다. 그때 비로소 Pod가 생성되고 기본 스케줄러가 배치한다.

이 구조는 단순하지만 강력하다. 아직 실행할 수 없는 작업은 Pod를 만들지 않으므로 GPU를 잡아먹지 않는다. 필요한 자원이 모두 준비될 때까지 깨끗하게 줄을 서 있다. CoreWeave도 Kueue의 핵심 가치를 "all-or-nothing 시작 조건, 세밀한 리소스 모델링, 멀티테넌트 자원 공유"로 설명한다2.

Kueue를 이해하려면 세 가지 리소스를 먼저 알아야 한다.

  • ResourceFlavor는 어떤 종류의 자원인지를 표현한다. 예를 들어 H100 GPU 노드, A100 GPU 노드, Spot GPU 노드처럼 하드웨어 특성을 나타낸다.

  • ClusterQueue는 클러스터 전체 관점의 쿼터와 공유 정책을 정의한다.

  • LocalQueue는 특정 네임스페이스에서 사용자가 Job을 제출하는 입구다.

이름이 조금 낯설지만, 회사 조직으로 비유하면 쉽다. ResourceFlavor는 "회의실 종류"다. 4인실, 10인실, 대형 강당이 있을 수 있다. ClusterQueue는 "부서별 예약 가능량"이다. 연구팀은 대형 강당을 하루 4시간 보장받고, 서비스팀은 2시간 보장받는다. LocalQueue는 각 부서원이 실제로 예약 요청을 넣는 창구다.

팀별 GPU 쿼터 구성하기

이제 간단한 Kueue 구성을 만들어 보자. 예제의 목표는 team-a 네임스페이스에 H100 GPU 8개를 보장하고, 필요할 때는 같은 cohort 안의 다른 팀이 쓰지 않는 GPU를 빌려 쓸 수 있게 하는 것이다.

먼저 GPU 노드를 나타내는 ResourceFlavor를 정의한다.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: h100
spec:
  nodeLabels:
    accelerator: nvidia-h100

nodeLabels는 이 flavor에 해당하는 노드를 찾는 힌트다. 운영 환경에서는 여기에 GPU 모델, 인터커넥트, 온디맨드/Spot 여부, 리전이나 존 정보를 더 세밀하게 넣을 수 있다.

다음은 ClusterQueue다.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: team-a-gpu
spec:
  cohort: ai-shared
  namespaceSelector:
    matchLabels:
      team: a
  resourceGroups:
    - coveredResources:
        - cpu
        - memory
        - nvidia.com/gpu
      flavors:
        - name: h100
          resources:
            - name: nvidia.com/gpu
              nominalQuota: "8"
              borrowingLimit: "8"
            - name: cpu
              nominalQuota: "512"
            - name: memory
              nominalQuota: 4Ti

nominalQuota는 기본적으로 보장되는 양이다. 여기서는 team-a가 H100 GPU 8개를 기준 쿼터로 가진다. borrowingLimit은 같은 cohort에 속한 다른 큐가 놀고 있을 때 추가로 빌려 쓸 수 있는 한도다. 즉, 연구팀이 밤에 GPU를 쓰지 않으면 플랫폼팀의 배치 작업이 잠시 가져다 쓰고, 연구팀의 높은 우선순위 작업이 들어오면 다시 돌려주는 식의 운영이 가능하다.

마지막으로 네임스페이스 안에 LocalQueue를 만든다.

apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: gpu-training
  namespace: team-a
spec:
  clusterQueue: team-a-gpu

사용자는 이제 team-a 네임스페이스에서 gpu-training이라는 큐 이름만 알고 있으면 된다. 클러스터 전체 쿼터, flavor, borrowing 정책은 플랫폼 팀이 관리한다. 이것이 Kueue를 도입했을 때 얻는 첫 번째 장점이다. 사용자는 복잡한 GPU 공유 정책을 몰라도 되고, 플랫폼 팀은 정책을 중앙에서 바꿀 수 있다.

Kueue로 Job 제출하기

Kueue에 Job을 제출할 때는 Job에 큐 라벨을 붙이고 suspend: true로 시작한다.

apiVersion: batch/v1
kind: Job
metadata:
  name: qwen-finetune
  namespace: team-a
  labels:
    kueue.x-k8s.io/queue-name: gpu-training
spec:
  suspend: true
  parallelism: 4
  completions: 4
  completionMode: Indexed
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: trainer
          image: ghcr.io/example/qwen-finetune:2026.05
          command:
            - bash
            - -lc
            - |
              torchrun \
                --nnodes=4 \
                --nproc_per_node=8 \
                --rdzv_backend=c10d \
                --rdzv_endpoint=qwen-finetune-master:29500 \
                train.py \
                --checkpoint-dir=/checkpoints
          resources:
            requests:
              cpu: "64"
              memory: 512Gi
              nvidia.com/gpu: "8"
            limits:
              cpu: "64"
              memory: 512Gi
              nvidia.com/gpu: "8"
          volumeMounts:
            - name: checkpoint
              mountPath: /checkpoints
      volumes:
        - name: checkpoint
          persistentVolumeClaim:
            claimName: team-a-checkpoints

이 Job은 총 32개의 GPU를 요구한다. team-a의 기본 쿼터가 8개뿐이라면 즉시 실행되지 않을 수 있다. 하지만 같은 cohort에서 GPU를 빌릴 수 있고 현재 여유가 있다면 Kueue가 admission을 승인한다. 여유가 없다면 Job은 Pending Pod를 만들지 않고 큐에서 기다린다.

상태 확인은 다음처럼 한다.

kubectl get localqueue -n team-a
kubectl get clusterqueue
kubectl get workloads -n team-a
kubectl describe workload -n team-a qwen-finetune-xxxxx

여기서 Workload는 Kueue가 내부적으로 만드는 리소스다. "이 Job이 얼마만큼의 자원을 요청했고, 어느 큐에 들어갔고, admission 되었는가"를 보여준다. 운영 중에는 사용자의 Job만 보지 말고 Kueue의 Workload 상태를 함께 봐야 한다.

Kueue에서 자주 보게 되는 상태는 대략 다음과 같다.

  • Admitted는 큐에서 승인되어 실행이 시작되었다는 뜻이다.

  • Pending 또는 admission 대기 상태는 쿼터나 실제 자원이 부족하다는 뜻이다.

  • Evicted는 preemption이나 정책 변경으로 실행 중인 작업이 회수되었다는 뜻이다.

학습 플랫폼을 운영할 때 가장 좋은 대시보드는 "GPU 사용률" 하나만 보여주지 않는다. 큐별 대기 작업 수, 큐별 nominal quota 사용량, borrowing 중인 GPU 수, preemption 발생 횟수, 평균 대기 시간을 함께 보여줘야 한다. GPU가 바쁘게 돌아가는 것만으로는 충분하지 않다. 중요한 작업이 공정하게 시작되고 있는지가 더 중요하다.

Fair sharing: 공정함은 평균이 아니라 정책이다

공유 GPU 클러스터에서 가장 어려운 질문은 "누가 먼저 써야 하는가?"다. 먼저 제출한 사람이 먼저 쓰는 FIFO 방식은 단순하지만 공정하지 않을 수 있다. 대형 학습 Job 하나가 줄 앞에 있으면 뒤의 작은 실험 20개가 모두 막힌다. 반대로 작은 작업을 계속 먼저 처리하면 대형 학습은 영원히 시작하지 못한다.

Kueue의 장점은 이런 문제를 쿼터와 cohort, borrowing, preemption으로 표현할 수 있다는 점이다. 예를 들어 다음과 같은 조직을 생각해 보자.

  • team-research는 장기 학습을 많이 돌리며 GPU 32개를 보장받는다.

  • team-serving은 프로덕션 모델 검증 작업을 돌리며 GPU 16개를 보장받는다.

  • team-platform은 임베딩 재생성, 평가 배치, 데이터 정제 작업을 돌리며 GPU 8개를 보장받는다.

낮에는 team-serving의 검증 작업이 중요하고, 밤에는 team-research의 대형 학습이 중요할 수 있다. 주말에는 team-platform의 대량 배치가 빈 GPU를 활용해도 된다. 이때 모든 팀을 하나의 cohort로 묶고, 팀별 nominal quota와 borrowing limit을 정하면 "보장"과 "활용률"을 동시에 잡을 수 있다.

중요한 운영 원칙은 이것이다. 쿼터는 GPU를 놀리기 위한 벽이 아니라, 필요할 때 돌려받을 수 있는 권리다. 어떤 팀이 지금 GPU를 쓰지 않는다면 다른 팀이 빌려 쓰는 것이 전체 비용 면에서 낫다. 단, 원래 주인이 필요할 때 되찾을 수 있어야 한다. 이때 preemption 정책과 PriorityClass가 함께 필요하다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: train-high
value: 100000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "중요한 학습 또는 릴리스 전 모델 검증 작업"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: train-low
value: 1000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "재시도 가능한 실험, 야간 배치, 낮은 우선순위 학습"

높은 우선순위 작업이 들어왔을 때 낮은 우선순위 작업을 중단할 수 있다. 하지만 학습 작업에서 preemption은 단순히 Pod를 죽이는 일이 아니다. 제대로 된 체크포인트 전략이 없으면 몇 시간의 학습이 날아간다. 그래서 큐 설계와 체크포인트 설계는 항상 같이 가야 한다.

JobSet: 여러 Job을 하나의 학습 단위로 묶기

기본 Job과 Kueue만으로도 많은 배치 작업을 운영할 수 있다. 하지만 분산 학습이 커지면 "하나의 Job 안에 같은 Pod 여러 개"만으로는 부족해진다. 어떤 작업은 leader와 worker가 다르다. 어떤 작업은 parameter server, evaluator, data loader가 따로 있다. 어떤 작업은 rack이나 GPU island 단위로 그룹을 나누어야 한다.

JobSet은 이런 분산 ML 학습과 HPC 워크로드를 위해 만들어진 쿠버네티스 네이티브 API다. JobSet은 여러 Kubernetes Job을 하나의 단위로 관리하며, leader와 worker처럼 서로 다른 Pod 템플릿을 가진 작업도 표현할 수 있다3. 또한 headless Service를 자동으로 구성해 Pod 간 안정적인 hostname 통신을 돕고, 실패 정책과 성공 정책, startup sequencing, topology domain별 exclusive placement 같은 기능을 제공한다4.

쉽게 말해 JobSet은 "분산 학습 작업 하나"를 더 정확하게 표현하는 그릇이다. 일반 Job이 한 종류의 반복 작업에 강하다면, JobSet은 역할이 다른 여러 Job의 묶음에 강하다.

다음은 leader 1개와 worker 4개로 구성된 단순한 JobSet 예시다.

apiVersion: jobset.x-k8s.io/v1alpha2
kind: JobSet
metadata:
  name: llama-train
  namespace: team-a
  labels:
    kueue.x-k8s.io/queue-name: gpu-training
spec:
  failurePolicy:
    maxRestarts: 3
  replicatedJobs:
    - name: leader
      replicas: 1
      template:
        spec:
          parallelism: 1
          completions: 1
          backoffLimit: 0
          template:
            spec:
              restartPolicy: Never
              containers:
                - name: leader
                  image: ghcr.io/example/llama-trainer:2026.05
                  command:
                    - bash
                    - -lc
                    - |
                      python launch.py \
                        --role=leader \
                        --checkpoint-dir=/checkpoints
                  resources:
                    limits:
                      cpu: "16"
                      memory: 128Gi
                      nvidia.com/gpu: "1"
                  volumeMounts:
                    - name: checkpoint
                      mountPath: /checkpoints
              volumes:
                - name: checkpoint
                  persistentVolumeClaim:
                    claimName: team-a-checkpoints
    - name: workers
      replicas: 4
      template:
        spec:
          parallelism: 1
          completions: 1
          backoffLimit: 0
          template:
            spec:
              restartPolicy: Never
              containers:
                - name: worker
                  image: ghcr.io/example/llama-trainer:2026.05
                  command:
                    - bash
                    - -lc
                    - |
                      python launch.py \
                        --role=worker \
                        --leader=llama-train-leader-0 \
                        --checkpoint-dir=/checkpoints
                  resources:
                    limits:
                      cpu: "64"
                      memory: 512Gi
                      nvidia.com/gpu: "8"
                  volumeMounts:
                    - name: checkpoint
                      mountPath: /checkpoints
              volumes:
                - name: checkpoint
                  persistentVolumeClaim:
                    claimName: team-a-checkpoints

이 예시는 단순화되어 있지만 중요한 패턴을 보여준다. leader와 worker의 리소스 요구량이 다르고, 실행 명령도 다르다. 일반 Job 하나로는 이런 구조를 깔끔하게 표현하기 어렵다. Helm 템플릿으로 여러 Job을 찍어낼 수도 있지만, 그러면 쿠버네티스 입장에서는 그것들이 하나의 학습 작업이라는 사실을 모른다. JobSet을 쓰면 컨트롤러가 전체를 하나의 단위로 보고 실패와 재시작을 관리할 수 있다.

JobSet의 failurePolicy.maxRestarts는 특히 중요하다. 분산 학습에서는 worker 하나가 죽으면 전체 통신 그룹이 깨지는 경우가 많다. 이때 죽은 Pod 하나만 재시작하는 것으로는 부족할 수 있다. JobSet은 실패 시 전체 child Job을 다시 만들 수 있고, 학습 코드는 마지막 체크포인트에서 재개한다. 이것이 "장애를 없애는 것"이 아니라 "장애를 전제로 진도를 보존하는 것"이다.

Kubeflow Trainer: 학습 프레임워크 친화적인 상위 레이어

Kueue와 JobSet이 인프라 엔지니어에게 친숙한 도구라면, Kubeflow Trainer는 ML 엔지니어에게 더 친숙한 추상화다. Kubeflow Trainer는 PyTorch, Hugging Face, DeepSpeed, JAX, XGBoost 등 여러 프레임워크의 분산 학습과 LLM 파인튜닝을 쿠버네티스 네이티브 방식으로 실행하기 위한 플랫폼이다5.

PyTorch 분산 학습에서는 torchrun, rank, world size, rendezvous endpoint, NCCL 설정이 필요하다. ML 엔지니어가 매번 이 값을 YAML과 쉘 스크립트로 조립하게 하면 실수가 잦다. Kubeflow Trainer는 이런 반복 작업을 줄여준다. 공식 PyTorch 가이드에서도 Kubeflow Trainer가 torchrun을 사용해 각 학습 노드의 분산 환경을 구성하고, WORLD_SIZE, RANK, LOCAL_RANK 같은 값을 자동으로 사용할 수 있게 한다고 설명한다6.

예를 들어 ML 엔지니어는 다음과 같은 Python 코드로 분산 학습 작업을 제출할 수 있다.

from kubeflow.trainer import TrainerClient, CustomTrainer


def fine_tune_llm():
    import os
    import torch
    import torch.distributed as dist

    dist.init_process_group(backend="nccl")

    rank = dist.get_rank()
    local_rank = int(os.environ["LOCAL_RANK"])
    torch.cuda.set_device(local_rank)

    # 실제 학습 코드
    # model = ...
    # dataset = ...
    # train(...)

    if rank == 0:
        # 체크포인트 또는 최종 모델 저장
        pass


job_id = TrainerClient().train(
    runtime="torch-distributed",
    trainer=CustomTrainer(
        func=fine_tune_llm,
        num_nodes=4,
        resources_per_node={
            "gpu": 8,
            "cpu": 64,
            "memory": "512Gi",
        },
        packages_to_install=[
            "transformers>=4.53.0",
            "datasets",
            "boto3",
        ],
    ),
)

TrainerClient().wait_for_job_status(job_id)

플랫폼 팀 입장에서는 Kubeflow Trainer가 Kueue, JobSet, Volcano 같은 스케줄링 생태계와 어떻게 연결되는지가 중요하다. Kubeflow Trainer는 Kueue와 Volcano를 활용한 topology-aware scheduling, JobSet과 LeaderWorkerSet 같은 AI 워크로드 오케스트레이션 도구와의 통합을 제공하는 방향으로 발전하고 있다5. 따라서 조직이 커질수록 "사용자는 Trainer API로 제출하고, 플랫폼은 Kueue와 JobSet으로 큐와 쿼터를 관리한다"는 구조가 자연스럽다.

이 계층을 나누면 책임이 명확해진다. ML 엔지니어는 학습 코드와 하이퍼파라미터에 집중한다. 플랫폼 엔지니어는 큐, 쿼터, GPU flavor, preemption, checkpoint storage, 네트워크 토폴로지를 관리한다. 둘 사이의 계약은 "몇 개 노드, 노드당 몇 개 GPU, 어떤 우선순위, 어떤 체크포인트 위치"가 된다.

Volcano, KAI Scheduler, Kueue는 어떻게 다른가

배치 학습 스케줄링을 찾아보면 Kueue 외에도 Volcano와 KAI Scheduler가 자주 등장한다. 셋은 모두 AI/ML 워크로드 운영에 쓰이지만 역할과 철학이 다르다. 이 차이를 모르고 도입하면 "왜 큐는 있는데 배치가 이상하지?", "왜 기본 스케줄러와 다르게 움직이지?" 같은 혼란을 겪는다.

도구핵심 역할잘 맞는 상황주의할 점
KueueJob admission과 큐·쿼터 관리쿠버네티스 기본 스케줄러와 함께 가고 싶을 때, 멀티테넌트 배치 큐가 필요할 때자체 스케줄러가 아니라서 실제 Pod 배치는 기본 스케줄러 정책의 영향을 받음
Volcano배치/HPC 지향 스케줄러MPI, Spark, Flink, PyTorchJob 등 다양한 배치 워크로드와 gang scheduling이 필요할 때별도 schedulerName을 사용하며 운영자가 스케줄러 동작을 이해해야 함
KAI SchedulerGPU AI 워크로드 특화 스케줄러대규모 NVIDIA GPU 클러스터, GPU 공유, 계층형 큐, topology-aware scheduling이 중요할 때GPU Operator, NVIDIA 생태계와의 통합을 함께 고려해야 함

Kueue는 "언제 시작할지"에 집중한다. Job이 큐에서 기다리다가 쿼터와 자원이 맞으면 시작된다. 실제 Pod 위치는 기본 kube-scheduler가 정한다. 그래서 기존 쿠버네티스 운영 모델을 크게 깨지 않고 배치 큐를 붙이고 싶을 때 좋다.

Volcano는 오래전부터 배치와 HPC 워크로드를 다뤄온 스케줄러다. gang scheduling, queue, fairshare, preemption 같은 기능을 제공하며, 쿠버네티스 기본 스케줄러 대신 Volcano scheduler가 Pod를 배치한다. 연구 클러스터나 HPC 성격의 워크로드가 많고, 다양한 프레임워크를 함께 돌린다면 여전히 강력한 선택지다.

KAI Scheduler는 GPU AI 워크로드에 초점을 맞춘 스케줄러다. 공개 저장소 설명에 따르면 KAI Scheduler는 대규모 GPU 클러스터에서 AI/ML 워크로드의 GPU 자원 할당을 최적화하고, gang scheduling, hierarchical queue, fairshare, Dynamic Resource Allocation, topology-aware scheduling, GPU sharing 등을 지원한다7. 특히 수백에서 수천 노드 규모의 GPU 클러스터, 여러 팀의 GPU 예약과 회수, disaggregated serving이나 복잡한 AI 파이프라인까지 고려한다면 KAI Scheduler가 매력적이다.

실무 선택 기준은 생각보다 단순하다.

작은 팀이 먼저 도입한다면 Kueue부터 시작하는 편이 안전하다. 기본 Job, JobSet, Kubeflow Trainer와 자연스럽게 연결되고, 쿠버네티스 기본 스케줄러를 그대로 활용한다. 이미 HPC 운영 경험이 있고 MPI나 Spark 같은 배치 작업도 함께 다룬다면 Volcano가 편할 수 있다. NVIDIA GPU Operator를 표준으로 쓰고 있고, GPU 클러스터 규모가 크며, topology-aware placement와 GPU 공유 정책이 핵심이라면 KAI Scheduler를 검토할 만하다.

한 가지를 더 기억하자. 이 도구들은 항상 상호 배타적인 선택지는 아니다. 어떤 조직은 Kueue로 admission과 쿼터를 관리하고, 특정 workload class는 Volcano나 KAI Scheduler로 배치한다. 다만 처음부터 모든 것을 섞으면 장애 분석이 어려워진다. 운영 경험이 쌓이기 전까지는 하나의 주 경로를 정하고, 예외를 신중히 늘리는 것이 좋다.

Gang scheduling의 핵심: 일부만 실행되는 일을 막기

Gang scheduling은 말로는 간단하다. "모두 실행할 수 있으면 실행하고, 아니면 아무것도 실행하지 않는다." 그러나 구현 방식은 도구마다 다르다.

Kueue에서는 Job이 admission되기 전까지 suspend 상태로 있기 때문에 Pod가 생성되지 않는다. 필요한 전체 리소스가 승인되었을 때 Job이 unsuspend되고 Pod들이 생성된다. 이 방식은 "부분 Pod가 GPU를 점유하는 문제"를 admission 단계에서 막는다.

Volcano나 KAI Scheduler는 PodGroup 또는 비슷한 개념을 사용해 여러 Pod를 하나의 scheduling unit으로 다룬다. 스케줄러가 "이 그룹의 최소 Pod 수를 동시에 배치할 수 있는가?"를 판단한다. 가능하면 한 번에 배치하고, 불가능하면 대기시킨다.

Kubernetes 자체도 AI/ML 워크로드를 위한 gang scheduling을 점점 더 중요한 주제로 다루고 있다. Kubernetes v1.35에서는 분산 학습과 HPC 워크로드를 염두에 둔 네이티브 Gang Scheduling 기능이 Alpha로 소개되었고, PodGroup을 통해 "이 그룹의 Pod들이 모두 스케줄링 가능할 때만 실행"하는 방향이 제시되었다8. 다만 Alpha 기능은 운영 환경에서 신중해야 한다. 2026년 현재 프로덕션에서는 Kueue, Volcano, KAI Scheduler 같은 검증된 생태계 도구를 사용하는 것이 현실적이다.

Gang scheduling을 도입할 때 가장 흔한 실수는 "모든 작업에 gang을 걸어버리는 것"이다. 모든 배치가 gang을 필요로 하지는 않는다. 독립적인 이미지 처리 작업 1,000개는 일부만 실행되어도 진행된다. 이런 작업은 queue와 quota가 필요할 뿐, all-or-nothing은 오히려 처리량을 떨어뜨릴 수 있다.

반대로 다음 작업은 gang scheduling이 거의 필수다.

  • PyTorch DDP, FSDP, DeepSpeed처럼 여러 rank가 동시에 통신 그룹을 만들어야 하는 학습

  • Megatron-LM처럼 tensor parallel, pipeline parallel 구성이 고정된 학습

  • MPI 기반 HPC 작업

  • leader, worker, parameter server가 함께 떠야 의미 있는 분산 작업

운영자는 Job 제출 템플릿에 workload-type 같은 라벨을 붙이고, 워크로드 유형별로 다른 큐와 정책을 적용하는 것이 좋다.

metadata:
  labels:
    workload.ai/type: distributed-training
    workload.ai/framework: pytorch
    workload.ai/checkpoint: required
    kueue.x-k8s.io/queue-name: gpu-training

라벨은 사소해 보이지만 나중에 비용 분석, 장애 분석, 쿼터 조정의 기준이 된다. "지난달 GPU 비용의 60%가 어느 팀의 어떤 유형 학습에 쓰였는가?"라는 질문에 답하려면 처음부터 라벨을 설계해야 한다.

체크포인트는 선택이 아니라 생명줄이다

학습 작업의 장애 대응은 웹 서버와 다르다. 웹 서버 Pod가 죽으면 새 Pod가 뜨면 된다. 상태는 데이터베이스나 외부 캐시에 있다. 하지만 학습 작업의 상태는 모델 파라미터, optimizer state, scheduler state, dataloader progress, random seed, epoch, step에 걸쳐 있다. 이것을 저장하지 않으면 작업이 죽었을 때 처음부터 다시 시작해야 한다.

체크포인트 전략을 설계할 때는 세 가지 질문을 먼저 해야 한다.

첫째, 어디에 저장할 것인가. 단일 노드 실험은 PVC로 충분할 수 있다. 하지만 여러 노드 학습, Spot 노드, 멀티 클러스터까지 고려하면 S3, GCS, Azure Blob, MinIO 같은 오브젝트 스토리지가 더 안전하다. PVC가 특정 zone이나 노드에 묶이면 재스케줄링이 어려워질 수 있다.

둘째, 얼마나 자주 저장할 것인가. 너무 자주 저장하면 학습 처리량이 떨어진다. 너무 드물게 저장하면 실패 시 손실이 커진다. 실무에서는 "재시작 비용"과 "체크포인트 저장 비용"의 균형을 잡는다. 예를 들어 Spot 노드 중단이 잦은 환경에서는 5~10분 단위 체크포인트가 합리적일 수 있고, 안정적인 예약 GPU에서는 30분~1시간 단위도 가능하다.

셋째, 누가 저장할 것인가. 분산 학습에서는 모든 rank가 동시에 같은 파일을 쓰면 난리가 난다. 보통 rank 0이 메타데이터와 최종 체크포인트를 저장하거나, shard별로 파일을 나누어 저장한다. FSDP나 DeepSpeed ZeRO를 사용하면 optimizer state가 여러 GPU에 나뉘어 있으므로 프레임워크가 제공하는 분산 체크포인트 기능을 이해해야 한다.

학습 코드에는 최소한 다음 흐름이 있어야 한다.

import os
import signal
import time

terminate = False


def handle_sigterm(signum, frame):
    global terminate
    terminate = True


signal.signal(signal.SIGTERM, handle_sigterm)

for step, batch in enumerate(train_loader, start=start_step):
    loss = train_one_step(batch)

    if step % checkpoint_interval == 0:
        save_checkpoint(step)

    if terminate:
        save_checkpoint(step)
        break

쿠버네티스에서 Pod가 종료될 때 컨테이너는 SIGTERM을 받는다. 이 신호를 무시하면 Spot 중단이나 preemption 시 마지막 진행 상태를 저장할 기회를 잃는다. terminationGracePeriodSeconds도 충분히 길게 잡아야 한다.

spec:
  terminationGracePeriodSeconds: 300
  containers:
    - name: trainer
      lifecycle:
        preStop:
          exec:
            command:
              - bash
              - -lc
              - "python /app/save_emergency_checkpoint.py || true"

preStop 훅은 보조 수단이지 완벽한 보장은 아니다. 컨테이너가 이미 불안정하거나 노드가 갑자기 사라지면 실행되지 못할 수 있다. 따라서 학습 루프 자체가 주기적으로 체크포인트를 남기는 것이 기본이고, preStop은 마지막 안전망으로 봐야 한다.

Spot과 Preemptible 노드에서 학습 돌리기

GPU 비용을 줄이려면 Spot 또는 Preemptible 노드를 쓰고 싶어진다. 학습 작업은 추론보다 Spot에 잘 맞는 경우가 많다. 사용자 요청을 실시간으로 처리하지 않고, 중간에 중단되어도 체크포인트에서 재개할 수 있기 때문이다. 하지만 "싸니까 Spot에 올리자"는 접근은 위험하다. 체크포인트, retry, priority가 함께 설계되어야 한다.

먼저 온디맨드와 Spot GPU를 다른 ResourceFlavor로 나누는 것이 좋다.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: h100-spot
spec:
  nodeLabels:
    accelerator: nvidia-h100
    capacity-type: spot
  tolerations:
    - key: spot
      operator: Equal
      value: "true"
      effect: NoSchedule
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: h100-ondemand
spec:
  nodeLabels:
    accelerator: nvidia-h100
    capacity-type: ondemand

그다음 ClusterQueue에서 Spot을 먼저 쓰되, 중요한 작업은 온디맨드 flavor도 사용할 수 있게 한다. 개발 실험과 야간 학습은 Spot 중심으로, 릴리스 전 모델 검증이나 마감이 있는 학습은 온디맨드 우선으로 설계할 수 있다.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: research-gpu
spec:
  cohort: ai-shared
  resourceGroups:
    - coveredResources:
        - cpu
        - memory
        - nvidia.com/gpu
      flavors:
        - name: h100-spot
          resources:
            - name: nvidia.com/gpu
              nominalQuota: "32"
        - name: h100-ondemand
          resources:
            - name: nvidia.com/gpu
              nominalQuota: "8"

Spot 학습 작업의 Job 또는 JobSet에는 실패를 전제로 한 설정이 필요하다.

apiVersion: jobset.x-k8s.io/v1alpha2
kind: JobSet
metadata:
  name: spot-train
  namespace: team-research
  labels:
    kueue.x-k8s.io/queue-name: spot-training
spec:
  failurePolicy:
    maxRestarts: 10
  replicatedJobs:
    - name: workers
      replicas: 4
      template:
        spec:
          parallelism: 1
          completions: 1
          backoffLimit: 0
          template:
            spec:
              priorityClassName: train-low
              terminationGracePeriodSeconds: 300
              restartPolicy: Never
              nodeSelector:
                capacity-type: spot
              tolerations:
                - key: spot
                  operator: Equal
                  value: "true"
                  effect: NoSchedule
              containers:
                - name: trainer
                  image: ghcr.io/example/spot-trainer:2026.05
                  command:
                    - bash
                    - -lc
                    - |
                      python train.py \
                        --resume=auto \
                        --checkpoint-uri=s3://ml-checkpoints/spot-train
                  resources:
                    limits:
                      nvidia.com/gpu: "8"
                      cpu: "64"
                      memory: 512Gi

여기서 중요한 것은 --resume=auto 같은 학습 코드의 재개 기능이다. 쿠버네티스가 아무리 재시작을 잘해도 애플리케이션이 체크포인트를 읽지 못하면 의미가 없다. 플랫폼 팀은 "Job이 재시도된다"까지만 보장할 수 있다. "학습이 이어진다"는 학습 코드와 체크포인트 포맷이 보장해야 한다.

Spot 작업의 우선순위는 보통 낮게 둔다. 낮은 우선순위의 Spot 학습은 남는 GPU를 잘 활용하지만, 프로덕션 검증이나 긴급 파인튜닝이 들어오면 양보해야 한다. 이때도 체크포인트가 있어야 양보가 가능하다. 체크포인트 없는 작업은 preemption할 수 없는 작업이 되고, 결국 클러스터의 유연성을 떨어뜨린다.

네트워크 토폴로지와 학습 성능

GPU 개수만 맞으면 학습이 잘 돌 것 같지만, 대형 분산 학습에서는 네트워크 토폴로지가 성능을 크게 좌우한다. 같은 노드 안의 GPU들은 NVLink나 PCIe를 통해 빠르게 통신할 수 있다. 같은 rack 또는 같은 GPU island 안의 노드들은 고성능 네트워크로 연결되어 있을 수 있다. 반면 다른 rack이나 zone을 넘어가면 지연 시간과 대역폭이 달라진다.

DDP는 주로 gradient all-reduce를 수행한다. FSDP와 tensor parallel은 더 빈번하고 민감한 통신을 요구할 수 있다. 따라서 "아무 H100 32개"보다 "같은 topology domain 안의 H100 32개"가 훨씬 중요할 때가 있다.

JobSet은 topology domain별 exclusive placement를 지원한다. 이는 child Job을 특정 rack, zone, TPU slice, GPU island 같은 도메인에 1:1로 배치하는 데 유용하다4. 예를 들어 4개의 accelerator island에 각각 동일한 worker 그룹을 배치하고, island 내부 통신은 빠른 네트워크를 사용하며, island 간 통신은 최소화하는 식의 전략을 세울 수 있다.

KAI Scheduler 역시 topology-aware scheduling을 중요한 기능으로 제공한다. 특히 최신 AI 클러스터에서는 단순히 GPU 수만 보는 것이 아니라 NVLink, NVSwitch, rack, InfiniBand 연결성을 고려해야 한다. KAI Scheduler의 공개 설명도 대규모 AI 워크로드를 위해 topology-aware scheduling과 hierarchical gang scheduling을 지원한다고 밝힌다7.

운영 초기에 모든 토폴로지를 완벽히 모델링하려고 하면 시작이 늦어진다. 대신 다음 순서로 성숙도를 높이는 것이 현실적이다.

첫 단계에서는 GPU 모델과 노드 풀을 구분한다. A100, H100, L40S를 같은 큐에 마구 섞지 않는다. 두 번째 단계에서는 full-node training과 partial-GPU 작업을 나눈다. 8-GPU 노드 전체를 쓰는 학습과 1-GPU 실험을 같은 정책으로 다루면 fragmentation이 심해진다. 세 번째 단계에서 rack, zone, NVLink island 같은 topology-aware placement를 도입한다.

이 순서가 중요한 이유는 운영 복잡도 때문이다. 토폴로지 최적화는 성능을 올리지만, 잘못 설정하면 Pending이 늘어난다. "가장 빠른 배치"와 "가장 잘 시작되는 배치" 사이에는 균형이 필요하다.

작은 배치, 큰 학습, 평가 작업을 분리하라

AI 클러스터에는 여러 종류의 배치 작업이 섞인다. 모든 작업을 하나의 gpu-training 큐에 넣으면 언젠가 문제가 생긴다. 작은 실험이 대형 학습에 막히거나, 대형 학습이 끝없이 작은 작업에 밀리거나, 평가 배치가 학습과 경쟁하다가 릴리스 일정에 영향을 준다.

실무에서는 최소한 다음 정도로 큐를 나누는 것이 좋다.

  • interactive-dev: 노트북, 짧은 실험, 1~2 GPU 작업

  • training-large: 장시간 분산 학습, gang scheduling 필수 작업

  • eval-batch: 모델 평가, 벤치마크, 임베딩 재생성 같은 재시도 가능한 작업

  • urgent-release: 릴리스 전 검증, 장애 대응용 긴급 파인튜닝

이 큐들은 같은 GPU pool을 공유할 수 있지만 quota와 priority는 달라야 한다. interactive-dev는 대기 시간이 짧아야 하지만 장시간 GPU를 붙잡으면 안 된다. training-large는 시작까지 오래 기다릴 수 있지만 시작하면 안정적으로 달려야 한다. eval-batch는 Spot과 낮은 우선순위가 잘 맞는다. urgent-release는 사용 빈도는 낮지만 들어오면 빠르게 실행되어야 한다.

Kueue의 ClusterQueue를 큐별로 나누고 cohort로 묶으면 이런 정책을 표현할 수 있다. Volcano나 KAI Scheduler를 사용한다면 queue hierarchy와 fairshare 정책으로 같은 구조를 만들 수 있다.

중요한 것은 기술보다 운영 합의다. 팀마다 "내 작업이 제일 중요하다"고 말한다. 플랫폼 팀은 YAML을 쓰기 전에 정책 문서를 먼저 써야 한다. 어떤 작업이 긴급인지, 누가 승인하는지, preemption되면 어떤 책임을 지는지, 체크포인트가 없는 작업은 어느 큐에 들어갈 수 없는지 명확히 해야 한다.

운영자가 보는 배치 학습 대시보드

배치 학습 플랫폼을 만들면 사용자는 보통 "내 Job이 왜 안 떠요?"라고 묻는다. 이 질문에 답하려면 GPU 사용률만 봐서는 부족하다. 다음 정보를 한 화면에서 볼 수 있어야 한다.

  • 큐별 대기 중인 Workload 수와 총 요청 GPU 수

  • ClusterQueue별 nominal quota, borrowing, 실제 사용량

  • Job별 대기 시간, 실행 시간, 재시작 횟수

  • Pending 이유: 쿼터 부족인지, 실제 노드 부족인지, affinity 문제인지, taint/toleration 문제인지

  • GPU 사용률, GPU 메모리 사용률, 네트워크 처리량, 체크포인트 저장 시간

Kueue를 쓴다면 Workload, LocalQueue, ClusterQueue 상태를 수집해야 한다. JobSet을 쓴다면 JobSet의 restart 횟수와 child Job 상태를 함께 봐야 한다. Kubeflow Trainer를 쓴다면 TrainJob status와 프레임워크 로그를 연결해야 한다.

장애 분석 명령은 다음처럼 시작할 수 있다.

# 큐 상태 확인
kubectl get clusterqueue
kubectl describe clusterqueue team-a-gpu

# 네임스페이스 큐 확인
kubectl get localqueue -n team-a
kubectl describe localqueue -n team-a gpu-training

# Kueue Workload 확인
kubectl get workloads -n team-a
kubectl describe workload -n team-a <workload-name>

# Job과 Pod 상태 확인
kubectl get job -n team-a
kubectl get pod -n team-a -o wide
kubectl describe pod -n team-a <pod-name>

# JobSet 상태 확인
kubectl get jobset -n team-a
kubectl describe jobset -n team-a llama-train

Pending 원인은 크게 두 계층으로 나뉜다. 첫 번째는 admission 전 문제다. Kueue가 아직 Job을 승인하지 않았다면 Pod가 없거나 Job이 suspend 상태일 수 있다. 이때는 ClusterQueue 쿼터, cohort borrowing, flavor 매칭을 봐야 한다. 두 번째는 admission 후 문제다. Job은 unsuspend되었는데 Pod가 Pending이라면 기본 스케줄러 단계의 문제다. 이때는 nodeSelector, affinity, taint/toleration, 실제 GPU allocatable, PVC 바인딩, RuntimeClass를 확인해야 한다.

이 둘을 구분하지 못하면 엉뚱한 곳을 보게 된다. admission 전 문제인데 Pod describe만 계속 보거나, Pod affinity 문제인데 Kueue 쿼터만 조정하는 식이다.

실전 설계 예: 64개 H100을 세 팀이 공유한다면

가상의 회사에서 64개 H100 GPU를 가진 클러스터를 운영한다고 해보자. 세 팀이 이 클러스터를 공유한다.

연구팀은 대형 모델 파인튜닝을 수행한다. 한 번에 32개 GPU를 쓰는 Job이 많고, 수 시간에서 수일 동안 실행된다. 서비스팀은 릴리스 전 모델 평가와 소규모 재학습을 수행한다. 지연이 길면 제품 출시가 밀린다. 플랫폼팀은 임베딩 재생성, 데이터 정제, 벤치마크를 돌린다. 대부분 재시도 가능하고 Spot에 잘 맞다.

이 경우 기본 정책은 다음처럼 잡을 수 있다.

팀보장 GPU빌릴 수 있는 GPU기본 우선순위특징
연구팀3216중간대형 gang scheduling 작업
서비스팀1616높음릴리스와 SLA 관련 작업
플랫폼팀824낮음Spot 친화적 배치 작업
공용 긴급 큐8필요 시 preemption매우 높음장애 대응, 긴급 검증

전체 합은 64개보다 커 보일 수 있다. borrowing이 있기 때문이다. nominal quota는 권리이고, borrowing은 남는 자원의 임시 사용이다. 플랫폼팀은 밤에 연구팀 GPU를 빌려 임베딩을 재생성할 수 있다. 다음 날 아침 연구팀의 중요한 학습이 들어오면 플랫폼팀 작업은 체크포인트를 남기고 중단되거나 낮은 우선순위로 밀린다.

이 설계에서 가장 중요한 규칙은 "낮은 우선순위 큐에 들어가는 작업은 반드시 재시작 가능해야 한다"는 것이다. 체크포인트가 없거나 중단되면 안 되는 작업은 낮은 우선순위 Spot 큐에 넣으면 안 된다. 반대로 체크포인트가 잘 되어 있는 작업은 낮은 우선순위로 양보할 수 있으므로 클러스터 전체 효율을 높인다.

학습 워크로드 제출 템플릿 만들기

조직에서 학습 작업이 반복되기 시작하면 사용자가 매번 YAML을 직접 쓰게 해서는 안 된다. 플랫폼 팀은 표준 템플릿을 제공해야 한다. Helm chart, Kustomize, 내부 CLI, Kubeflow SDK 중 무엇이든 좋다. 핵심은 사용자가 반드시 입력해야 하는 값을 줄이고, 운영 정책은 자동으로 붙이는 것이다.

사용자가 입력할 값은 대략 다음 정도면 충분하다.

  • 모델 또는 학습 이미지

  • 노드 수와 노드당 GPU 수

  • 큐 이름과 우선순위

  • 체크포인트 저장 위치

  • 최대 실행 시간과 재시도 정책

  • 학습 스크립트 인자

반대로 다음 값은 플랫폼 템플릿이 기본으로 넣어주는 편이 좋다.

  • kueue.x-k8s.io/queue-name 라벨

  • restartPolicy, backoffLimit, failurePolicy

  • GPU 노드 toleration과 nodeSelector

  • terminationGracePeriodSeconds

  • 표준 로그 경로와 메트릭 sidecar

  • 보안 컨텍스트, ServiceAccount, 네트워크 정책

예를 들어 내부 CLI는 다음처럼 보일 수 있다.

ai-train submit \
  --name qwen-sft-202605 \
  --image ghcr.io/company/qwen-trainer:2026.05 \
  --queue research-large \
  --nodes 4 \
  --gpus-per-node 8 \
  --priority normal \
  --checkpoint s3://company-ml-checkpoints/qwen-sft-202605 \
  -- python train.py --config configs/qwen-sft.yaml

이 명령은 내부적으로 Kueue 라벨이 붙은 JobSet 또는 Kubeflow TrainJob을 생성한다. 사용자는 쿠버네티스 리소스의 모든 세부사항을 몰라도 된다. 그러나 플랫폼 팀은 생성된 YAML을 항상 추적 가능하게 남겨야 한다. 문제가 생겼을 때 실제로 어떤 리소스가 만들어졌는지 확인할 수 있어야 하기 때문이다.

언제 쿠버네티스가 아니라 다른 배치 시스템을 쓸까

이 책은 쿠버네티스 책이지만, 모든 학습 작업을 반드시 쿠버네티스에서 돌려야 한다고 말하지는 않는다. 이미 Slurm 기반 HPC 클러스터가 안정적으로 운영되고 있고 연구자들이 그 환경에 익숙하다면, 굳이 모든 것을 한 번에 쿠버네티스로 옮길 필요는 없다. 클라우드 사업자의 managed batch 서비스가 팀의 요구사항을 더 단순하게 만족할 수도 있다.

쿠버네티스가 특히 잘 맞는 경우는 다음과 같다. 이미 서비스, 데이터 파이프라인, 모델 서빙이 쿠버네티스 위에 있고, 같은 인증·모니터링·CI/CD·보안 정책을 학습에도 적용하고 싶을 때다. 여러 팀이 하나의 GPU 풀을 공유하고, 네임스페이스와 RBAC로 권한을 나누며, 모델 학습부터 배포까지 같은 플랫폼에서 이어가고 싶다면 쿠버네티스의 장점이 크다.

반대로 단일 연구팀이 전용 GPU 클러스터에서 대형 학습만 수행하고, 쿠버네티스 운영 경험이 거의 없다면 Slurm이나 provider batch API가 더 단순할 수 있다. 중요한 것은 유행이 아니라 운영 모델이다. 플랫폼 팀이 쿠버네티스를 이미 잘 운영하고 있다면 AI 학습 워크로드를 쿠버네티스로 가져오는 것은 자연스럽다. 그렇지 않다면 Kueue와 JobSet을 설치하기 전에 클러스터 운영 역량부터 점검해야 한다.

이 장의 결론

배치 학습 워크로드 운영의 핵심은 GPU를 "많이 확보하는 것"이 아니라 "낭비 없이, 공정하게, 재시작 가능하게 쓰는 것"이다. 분산 학습은 웹 서버처럼 일부 Pod만 떠도 되는 세계가 아니다. 필요한 Pod가 모두 준비되어야 시작할 수 있고, 중간에 실패하면 체크포인트에서 이어가야 한다.

Kueue는 Job을 큐에 넣고 쿼터와 admission control을 적용해 일부 Pod만 실행되는 상황을 줄인다. JobSet은 leader, worker, parameter server처럼 여러 역할을 가진 분산 작업을 하나의 단위로 관리한다. Kubeflow Trainer는 ML 엔지니어가 PyTorch, DeepSpeed, Hugging Face 기반 학습을 더 쉽게 제출할 수 있는 상위 추상화를 제공한다. Volcano와 KAI Scheduler는 더 강한 배치 스케줄링, GPU 특화 스케줄링, topology-aware placement가 필요할 때 선택지가 된다.

Spot과 Preemptible 노드는 비용을 줄여주지만, 체크포인트 없는 Spot 학습은 도박이다. 낮은 우선순위 작업은 언제든 중단될 수 있어야 하고, 중요한 작업은 필요한 경우 preemption을 통해 자원을 회수할 수 있어야 한다. 큐, 쿼터, priority, checkpoint는 따로 설계하는 기능이 아니라 하나의 운영 정책이다.

다음 장에서는 학습이 아니라 추론으로 넘어간다. 학습 작업이 GPU를 오래 붙잡고 하나의 결과물을 만드는 일이라면, LLM 추론 서빙은 사용자 요청을 낮은 지연 시간으로 계속 처리하는 일이다. 같은 GPU를 쓰지만 병목도, 메트릭도, 스케일링 방식도 다르다. 이제 vLLM, SGLang, KServe가 등장할 차례다.

참고

1Kueue 공식 문서

2Kueue: Kubernetes-Native AI Workload Scheduling with CoreWeave

3JobSet GitHub 저장소

4JobSet 개요 문서

5Kubeflow Trainer GitHub 저장소

6Kubeflow PyTorch Guide

7KAI Scheduler GitHub 저장소

8kt cloud Kubernetes v1.35 Timbernetes 기술 블로그

책 목록

AI 시대의 쿠버네티스: LLM 추론과 에이전트를 운영하는 인프라 전략

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기