Skip to content
Published on

Kubernetes v1.36 の Workload/PodGroup API — ギャングスケジューリングが kube-scheduler に入りつつある

シェア
Authors

はじめに — ポッドを一つずつスケジューリングすると何が起きるか

分散学習ジョブをKubernetesに載せたことがある人なら知っている失敗パターンがあります。ワーカー4つが全部立ち上がって初めて学習が始まるのに、デフォルトのスケジューラはポッドを一つずつ独立に配置します。GPUが3個分しか残っていなければ、ポッド3つがノードを確保して座り込み、残り1つを永遠に待ちます。GPU3個は占有されたまま何もしません。ジョブが2つ同時にこれをやると、互いに互いのリソースを握り合うデッドロックにまで行き着きます。

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自体に取り込む作業が第2段階に入りました。そしてその過程で、半年前に出たばかりのAPIがまるごと作り直されました。

Kubernetesが直接乗り出した理由

外にすでによく動く実装があるのに、なぜコアが乗り出すのか。KEP-4671が直接答えています — ギャングスケジューリングはkube-schedulerの外で少なくとも4回実装されており、一部のコントローラはクラスタごとに異なるスケジューラへ移植可能であるために複数のギャングスケジューラを同時にサポートし始めた、というのです。

ここで問題が露わになります。ギャングスケジューリングを求めるのはワークロードコントローラ(Job、JobSet、LeaderWorkerSet、MPIJob、TrainJob)ですが、それぞれがVolcano用のコードとKueue用のコードを別々に抱えなければなりません。標準インターフェースがないからです。KEPは3つを狙っています — あらゆるKubernetesディストリビューションでギャングスケジューリングを使えるようにする、コントローラが標準・カスタムスケジューラの両方に同じ方法でリクエストできるようにする、クラスタオートスケーラーのような他のコンポーネントもワークロードの要求を理解できるようにする、です。

最後の項目が地味に重要です。今のオートスケーラーは「ポッド3つがPending」ということしか見えず、「この3つは4つ組のギャングの一部であり、もう1つ立たなければ3つも意味がない」ことを知りません。ワークロードがAPIとして表現されれば、それを読み取れます。

そしてトポロジーの話が続きます。ギャングを必要とするワークロードは、たいていギャングのメンバーがトポロジー上近くに固まっていて初めて性能が出ます。既存のポッドアフィニティは配置に影響を与えはしますが、KEPが指摘する通り、ギャングをスケジューリング単位として扱わず、ポッド集合に対して互いに排他的な複数の配置候補を効率的に試すこともしません。アフィニティはポッド1つの視点で作られた道具です。

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でベータとしてデフォルトで有効になっています。スケジューリング要求が同一のポッド(コンテナイメージ、リソースリクエスト、アフィニティなど)を認識し、1つのポッドに対する実現可能性計算を、後続の同一ポッドに使い回すことで処理速度を上げます。ギャングを構成するポッドは普段互いに同一なので相性が良いです。ただしスケジューラが配置を探すのに使うすべてのフィールドがポッド間で一致していなければならず、一部の機能を使うと正確性のためにバッチングがオフになります。あなたのkube-scheduler設定が暗黙にバッチングを無効化していないか確認せよと、ドキュメントがわざわざ念を押しています。

v1.35のブログ記事は、これが最初の実装だと明言し、ギャング全体を1サイクルで処理するスケジューリング段階と、ワークロードレベルのプリエンプションを次として予告していました。まさにそれが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オブジェクトサイズ上限に容易に達し、PodGroup1つの状態を更新するために巨大な中央の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はこれがどのテンプレートから生まれたかを語るだけです。所有権と出自が分離されています。

こうすることでスケジューラのロジックも単純になります。PodGroup1つにスケジューラが必要な情報が全部入っているので、Workloadオブジェクトをwatchしたりパースしたりする理由がありません。

PodGroupスケジューリングサイクル — グループを原子的に評価する

v1.36の本当の中身はここです。ポッドごとに順次リソースを評価・予約すればデッドロックのリスクがあるので、スケジューラがグループを一つの単位として評価する専用サイクルを持つようになりました。

スケジューラがキューからPodGroupメンバーを一つ取り出すと、ポリシーに関わらずそのグループの残りの待機ポッドを集め、決定論的に並べ替えたうえで原子的サイクルを回します。

  1. クラスタ状態のスナップショットを一度だけ取る — グループ全体を評価している間の競合状態を防ぎ、一貫性を確保するため。
  2. PodGroupスケジューリングアルゴリズムで、グループのすべてのポッドに対する有効なノード配置を探す。このときフィルタリング・スコアリング段階は既存のポッドベースロジックをそのまま使う。
  3. 結果をグループ全体に原子的に適用する。
    • 成功すればスケジュール可能なメンバーポッドが揃ってバインド段階に進み、残ったポッドはキューに戻ってリソースが空くのを待つ。
    • 失敗すればグループ全体がスケジュール不可として扱われ、どのポッドもバインドされないままバックオフ後に再試行する。

ここに、静かだが運用上重要な注意点が一つ付きます。すでにスケジュール済みのポッドがあるPodGroupにポッドが追加されると、サイクルは既存のポッドを踏まえて新しいポッドを評価します。そしてすでにノードに割り当てられたポッドは動き続けます — 以降のサイクルでグループが要求を満たせなくなっても、スケジューラはそれらのポッドの割り当てを解除したり追い出したりしません。つまり「全部かゼロか」はバインド時点での原子性であり、グループの寿命全体を通じて維持される不変条件ではありません。

minCountゲーティング自体は残っています。スケジューラは依然としてPreEnqueueでポッドを止めており、実際のスケジューリング段階だけが新しいPodGroupサイクルに全面的に依存するようになりました。アルゴリズム実行中にスケジュール可能なポッド数がminCountを満たすか確認し、クラスタが最小数を支えられなければ何もバインドしません。

アルゴリズムの正直な限界

この部分が本稿で一番重要です。そして評価すべきことに、これは私の観察ではなく、Kubernetesプロジェクトが自分のブログに「Limitations」という見出しで直接書いた内容です。

初版のPodGroupスケジューリングサイクルの限界はこうです。

  • 同種(homogeneous)なポッドグループ — すべてのポッドのスケジューリング要求が同一で、アフィニティ・アンチアフィニティ・トポロジー分散制約のようなポッド間依存がない場合 — については、配置が存在すればアルゴリズムが見つけることが期待される。
  • 異種(heterogeneous)なポッドグループについては、配置が存在しても見つかる保証はない。答えが自明に見える場合でさえも。
  • ポッド間依存があるグループについても保証はない。

さらに、グループ内部の依存関係(たとえばポッド間アフィニティによって1つのポッドのスケジュール可能性が他のメンバーに左右される場合)では、決定論的な処理順序のせいでクラスタの状態に関わらず配置が見つからないことがあります。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ポリシーと併用したときの堅牢性は、今後の課題として残っています。つまり今使えるのは単一レベルのハード制約1つだけです。

ワークロード認識プリエンプション

PodGroupがスケジュールできないときに使う新しいプリエンプションのメカニズムも入りました。既存のポッド単位のプリエンプションと違う点は、PodGroup全体を1つのプリエンプター単位として扱うことです。ノードごとに別々に犠牲者を探す代わりに、クラスタ全体を検索して、複数のノードで同時にポッドを追い出し、グループ全体が入る空間を作ります。これがないと、ギャングのプリエンプションは奇妙になります — ノードごとに少しずつ追い出しても、ギャング全体が入るほどの空間はできないからです。

PodGroup APIに2つの概念が追加されました。

  • priority — PodGroupを構成する個々のポッドの優先度を上書きする。
  • disruptionMode — グループ内のポッドが個別にプリエンプトされうるのか、それとも全部かゼロかで一緒にプリエンプトされるべきかを指定する。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
  name: victim-pg
spec:
  priorityClassName: high-priority
  priority: 1000
  disruptionMode: PodGroup

ここに落とし穴があります。v1.36ではこの2つのフィールドはワークロード認識プリエンプションのメカニズムでのみ尊重されます。既存のポッド単位サイクルのデフォルトプリエンプションを含む他の混乱(disruption)経路は、このフィールドを無視します。他の混乱の発生源へ拡張することは今後の希望事項です。ですから、disruptionMode: PodGroupを設定したからといって、あなたのギャングがあらゆる追い出しから丸ごと保護されると読んではいけません。

DRAとの結合 — 256個の壁が開いた

DRAはv1.34でGAになり(DRA自体はKubernetes動的リソース割り当てとGPUスケジューリングを参照)、今やPodGroupがResourceClaimTemplateの複製単位になれます。

以前はResourceClaimTemplateを参照すると、ポッドごとにResourceClaimが一つずつ生成されていました。複数のポッドが同じデバイスを共有するには、ResourceClaimを名前で直接参照する必要があり、そのためにはそのクレームをユーザーが自分で作って管理しなければなりませんでした。今はPodGroupのspec.resourceClaimsが参照するResourceClaimTemplateについては、グループにポッドが何個あろうとグループ全体で1つのResourceClaimが生成されます。ポッドのspec.resourceClaims項目が所属PodGroupのものと一致すれば、そのポッドのクレームはグループ用に生成されたResourceClaimで解決され、ポッド個別のクレームは作られません。

より実質的なのはこちらです。以前はkube-schedulerがResourceClaimのstatus.reservedForに個々のポッドしか列挙できず、そのフィールドには256個の上限がありました。今はstatus.reservedForにPodGroup参照が1つ入れば、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-apiserverkube-controller-managerkube-schedulerkubelet全部に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という2つのオブジェクト間の一貫性の問題が生じます。GCとadmissionコントローラで緩和するとはいえ、タダではありません。

それでもなぜ見ておくべきか

方向性がはっきりしているからです。Kubernetesはこれまで「ポッドがスケジューリングの最小単位である」という前提の上に立っていましたが、その前提はAIワークロードで崩れました。この数年の対応は全部、外側から迂回することでした。今、コアがワークロードをファーストクラスの市民として認めつつあります。

v1.37のロードマップを見ると、どこへ向かっているかが読み取れます — Workload/PodGroup APIのベータ昇格と併せて、弾力的なジョブのためのminCount可変化の導入、JobSetやLWSベースの分散推論のような複雑なAIワークロードのための多段階の階層構造、トポロジー認識スケジューリングとワークロード認識プリエンプションのベータ昇格、そして実際のワークロードコントローラ向けの統合コントローラ連携API。プロジェクトは、この優先順位と実装順序は変わりうると付け加えています。

今あなたがやるべきことはマイグレーションではありません。テストクラスタにkindでv1.36を立て、ゲートを有効にして、あなたの学習ジョブの形がJobコントローラ統合の条件(Indexedparallelism == completions)に合うか、あなたのポッドグループが「同種」に該当するかを確かめることです。もし両方とも合うなら、あなたはこの機能のまさにターゲットであり、ベータが来ればかなり早い時期にVolcano依存を軽くできます。どちらか一方でも外れるなら — そしてクォータ・キューイングが必要なら — 今後もしばらくKueueとVolcanoが答えです。それは失敗ではなく、KEPが設計したとおりのことです。

おわりに

まとめるとこうです。ギャングスケジューリングはkube-schedulerの外で少なくとも4回実装され、Kubernetesはv1.35でWorkload APIによりこの機能をコアに取り込み始め、v1.36で構造を整えました。Workloadは静的テンプレート、PodGroupはランタイムオブジェクト、そしてグループ全体をスナップショット上で原子的に評価するPodGroupスケジューリングサイクル。ここにトポロジー制約、ワークロード単位のプリエンプション、PodGroup単位のResourceClaim共有(256個の壁の解放)、Jobコントローラ自動統合の最初のイテレーションが乗りました。

代わりに全部アルファであり、APIは半年で一度壊れ、もう一度壊れる計画であり、アルゴリズムの保証は同種のポッドグループに狭く限られ、公平性とキューイングはそもそも目標ではありません。ですからこれは「Volcanoの代替品」ではなく「Volcanoが立つことになる標準の床」です。床ができていく様子を見守りつつ、まだその上にプロダクションを乗せないでください。

参考資料