Kubernetes 1.37 Gang Scheduling - Το Workload API, ένα field που χάνεται σιωπηλά και το σημείο όπου σταματά το preemption
~/posts/kubernetes-gang-scheduling-workload-api.el.md19 min · 3845 words

Kubernetes 1.37 Gang Scheduling - Το Workload API, ένα field που χάνεται σιωπηλά και το σημείο όπου σταματά το preemption

// Το KEP-4671 έφερε το gang scheduling σε beta στο Kubernetes 1.37, πίσω από ένα feature gate που είναι κλειστό by default. Ένα Kind cluster τεσσάρων nodes με έξι ψεύτικες DRA GPUs δείχνει το partial-placement deadlock, το ένα field που το λύνει, και τα δύο όρια που δεν τεκμηριώνει κανείς: το field του Job κόβεται σιωπηλά αν λείπει ένα δεύτερο, alpha gate, και το workload-aware preemption δεν μπορεί να ανακτήσει DRA devices.

$ date

Δίνεις σε ένα training job με τέσσερα ranks ένα cluster με έξι GPUs. Ο scheduler τοποθετεί τρία ranks, ξεμένει από devices και αφήνει το τέταρτο Pending. Έρχεται ένα δεύτερο job με τέσσερα ranks και παίρνει τις άλλες τρεις. Τώρα έχεις έξι GPUs δεσμευμένες, δύο pods Pending για πάντα και μηδέν jobs που κάνουν training. Τίποτα δεν έχει χαλάσει. Κάθε pod που μπορούσε να γίνει schedule, έγινε. Αυτό ακριβώς είναι το πρόβλημα.

Αυτό λέγεται partial-placement deadlock, και μέχρι πρόσφατα το Kubernetes δεν είχε απάντηση in tree. Κατέληγες στο Volcano, στο Kueue ή στο coscheduling plugin, και δεχόσουν έναν δεύτερο scheduler ή έναν controller που έβαζε τη δουλειά σε queue έξω από το API που είχες ήδη.

Το KEP-4671 το αλλάζει αυτό. Το Kubernetes v1.37, που κυκλοφόρησε στις 2026-08-26, περνάει τα APIs Workload και PodGroup σε beta, μαζί με το workload-aware preemption. Το all-or-nothing placement είναι πλέον ένα field πάνω στο Job.

Είναι επίσης κλειστό by default, και το κομμάτι που το κάνει εύχρηστο είναι alpha και αποτυγχάνει χωρίς να σου πει τίποτα. Έστησα το deadlock σε ένα laptop, το έλυσα με ένα field, και μετά έφαγα περισσότερη ώρα απ’ όση θέλω να παραδεχτώ για να βρω γιατί το field δεν έκανε τίποτα την πρώτη φορά.

Προέλευση: κάθε default των gates, κάθε σχήμα του API και κάθε milestone παρακάτω είναι διαβασμένο από το kubernetes/kubernetes στο release-1.37 και από το κείμενο του KEP, με παραπομπή σε file:line. Κάθε output εντολής είναι από ένα Kind cluster τεσσάρων nodes που τρέχει kindest/node:v1.37.0, με τον dra-example-driver v0.5.0 να δηλώνει δύο ψεύτικες GPUs ανά node. Όπου δεν επαλήθευσα κάτι, το λέω.

Ποιος πρέπει να το διαβάσει;#

Το post απευθύνεται σε:

  • Platform Engineers που τρέχουν batch ή training workloads και κουβαλάνε το Volcano ή το Kueue αποκλειστικά για τα gang semantics, και θέλουν να ξέρουν αν το in-tree είναι έτοιμο
  • SREs που έχουν στην ευθύνη τους ένα κοινόχρηστο GPU pool και έχουν δει accelerators να κάθονται δεσμευμένοι και άεργοι πίσω από ένα job που δεν θα φτάσει ποτέ σε quorum
  • Όποιον σχεδιάζει upgrade από 1.36 σε 1.37 με τα alpha gates του gang scheduling ήδη ανοιχτά, γιατί δύο από αυτά άλλαξαν όνομα ή σχήμα με τρόπο που θα σου σπάσει το config
  • Όποιον χτίζει πάνω σε DRA, αφού η ιστορία του gang scheduling και η ιστορία του DRA συναντιούνται σε ένα ακριβώς σημείο, και εκεί υπάρχει κενό

TL;DR#

  • Το gang scheduling είναι beta στο 1.37 και απενεργοποιημένο by default. Ένα gate, το GenericWorkload, καλύπτει πλέον το Workload API, το gang scheduling και το workload-aware preemption. Μπαίνει σε kube-apiserver, kube-scheduler και kube-controller-manager.
  • Τα gates GangScheduling και WorkloadAwarePreemption συγχωνεύτηκαν στο GenericWorkload στο 1.37. Αν τα έχεις στο config σου, βγάλ’ τα πριν το upgrade.
  • Το field που θέλεις στην πράξη, το Job.spec.scheduling.schedulingPolicy.gang.minCount, χρειάζεται ένα δεύτερο gate, το WorkloadWithJob, που είναι alpha και κλειστό. Χωρίς αυτό το field κόβεται σιωπηλά: το kubectl apply λέει ότι πέτυχε, κανένα warning, κανένα event, και δεν αλλάζει τίποτα.
  • Το workload-aware preemption δεν ανακτά DRA devices στο 1.37. Μπορείς να κάνεις gang scheduling σε GPUs που έχουν δοθεί μέσω DRA. Δεν μπορείς να τους κάνεις gang preemption. Τα node-local extended resources γίνονται preempt κανονικά.
  • Το default disruptionMode είναι single. Το gang είναι all-or-nothing στο admission, όχι στο eviction. Ένα gang με υψηλότερο priority θα διαλύσει ένα gang που τρέχει, pod προς pod, εκτός αν βάλεις disruptionMode: all.
  • Μαζί με το beta έρχονται τρία νέα metrics του scheduler, όλα με πρόθεμα scheduler_podgroup_.

Τι έφερε στην πράξη το KEP-4671#

Δύο objects και ένα field στο pod.

ObjectAPIΤο δημιουργείΣκοπός
Workloadscheduling.k8s.io/v1beta1Ένας workload controller (Job, LWS, JobSet)Το template. Δηλώνει ένα ή περισσότερα podGroupTemplates, το καθένα με δικό του scheduling policy
PodGroupscheduling.k8s.io/v1beta1Ο ίδιος controller, από το templateΤο runtime instance. Αυτό πάνω στο οποίο αποφασίζει πραγματικά ο scheduler
pod.spec.schedulingGroup.podGroupNamecore/v1Ο controller, σε κάθε podΟ pointer από ένα pod στο group του

Ο διαχωρισμός έχει σημασία. Το Workload είναι το σχήμα που δηλώνεις μία φορά. Το PodGroup είναι το instance που ο scheduler δέχεται ή απορρίπτει ως ενιαίο σύνολο. Από το staging/src/k8s.io/api/scheduling/v1beta1/types.go, το ίδιο το policy είναι όσο μικρό γίνεται:

type PodGroupSchedulingPolicy struct {
	Basic *BasicSchedulingPolicy `json:"basic,omitempty"`
	Gang  *GangSchedulingPolicy  `json:"gang,omitempty"`
}

type GangSchedulingPolicy struct {
	MinCount int32 `json:"minCount"`
}

Αυτό είναι όλο το feature. Ή τοποθετούνται minCount pods μαζί ή κανένα.

Εξίσου μικρή είναι και η πλευρά του pod, από το staging/src/k8s.io/api/core/v1/types.go:4885:

type PodSchedulingGroup struct {
	PodGroupName *string `json:"podGroupName,omitempty"`
}

Η ροή#

flowchart TB
    subgraph User["What you write"]
        J["Job
        spec.scheduling.schedulingPolicy.gang.minCount: 4"]
    end

    subgraph Controllers["kube-controller-manager"]
        W["Workload
        podGroupTemplates[0]"]
        PG["PodGroup
        schedulingPolicy.gang.minCount: 4"]
    end

    subgraph Pods["Pods"]
        P0["rank 0
        spec.schedulingGroup.podGroupName"]
        P1["rank 1"]
        P2["rank 2"]
        P3["rank 3"]
    end

    subgraph Sched["kube-scheduler"]
        D{"Can all 4
        be placed?"}
        Y["Bind all 4"]
        N["Bind none
        PodGroup Unschedulable"]
    end

    J --> W --> PG
    J --> P0 & P1 & P2 & P3
    P0 & P1 & P2 & P3 --> D
    PG --> D
    D -->|yes| Y
    D -->|no| N

Τα feature gates, και αυτό που θα σου φάει το απόγευμα#

Αυτό είναι το κομμάτι που τα release notes προσπερνάνε. Κάθε gate σε αυτή την περιοχή είναι κλειστό by default στο 1.37, και δεν είναι ανεξάρτητα μεταξύ τους. Διαβασμένα από το pkg/features/kube_features.go στο release-1.37:

GateΣτάδιο στο 1.37DefaultΕξαρτάται απόΠηγή
GenericWorkloadBetafalseκανέναkube_features.go:1622-1625
WorkloadWithJobAlpha (από το 1.36)falseGenericWorkloadkube_features.go:2308-2310, :2922
DRAWorkloadResourceClaimsBetafalseκανέναkube_features.go:1545-1548
CompositePodGroupAlphafalseκανέναkube_features.go:1429-1431
PodGroupPreemptionPolicyAlphafalseκανέναkube_features.go:1956-1958
TopologyAwareWorkloadSchedulingAlpha (από το 1.36)falseκανέναkube_features.go:2231-2233

Το GenericWorkload σου δίνει το API. Δεν σου δίνει το field του Job.

Το WorkloadWithJob είναι το KEP-5547, και ο κώδικας το περιγράφει ως αυτό που επιτρέπει «the Job controller to automatically create Workload and PodGroup objects for Jobs that qualify for gang scheduling». Είναι alpha, είναι κλειστό, και χωρίς αυτό το field του Job δεν βγάζει error. Εξαφανίζεται.

$ kubectl apply -f 20-gang.yaml
job.batch/train-a created
job.batch/train-b created

$ kubectl -n ml-team get job train-a -o jsonpath='{.spec.scheduling}'
                                      # nothing. the field is gone.

Κανένα warning, κανένα event, κανένα condition. Το Job τρέχει με την παλιά συμπεριφορά, pod προς pod, κι εσύ ψάχνεις ένα bug στον scheduler που δεν υπάρχει.

Γιατί έχει σημασία: ένα beta feature που έχει τη βασική του ευχρηστία πίσω από ένα alpha gate είναι ένα feature που οι περισσότεροι θα δοκιμάσουν μία φορά, θα βγάλουν το συμπέρασμα ότι είναι χαλασμένο και θα το παρατήσουν. Αν το αξιολογείς στο 1.37, άνοιξε και τα δύο gates, αλλιώς δεν το αξιολογείς καθόλου.

Upgrade από το 1.36#

Δύο πράγματα θα σε δαγκώσουν, και τα δύο από τα upgrade notes του ίδιου του KEP (README.md:1564-1572):

  1. Το GangScheduling δεν υπάρχει πια. Συγχωνεύτηκε στο GenericWorkload, μαζί με το WorkloadAwarePreemption από το KEP-5710. Βγάλ’ το από το config των feature gates πριν το upgrade. Σε downgrade στο 1.36 πρέπει να το ξανανοίξεις με το χέρι.
  2. Το scheduling.k8s.io/v1alpha2 έφυγε, και στη θέση του ήρθε το v1alpha3. Σβήσε κάθε v1alpha2 resource πριν το upgrade. Δεν υπάρχει conversion προς τα πίσω από το v1alpha3.

Γιατί όχι Kueue, Volcano ή το coscheduling plugin;#

ΚριτήριοKEP-4671 (in tree)KueueVolcanoCoscheduling plugin
Schedulerkube-schedulerkube-schedulerΔικός τουkube-scheduler + plugin
ΕγκατάστασηΈνα feature gateΈνας controller + CRDsΈνας scheduler + controllersBuild ή config του scheduler
Gang semanticsminCount στο pod groupΜέσω του scheduler που έχει από κάτωNative PodGroupNative PodGroup
Quota / fair shareΚαθόλουΝαι, αυτή είναι η δουλειά τουΝαιΌχι
PreemptionWorkload-aware, σε επίπεδο clusterΤο αναθέτει αλλούNativeΜόνο σε επίπεδο pod
Ωριμότητα στο 1.37Beta, gates κλειστάStable, v0.19.xΏριμοΏριμο, αντικαθίσταται από αυτό το KEP

Το KEP δηλώνει ρητά, στο kep.yaml, ότι replaces το KEP-583 (coscheduling) και το KEP-5832. Αυτό που αποσύρεται εδώ είναι το coscheduling plugin.

Το Kueue δεν αποσύρεται, και το KEP το λέει ευθέως: βραχυπρόθεσμα ο στόχος είναι το Kueue να γνωρίζει το workload-aware scheduling, και μακροπρόθεσμα να το χρησιμοποιεί ως engine του. Το Kueue σου δίνει queues, quota και fair sharing. Το KEP-4671 σου δίνει atomic placement. Είναι διαφορετικά layers.

Με δυο λόγια: αν το μόνο που ήθελες από το Volcano ήταν τα gang semantics, το 1.37 σε αφήνει να το σβήσεις. Αν ήθελες quota και fair sharing, το Kueue εξακολουθεί να σου χρειάζεται, και θα κάθεται πάνω από αυτό, όχι δίπλα του.

Τι ΔΕΝ είναι#

  • Δεν είναι σύστημα για queueing. Δεν έχει quota, fair share, borrowing ή admission με βάση το priority, πέρα από το preemption. Ένα PodGroup που απορρίφθηκε ξαναδοκιμάζει. Δεν περιμένει σε queue μαζί με άλλα.
  • Δεν είναι ακόμα topology-aware. Το TopologyAwareWorkloadScheduling είναι ξεχωριστό alpha gate. Το να τοποθετήσεις ένα gang μαζί δεν είναι το ίδιο με το να το τοποθετήσεις στο ίδιο rack.
  • Δεν λύνει το preemption για DRA. Το βλέπουμε παρακάτω, και είναι το μεγαλύτερο κενό.
  • Δεν είναι ανοιχτό by default. Αξίζει να το ξαναπώ, γιατί το «beta» συνήθως υπονοεί το αντίθετο.

Το demo: έξι ψεύτικες GPUs, καθόλου hardware#

Τέσσερα Kind nodes, dra-example-driver v0.5.0 με numDevices: 2, άρα τρεις workers που δηλώνουν δύο ψεύτικες GPUs ο καθένας. Έξι devices συνολικά. Δύο jobs, τέσσερα ranks το καθένα, μία GPU ανά rank. Ζήτηση οκτώ, προσφορά έξι.

Το config του cluster έχει και τα τρία gates:

# kubernetes/cluster/kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: gang-demo

featureGates:
  # API + gang + workload-aware preemption
  GenericWorkload: true
  # without it spec.scheduling is pruned silently
  WorkloadWithJob: true
  # shared claims per PodGroup
  DRAWorkloadResourceClaims: true

runtimeConfig:
  "scheduling.k8s.io/v1beta1": "true"

nodes:
  - role: control-plane
    image: kindest/node:v1.37.0
  - role: worker
    image: kindest/node:v1.37.0
  - role: worker
    image: kindest/node:v1.37.0
  - role: worker
    image: kindest/node:v1.37.0

Πριν από οτιδήποτε άλλο, επιβεβαίωσε ότι το API σερβίρεται πραγματικά. Αν αυτό βγει άδειο, ό,τι ακολουθεί αποτυγχάνει αθόρυβα:

$ kubectl api-resources --api-group=scheduling.k8s.io
NAME              SHORTNAMES   APIVERSION                  NAMESPACED   KIND
podgroups                      scheduling.k8s.io/v1beta1   true         PodGroup
priorityclasses   pc           scheduling.k8s.io/v1        false        PriorityClass
workloads                      scheduling.k8s.io/v1beta1   true         Workload

$ kubectl get resourceslices \
    -o jsonpath='{range .items[*]}{.spec.nodeName}{": "}{range .spec.devices[*]}{.name}{" "}{end}{"\n"}{end}'
gang-demo-worker: gpu-0 gpu-1
gang-demo-worker2: gpu-0 gpu-1
gang-demo-worker3: gpu-0 gpu-1

Παράδειγμα 1: το deadlock#

Δύο συνηθισμένα Jobs, χωρίς scheduling policy. Κάθε rank παίρνει μία GPU μέσω ενός ResourceClaimTemplate:

# kubernetes/workloads/10-no-gang.yaml
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: one-gpu
  namespace: ml-team
spec:
  spec:
    devices:
      requests:
        - name: gpu
          exactly:
            deviceClassName: gpu.example.com
            allocationMode: ExactCount
            count: 1
---
apiVersion: batch/v1
kind: Job
metadata:
  name: train-a
  namespace: ml-team
spec:
  completions: 4
  parallelism: 4
  backoffLimit: 0
  completionMode: Indexed
  template:
    spec:
      restartPolicy: Never
      resourceClaims:
        - name: gpu
          resourceClaimTemplateName: one-gpu
      containers:
        - name: rank
          image: registry.k8s.io/e2e-test-images/busybox@sha256:0ffbe172f8d245c83f285c6992b452c53d085661e03ddfd3b484332026e6c8bb
          command: ["sh", "-c", "echo rank $JOB_COMPLETION_INDEX holding a GPU; sleep 3600"]
          resources:
            requests: { cpu: 50m, memory: 32Mi }
            limits: { cpu: 100m, memory: 64Mi }
            claims:
              - name: gpu

Μετά το apply, με το train-b πανομοιότυπο:

$ kubectl -n ml-team get pods \
    -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName
NAME              STATUS    NODE
train-a-0-g5dg8   Running   gang-demo-worker
train-a-1-wmbm2   Running   gang-demo-worker2
train-a-2-6gxvp   Running   gang-demo-worker
train-a-3-fn2n9   Pending   <none>
train-b-0-ccwqd   Running   gang-demo-worker3
train-b-1-z2rjp   Running   gang-demo-worker3
train-b-2-s67tv   Running   gang-demo-worker2
train-b-3-kdwvr   Pending   <none>

Τρία και τρία. Έξι GPUs δεσμευμένες, έξι containers που τρέχουν, και κανένα τους δεν μπορεί να κάνει ούτε ένα all-reduce, γιατί κανένα από τα δύο jobs δεν έχει τέσσερα ranks. Ο scheduler δεν έχει μπερδευτεί:

FailedScheduling: 0/4 nodes are available: 1 node(s) had untolerated taint(s),
3 cannot allocate all claims. preemption: 0/4 nodes are available:
4 Preemption is not helpful for scheduling.

Αποτέλεσμα: 6/6 GPUs πιασμένες, 0 jobs που μπορούν να κάνουν training, και πουθενά ένα error που να σου το λέει.

Μια ειλικρινής επιφύλαξη: αυτή η μοιρασιά δεν είναι deterministic. Σε επόμενο τρέξιμο των ίδιων manifests το train-a πήρε και τα τέσσερα ranks, ενώ το train-b έμεινε με δύο Running και δύο Pending, δηλαδή ένα job που κάνει training και ένα κολλημένο. Χωρίς gang semantics το αποτέλεσμα εξαρτάται από τη σειρά με την οποία φτάνουν τα pods. Καμιά φορά ένα job στέκεται τυχερό. Αυτό είναι και το πραγματικό επιχείρημα υπέρ του feature: βάζει μια εγγύηση στη θέση της τύχης.

Παράδειγμα 2: το ένα field#

Τα ίδια δύο jobs. Ένα block παραπάνω:

spec:
  completions: 4
  parallelism: 4
  scheduling:
    schedulingPolicy:
      gang:
        minCount: 4
$ kubectl -n ml-team get podgroups
NAME                                         POLICY   WORKLOAD             STATUS          AGE
train-a-88856b5bc-train-a-pgt-0-cc865b9db    Gang     train-a-88856b5bc    Scheduled       93s
train-b-66c995d6cb-train-b-pgt-0-fc65464f8   Gang     train-b-66c995d6cb   Unschedulable   93s

$ kubectl -n ml-team get pods --no-headers \
    -o 'custom-columns=N:.metadata.labels.app\.kubernetes\.io/name,S:.status.phase' \
  | sort | uniq -c
   4 train-a   Running
   4 train-b   Pending

Ο Job controller έφτιαξε ένα Workload και ένα PodGroup ανά job, έδεσε κάθε pod με το group του, και ο scheduler δέχτηκε το ένα gang και απέρριψε το άλλο ολόκληρο:

PodGroupInitiallyScheduled=False  Unschedulable
  minCount (4) cannot be satisfied: 2 scheduled, 1 remaining,
  pod group preemption: No preemption victims found for incoming preemptor

Αποτέλεσμα: τέσσερις GPUs που κάνουν πραγματική δουλειά, δύο ελεύθερες για ό,τι έρθει μετά, και ένα PodGroup που το status του λέει ξεκάθαρα γιατί περιμένει το δεύτερο job.

GPUs δεσμευμένεςJobs σε trainingGPUs διαθέσιμες
Χωρίς gang policy6 / 600
gang.minCount: 44 / 612

Σημείωση: ο counter σε αυτό το μήνυμα δεν συμφωνούσε με το minCount σε κανένα από τα τρεξίματά μου («2 scheduled, 1 remaining» απέναντι σε minCount 4). Η συμπεριφορά του scheduling ήταν σωστή κάθε φορά. Δεν κυνήγησα την αριθμητική του μηνύματος, οπότε πάρε το κείμενο ως ενδεικτικό και το status του condition ως αυτό που ισχύει.

Το preemption, και τα δύο σημεία όπου σταματά#

Εδώ έχασα το απόγευμα, και εδώ το post βγάζει τα λεφτά του.

Δεν ανακτά DRA devices#

Με το train-a να κρατάει τέσσερις GPUs σε priority 100 και δύο GPUs ελεύθερες, πρόσθεσα ένα τρίτο gang σε priority 10000. Θα έπρεπε να κάνει preempt. Δεν κάνει:

$ kubectl -n ml-team get podgroup \
    -o jsonpath='{range .items[*]}{.metadata.name}{" priority="}{.spec.priority}{" "}{range .status.conditions[*]}{.reason}{end}{"\n"}{end}'
train-a-...      priority=100     Scheduled
train-b-...      priority=100     Unschedulable
train-urgent-... priority=10000   Unschedulable

Το priority πέρασε σωστά. Το gang preemption έτρεξε. Δεν βρήκε victims. Ο λόγος βρίσκεται στο KEP-5710, σε μια λίστα με γνωστά κενά:

Fixing global constraint tracking for nominations will be pursued as a separate enhancement, most likely before KEP-5690: DRA Workload Resource Claims starts supporting Workload-Aware Preemption (since DRA resources are often not node-local).

Το workload-aware preemption δουλεύει σε επίπεδο cluster, αλλά τα nominations εξακολουθούν να παρακολουθούνται ανά node. Τα DRA devices συχνά δεν είναι node-local. Άρα η υποστήριξη του DRA στο workload-aware preemption είναι μελλοντική δουλειά, που παρακολουθείται στο KEP-5690.

Για να επιβεβαιώσω ότι το όριο αφορά ειδικά το DRA και δεν είναι απλώς χαλασμένο το preemption, δήλωσα στη θέση του ένα απλό node-local extended resource:

$ kubectl patch node gang-demo-worker --subresource=status --type=json \
    -p '[{"op":"add","path":"/status/capacity/example.com~1accel","value":"2"}]'

Τα ίδια δύο gangs, τα ίδια priorities, example.com/accel: 1 ανά rank αντί για ResourceClaim. Το preemption πυροδοτείται αμέσως:

accel-research-0-thdnd   Preempted by podgroup 11a0e2df-a4c4-4c67-a0dc-04a479f774ba on node cluster
accel-research-3-dn4gv   Preempted by podgroup 11a0e2df-a4c4-4c67-a0dc-04a479f774ba on node cluster

Πρόσεξε το on node cluster. Αυτό είναι το cluster-wide scope που δουλεύει όπως σχεδιάστηκε.

Αποτέλεσμα: στο 1.37 μπορείς να κάνεις gang scheduling σε DRA GPUs. Δεν μπορείς να τους κάνεις gang preemption. Αν το GPU pool σου στηρίζεται στο preemption για να έχει το production προτεραιότητα έναντι του research, αυτό το feature δεν σε καλύπτει ακόμα.

Το gang είναι atomic στο admission, όχι στο eviction#

Ξανακοίτα εκείνο το output. Δύο pods έγιναν preempt, από ένα gang των τεσσάρων. Τα άλλα δύο συνέχισαν να τρέχουν, κρατώντας accelerators, χωρίς να μπορούν να φτάσουν σε quorum. Το ίδιο ακριβώς deadlock που το feature υπάρχει για να αποτρέπει, μόνο που εδώ φτάσαμε από την ανάποδη.

Ο λόγος είναι ένα default:

$ kubectl -n ml-team get podgroups -o custom-columns=NAME:.metadata.name,DISRUPT:.spec.disruptionMode
NAME                       DISRUPT
accel-research-...         map[single:map[]]

Το disruptionMode είναι by default single. Η λύση είναι ένα ακόμα block στο Job:

  scheduling:
    schedulingPolicy:
      gang:
        minCount: 4
    # evict the gang as a unit
    disruptionMode:
      all: {}

Το ίδιο σενάριο με all, μετρώντας τα events Preempted για τα pods του research gang:

accel-research-0-xrh27
accel-research-1-d4k2w
accel-research-2-rvs82
accel-research-3-849fs

Και τα τέσσερα, ως ενιαίο σύνολο.

Συμβουλή: βλέπε το disruptionMode: all ως κομμάτι του config του gang, όχι ως προαιρετικό έξτρα. Το minCount χωρίς αυτό σου εξασφαλίζει ένα atomic ξεκίνημα και έναν μη atomic θάνατο, που για ένα distributed training job σημαίνει ότι σου γυρίζει πίσω το μεγαλύτερο μέρος του προβλήματος.

Κάτι ακόμα που αξίζει να ξέρεις: ολόκληρο το block spec.scheduling είναι immutable μετά τη δημιουργία. Μόνο το schedulingPolicy.gang.minCount μπορεί να αλλάξει. Αν το κάνεις λάθος, ξαναφτιάχνεις το Job:

The Job "accel-research" is invalid: spec.scheduling.disruptionMode:
Invalid value: null: field is immutable

Metrics#

Με το πέρασμα σε beta έρχονται τρία metrics, που αναφέρονται στο kep.yaml. Το image του scheduler είναι distroless, οπότε κάν’ του scrape μέσω port-forward με το client certificate του admin:

$ curl -sk --cert c.crt --key c.key https://127.0.0.1:10259/metrics | grep '^scheduler_podgroup'
scheduler_podgroup_schedule_attempts_total{profile="default-scheduler",result="scheduled"} 2
scheduler_podgroup_schedule_attempts_total{profile="default-scheduler",result="unschedulable"} 22
scheduler_podgroup_scheduling_algorithm_duration_seconds_count 24
scheduler_podgroup_scheduling_algorithm_duration_seconds_sum 0.039103

Ο counter unschedulable είναι αυτός πάνω στον οποίο θέλεις alert. Ένα gang που δεν μπορεί να τοποθετηθεί ξαναδοκιμάζει συνεχώς, άρα ένα result="unschedulable" που ανεβαίνει με το result="scheduled" στάσιμο σημαίνει είτε pool που έχει γεμίσει είτε minCount που δεν μπορεί να ικανοποιήσει κανείς.

MetricΣε τι το χρησιμοποιείς
scheduler_podgroup_schedule_attempts_totalAlert όταν το result="unschedulable" μεγαλώνει χωρίς αντίστοιχο scheduled
scheduler_podgroup_scheduling_attempt_duration_secondsEnd-to-end latency μιας προσπάθειας, μαζί με τις αναμονές
scheduler_podgroup_scheduling_algorithm_duration_secondsΤο κόστος του gang placement από την πλευρά του scheduler

Υπέρ και κατά#

Υπέρ#

ΠλεονέκτημαΠεριγραφή
Χωρίς δεύτερο schedulerGang semantics από τον kube-scheduler. Ούτε deployment του Volcano, ούτε scheduler name σε κάθε pod
Ένα fieldgang.minCount σε ένα Job που έχεις ήδη. Το diff σε σχέση με ένα manifest που δουλεύει είναι τέσσερις γραμμές
Ειλικρινές statusΤα conditions του PodGroup λένε γιατί ένα gang δεν έχει τοποθετηθεί, αντί να σε αφήνουν να το μαντέψεις από τα Pending pods
Preemption σε επίπεδο clusterΤο workload-aware preemption αποφασίζει για ολόκληρο το group, όχι για ένα node τη φορά
Ανεξάρτητο από controllerΤα Workload και PodGroup είναι φτιαγμένα για Job, LeaderWorkerSet και JobSet, όχι μόνο για Job
Αποσύρει το coscheduling pluginΤο kep.yaml έχει το KEP-583 κάτω από το replaces. Ένα out-of-tree build του scheduler λιγότερο να κουβαλάς

Κατά#

ΠεριορισμόςΠεριγραφή
Καθόλου preemption για DRAΤο μεγαλύτερο κενό με διαφορά. Gang scheduling σε DRA GPUs ναι, gang preemption όχι, και οι GPUs είναι ο βασικός λόγος που το θέλει κανείς
Δύο gates, το ένα alphaΤο field του Job θέλει το WorkloadWithJob, που είναι alpha και κλειστό, και όταν λείπει το field κόβεται σιωπηλά
Το default του disruptionMode είναι λάθος για gangsBy default single, που διαλύει ένα gang που τρέχει όταν γίνει preemption
Ούτε quota ούτε fair sharingΠαραμένει δουλειά του Kueue. Εδώ έχεις atomic placement, τίποτα παραπάνω
Immutable configΤο spec.scheduling δεν μπορεί να προστεθεί, να αφαιρεθεί ή να αλλάξει σχήμα σε Job που υπάρχει ήδη
Όχι topology-awareΞεχωριστό alpha gate. Το μαζί δεν είναι το ίδιο με το κοντά
Κλειστό by defaultBeta, αλλά τίποτα δεν δουλεύει μέχρι να ανοίξεις gates σε τρία components

Πότε να το χρησιμοποιήσεις, και πότε όχι#

Χρησιμοποίησέ το όταν:

  • Τρέχεις distributed training ή jobs τύπου MPI, όπου το μερικό placement δεν αξίζει τίποτα
  • Έχεις ήδη τον kube-scheduler και θέλεις να ξεφορτωθείς το Volcano, που το κουβαλάς μόνο για τα gang semantics
  • Το resource που σου λείπει είναι node-local: CPU, memory ή ένα extended resource όπως μια GPU από device plugin
  • Είσαι σε 1.37+ και μπορείς να ορίσεις feature gates στο control plane, πράγμα που αποκλείει τα περισσότερα managed control planes σήμερα

Μην το χρησιμοποιήσεις όταν:

  • Χρειάζεσαι quota, fair sharing ή borrowing ανάμεσα σε ομάδες. Πάρε το Kueue
  • Οι GPUs σου έρχονται μέσω DRA και στηρίζεσαι στο preemption. Περίμενε το KEP-5690
  • Χρειάζεσαι topology-aware placement τώρα. Αυτό είναι ξεχωριστό alpha
  • Δεν μπορείς να ορίσεις feature gates σε kube-apiserver και kube-controller-manager

Troubleshooting#

ΠρόβλημαΣυμπτώματαΛύση
Το field του Job εξαφανίζεταιΤο kubectl apply πετυχαίνει, το .spec.scheduling είναι άδειο, τα pods γίνονται schedule ένα έναΆνοιξε το WorkloadWithJob και το GenericWorkload σε apiserver, scheduler και controller-manager
Δεν υπάρχουν podgroups στο api-resourcesΔεν υπάρχει τίποτα σχετικό με gangΤο GenericWorkload είναι κλειστό, ή το scheduling.k8s.io/v1beta1 λείπει από το runtimeConfig
Ο apiserver δεν σηκώνεται μετά το upgradeΆγνωστο feature gate GangSchedulingΣυγχωνεύτηκε στο GenericWorkload στο 1.37. Βγάλ’ το
Το upgrade απορρίπτει υπάρχοντα objectsΥπάρχουν v1alpha2 resourcesΣβήσε όλα τα scheduling.k8s.io/v1alpha2 objects πριν το upgrade. Δεν υπάρχει conversion προς τα πίσω
Gang με υψηλό priority δεν κάνει ποτέ preemptPodGroup Unschedulable, κανένα event Preempted, τα victims χρησιμοποιούν DRAΑναμενόμενο στο 1.37. Το workload-aware preemption δεν ανακτά DRA devices
Gang που τρέχει γίνεται evict κατά το ήμισυΚάποια ranks Preempted, τα υπόλοιπα κρατάνε resourcesΒάλε disruptionMode: all. Θέλει να ξαναφτιάξεις το Job, το field είναι immutable
Η αριθμητική στο μήνυμα του PodGroup μοιάζει λάθος«2 scheduled, 1 remaining» απέναντι σε minCount: 4Το είδα σε κάθε τρέξιμο. Εμπιστέψου το status του condition, όχι το κείμενο του counter

Repository με hands-on demo#

Όλα τα παραπάνω αναπαράγονται σε ένα laptop, χωρίς GPU. Το demo βρίσκεται στο monorepo srekubecraft-demo, κάτω από το gang-scheduling/:

  • kubernetes/cluster/kind.yaml - τέσσερα nodes σε v1.37.0 με τα τρία gates
  • kubernetes/cluster/dra-example-driver.yaml - το upstream chart v0.5.0, rendered με δύο ψεύτικες GPUs ανά node
  • kubernetes/workloads/ - το deadlock, η λύση με gang, το κενό στο DRA preemption και οι δύο παραλλαγές του disruptionMode
  • Taskfile.yml - ένα task ανά πράξη
git clone https://github.com/nicknikolakakis/srekubecraft-demo
cd srekubecraft-demo/gang-scheduling

task up          # cluster, DRA driver, extended resource, namespace, priority classes
task deadlock    # two jobs, six GPUs, nothing trains
task gang        # the same jobs with gang.minCount
task dra-preempt # the DRA preemption gap
task preempt     # node-local preemption, disruptionMode single
task preempt:all # the same, evicted as a unit
task metrics     # the three PodGroup metrics
task down        # delete the cluster

Συμπέρασμα#

Το gang scheduling in tree είναι η σωστή απόφαση. Το coscheduling plugin ήταν πάντα ένα workaround, το Volcano είναι πολύς μηχανισμός για να τον κουβαλάς για ένα μόνο semantic, και το minCount πάνω στο Job είναι το API που έπρεπε να υπάρχει εδώ και χρόνια. Όταν δουλεύει, δουλεύει καθαρά: το ένα gang κάνει training, το άλλο περιμένει, και το status του PodGroup σου λέει ποιο και γιατί.

Το θέμα είναι ότι το 1.37 δεν είναι η έκδοση στην οποία το υιοθετείς. Τρία πράγματα στέκονται στη μέση. Το field του Job είναι alpha και αποτυγχάνει σιωπηλά, και αυτό θα κάψει ένα απόγευμα σε όποιον το αξιολογήσει. Το default του disruptionMode αναιρεί τη μισή εγγύηση όταν γίνει preemption. Και το workload-aware preemption δεν μπορεί να ανακτήσει DRA devices, κάτι που μετράει, γιατί με DRA γίνεται σήμερα το GPU scheduling και με preemption εμποδίζεις τα research jobs να αφήσουν τα production χωρίς πόρους.

Πρακτικές συμβουλές, με τη σειρά:

  • Στο 1.37, άνοιξε και τα δύο gates σε ένα test cluster και τρέξε τα πραγματικά σου jobs. Η συμπεριφορά είναι στέρεη. Το πακετάρισμα όχι.
  • Βάλε disruptionMode: all από το πρώτο manifest. Είναι immutable, και το default είναι λάθος για οτιδήποτε αξίζει gang.
  • Αν οι GPUs σου έρχονται μέσω DRA, κράτα προς το παρόν το Volcano ή το Kueue. Το scheduling καλύπτεται. Το preemption όχι.
  • Αν το resource που σου λείπει είναι node-local, αυτό αρκεί ήδη για να σβήσεις έναν scheduler.
  • Έχε τον νου σου στο 1.38. Το kep.yaml στοχεύει σε stable, δηλαδή τότε που τα gates ανοίγουν by default και αυτό παύει να είναι opt-in.

Θα επανέλθω όταν βγει το 1.38, με το ίδιο demo, για να δω αν το KEP-5690 έκλεισε το κενό στο DRA preemption.


Αν το βρήκες χρήσιμο, ίσως σου αρέσουν και τα σχετικά posts μου:

Kubernetes

EOF · 19 min · 3845 words
$ continue exploring
← Immutable Kubernetes Nodes ως OCI Images - Μια ματιά από κοντά στο bootc // Τι αλλάζει στην πράξη στο πώς κάνεις build, rollout και rollback ένα Kubernetes node όταν το λειτουργικό του έρχεται ως OCI image αντί για ένα σύνολο πακέτων, με το bootc και ένα μικρό πρακτικό παράδειγμα που λέγεται Corium. #sre #kubernetes #bootc Modelplane πέρα από το placement - Routing policy, το Dynamo stack και τέσσερα gotchas // Το δεύτερο μέρος της σειράς για το fleet inference με το Modelplane, αφιερωμένο στο layer πάνω από τον scheduler. Το ModelService είναι routing policy και όχι load balancer, το spec.stack Dynamo αλλάζει ολόκληρο τον μηχανισμό του gang scheduling, και τέσσερα τεκμηριωμένα gotchas θα σου φάνε από ένα απόγευμα το καθένα. #sre #kubernetes #modelplane →
$ grep -r --related
// author
Nick Nikolakakis
Nick Nikolakakis Principal SRE, Platform & AI Engineer // Writing about Kubernetes, SRE practices, cloud-native infrastructure, and AI systems
$ exit█ logout connection closed. cd ~/home ↵
ESC
Type to search...