Kubernetesスペシャリストが注目する関連ツール探求 第24回

「Kueue」と「Volcano」で分散学習JobのGang Schedulingを実現する

第24回の今回は、分散学習Jobの「All or Nothing」配置を実現するため、「Kueue」のTopology Aware Schedulingと「Volcano」によるGang Schedulingを検証・比較しながら解説します。

李 賢在

6:30

はじめに

スリーシェイクの Sreake 事業部に所属する李(LinkedIn)です。

近年、LLM をはじめとするモデルの大規模化が進んでいます。1 台の GPU では扱いきれない規模の学習を複数の GPU に分散して実行する「分散学習」も、いまや一般的な手法です。

分散学習は、モデルやデータを複数のワーカーに分割し、ワーカーごとに GPU を割り当てて並列に計算する方式です。各ワーカーはステップごとに計算結果を交換し、全員が揃うのを待ってから次のステップへ進みます。つまり、 1 つでも欠ければ残りのワーカーはそこで停止してしまいます。Kubernetes 上ではこのワーカー 1つひとつが Pod となり、それらをまとめた単位が 1 つの Job になります。

そのため、分散学習の Job は、すべての Pod がそろって配置されるか、それが不可能であれば 1 つも配置されないか、のどちらかでなければなりません(いわゆる「All or Nothing」の概念)。

Kubernetes でこれを扱うためのツールとして「Kueue」があります。Kueue は、アドミッションコントロール(admission control)によって Job をクラスタに受け入れるかどうかを判断します。クラスタの GPU クォータが不足している場合、Job は admit されず、空きが出るまでキューで待機します。

しかし、受け入れが許可されたとしても、その Job が実際にどのノードへ配置されるかは別の問題です。例えば、GPU を 1 枚ずつ要求する 4 個の Pod で構成され、4 個すべてが起動してはじめて意味を持つ Job を考えてみます。

クォータはクラスタ全体の「総量」を管理するだけなので、実際のノードの空き状況と一致するとは保証されません。Kueue の管理外で消費されているリソースがあったり、空きが複数のノードに細かく分かれていたりすると、クォータ上は足りていても全 Pod を配置できないことがあります。その場合、4 個のうち 3 個だけが Running となり、残る 1 個は Pending のまま止まります。All or Nothing が崩れた状態です。

これを防ぐには、全 Pod を同時に配置できるときだけ配置する仕組みが必要です。これを「gang scheduling」と呼び、実現する方法は大きく 2 つあります。

  • Kueue の Topology Aware Scheduling(以下TAS): クラスタのトポロジーを Kueue に教え、全 Pod が収まらなければ admit しない
  • Volcano による gang scheduling:  配置を Volcano に任せ、全 Pod がそろうときだけ配置する

本記事では、Kueue単体と上記2つの解法について、実際の環境で検証・比較します。

検証環境

本記事では kind でクラスタを作成し、各ノードに extended resource(example.com/gpu)を付与して環境を構築します。

検証したいのは GPU 演算そのものではなく「スケジューラが Pod をどう配置するか」なので、実際の GPU がなくても、Fake GPU を持つノードを用意した環境で十分です。

Jobは「はじめに」で挙げた例に合わせ、GPU を 1 枚ずつ要求する 4 個の Pod を 1 つの分散学習Jobと見立てます。4 個すべてがそろって起動しなければ意味を持たない、という前提です。

  • Kubernetes: v1.36.1(kind のノードイメージ)
  • コントロールプレーン 1 台
  • ワーカーノード 2 台(example.com/gpu: 2 を付与)
    • 合計 4 枚の Fake GPU
  • Kueue ClusterQueue の example.com/gpu
    • nominalQuota = 4
  • 分散 Job
    • parallelism: 4、Pod ごとに GPU 1 枚を要求(合計 4 枚が必要)

クラスタの作成と Fake GPU の設定

kind を用いて Kubernetes クラスタを構築します。

kind create cluster --config kind-config.yaml --name kueue-volcano-test

kubectl get nodes
NAME                               STATUS   ROLES           AGE   VERSION
kueue-volcano-test-control-plane   Ready    control-plane   34s   v1.36.1
kueue-volcano-test-worker          Ready    <none>          19s   v1.36.1
kueue-volcano-test-worker2         Ready    <none>          19s   v1.36.1

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.36.1
  - role: worker
    image: kindest/node:v1.36.1
  - role: worker
    image: kindest/node:v1.36.1

各ワーカーノードに extended resource を付与します。実際の GPU はなくても、スケジューラの配置動作は再現されます。

for n in kueue-volcano-test-worker kueue-volcano-test-worker2; do
  kubectl patch node "$n" --subresource=status --type=json \
    -p '[{"op":"add","path":"/status/capacity/example.com~1gpu","value":"2"}]'
done

kubectl get nodes -o custom-columns='NODE:.metadata.name,GPU:.status.capacity.example\.com/gpu'

NODE                               GPU
kueue-volcano-test-control-plane   <none>
kueue-volcano-test-worker          2
kueue-volcano-test-worker2         2

Kueue のインストールとクォータ構成

まず Kueue をインストールします。

VERSION=v0.18.1
kubectl apply --server-side -f \
  https://github.com/kubernetes-sigs/kueue/releases/download/${VERSION}/manifests.yaml

# コントローラーが Running になるまで待機
kubectl wait --for=condition=Available -n kueue-system \
  deployment/kueue-controller-manager --timeout=120s
deployment.apps/kueue-controller-manager condition met

続いて、Kueue のクォータを構成します。ResourceFlavor → ClusterQueue → LocalQueue の階層で構成し、ここでは example.com/gpu の nominalQuota を 4 に指定します。

  • ResourceFlavor: リソースの「種類」を定義します。今回はラベル指定なしで、すべてのノードにマッチさせています
  • ClusterQueue: クラスタ全体で使えるクォータの総量(CPU・メモリ・GPU)を決めます
  • LocalQueue: 各 Namespace(チーム)から利用する入り口です。ここに Job を投入し、その先の ClusterQueue のクォータを消費します
kubectl apply -f kueue-setup.yaml

# kueue-setup.yaml
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: default-flavor
# nodeLabels なし = すべてのノードにマッチ
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: gpu-queue
spec:
  namespaceSelector: {}          # すべての namespace から参照可能
  resourceGroups:
    - coveredResources: ["cpu", "memory", "example.com/gpu"]
      flavors:
        - name: default-flavor
          resources:
            - name: "cpu"
              nominalQuota: 16
            - name: "memory"
              nominalQuota: 32Gi
            - name: "example.com/gpu"
              nominalQuota: 4      # クォータは 4 枚
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: team-a
  namespace: default
spec:
  clusterQueue: gpu-queue

作成されたリソースを確認します。まだJobを生成していないため、ClusterQueue と LocalQueue の PENDING WORKLOADS / ADMITTED WORKLOADS はどちらも 0 になっています。

kubectl get resourceflavor
NAME             AGE
default-flavor   6m22s

kubectl get clusterqueue gpu-queue -o wide
NAME        COHORT   STRATEGY         PENDING WORKLOADS   ADMITTED WORKLOADS
gpu-queue            BestEffortFIFO   0                   0

kubectl get localqueue team-a -n default
NAME     CLUSTERQUEUE   PENDING WORKLOADS   ADMITTED WORKLOADS
team-a   gpu-queue      0                   0

Kueue 単体での限界の検証

アドミッションは通るが、配置は保証されない

検証のため、まず GPU を 1 枚占有するダミー Pod をデプロイします。これにより実際に空いている GPU は worker: 1 枚 + worker2: 2 枚 = 3 枚 となります。この状態なら、GPU を 4 枚必要とする Job は admit されないはずです。

kubectl apply -f dummy-hog.yaml

# dummy-hog.yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-hog
  namespace: default
spec:
  nodeName: kueue-volcano-test-worker
  containers:
    - name: hog
      image: registry.k8s.io/pause:3.9
      resources:
        requests:
          example.com/gpu: "1"
        limits:
          example.com/gpu: "1"

kubectl get pod gpu-hog -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP           NODE                        NOMINATED NODE   READINESS GATES
gpu-hog   1/1     Running   0          3s    10.244.1.2   kueue-volcano-test-worker   <none>           <none>

その後、GPU が 4 枚必要なJobをデプロイします。

kubectl apply -f distributed-job.yaml

# distributed-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-training
  namespace: default
  labels:
    kueue.x-k8s.io/queue-name: team-a    # Kueue: この LocalQueue へ投入
spec:
  parallelism: 4
  completions: 4
  suspend: true                         # Kueue が admit するまで待機
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: registry.k8s.io/pause:3.9 # 実際の演算はせずリソースだけ占有(スケジューリング検証用)
          resources:
            requests:
              cpu: "500m"
              memory: "128Mi"
              example.com/gpu: "1"
            limits:
              example.com/gpu: "1"

ところが Workload を確認すると、ADMITTED = True になっています。空きが足りないはずなのに、Kueue はこの Job を受け入れました。

kubectl get workloads -n default
NAME                             QUEUE    RESERVED IN   ADMITTED   FINISHED   AGE
job-distributed-training-ad89a   team-a   gpu-queue     True                  5s

ここで問題になるのは、先ほどデプロイした gpu-hog が Kueue の管理外にあるという点です。gpu-hog は LocalQueue を経由していないため、Kueue はこの Job によるリソース消費を認識しません。Kueue がクォータの消費として数えるのは、ClusterQueue でクォータを予約した Workload だけだからです。

そのため、クラスタの実際の空きが 3 枚に減っていても、Kueue から見れば GPU の 4 枚はすべて未使用であり、このJobは admit されます。この「クォータ上の空き」と「実際の空き」のズレが、これから見る部分配置を引き起こします。

kubectl get pods -n default -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE                         NOMINATED NODE   READINESS GATES
distributed-training-6hx2f   0/1     Pending   0          33s   <none>       <none>                       <none>           <none>
distributed-training-854j2   1/1     Running   0          33s   10.244.1.3   kueue-volcano-test-worker    <none>           <none>
distributed-training-pd7q5   1/1     Running   0          33s   10.244.2.4   kueue-volcano-test-worker2   <none>           <none>
distributed-training-qff6b   1/1     Running   0          33s   10.244.2.3   kueue-volcano-test-worker2   <none>           <none>
gpu-hog                      1/1     Running   0          99s   10.244.1.2   kueue-volcano-test-worker    <none>           <none>

Workload は Admitted(True)であるにもかかわらず、起動できたのは 3 個だけで、残りの 1 個は Pending のままです。Kueue が admit したことで Pod は 4 個作成されましたが、kube-scheduler は Pod を 1 つずつ独立に配置していくため、空いている 3 枚に 3 個だけを載せ、4 個目の Pod はどのノードにも配置できませんでした。結果として、3 個の Pod が GPU 3 枚を占有したまま、4 個目の Pod が配置されるのを待ち続ける状態になります。

分散学習はすべての Pod が起動してはじめて意味を持つため、3 個だけが起動しているこの状態は、GPU 3 枚を無駄にしながら処理も進められないという非効率を生みます。Kueue の役割はアドミッションまでで、その先の配置には関与しないからです。

Kueue の Topology Aware Scheduling による Gang Scheduling

前回の問題は、Kueue がクラスタ全体の総量しか見ていないことでした。TAS を有効にすると、Kueue はノードやトポロジードメイン単位の空き容量を追跡し、全 Pod が収まる配置を見つけられたときにだけ admit します。配置先もそのとき確定します。

まず、ワーカー 2 台を同じ rack として扱うラベルを付けます。

or n in kueue-volcano-test-worker kueue-volcano-test-worker2; do
  kubectl patch node "$n" --subresource=status --type=json \
    -p '[{"op":"add","path":"/status/capacity/example.com~1gpu","value":"2"}]'
  kubectl label node "$n" cloud.provider.com/topology-rack=rack-1
done

kubectl get nodes -o custom-columns='NODE:.metadata.name,GPU:.status.capacity.example\.com/gpu,RACK:.metadata.labels.cloud\.provider\.com/topology-rack'
NODE                           GPU      RACK
kueue-tas-test-control-plane   <none>   <none>
kueue-tas-test-worker          2        rack-1
kueue-tas-test-worker2         2        rack-1

Kueue 単体との違いは、Topology の追加と ResourceFlavor の topologyName の 2 点だけです。

kubectl apply -f kueue-tas-setup.yaml

# kueue-tas-setup.yaml
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: Topology
metadata:
  name: gpu-topology
spec:
  levels:
    - nodeLabel: cloud.provider.com/topology-rack
    - nodeLabel: kubernetes.io/hostname
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: tas-flavor
spec:
  nodeLabels:
    cloud.provider.com/topology-rack: rack-1
  topologyName: gpu-topology          # ★ これで TAS が有効になる
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: gpu-queue
spec:
  namespaceSelector: {}
  resourceGroups:
    - coveredResources: ["cpu", "memory", "example.com/gpu"]
      flavors:
        - name: tas-flavor
          resources:
            - name: "cpu"
              nominalQuota: 16
            - name: "memory"
              nominalQuota: 32Gi
            - name: "example.com/gpu"
              nominalQuota: 4
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: team-a
  namespace: default
spec:
  clusterQueue: gpu-queue

Job 側は Podのテンプレートにアノテーションを 1 行足すだけです。

kubectl apply -f distributed-training-tas.yaml

# distributed-training-tas.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-training-tas
  namespace: default
  labels:
    kueue.x-k8s.io/queue-name: team-a
spec:
  parallelism: 4
  completions: 4
  suspend: true
  backoffLimit: 0
  template:
    metadata:
      annotations:
        kueue.x-k8s.io/podset-required-topology: cloud.provider.com/topology-rack  # ここ
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: registry.k8s.io/pause:3.9
          resources:
            requests:
              cpu: "500m"
              memory: "128Mi"
              example.com/gpu: "1"
            limits:
              example.com/gpu: "1"

kubectl get workloads -n default
NAME                                 QUEUE    RESERVED IN   ADMITTED   FINISHED   AGE
job-distributed-training-tas-29aa5   team-a                                       10s

Podの状態を確認するとgpu-hogのみです。Job の Pod は 1 つも作られていません。TAS はノードの容量からKueue 管理外の Pod による使用量も差し引いて空きを計算します。クォータ上は 4 枚空いていても、配置できないと判断して admit を見送ります。

kubectl get pods -n default
NAME      READY   STATUS    RESTARTS   AGE
gpu-hog   1/1     Running   0          2m16s

kubectl describe workload -n default job-distributed-training-tas-29aa5 | grep -A 10 Events
Events:
  Type     Reason   Age   From             Message
  ----     ------   ----  ----             -------
  Warning  Pending  19m   kueue-admission  couldn't assign flavors to pod set main: topology "gpu-topology" allows to fit only 3 out of 4 pod(s)

gpu-hog を削除してGPU 4 枚をすべて確保します。

kubectl delete pod gpu-hog

ところが、しばらく待っても admit されず、Pod も作れません。GPU は 4 枚とも空いているのに Pending のままです。Kueue は管理外の Pod が消えても何も反応しないためです。

kubectl get workloads -n default
NAME                                 QUEUE    RESERVED IN   ADMITTED   FINISHED   AGE
job-distributed-training-tas-29aa5   team-a                                       3m26s

kubectl get pods -n default -o custom-columns='POD:.metadata.name,NODE:.spec.nodeName,PHASE:.status.phase'
POD   NODE   PHASE

ノードにラベルを付けて更新イベントを起こすと2 秒ほどで admit され、4 個の Pod がすべて Running になりました。

kubectl label node kueue-volcano-test-worker probe=1

kubectl get workloads -n default
NAME                                 QUEUE    RESERVED IN   ADMITTED   FINISHED   AGE
job-distributed-training-tas-29aa5   team-a   gpu-queue     True                  3m34s

kubectl get pods -n default -o custom-columns='POD:.metadata.name,NODE:.spec.nodeName,PHASE:.status.phase'
POD                              NODE                     PHASE
distributed-training-tas-6jj86   kueue-volcano-test-worker    Running
distributed-training-tas-k2gb5   kueue-volcano-test-worker    Running
distributed-training-tas-khmv6   kueue-volcano-test-worker2   Running
distributed-training-tas-rj698   kueue-volcano-test-worker2   Running

Volcano を用いた Gang Scheduling

Gang schedulingを実現するもう 1 つの方法は、admit したうえで配置を別のスケジューラに任せる方法です。Volcano は PodGroup という単位で「最低 N 個そろわなければ 1 つも配置しない」を実現します。

その前に: TAS を無効化する

TAS を有効にしたまま進むと Kueue のadmit段階で止まり、Volcano の出番が来ません。ResourceFlavor を default-flavor に戻し、改めてGPU1枚を占有するdummy Podを生成します。

kubectl apply -f kueue-setup.yaml
clusterqueue.kueue.x-k8s.io/gpu-queue configured

kubectl apply -f dummy-hog.yaml

Volcano のインストール

helm repo add volcano-sh https://volcano-sh.github.io/helm-charts
helm repo update

helm install volcano volcano-sh/volcano -n volcano-system --create-namespace

# scheduler / controllers / admission の確認
kubectl get pods -n volcano-system
NAME                                   READY   STATUS    RESTARTS   AGE
volcano-admission-7798f5c6bc-9n9kl     1/1     Running   0          21s
volcano-controllers-64c9d4fdc7-bbjsf   1/1     Running   0          21s
volcano-scheduler-84f4b669dd-8fvrq     1/1     Running   0          21s

Volcano は 3 つのコンポーネントで構成されます。実際にgang scheduling を行う volcano-scheduler、PodGroup などのカスタムリソースを管理する volcano-controllers、admission webhook として検証やデフォルト値の補完を行う volcano-admission です。3 つとも Running になっていれば準備完了です。

Jobのマニフェスト修正

Volcano に配置を任せるため、Job のマニフェストを次のように変更します。

  • suspend: true: Kueue が admit したあとに Volcano が動作するようにします
  • minMember: 4 の PodGroup を明示的に作成: 4 個未満では配置しないようにします
  • schedulerName: volcano: 配置を Volcano に委ねます
kubectl apply -f volcano-job.yaml

# volcano-job.yaml
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
  name: distributed-training-pg
  namespace: default
spec:
  minMember: 4                         # ★ 4 個未満では配置しない
---
apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-training-volcano
  namespace: default
  labels:
    kueue.x-k8s.io/queue-name: team-a  # Kueue: この LocalQueue へ投入
spec:
  parallelism: 4
  completions: 4
  suspend: true                        # Kueue が admit するまで待機
  template:
    metadata:
      annotations:
        scheduling.k8s.io/group-name: distributed-training-pg
    spec:
      schedulerName: volcano           # 配置は Volcano が担当
      restartPolicy: Never
      containers:
        - name: trainer
          image: registry.k8s.io/pause:3.9
          resources:
            requests:
              cpu: "500m"
              memory: "128Mi"
              example.com/gpu: "1"
            limits:
              example.com/gpu: "1"

クォータ上は 4 枚が空いているため admit されます。ここまでは Kueue 単体と同じです。

ubectl get workloads -n default
NAME                                     QUEUE    RESERVED IN   ADMITTED   AGE
job-distributed-training-volcano-28a5c   team-a   gpu-queue     True       22s

ただし、空きは 3 枚しかないため Volcano は minMember(4)を満たせず、Pod は 1 つもスケジュールされません。PodGroup は Inqueue かつ Unschedulable です。

kubectl get podgroup -n default
NAME                      STATUS    MINMEMBER   RUNNINGS   AGE
distributed-training-pg   Inqueue   4                      4m27s

kubectl describe podgroup -n default distributed-training-pg
...
    Message:               1/4 tasks in gang unschedulable: pod group is not ready, 4 Pending, 4 minAvailable; Pending: 1 Unschedulable, 3 Schedulable. Origin reason is distributed-training-volcano-kc2xq: 0/3 nodes are unavailable: 3 Insufficient example.com/gpu.
    Reason:                NotEnoughResources
    Status:                True
    Type:                  Unschedulable
  Phase:                   Inqueue
Events:
  Type     Reason         Age              From     Message
  ----     ------         ----             ----     -------
  Warning  Unschedulable  1s (x9 over 9s)  volcano  4/4 tasks in gang unschedulable: pod group is not ready, 4 Pending, 4 minAvailable; Pending: 1 Unschedulable, 3 Schedulable. Origin reason is distributed-training-volcano-rd2jp: 0/3 nodes are unavailable: 3 Insufficient example.com/gpu.

Pod は「3 個 Running / 1 個 Pending」ではなく、全 Pod が Pending です。

kubectl get pods -n default -l job-name=distributed-training-volcano -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP       NODE     NOMINATED NODE   READINESS GATES
distributed-training-volcano-c46lh   0/1     Pending   0          65s   <none>   <none>   <none>           <none>
distributed-training-volcano-j8pfk   0/1     Pending   0          65s   <none>   <none>   <none>           <none>
distributed-training-volcano-jdlqj   0/1     Pending   0          65s   <none>   <none>   <none>           <none>
distributed-training-volcano-kc2xq   0/1     Pending   0          65s   <none>   <none>   <none>           <none>

schedulerName も確認します。kube-scheduler なら 3 個が Running になっていたはずです。

kubectl get pods -n default -l job-name=distributed-training-volcano -o custom-columns='POD:.metadata.name,SCHED:.spec.schedulerName,NODE:.spec.nodeName,PHASE:.status.phase'
POD                                  SCHED     NODE     PHASE
distributed-training-volcano-c46lh   volcano   <none>   Pending
distributed-training-volcano-j8pfk   volcano   <none>   Pending
distributed-training-volcano-jdlqj   volcano   <none>   Pending
distributed-training-volcano-kc2xq   volcano   <none>   Pending

gpu-hog を削除して 4 枚すべてを空けると、Job は正常に配置されます。

kubectl delete -f dummy-hog.yaml

kubectl get pods -n default -l job-name=distributed-training-volcano -o custom-columns='POD:.metadata.name,SCHED:.spec.schedulerName,NODE:.spec.nodeName,PHASE:.status.phase'
POD                                  SCHED     NODE                         PHASE
distributed-training-volcano-c46lh   volcano   kueue-volcano-test-worker2   Running
distributed-training-volcano-j8pfk   volcano   kueue-volcano-test-worker2   Running
distributed-training-volcano-jdlqj   volcano   kueue-volcano-test-worker    Running
distributed-training-volcano-kc2xq   volcano   kueue-volcano-test-worker    Running

PodGroup 側も Unschedulable が解消され、minMember の 4 個がそろって配置されたことが分かります。

kubectl get podgroup -n default
NAME                      STATUS    MINMEMBER   RUNNINGS   AGE
distributed-training-pg   Running   4           4          5m31s

kubectl describe podgroup -n default distributed-training-pg
...
    Reason:                tasks in gang are ready to be scheduled
    Status:                True
    Type:                  Scheduled
  Phase:                   Running
  Running:                 4
Events:
  Type     Reason         Age                 From     Message
  ----     ------         ----                ----     -------
  Warning  Unschedulable  41s (x25 over 65s)  volcano  4/4 tasks in gang unschedulable: pod group is not ready, 4 Pending, 4 minAvailable; Pending: 1 Unschedulable, 3 Schedulable. Origin reason is distributed-training-volcano-rd2jp: 0/3 nodes are unavailable: 3 Insufficient example.com/gpu.
  Normal   Scheduled      11s

これにより部分配置の問題は解消されました。しかし、この待機中も Workload のステータスは ADMITTED = True のままとなります。つまり、Pod が 1 つも稼働していない状態でも、Kueue 上ではすでに GPU 4 枚が消費されていると見なされている点に注意が必要です。

3 つの構成の比較

構成Admit判断の時点クォータ消費Podの結果
Kueue単体(クォータのみ)Trueadmit時の1回(総量のみ)4枚3 Running / 1 Pending(部分配置)
Kueue + TASFalseadmit時の1回(総量 + 配置)0枚Pod が 1 つも作られない
Kueue + VolcanoTrueスケジューリングのたび4枚(占有したまま何も進まない)0 Running / 4 Pending

注目すべきは Kueue + Volcano の行です。All or Nothing は守られますが、クォータ 4 枚を占有したまま Pod は 0 個で、その間ほかの Job はこのクォータを使えません。Kueue はクォータだけを見て admit し、Volcano は配置できないと判断します。2 つのツールが互いを知らないからです。TAS では判断が 1 つなので、この隙間がありません。 

もう 1 つは「判断の時点」です。TAS は一度計算して nodeSelector で配置先を固定します。Volcano はスケジューラそのものなので判定はループになり、クラスタの状態が変われば追随します。admit のあとに状態が変わると、TAS では Pod が Pending のまま取り残されることがあります。

おわりに

今回はkind 環境の Fake GPU で 3 つの構成を比較しました。Kueue 単体ではクラスタ全体の総量しか見ないため、「admit されたのに配置できない」という状態が発生してしまうことが分かりました。

Kueue の TAS は、admit の判断にノード単位の配置検査を加えます。配置できない Job は admit されないので、クォータは空いたままです。追加のコンポーネントも要りません。ただし判断は admit 時の 1 回きりで、配置は nodeSelector で固定されます。スケジューリングレイヤーの機能ではないため、状態変化による隙間が生じうる「近似的(擬似的)な Gang Scheduling」と言えます。

Volcano は配置の実行そのものを担います。全 Pod がそろうかどうかをスケジューリングのたびに判定するため、クラスタの状態が変われば追随します。Kueue の管理外のワークロードにも適用できます。ただし Kueue の admit とは独立に動くため、クォータを押さえたまま何も進まない時間が生まれます。

Kueue は「いつ実行を始めてよいか」を、Volcano は「どこに配置するか」を担当します。この分担は TAS を入れても変わりません。TAS が変えたのは、admit の判断が配置まで見るようになったことでした。KueueとVolcanoは担当するレイヤーがそもそも異なります。どちらが優れているという話ではなく、適材適所で使い分けるのがよいでしょう。

【参考資料】

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored