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.
Δίνεις σε ένα 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-driverv0.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.
| Object | API | Το δημιουργεί | Σκοπός |
|---|---|---|---|
Workload | scheduling.k8s.io/v1beta1 | Ένας workload controller (Job, LWS, JobSet) | Το template. Δηλώνει ένα ή περισσότερα podGroupTemplates, το καθένα με δικό του scheduling policy |
PodGroup | scheduling.k8s.io/v1beta1 | Ο ίδιος controller, από το template | Το runtime instance. Αυτό πάνω στο οποίο αποφασίζει πραγματικά ο scheduler |
pod.spec.schedulingGroup.podGroupName | core/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.37 | Default | Εξαρτάται από | Πηγή |
|---|---|---|---|---|
GenericWorkload | Beta | false | κανένα | kube_features.go:1622-1625 |
WorkloadWithJob | Alpha (από το 1.36) | false | GenericWorkload | kube_features.go:2308-2310, :2922 |
DRAWorkloadResourceClaims | Beta | false | κανένα | kube_features.go:1545-1548 |
CompositePodGroup | Alpha | false | κανένα | kube_features.go:1429-1431 |
PodGroupPreemptionPolicy | Alpha | false | κανένα | kube_features.go:1956-1958 |
TopologyAwareWorkloadScheduling | Alpha (από το 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):
- Το
GangSchedulingδεν υπάρχει πια. Συγχωνεύτηκε στοGenericWorkload, μαζί με τοWorkloadAwarePreemptionαπό το KEP-5710. Βγάλ’ το από το config των feature gates πριν το upgrade. Σε downgrade στο 1.36 πρέπει να το ξανανοίξεις με το χέρι. - Το
scheduling.k8s.io/v1alpha2έφυγε, και στη θέση του ήρθε τοv1alpha3. Σβήσε κάθεv1alpha2resource πριν το upgrade. Δεν υπάρχει conversion προς τα πίσω από τοv1alpha3.
Γιατί όχι Kueue, Volcano ή το coscheduling plugin;#
| Κριτήριο | KEP-4671 (in tree) | Kueue | Volcano | Coscheduling plugin |
|---|---|---|---|---|
| Scheduler | kube-scheduler | kube-scheduler | Δικός του | kube-scheduler + plugin |
| Εγκατάσταση | Ένα feature gate | Ένας controller + CRDs | Ένας scheduler + controllers | Build ή config του scheduler |
| Gang semantics | minCount στο pod group | Μέσω του scheduler που έχει από κάτω | Native PodGroup | Native PodGroup |
| Quota / fair share | Καθόλου | Ναι, αυτή είναι η δουλειά του | Ναι | Όχι |
| Preemption | Workload-aware, σε επίπεδο cluster | Το αναθέτει αλλού | Native | Μόνο σε επίπεδο pod |
| Ωριμότητα στο 1.37 | Beta, 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 σε training | GPUs διαθέσιμες | |
|---|---|---|---|
| Χωρίς gang policy | 6 / 6 | 0 | 0 |
gang.minCount: 4 | 4 / 6 | 1 | 2 |
Σημείωση: ο counter σε αυτό το μήνυμα δεν συμφωνούσε με το
minCountσε κανένα από τα τρεξίματά μου («2 scheduled, 1 remaining» απέναντι σεminCount4). Η συμπεριφορά του 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_total | Alert όταν το result="unschedulable" μεγαλώνει χωρίς αντίστοιχο scheduled |
scheduler_podgroup_scheduling_attempt_duration_seconds | End-to-end latency μιας προσπάθειας, μαζί με τις αναμονές |
scheduler_podgroup_scheduling_algorithm_duration_seconds | Το κόστος του gang placement από την πλευρά του scheduler |
Υπέρ και κατά#
Υπέρ#
| Πλεονέκτημα | Περιγραφή |
|---|---|
| Χωρίς δεύτερο scheduler | Gang semantics από τον kube-scheduler. Ούτε deployment του Volcano, ούτε scheduler name σε κάθε pod |
| Ένα field | gang.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 είναι λάθος για gangs | By default single, που διαλύει ένα gang που τρέχει όταν γίνει preemption |
| Ούτε quota ούτε fair sharing | Παραμένει δουλειά του Kueue. Εδώ έχεις atomic placement, τίποτα παραπάνω |
| Immutable config | Το spec.scheduling δεν μπορεί να προστεθεί, να αφαιρεθεί ή να αλλάξει σχήμα σε Job που υπάρχει ήδη |
| Όχι topology-aware | Ξεχωριστό alpha gate. Το μαζί δεν είναι το ίδιο με το κοντά |
| Κλειστό by default | Beta, αλλά τίποτα δεν δουλεύει μέχρι να ανοίξεις 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 δεν κάνει ποτέ preempt | PodGroup 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με τα τρία gateskubernetes/cluster/dra-example-driver.yaml- το upstream chart v0.5.0, rendered με δύο ψεύτικες GPUs ανά nodekubernetes/workloads/- το deadlock, η λύση με gang, το κενό στο DRA preemption και οι δύο παραλλαγές τουdisruptionModeTaskfile.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 μου:
- Modelplane - Fleet inference σε πολλά clusters, demo χωρίς ούτε μία GPU
- Modelplane πέρα από το placement - Routing policy, το Dynamo stack και τέσσερα gotchas
- llm-d - Kubernetes-native distributed LLM inference σε κλίμακα
- KServe - ML serving σε production πάνω σε Kubernetes, από sklearn μέχρι LLMs
- Kubernetes autoscaling πέρα από το HPA - Event-driven KEDA και βελτιστοποίηση resources με VPA
