Skip to content

Split View: Kubernetes v1.36의 Workload/PodGroup API — 갱 스케줄링이 kube-scheduler 안으로 들어오는 중

|

Kubernetes v1.36의 Workload/PodGroup API — 갱 스케줄링이 kube-scheduler 안으로 들어오는 중

들어가며 — 파드를 하나씩 스케줄링하면 생기는 일

분산 학습 잡을 Kubernetes에 올려 본 사람이면 아는 실패 모드가 있습니다. 워커 4개가 다 떠야 학습이 시작되는데, 기본 스케줄러는 파드를 하나씩 독립적으로 배치합니다. GPU가 3개분만 남아 있으면 파드 3개가 노드를 잡고 앉아서, 나머지 1개를 영원히 기다립니다. GPU 3개는 점유된 채 아무 일도 하지 않습니다. 잡 두 개가 동시에 이러면 서로가 서로의 자원을 물고 늘어지는 교착까지 갑니다.

KEP-4671의 동기 문단은 이 상황을 담백하게 씁니다 — 병렬 애플리케이션은 실행을 시작하려면 모든 파드 간 통신이 필요하고, 진행하려면 배리어나 all-reduce 같은 지속적 통신이 필요하다. 그래서 모든 파드를 최대한 같은 시점에 시작해야 하고, 아니면 비싼 컴퓨트가 놀거나 애플리케이션이 통신 타임아웃으로 죽는다.

해법은 오래전부터 있었습니다. 갱 스케줄링(gang scheduling), 즉 "전부 아니면 전무" 배치입니다. 문제는 그 해법이 항상 클러스터 바깥에 있었다는 점입니다. Volcano를 깔거나, Kueue를 붙이거나, coscheduling 플러그인을 얹거나. 이 생태계 쪽 이야기는 Kubernetes AI 학습 파이프라인 — Volcano, Training Operator, Kueue 편에서 이미 다뤘습니다.

이 글은 다른 이야기입니다. 2026년 4월 22일 나온 Kubernetes v1.36(코드명 ハル / Haru, 총 70개 개선 — Stable 18, Beta 25, Alpha 25)에서, 갱 스케줄링이 kube-scheduler 자체로 들어오는 작업이 두 번째 단계에 접어들었습니다. 그리고 그 과정에서 여섯 달 전에 나온 API가 통째로 갈렸습니다.

Kubernetes가 직접 나선 이유

바깥에 잘 돌아가는 구현이 이미 있는데 왜 코어가 나서느냐. KEP-4671이 직접 답합니다 — 갱 스케줄링은 kube-scheduler 바깥에서 최소 네 번 구현됐고, 일부 컨트롤러는 클러스터마다 다른 스케줄러에 이식되려고 여러 갱 스케줄러를 동시에 지원하기 시작했다는 것입니다.

여기서 문제가 드러납니다. 갱 스케줄링을 원하는 쪽은 워크로드 컨트롤러(Job, JobSet, LeaderWorkerSet, MPIJob, TrainJob)인데, 이들이 각자 Volcano용 코드와 Kueue용 코드를 따로 들고 다녀야 합니다. 표준 인터페이스가 없으니까요. KEP는 세 가지를 노립니다 — 모든 Kubernetes 배포판에서 갱 스케줄링을 쓸 수 있게 하고, 컨트롤러가 표준·커스텀 스케줄러 양쪽에 같은 방식으로 요청할 수 있게 하고, 클러스터 오토스케일러 같은 다른 컴포넌트도 워크로드의 요구사항을 이해할 수 있게 한다.

마지막 항목이 은근히 중요합니다. 지금 오토스케일러는 "파드 3개가 Pending"이라는 것만 보지, "이 3개는 4개짜리 갱의 일부라서 1개를 더 못 띄우면 3개도 의미가 없다"는 걸 모릅니다. 워크로드가 API로 표현되면 그걸 읽을 수 있습니다.

그리고 토폴로지 이야기가 붙습니다. 갱이 필요한 워크로드는 대개 갱 멤버들이 토폴로지상 가까이 붙어 있어야 성능이 나옵니다. 기존 파드 어피니티는 배치에 영향을 주긴 하지만, KEP의 지적대로 갱을 스케줄링 단위로 보지 않고, 파드 집합에 대해 상호 배타적인 여러 배치 후보를 효율적으로 시도하지도 않습니다. 어피니티는 파드 한 개 관점의 도구입니다.

v1.35 — 첫 조각: Workload API와 Permit 게이트

v1.35(2025년 12월 17일)가 첫 묶음을 냈습니다. scheduling.k8s.io/v1alpha1Workload 리소스가 생겼습니다.

apiVersion: scheduling.k8s.io/v1alpha1
kind: Workload
metadata:
  name: training-job-workload
  namespace: some-ns
spec:
  podGroups:
  - name: workers
    policy:
      gang:
        # The gang is schedulable only if 4 pods can run at once
        minCount: 4

파드는 workloadRef로 여기에 연결됐습니다. 동작은 이랬습니다.

  1. 파드가 생기면 스케줄러가 PreEnqueue에서 붙잡아 둔다 — Workload 객체가 있고, 그 안에 해당 파드 그룹이 있고, 대기 중인 파드 수가 minCount에 도달할 때까지.
  2. 충분히 모이면 배치를 시도하되, 바로 바인딩하지 않고 Permit 게이트에서 대기시킨다.
  3. 그룹 전체(최소 minCount)에 유효한 배치를 찾았으면 게이트를 열어 한꺼번에 바인딩하고, 타임아웃(5분) 안에 일부만 스케줄됐으면 그룹의 파드를 전부 거절해 큐로 돌려보내고 잡아 뒀던 자원을 푼다.

같이 들어온 게 opportunistic batching입니다. 이건 Workload API도, 사용자의 명시적 opt-in도 필요 없고, v1.35에서 베타로 기본 켜져 있습니다. 스케줄링 요구사항이 동일한 파드들(컨테이너 이미지, 리소스 요청, 어피니티 등)을 알아보고, 한 파드에 대한 실현 가능성 계산을 뒤따르는 동일 파드들에 재사용해 처리 속도를 올립니다. 갱을 이루는 파드는 보통 서로 똑같으니 궁합이 맞습니다. 다만 스케줄러가 배치를 찾는 데 쓰는 모든 필드가 파드 간 동일해야 하고, 일부 기능을 쓰면 정확성을 위해 배칭이 꺼집니다. 여러분의 kube-scheduler 설정이 암묵적으로 배칭을 끄고 있지 않은지 확인하라고 문서가 굳이 당부합니다.

v1.35 블로그는 이게 첫 구현이라고 못박으면서, 갱 전체를 한 사이클에 처리하는 스케줄링 단계와 워크로드 수준 선점을 다음으로 예고했습니다. 정확히 그게 v1.36에 왔습니다.

v1.36 — Workload는 템플릿, PodGroup은 런타임

v1.36은 scheduling.k8s.io/v1alpha2를 내놓으면서 이전 v1alpha1을 완전히 대체했습니다. 여섯 달 만에 API가 갈린 겁니다. 이유는 KEP에 자세히 적혀 있고, 납득이 갑니다.

원래 설계는 PodGroup을 Workload 스펙 안에 박아 넣었습니다. 그런데 Workload는 오래 사는 설정-의도(configuration-intent)인 반면 PodGroup은 일시적인 스케줄링 단위입니다. 런타임 실행 단위를 영속적 정의 객체에 묶으면 관심사 분리가 깨집니다. 게다가 확장성 문제가 정면으로 걸립니다 — PodGroup이 많은 큰 Workload는 etcd의 1.5MB 객체 크기 제한에 쉽게 닿고, PodGroup 하나의 상태를 갱신하려고 거대한 중앙 Workload 객체를 read-modify-write 해야 하니 경합이 생깁니다.

그래서 갈랐습니다. 이제 Workload는 정적 템플릿입니다.

apiVersion: scheduling.k8s.io/v1alpha2
kind: Workload
metadata:
  name: training-job-workload
  namespace: some-ns
spec:
  # v1alpha1의 podGroups -> podGroupTemplates 로 이름이 바뀜
  podGroupTemplates:
  - name: workers
    # v1alpha1의 policy -> schedulingPolicy 로 이름이 바뀜
    schedulingPolicy:
      gang:
        minCount: 4

그리고 컨트롤러가 이 템플릿에서 런타임 PodGroup을 찍어 냅니다.

apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: training-job-workers-pg
  namespace: some-ns
spec:
  podGroupTemplateRef:
    workload:
      workloadName: training-job-workload
      podGroupTemplateName: workers
  schedulingPolicy:
    gang:
      minCount: 4
status:
  conditions:
  - type: PodGroupScheduled
    status: "True"
    lastTransitionTime: 2026-04-03T00:00:00Z

파드 쪽 연결 필드도 바뀌었습니다. workloadRef가 사라지고 schedulingGroup이 들어왔습니다.

apiVersion: v1
kind: Pod
metadata:
  name: worker-0
  namespace: some-ns
spec:
  schedulingGroup:
    podGroupName: training-job-workers-pg

주목할 점은 .metadata.ownerReferences는 여전히 "진짜" 워크로드 객체(예: Job)를 가리킨다는 것입니다. podGroupTemplateRef는 이게 어느 템플릿에서 나왔는지를 말할 뿐입니다. 소유권과 출처가 분리돼 있습니다.

이렇게 하면 스케줄러 로직도 단순해집니다. PodGroup 하나에 스케줄러가 필요한 정보가 다 들어 있으니, Workload 객체를 watch하거나 파싱할 이유가 없습니다.

PodGroup 스케줄링 사이클 — 그룹을 원자적으로 평가한다

v1.36의 진짜 알맹이는 여기입니다. 파드별로 순차적으로 자원을 평가·예약하면 교착 위험이 있으니, 스케줄러가 그룹을 하나의 단위로 평가하는 전용 사이클을 갖게 됐습니다.

스케줄러가 큐에서 PodGroup 멤버를 하나 꺼내면, 정책과 무관하게 그 그룹의 나머지 대기 파드들을 모아 결정론적으로 정렬한 뒤 원자적 사이클을 돕니다.

  1. 클러스터 상태의 스냅샷을 한 번 뜬다 — 그룹 전체를 평가하는 동안 경쟁 상태를 막고 일관성을 확보하기 위해.
  2. PodGroup 스케줄링 알고리즘으로 그룹의 모든 파드에 대한 유효한 노드 배치를 찾는다. 이때 필터링·스코어링 단계는 기존 파드 기반 로직을 그대로 쓴다.
  3. 결과를 그룹 전체에 원자적으로 적용한다.
    • 성공하면 스케줄 가능한 멤버 파드들이 함께 바인딩 단계로 넘어가고, 남은 파드들은 큐로 돌아가 자원이 나기를 기다린다.
    • 실패하면 그룹 전체가 스케줄 불가로 처리되고, 아무 파드도 바인딩되지 않은 채 백오프 후 재시도한다.

여기 조용하지만 운영상 중요한 단서가 하나 붙습니다. 이미 스케줄된 파드가 있는 PodGroup에 파드가 추가되면, 사이클은 기존 파드를 감안해 새 파드를 평가합니다. 그리고 이미 노드에 배정된 파드는 계속 돌아갑니다 — 이후 사이클에서 그룹이 요구사항을 못 채우더라도 스케줄러가 그 파드들을 배정 해제하거나 축출하지 않습니다. 즉 "전부 아니면 전무"는 바인딩 시점의 원자성이지, 그룹 수명 내내 유지되는 불변식이 아닙니다.

minCount 게이팅 자체는 남아 있습니다. 스케줄러는 여전히 PreEnqueue에서 파드를 붙잡아 두고, 실제 스케줄링 단계만 새 PodGroup 사이클에 전적으로 의존합니다. 알고리즘 실행 중에 스케줄 가능한 파드 수가 minCount를 만족하는지 확인하고, 클러스터가 최소치를 못 받쳐 주면 아무것도 바인딩하지 않습니다.

알고리즘의 정직한 한계

이 부분이 이 글에서 제일 중요합니다. 그리고 칭찬할 만하게도, 이건 제 관찰이 아니라 Kubernetes 프로젝트가 자기 블로그에 "Limitations"라는 제목으로 직접 써 놓은 내용입니다.

첫 버전 PodGroup 스케줄링 사이클의 한계는 이렇습니다.

  • 동종(homogeneous) 파드 그룹 — 모든 파드의 스케줄링 요구사항이 동일하고 어피니티·안티어피니티·토폴로지 분산 제약 같은 파드 간 의존성이 없는 경우 — 에 대해서는, 배치가 존재한다면 알고리즘이 찾아낼 것으로 기대된다.
  • 이종(heterogeneous) 파드 그룹에 대해서는, 배치가 존재해도 찾는다는 보장이 없다. 해답이 자명해 보이는 경우조차도.
  • 파드 간 의존성이 있는 그룹에 대해서도 보장이 없다.

거기에 더해, 그룹 내부 의존성(예: 인터-파드 어피니티로 한 파드의 스케줄 가능성이 다른 멤버에 달린 경우)에서는 결정론적 처리 순서 때문에 클러스터 상태와 무관하게 배치를 못 찾을 수 있습니다. KEP는 같은 얘기를 이렇게 표현합니다 — 기본 알고리즘은 그룹의 파드를 다른 순서로 처리했다면 발견할 수 있었을 유효한 배치를 못 찾을 수 있다.

읽고 나면 적용 범위가 선명해집니다. 지금 이 기능이 제대로 겨냥하는 건 워커가 전부 똑같은 PyTorch DDP 스타일 학습 잡입니다. 리더와 워커 스펙이 다른 LeaderWorkerSet, 헤테로 GPU 풀, 토폴로지 분산 제약이 얽힌 워크로드는 아직 보장 밖입니다. 이건 버그가 아니라 알파의 스코프입니다.

토폴로지 인지 스케줄링 — 첫 이터레이션

v1.36은 PodGroup에 토폴로지 제약을 직접 걸 수 있게 했습니다.

apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: topology-aware-workers-pg
spec:
  schedulingPolicy:
    gang:
      minCount: 4
  schedulingConstraints:
    topology:
      - key: topology.kubernetes.io/rack

스케줄러는 rack 제약에 맞는 노드 조합들을 시도하고, PodGroup이 자원을 얼마나 효율적으로 쓰는지와 해당 도메인 안에 몇 개의 파드를 넣을 수 있는지를 기준으로 최적 배치를 고릅니다. 이를 위해 PodGroup 사이클에 배치(placement) 기반 알고리즘 3단계가 붙었습니다 — 후보 배치 생성(PlacementGenerate 확장점), 각 후보에 그룹 전체가 실제로 들어가는지 검증, 실현 가능한 배치들을 스코어링해 최적 선택(PlacementScore 확장점).

한계도 명시돼 있습니다. 현재 토폴로지 인지 스케줄링은 제약을 만족시키려고 파드 선점을 유발하지 않습니다. 다음 릴리스에서 워크로드 인지 선점과 통합할 계획이라고 합니다. 그리고 다중 토폴로지 레벨, 소프트 제약(선호), DRA와의 깊은 통합, basic 정책과 함께 쓸 때의 견고함은 앞으로의 과제로 남아 있습니다. 즉 지금 쓸 수 있는 건 단일 레벨 하드 제약 하나입니다.

워크로드 인지 선점

PodGroup이 스케줄될 수 없을 때 쓰는 새 선점 메커니즘도 들어왔습니다. 기존 파드별 선점과 다른 점은 PodGroup 전체를 하나의 선점자 단위로 본다는 것입니다. 노드마다 따로 희생자를 찾는 대신 클러스터 전역을 검색해서, 여러 노드에서 동시에 파드를 밀어내 그룹 전체가 들어갈 공간을 만듭니다. 이게 없으면 갱 선점은 이상해집니다 — 노드별로 조금씩 밀어내 봐야 갱 전체가 들어갈 만한 공간이 안 생기니까요.

PodGroup API에 두 개념이 추가됐습니다.

  • priority — PodGroup을 이루는 개별 파드의 우선순위를 덮어쓴다.
  • disruptionMode — 그룹 안의 파드가 개별적으로 선점될 수 있는지, 아니면 전부 아니면 전무로 함께 선점돼야 하는지를 지정한다.
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: victim-pg
spec:
  priorityClassName: high-priority
  priority: 1000
  disruptionMode: PodGroup

여기 함정이 있습니다. v1.36에서 이 두 필드는 워크로드 인지 선점 메커니즘에서만 존중됩니다. 기존 파드별 사이클의 기본 선점을 포함한 다른 교란(disruption) 경로는 이 필드를 무시합니다. 다른 교란 소스로 확장하는 건 앞으로의 희망사항입니다. 그러니 disruptionMode: PodGroup을 걸어 놨다고 해서 여러분의 갱이 모든 축출로부터 통째로 보호된다고 읽으면 안 됩니다.

DRA와의 결합 — 256개 벽이 열렸다

DRA는 v1.34에서 GA가 됐고(DRA 자체는 Kubernetes DRA와 GPU 스케줄링 편 참고), 이제 PodGroup이 ResourceClaimTemplate의 복제 단위가 될 수 있습니다.

기존에는 ResourceClaimTemplate을 참조하면 파드마다 ResourceClaim이 하나씩 생성됐습니다. 여러 파드가 같은 디바이스를 공유하려면 ResourceClaim을 이름으로 직접 참조해야 했고, 그러려면 그 클레임을 사용자가 직접 만들어 관리해야 했습니다. 이제 PodGroup의 spec.resourceClaims가 참조하는 ResourceClaimTemplate에 대해서는, 그룹에 파드가 몇 개든 그룹 전체에 하나의 ResourceClaim이 생성됩니다. 파드의 spec.resourceClaims 항목이 소속 PodGroup의 것과 일치하면, 그 파드의 클레임은 그룹용으로 생성된 ResourceClaim으로 해소되고 파드 개별 클레임은 만들어지지 않습니다.

더 실질적인 건 이겁니다. 예전엔 kube-scheduler가 ResourceClaim의 status.reservedFor에 개별 파드만 나열할 수 있었고, 그 필드는 256개 제한이 있었습니다. 이제 status.reservedFor에 PodGroup 참조 하나가 들어가면 256개를 훨씬 넘는 파드를 대표할 수 있습니다. 대규모 워크로드에서 디바이스를 고카디널리티로 공유하는 길이 열린 겁니다. 수백~수천 파드짜리 학습 잡에서 이건 이론적 개선이 아니라 실제로 막혀 있던 벽입니다.

Job 컨트롤러 통합 — 조건이 꽤 까다롭다

지금까지의 얘기를 다 읽고 "그래서 Workload랑 PodGroup을 내가 직접 만들어야 하나" 싶다면, v1.36의 답은 "Job이라면 아니오"입니다. WorkloadWithJob 피처 게이트를 켜면 Job 컨트롤러가 알아서 Workload와 런타임 PodGroup을 만들고, Job이 만드는 모든 파드에 .spec.schedulingGroup을 설정하고, 생성된 객체의 소유자를 Job으로 지정해 Job 삭제 시 GC되게 합니다.

다만 첫 이터레이션을 예측 가능하게 유지하려고 조건을 좁게 잡았습니다. Job이 아래를 전부 만족할 때만 통합이 작동합니다.

  • .spec.parallelism이 1보다 크다
  • .spec.completionModeIndexed
  • .spec.completions.spec.parallelism과 같다
  • 파드 템플릿에 schedulingGroup이 아직 설정돼 있지 않다

논리는 명확합니다 — 각 파드가 안정적 정체성을 갖고(Indexed), 갱 크기가 admission 시점에 고정돼 있고(parallelism == completions), 다른 컨트롤러가 이미 스케줄링 책임을 가져가지 않았어야 합니다. 조건을 못 채우는 Job은 예전처럼 파드별로 스케줄됩니다.

마지막 조건이 특히 사려 깊습니다. 여러분이 파드 템플릿에 schedulingGroup을 직접 설정해 뒀다면(예: 상위 컨트롤러가 워크로드를 관리 중이라면) Job 컨트롤러는 템플릿에 손대지 않고 자기 Workload/PodGroup도 만들지 않습니다. 즉 이미 외부 배치 시스템을 쓰는 클러스터에서 이 게이트를 켜도 안전합니다.

빠진 것도 분명합니다. 현재 제약은 이 통합을 정적·인덱스드·완전 병렬 Job으로 한정합니다. 탄력적(elastic) Job과 다른 빌트인 컨트롤러 지원은 KEP-5547에서 추적 중입니다.

켜는 법

전부 v1.36에서 알파입니다. 전제 조건부터.

  • Workload/PodGroup APIkube-apiserverkube-scheduler 양쪽에 GenericWorkload 게이트를 켜고, scheduling.k8s.io/v1alpha2 API 그룹을 활성화한다.

그다음 원하는 기능별로.

  • 갱 스케줄링kube-schedulerGangScheduling
  • 토폴로지 인지 스케줄링kube-schedulerTopologyAwareWorkloadScheduling
  • 워크로드 인지 선점kube-schedulerWorkloadAwarePreemption (GangScheduling도 함께 켜야 함)
  • DRA ResourceClaim 지원kube-apiserver, kube-controller-manager, kube-scheduler, kubelet 전부에 DRAWorkloadResourceClaims
  • Job 컨트롤러 통합kube-apiserverkube-controller-managerWorkloadWithJob

게이트가 컴포넌트마다 다르게 걸린다는 점을 눈여겨보세요. 이 목록만 봐도 관리형 클러스터(EKS·GKE·AKS)에서는 당장 만져 볼 수 없다는 결론이 나옵니다. 알파 게이트를 컨트롤 플레인에 켤 수 있어야 하니, kubeadm이나 kind로 세운 테스트 클러스터가 현실적인 실험장입니다.

그래서 Volcano와 Kueue를 걷어내야 하나

아니오. 그리고 이건 제 의견이 아니라 KEP-4671의 Non-Goals에 적힌 내용입니다 — kube-scheduler에 공정성이나 다중 워크로드 큐를 들여오는 것은 목표가 아니며, Kueue와 Volcano.sh가 계속 그것을 제공할 것이다.

이게 경계선을 정확히 그어 줍니다. 코어가 가져가는 건 "이 파드 묶음을 함께, 원자적으로, 토폴로지를 고려해 배치하라"는 메커니즘입니다. 반면 "어느 팀에 쿼터를 얼마나 주고, 대기열에서 누굴 먼저 꺼내고, 빌려준 자원을 언제 회수하나" 같은 정책은 여전히 Kueue와 Volcano의 영역입니다. 실제로 대부분의 조직이 갱 스케줄러를 도입한 이유는 후자입니다. 갱 스케줄링만 필요했던 팀은 생각보다 적습니다.

같은 Non-Goals에 이런 것들도 있습니다 — 컨트롤러에서 파드 생성 책임을 뺏지 않는다, 클러스터 오토스케일링과 갱 스케줄링의 통합은 (아직) 아니다, 워크로드 수준 선점은 (이 KEP의) 목표가 아니다, 서로 다른 스케줄러 간 자원 경합(교착 가능성 포함)은 다루지 않는다. 마지막 항목이 의미심장합니다 — kube-scheduler와 Volcano를 한 클러스터에서 같이 돌릴 때 생기는 문제는 여전히 여러분의 문제입니다.

지금 이걸 쓰면 안 되는 이유

기술 자체보다 이 문단이 실무자에게 더 값어치가 있을 겁니다.

전부 알파입니다. opportunistic batching만 베타(기본 켜짐)이고, Workload API·갱 스케줄링·토폴로지·선점·DRA 연동·Job 통합은 모두 알파입니다.

API가 이미 한 번 깨졌고, 또 깨질 예정입니다. v1.35의 v1alpha1은 v1.36에서 v1alpha2완전히 대체됐습니다. podGroupspodGroupTemplates가 됐고, policyschedulingPolicy가 됐고, 파드의 workloadRefschedulingGroup이 됐고, 런타임 상태는 별도 객체로 나갔습니다. 여섯 달 만에요.

그리고 KEP에 적힌 v1.37 계획을 보면 한 번 더 갑니다. API를 v1beta1로 승격시키면서, 동시에 계획된 알파 기능들의 DisruptionMode 관련 하위 호환 불가 변경을 위해 v1alpha3를 새로 만들어 v1alpha2를 대체한다는 것입니다. 업그레이드 시 필요한 조치가 KEP에 명시돼 있습니다 — v1alpha2 리소스를 전부 삭제한 뒤 업그레이드해야 하고(v1.37에서 미지원), v1alpha3에서 v1alpha2로의 역변환은 다운그레이드 시 지원되지 않습니다. 게다가 GangScheduling 게이트가 GenericWorkload로 병합되므로 업그레이드할 때 설정에서 GangScheduling을 빼야 하고, v1.36으로 다운그레이드하면 다시 켜야 합니다. (v1.37은 2026년 8월 26일 예정이고, 여기 적은 건 전부 KEP에 적힌 계획입니다. 릴리스 전까지 바뀔 수 있습니다.)

알고리즘 보장이 좁습니다. 앞서 본 대로 동종 그룹 밖에서는 배치를 못 찾을 수 있습니다.

KEP가 스스로 든 리스크도 있습니다 — 바인딩 단계의 경쟁 구간이 표준 스케줄링보다 길어지고, 객체가 늘어나니 API 호출량과 etcd 객체 수가 늘고, Workload와 PodGroup 두 객체 간 일관성 문제가 생깁니다. GC와 admission 컨트롤러로 완화한다지만 공짜는 아닙니다.

그럼에도 왜 봐야 하나

방향이 분명하기 때문입니다. Kubernetes는 지금까지 "파드가 스케줄링의 최소 단위"라는 전제 위에 서 있었고, 그 전제가 AI 워크로드에서 깨졌습니다. 지난 몇 년간의 대응은 전부 바깥에서 우회하는 것이었습니다. 이제 코어가 워크로드를 일급 시민으로 인정하는 중입니다.

v1.37 로드맵을 보면 어디로 가는지 읽힙니다 — Workload/PodGroup API의 베타 승격과 함께 탄력적 잡을 위한 minCount 가변성 도입, JobSet이나 LWS 기반 분산 추론 같은 복잡한 AI 워크로드를 위한 다중 레벨 계층 구조, 토폴로지 인지 스케줄링과 워크로드 인지 선점의 베타 승격, 그리고 실제 워크로드 컨트롤러들을 위한 통합 컨트롤러 연동 API. 프로젝트는 이 우선순위와 구현 순서가 바뀔 수 있다고 덧붙입니다.

지금 여러분이 할 일은 마이그레이션이 아닙니다. 테스트 클러스터에 kind로 v1.36을 띄우고 게이트를 켜서, 여러분의 학습 잡 모양이 Job 컨트롤러 통합 조건(Indexed, parallelism == completions)에 맞는지, 여러분의 파드 그룹이 "동종"에 해당하는지 확인해 보는 것입니다. 만약 둘 다 맞다면 여러분은 이 기능의 정확한 타깃이고, 베타가 오면 꽤 이른 시점에 Volcano 의존을 덜어 낼 수 있습니다. 둘 중 하나라도 어긋난다면 — 그리고 쿼터·큐잉이 필요하다면 — 앞으로도 한동안 Kueue와 Volcano가 답입니다. 그건 실패가 아니라 KEP가 설계한 그대로입니다.

마치며

정리하면 이렇습니다. 갱 스케줄링은 kube-scheduler 바깥에서 최소 네 번 구현됐고, Kubernetes는 v1.35에서 Workload API로 이 기능을 코어로 들이기 시작해 v1.36에서 구조를 바로잡았습니다. Workload는 정적 템플릿, PodGroup은 런타임 객체, 그리고 그룹 전체를 스냅샷 위에서 원자적으로 평가하는 PodGroup 스케줄링 사이클. 여기에 토폴로지 제약, 워크로드 단위 선점, PodGroup 단위 ResourceClaim 공유(256개 벽 해제), Job 컨트롤러 자동 통합의 첫 이터레이션이 얹혔습니다.

대신 전부 알파이고, API는 여섯 달 만에 한 번 깨졌고 한 번 더 깨질 계획이고, 알고리즘 보장은 동종 파드 그룹으로 좁고, 공정성과 큐잉은 애초에 목표가 아닙니다. 그러니 이건 "Volcano 대체재"가 아니라 "Volcano가 서 있을 표준 바닥"입니다. 바닥이 생기는 걸 지켜보되, 아직 그 위에 프로덕션을 올리진 마십시오.

참고 자료

The Workload/PodGroup API in Kubernetes v1.36 — Gang Scheduling Is Moving Into kube-scheduler

Introduction — What Happens When You Schedule Pods One at a Time

Anyone who has run a distributed training job on Kubernetes knows this failure mode. Training only starts once all 4 workers are up, but the default scheduler places pods one at a time, independently. If only enough GPU capacity for 3 pods is left, those 3 pods claim nodes and sit there, waiting forever for the 4th. Three GPUs sit occupied, doing nothing. If two jobs do this at the same time, you can end up with a deadlock where each holds resources the other needs.

KEP-4671's motivation section states this plainly — parallel applications need communication among all pods to begin execution, and ongoing communication such as barriers or all-reduce to make progress. So all pods need to start at close to the same time, or expensive compute sits idle, or the application dies from a communication timeout.

The solution has existed for a long time: gang scheduling, "all or nothing" placement. The problem is that the solution has always lived outside the cluster — install Volcano, bolt on Kueue, or layer in the coscheduling plugin. That ecosystem side of the story is already covered in Kubernetes AI Training Pipeline: Analyzing Volcano, Training Operator, and Kueue.

This post is about a different story. In Kubernetes v1.36, released April 22, 2026 (codename ハル / Haru, 70 enhancements total — 18 Stable, 25 Beta, 25 Alpha), the work of bringing gang scheduling into kube-scheduler itself entered its second phase. And along the way, the API that had shipped just six months earlier got replaced wholesale.

Why Kubernetes Decided to Step In

Why does the core need to get involved when working implementations already exist outside it? KEP-4671 answers directly: gang scheduling has been implemented at least four times outside kube-scheduler, and some controllers have started supporting multiple gang schedulers simultaneously just to be portable across clusters that use different ones.

That's where the problem shows up. The parties that want gang scheduling are workload controllers — Job, JobSet, LeaderWorkerSet, MPIJob, TrainJob — and each of them has to carry separate code paths for Volcano and for Kueue, because there's no standard interface. The KEP aims at three things: make gang scheduling available on every Kubernetes distribution, let controllers request it the same way against both standard and custom schedulers, and let other components such as the cluster autoscaler understand a workload's requirements too.

That last item matters more than it looks. Today's autoscaler only sees "3 pods are Pending" — it has no idea that "these 3 are part of a gang of 4, and if the 4th can't come up, the other 3 are pointless too." Once the workload is expressed as an API object, that can be read.

Topology comes into it too. Workloads that need a gang usually only perform well when the gang's members sit close together topologically. Existing pod affinity does influence placement, but as the KEP points out, it doesn't treat the gang as the scheduling unit, and it doesn't efficiently try multiple mutually exclusive placement candidates for a set of pods. Affinity is a tool built from the perspective of a single pod.

v1.35 — The First Piece: the Workload API and the Permit Gate

v1.35 (December 17, 2025) shipped the first batch. A Workload resource appeared in scheduling.k8s.io/v1alpha1.

apiVersion: scheduling.k8s.io/v1alpha1
kind: Workload
metadata:
  name: training-job-workload
  namespace: some-ns
spec:
  podGroups:
  - name: workers
    policy:
      gang:
        # The gang is schedulable only if 4 pods can run at once
        minCount: 4

Pods connected to it via workloadRef. The mechanics worked like this.

  1. When a pod is created, the scheduler holds it at PreEnqueue — as long as a Workload object exists, contains the matching pod group, and the number of waiting pods hasn't yet reached minCount.
  2. Once enough pods have gathered, it attempts placement, but instead of binding immediately, it holds them at a Permit gate.
  3. If a valid placement is found for the whole group (at least minCount), the gate opens and they're bound together; if only part of the group gets scheduled within the timeout (5 minutes), all of the group's pods are rejected back to the queue and the reserved resources are released.

Shipping alongside it was opportunistic batching. It needs neither the Workload API nor explicit user opt-in, and in v1.35 it's beta and on by default. It recognizes pods with identical scheduling requirements (container image, resource requests, affinity, and so on) and reuses the feasibility computation done for one pod on the identical pods that follow, speeding up processing. Pods that make up a gang are usually identical to each other, so this pairs well. That said, every field the scheduler uses to find a placement has to match across pods, and using certain features turns batching off for correctness. The docs go out of their way to warn you to check whether your kube-scheduler configuration is implicitly disabling batching.

The v1.35 blog post was explicit that this was a first implementation, and flagged a scheduling phase that processes the whole gang in one cycle, plus workload-level preemption, as next. That's exactly what arrived in v1.36.

v1.36 — Workload as Template, PodGroup as Runtime

v1.36 shipped scheduling.k8s.io/v1alpha2, completely replacing the previous v1alpha1. The API broke just six months in. The reasoning is spelled out in detail in the KEP, and it's convincing.

The original design embedded PodGroup inside the Workload spec. But Workload is a long-lived configuration-intent object, while PodGroup is a transient scheduling unit. Tying a runtime execution unit to a persistent definition object breaks separation of concerns. On top of that, it runs straight into a scalability problem — a large Workload with many PodGroups easily hits etcd's 1.5MB object size limit, and updating the status of a single PodGroup requires a read-modify-write of the huge central Workload object, which creates contention.

So they split it. Workload is now a static template.

apiVersion: scheduling.k8s.io/v1alpha2
kind: Workload
metadata:
  name: training-job-workload
  namespace: some-ns
spec:
  # renamed from v1alpha1's podGroups -> podGroupTemplates
  podGroupTemplates:
  - name: workers
    # renamed from v1alpha1's policy -> schedulingPolicy
    schedulingPolicy:
      gang:
        minCount: 4

And a controller stamps out a runtime PodGroup from this template.

apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: training-job-workers-pg
  namespace: some-ns
spec:
  podGroupTemplateRef:
    workload:
      workloadName: training-job-workload
      podGroupTemplateName: workers
  schedulingPolicy:
    gang:
      minCount: 4
status:
  conditions:
  - type: PodGroupScheduled
    status: "True"
    lastTransitionTime: 2026-04-03T00:00:00Z

The pod-side linking field changed too. workloadRef is gone, replaced by schedulingGroup.

apiVersion: v1
kind: Pod
metadata:
  name: worker-0
  namespace: some-ns
spec:
  schedulingGroup:
    podGroupName: training-job-workers-pg

Worth noting: .metadata.ownerReferences still points at the "real" workload object (e.g., a Job). podGroupTemplateRef only says which template this came from. Ownership and provenance are kept separate.

This also simplifies the scheduler's logic. Since a single PodGroup carries everything the scheduler needs, there's no reason to watch or parse the Workload object.

The PodGroup Scheduling Cycle — Evaluating the Group Atomically

This is the real substance of v1.36. Evaluating and reserving resources sequentially, pod by pod, carries deadlock risk, so the scheduler now has a dedicated cycle that evaluates the group as a single unit.

When the scheduler pulls a PodGroup member off the queue, regardless of policy it gathers the group's remaining waiting pods, sorts them deterministically, and runs an atomic cycle.

  1. It takes a single snapshot of cluster state — to prevent race conditions and ensure consistency while evaluating the whole group.
  2. The PodGroup scheduling algorithm searches for a valid node placement for every pod in the group. The filtering and scoring stages reuse the existing pod-based logic as-is.
  3. It applies the result to the whole group atomically.
    • On success, the member pods that can be scheduled move on to the binding stage together, and the remaining pods go back to the queue to wait for resources to free up.
    • On failure, the entire group is treated as unschedulable; no pod is bound, and it retries after a backoff.

There's a quiet but operationally important caveat here. If pods are added to a PodGroup that already has scheduled pods, the cycle evaluates the new pods taking the existing ones into account. And pods already assigned to a node keep running — even if the group fails to meet its requirements in a later cycle, the scheduler does not unassign or evict those pods. In other words, "all or nothing" is atomicity at bind time, not an invariant maintained for the group's whole lifetime.

minCount gating itself still remains. The scheduler still holds pods at PreEnqueue, and only the actual scheduling phase now depends entirely on the new PodGroup cycle. While the algorithm runs, it checks whether the number of schedulable pods satisfies minCount, and if the cluster can't support the minimum, nothing gets bound.

The Algorithm's Honest Limits

This is the most important part of this post. And credit where due — this isn't my observation; it's what the Kubernetes project itself wrote in its own blog post, under a heading titled "Limitations."

The limits of the first version of the PodGroup scheduling cycle are as follows.

  • For homogeneous pod groups — where every pod's scheduling requirements are identical and there are no inter-pod dependencies such as affinity, anti-affinity, or topology spread constraints — the algorithm is expected to find a placement if one exists.
  • For heterogeneous pod groups, there's no guarantee it will find a placement even if one exists — even when the answer looks obvious.
  • There's no guarantee for groups with inter-pod dependencies either.

On top of that, with dependencies inside the group — for instance, where inter-pod affinity makes one pod's schedulability depend on another member — the deterministic processing order can cause it to fail to find a placement regardless of cluster state. The KEP puts the same point this way: the default algorithm may fail to find a valid placement that would have been found had the group's pods been processed in a different order.

Once you read this, the scope of applicability becomes clear. What this feature is properly aimed at right now is PyTorch DDP-style training jobs where every worker is identical. Workloads like LeaderWorkerSet with different leader and worker specs, heterogeneous GPU pools, or ones tangled up with topology spread constraints, are still outside the guarantee. That's not a bug — it's the scope of alpha.

Topology-Aware Scheduling — First Iteration

v1.36 lets you attach topology constraints directly to a PodGroup.

apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: topology-aware-workers-pg
spec:
  schedulingPolicy:
    gang:
      minCount: 4
  schedulingConstraints:
    topology:
      - key: topology.kubernetes.io/rack

The scheduler tries node combinations that satisfy the rack constraint, and picks the best placement based on how efficiently the PodGroup uses resources and how many pods fit within that domain. To do this, three placement-based algorithm stages were added to the PodGroup cycle — generating candidate placements (the PlacementGenerate extension point), verifying that the whole group actually fits into each candidate, and scoring the feasible placements to pick the best one (the PlacementScore extension point).

The limits are stated too. Topology-aware scheduling currently does not trigger pod preemption to satisfy a constraint; the plan is to integrate it with workload-aware preemption in a future release. And multiple topology levels, soft constraints (preferences), deep integration with DRA, and robustness when combined with the basic policy all remain future work. So what's usable right now is just one single-level hard constraint.

Workload-Aware Preemption

A new preemption mechanism for when a PodGroup can't be scheduled also arrived. Unlike existing per-pod preemption, it treats the whole PodGroup as a single preemptor unit. Instead of picking victims node by node, it searches across the whole cluster and evicts pods on multiple nodes at once, to make room for the entire group. Without this, gang preemption behaves strangely — nibbling away a little on each node never adds up to enough room for the whole gang.

Two concepts were added to the PodGroup API.

  • priority — overrides the priority of the individual pods that make up the PodGroup.
  • disruptionMode — specifies whether pods within the group can be preempted individually, or must be preempted together, all or nothing.
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: victim-pg
spec:
  priorityClassName: high-priority
  priority: 1000
  disruptionMode: PodGroup

There's a trap here. In v1.36, these two fields are honored only by the workload-aware preemption mechanism. Other disruption paths — including default preemption in the existing per-pod cycle — ignore these fields. Extending them to other disruption sources is a future aspiration. So don't read setting disruptionMode: PodGroup as meaning your gang is wholly protected from every kind of eviction.

Combining With DRA — the 256 Wall Opens Up

DRA graduated to GA in v1.34 (for DRA itself, see Kubernetes Dynamic Resource Allocation and GPU Scheduling), and now PodGroup can serve as the replication unit for a ResourceClaimTemplate.

Previously, referencing a ResourceClaimTemplate created one ResourceClaim per pod. For multiple pods to share the same device, you had to reference a ResourceClaim directly by name, which meant a user had to create and manage that claim by hand. Now, for a ResourceClaimTemplate referenced by a PodGroup's spec.resourceClaims, a single ResourceClaim is created for the whole group, no matter how many pods are in it. If a pod's spec.resourceClaims entry matches the one on its PodGroup, that pod's claim resolves to the ResourceClaim created for the group, and no per-pod claim is created.

The more practical part is this. Previously, kube-scheduler could only list individual pods in a ResourceClaim's status.reservedFor, and that field had a 256-entry limit. Now a single PodGroup reference in status.reservedFor can represent far more than 256 pods. That opens a path to high-cardinality device sharing for large-scale workloads. For training jobs with hundreds to thousands of pods, this isn't a theoretical improvement — it was an actual wall that was blocking things.

Job Controller Integration — the Conditions Are Fairly Strict

If you've read everything so far and are wondering "so do I have to create the Workload and PodGroup myself," v1.36's answer is "not if it's a Job." Turn on the WorkloadWithJob feature gate and the Job controller creates the Workload and runtime PodGroup on its own, sets .spec.schedulingGroup on every pod the Job creates, and sets the owner of the created objects to the Job so they get garbage-collected when the Job is deleted.

But to keep the first iteration predictable, the conditions are narrow. The integration only kicks in when a Job satisfies all of the following.

  • .spec.parallelism is greater than 1
  • .spec.completionMode is Indexed
  • .spec.completions equals .spec.parallelism
  • the pod template doesn't already have schedulingGroup set

The logic is clear — each pod needs a stable identity (Indexed), the gang size needs to be fixed at admission time (parallelism == completions), and no other controller should have already taken over scheduling responsibility. Jobs that don't meet the conditions get scheduled pod-by-pod as before.

The last condition is particularly thoughtful. If you've already set schedulingGroup on the pod template yourself — say, because a higher-level controller is managing the workload — the Job controller leaves the template alone and doesn't create its own Workload/PodGroup either. In other words, it's safe to turn this gate on even in a cluster that already uses an external placement system.

What's missing is clear too. The current constraints limit this integration to static, indexed, fully-parallel Jobs. Support for elastic Jobs and other built-in controllers is tracked in KEP-5547.

How to Turn It On

All of it is alpha in v1.36. Prerequisites first.

  • Workload/PodGroup API — turn on the GenericWorkload gate on both kube-apiserver and kube-scheduler, and enable the scheduling.k8s.io/v1alpha2 API group.

Then, per feature you want.

  • Gang schedulingGangScheduling on kube-scheduler
  • Topology-aware schedulingTopologyAwareWorkloadScheduling on kube-scheduler
  • Workload-aware preemptionWorkloadAwarePreemption on kube-scheduler (GangScheduling must also be on)
  • DRA ResourceClaim supportDRAWorkloadResourceClaims on kube-apiserver, kube-controller-manager, kube-scheduler, and kubelet, all of them
  • Job controller integrationWorkloadWithJob on kube-apiserver and kube-controller-manager

Notice that the gate is different per component. Just from this list you can conclude you won't be able to try this on managed clusters (EKS, GKE, AKS) any time soon — you need the ability to turn alpha gates on in the control plane, so a test cluster set up with kubeadm or kind is the realistic place to experiment.

So Should You Rip Out Volcano and Kueue

No. And this isn't my opinion — it's written into KEP-4671's Non-Goals: bringing fairness or multi-workload queuing into kube-scheduler is not a goal, and Kueue and Volcano.sh will continue to provide it.

This draws the boundary precisely. What the core is taking on is the mechanism — "place this bundle of pods together, atomically, with topology in mind." The policy — "how much quota does which team get, who comes off the queue first, when do you reclaim borrowed resources" — is still Kueue and Volcano's territory. And in practice, the latter is why most organizations adopted a gang scheduler in the first place. Fewer teams than you'd think needed only gang scheduling.

The same Non-Goals list also includes: not taking pod-creation responsibility away from controllers; integrating cluster autoscaling with gang scheduling is not (yet) in scope; workload-level preemption is not a goal (of this KEP); and resource contention between different schedulers — including possible deadlock — is not addressed. That last one is telling — problems that arise from running kube-scheduler and Volcano together in one cluster are still your problem.

Why You Shouldn't Use This Right Now

This section is probably worth more to practitioners than the technology itself.

It's all alpha. Only opportunistic batching is beta (on by default); the Workload API, gang scheduling, topology, preemption, DRA integration, and Job integration are all alpha.

The API has already broken once, and is set to break again. v1.35's v1alpha1 was completely replaced by v1alpha2 in v1.36. podGroups became podGroupTemplates, policy became schedulingPolicy, a pod's workloadRef became schedulingGroup, and runtime state moved out into a separate object. In six months.

And the v1.37 plan written into the KEP goes one step further. It plans to promote the API to v1beta1 while simultaneously creating a new v1alpha3 to replace v1alpha2, for backward-incompatible DisruptionMode-related changes to planned alpha features. The KEP spells out what's required to upgrade — v1alpha2 resources must be deleted entirely before upgrading (they're unsupported in v1.37), and converting back from v1alpha3 to v1alpha2 is not supported on downgrade. On top of that, since the GangScheduling gate is being merged into GenericWorkload, you'll need to drop GangScheduling from your configuration when upgrading, and turn it back on if you downgrade to v1.36. (v1.37 is planned for August 26, 2026, and everything written here is a plan as stated in the KEP — it can change before release.)

The algorithm's guarantees are narrow. As covered above, it may fail to find a placement outside of homogeneous groups.

There are risks the KEP itself calls out — the contention window at the binding stage is longer than standard scheduling, the extra objects mean more API calls and more etcd objects, and there are consistency issues between the two objects, Workload and PodGroup. It's mitigated with GC and admission controllers, but it's not free.

So Why Pay Attention to This Anyway

Because the direction is clear. Kubernetes has stood, until now, on the premise that "the pod is the minimum unit of scheduling," and that premise broke down for AI workloads. The response over the past several years has been to work around it entirely from outside. Now the core is in the process of recognizing the workload as a first-class citizen.

The v1.37 roadmap tells you where this is going — promoting the Workload/PodGroup API to beta alongside introducing mutable minCount for elastic jobs, a multi-level hierarchy for complex AI workloads like JobSet- or LWS-based distributed inference, promoting topology-aware scheduling and workload-aware preemption to beta, and a unified controller-integration API for real workload controllers. The project adds that this priority and implementation order can change.

What you should do right now is not migrate. Spin up v1.36 on a test cluster with kind, turn on the gates, and check whether the shape of your training jobs fits the Job controller integration conditions (Indexed, parallelism == completions), and whether your pod groups qualify as "homogeneous." If both hold, you're the exact target of this feature, and once beta arrives you could shed your Volcano dependency fairly early. If either one is off — and if you need quota and queuing — Kueue and Volcano remain the answer for a while yet. That's not a failure; it's exactly how the KEP was designed.

Closing

To sum up: gang scheduling has been implemented at least four times outside kube-scheduler, and Kubernetes started bringing it into the core with the Workload API in v1.35, then fixed the structure in v1.36. Workload is a static template, PodGroup is a runtime object, and there's a PodGroup scheduling cycle that atomically evaluates the whole group over a snapshot. On top of that came topology constraints, workload-level preemption, PodGroup-level ResourceClaim sharing (opening up the 256 wall), and a first iteration of automatic Job controller integration.

In exchange, all of it is alpha, the API broke once in six months and is planned to break again, the algorithm's guarantees are narrow, limited to homogeneous pod groups, and fairness and queuing were never the goal to begin with. So this isn't a "Volcano replacement" — it's "the standard floor Volcano will stand on." Watch that floor get built, but don't put production on top of it yet.

References