Karmada v1.19 Multi-Component Scheduling - Ένα training job, τρία clusters, τέσσερις σιωπηλές αποτυχίες
~/posts/karmada-multi-component-scheduling.el.md9 min · 1828 words

Karmada v1.19 Multi-Component Scheduling - Ένα training job, τρία clusters, τέσσερις σιωπηλές αποτυχίες

// Το Karmada v1.19 μπορεί να βάλει ολόκληρο PyTorchJob, master και workers, σε ένα member cluster. Τέσσερα kind clusters με ψεύτικες GPU δείχνουν τις τέσσερις προϋποθέσεις που χρειάζεται και τι γίνεται όταν λείπει η καθεμία: το job τρέχει τρεις φορές, ο scheduler δεν βρίσκει χώρο με 16 ελεύθερες GPU, και ένα job καταλήγει εκεί όπου οι workers του δεν θα ξεκινήσουν ποτέ.

$ date

Έχεις τρία GPU clusters και ένα control plane μπροστά τους. Κάποιος υποβάλλει ένα distributed training job: ένας master, δύο workers. Θέλεις ολόκληρο το job σε ένα cluster, γιατί NCCL ανάμεσα σε regions δεν είναι κάτι που το κάνεις επίτηδες. Γράφεις το προφανές policy: μοίρασε τα replicas και στρίμωξέ τα σε όσο το δυνατόν λιγότερα clusters.

Το job τρέχει τρεις φορές. Μία σε κάθε cluster. Στο control plane όλα τα status είναι πράσινα.

Αυτή ήταν η πρώτη μου δοκιμή του Karmada v1.19 με ένα PyTorchJob. Το Karmada έγινε graduated project του CNCF τον Σεπτέμβριο του 2026, και το βασικό νέο του v1.19 είναι ακριβώς αυτό το use case. Το feature δουλεύει. Θέλει όμως τέσσερα πράγματα να ισχύουν ταυτόχρονα, και όταν λείπει κάποιο, δεν σου το λέει τίποτα.

Προέλευση: οι αναφορές σε κώδικα είναι από το karmada-io/karmada στο tag v1.19.0. Κάθε output βγήκε από τέσσερα kind clusters (kindest/node:v1.37.0) σε laptop: ένα Karmada host και τρία members με ψεύτικο capacity nvidia.com/gpu.

TL;DR#

  • Το MultiplePodTemplatesScheduling είναι alpha και κλειστό by default. Μπαίνει στα karmada-controller-manager, karmada-scheduler και karmada-webhook.
  • Gate κλειστό: ένα PyTorchJob δεν έχει πληροφορία για replicas, το Karmada το αντιμετωπίζει σαν ConfigMap και το αντιγράφει σε κάθε cluster.
  • Gate ανοιχτό, χωρίς spread constraint: πάλι αντιγράφεται σε κάθε cluster. Το νέο path τρέχει μόνο με spreadByField: cluster, minGroups: 1, maxGroups: 1.
  • Με το constraint, κάθε GPU job αποτυγχάνει με no enough resource. Τα default resource models ξέρουν μόνο CPU και memory. Κλείσε το CustomizedClusterResourceModeling στον scheduler.
  • Ο general estimator αθροίζει τα σύνολα του cluster. Ένα job μπορεί να καταλήξει εκεί όπου κανένα node δεν χωράει τους workers του, να δηλώνει Scheduled και FullyApplied, και να μείνει Pending για πάντα. Στήσε τον ακριβή scheduler-estimator.

Τι έφερε στην πράξη το v1.19#

Το proposal για multiple pod templates μαθαίνει στον interpreter, στον scheduler και στους estimators τι είναι ένα workload με περισσότερα από ένα pod template. Ο interpreter επιστρέφει ένα component ανά template, ο scheduler τα τοποθετεί όλα μαζί, σαν ένα πράγμα, σε ένα cluster, και το v1.19 καταγράφει το αποτέλεσμα ανά component στο ResourceBinding, στο spec.clusters[*].components.

Built-in interpreters υπάρχουν για τα PyTorchJob, TFJob και MPIJob του Kubeflow, τα RayCluster και RayJob, το Job του Volcano, το FlinkDeployment και το SparkApplication. Αυτοί του Kubeflow αφορούν τα kubeflow.org/v1 APIs του training-operator. Το TrainJob του Kubeflow Trainer v2 δεν έχει built-in interpreter.

Γιατί όχι MultiKueue;#

Το MultiKueue λύνει το ίδιο πρόβλημα από την πλευρά του queueing. Δεν το έτρεξα, η δεξιά στήλη είναι από το documentation του.

ΚριτήριοKarmada v1.19MultiKueue
Πώς επιλέγεται το clusterScheduler + estimators, πριν δημιουργηθεί οτιδήποτεΑντιγράφεται στους workers, κερδίζει όποιος κάνει πρώτος admit
Ωριμότητα του job pathAlpha, κλειστό by defaultBeta από το Kueue v0.9, ανοιχτό by default
ΕύροςΟποιοδήποτε resource, ίδια policies με τα Deployments σουJobs που διαχειρίζεται το Kueue

Με δυο λόγια: αν τρέχεις ήδη Karmada για services, το v1.19 είναι το πρώτο release όπου το training στο ίδιο control plane είναι ρεαλιστικό. Αν θέλεις μόνο batch dispatch, το MultiKueue σε πάει εκεί με ένα beta feature.

Τι ΔΕΝ είναι#

  • Δεν είναι gang scheduling μέσα στο member. Αυτό είναι δουλειά του kube-scheduler, δες το Kubernetes 1.37 Gang Scheduling
  • Δεν σπάει το job. Ένα multi-component job δεν απλώνεται ποτέ σε πολλά clusters
  • Δεν κάνει rescheduling. Το v1.19 απλώς «laying the foundation» γι’ αυτό. Ένα job που μπήκε σε λάθος μέρος μένει εκεί

Το demo#

flowchart TB
    subgraph Host["kind: karmada-host"]
        API["karmada-apiserver"]
        SCH["karmada-scheduler"]
        EST["scheduler-estimator x3"]
    end
    subgraph A["member-a"]
        A1["1 node, 2 GPU"]
    end
    subgraph B["member-b"]
        B1["3 nodes, 2 GPU each"]
    end
    subgraph C["member-c"]
        C1["1 node, 8 GPU"]
    end
    API --> SCH
    SCH --> A & B & C
    EST -. per-node view .-> A & B & C

Οι GPU είναι ένα patch στο status του node. Το Karmada αθροίζει το allocatable των nodes στο resource summary κάθε cluster, και ο kube-scheduler κάθε member το επιβάλλει ανά node:

$ kubectl --kubeconfig .karmada/karmada-apiserver.config get clusters -o custom-columns=...
NAME       ENDPOINT                              GPU   CPU
member-a   https://member-a-control-plane:6443   2     12
member-b   https://member-b-control-plane:6443   6     48
member-c   https://member-c-control-plane:6443   8     12

Κάθε member τρέχει τον Kubeflow training-operator v1.9.4, οπότε τα jobs γίνονται κανονικά pods που τρέχουν pause. Το train-small είναι ένας master μόνο με CPU και δύο workers με 1 GPU ο καθένας. Το train-big είναι το ίδιο, με 3 GPU ανά worker:

    Worker:
      replicas: 2
      template:
        spec:
          containers:
            - name: pytorch
              image: registry.k8s.io/pause:3.10.1
              resources:
                requests: {cpu: 50m, memory: 32Mi, nvidia.com/gpu: "3"}
                limits: {cpu: 100m, memory: 64Mi, nvidia.com/gpu: "3"}

Αποτυχία 1: gate κλειστό, το PyTorchJob είναι ένα ConfigMap#

Το policy που γράφουν οι περισσότεροι για ένα training job:

apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: training
  namespace: ml-team
spec:
  resourceSelectors:
    - apiVersion: kubeflow.org/v1
      kind: PyTorchJob
  placement:
    clusterAffinity:
      clusterNames: [member-a, member-b, member-c]
    replicaScheduling:
      replicaSchedulingType: Divided
      replicaDivisionPreference: Aggregated

Σε φρέσκο karmadactl init:

replicas: null
components: null
Binding has been scheduled successfully. Result: {member-a:0, member-b:0, member-c:0}

-- member-a
pod/train-small-master-0   1/1   Running
pod/train-small-worker-0   1/1   Running
pod/train-small-worker-1   1/1   Running
-- member-b  (same three pods)
-- member-c  (same three pods)

Μηδέν replicas ανατέθηκαν, και το job έγινε apply παντού. Ο built-in interpreter ορίζει componentResource αλλά όχι replicaResource. Με το gate κλειστό, το Karmada πέφτει πίσω στο replica hook (pkg/detector/detector.go:1513), δεν βρίσκει τίποτα, και ο scheduler θεωρεί non-workload ένα binding χωρίς replicas και χωρίς components (pkg/scheduler/core/util.go:66-69). Τα non-workloads πάνε σε κάθε cluster. Τα Divided και Aggregated δεν τα ρωτάει κανείς.

Αποτέλεσμα: με το gate κλειστό, ένα Karmada policy για Kubeflow v1 jobs είναι broadcast.

Αποτυχία 2: gate ανοιχτό, πάλι τρία αντίγραφα#

Ανοίγεις το gate και στα τρία components και τρέχεις το ίδιο policy:

NAME                     REPLICAS   COMPONENTS      CLUSTERS
train-small-pytorchjob   <none>     master,worker   member-b,member-a,member-c

Τα ίδια εννέα pods. Το Karmada τώρα ξέρει τα components, και δεν τα χρησιμοποιεί κανείς. Το multi-component path κρύβεται πίσω από έναν έλεγχο (pkg/scheduler/core/estimation.go:43-65):

for i := range spec.Placement.SpreadConstraints {
	if spec.Placement.SpreadConstraints[i].SpreadByField == policyv1alpha1.SpreadByFieldCluster &&
		spec.Placement.SpreadConstraints[i].MinGroups == 1 &&
		spec.Placement.SpreadConstraints[i].MaxGroups == 1 {
		return true
	}
}
return false

Χωρίς ακριβώς αυτό το constraint, ένα multi-component workload γίνεται propagate «to all candidate clusters» (pkg/scheduler/core/common.go:72).

Αποτέλεσμα: το replicaDivisionPreference: Aggregated διαβάζεται σαν «ένα cluster, αν γίνεται». Για multi-component workloads δεν σημαίνει τίποτα.

Αποτυχία 3: 16 ελεύθερες GPU, και χώρος πουθενά#

Προσθέτεις το constraint:

    spreadConstraints:
      - spreadByField: cluster
        minGroups: 1
        maxGroups: 1
==> Scheduled:
False failed to select clusters: no enough resource when selecting 1 clusters

Ζητήθηκαν δύο GPU, ελεύθερες είναι δεκαέξι. Το ίδιο job χωρίς το GPU request γίνεται schedule αμέσως, σε ένα cluster, με το αποτέλεσμα ανά component καταγεγραμμένο. Άρα το path δουλεύει, και κάτι μέσα του δεν ξέρει να μετράει GPU.

Αυτό το κάτι είναι το cluster resource modeling: beta, ανοιχτό by default από το v1.4. Ο general estimator προσομοιώνει το job πάνω σε ψεύτικα nodes που φτιάχνει από τα resource models κάθε cluster (pkg/estimator/client/general.go:241), και τα default models έχουν μόνο δύο διαστάσεις:

- grade: 0
  ranges:
    - {name: cpu, min: "0", max: "1"}
    - {name: memory, min: "0", max: 4Gi}
# ... up to grade 8, cpu and memory only

Κανένα προσομοιωμένο node δεν έχει GPU, άρα κάθε GPU job χωράει μηδέν φορές. Με custom models δεν διορθώνεται, το documentation επιτρέπει μόνο «cpu, memory, storage, ephemeral-storage». Η λύση είναι να κλείσεις το CustomizedClusterResourceModeling στον scheduler:

NAME                     COMPONENTS      CLUSTERS   PER-CLUSTER
train-small-pytorchjob   master,worker   member-c   [map[name:master replicas:1] map[name:worker replicas:2]]

member-c  train-small-master-0   Running
member-c  train-small-worker-0   Running
member-c  train-small-worker-1   Running

Ολόκληρο το job σε ένα cluster. Έτσι μοιάζει το feature του v1.19 όταν δουλεύει.

Αποτέλεσμα: με τις default ρυθμίσεις, το multi-component scheduling δεν μπορεί να τοποθετήσει GPU job, και το error σε στέλνει να ψάχνεις capacity. Τα demos μόνο με CPU δεν θα σου το δείξουν.

Αποτυχία 4: Scheduled, FullyApplied, Pending για πάντα#

Το train-big θέλει δύο workers με 3 GPU ο καθένας. Το member-b έχει έξι GPU, αλλά δύο ανά node:

cond:
  - Scheduled=True Binding has been scheduled successfully.
  - FullyApplied=True All works have been successfully applied
clusters:
  - member-b

member-b  train-big-master-0   Running
member-b  train-big-worker-0   Pending
member-b  train-big-worker-1   Pending
# 0/4 nodes are available: ... 3 Insufficient nvidia.com/gpu

Πράσινο στο Karmada, κολλημένο για πάντα στο member, την ώρα που το member-c έχει οκτώ ελεύθερες GPU σε ένα node. Ο general estimator διαιρεί τα σύνολα του cluster με το request του job, και όπως λέει ο ίδιος «has no per-node view» (pkg/estimator/client/general.go:176).

Η λύση είναι ο ακριβής scheduler-estimator, ένας ανά member, που απαντάει με βάση τα πραγματικά nodes:

clusters:
  - member-c

member-c  train-big-master-0   Running
member-c  train-big-worker-0   Running
member-c  train-big-worker-1   Running

Από την Αποτυχία 3 δεν σε γλιτώνει. Ο scheduler κρατάει το ελάχιστο από όλους τους estimators, και με το modeling ξανά ανοιχτό κερδίζει πάλι το μηδέν του general estimator: no enough resource when selecting 1 clusters.

Αποτέλεσμα: για GPU jobs χρειάζεσαι και τα δύο: modeling κλειστό και τον ακριβή estimator.

Το configuration που δουλεύει#

ΡύθμισηΠούΓιατί
MultiplePodTemplatesScheduling=truecontroller-manager, scheduler, webhookParsing, χρήση και validation των components
spreadByField: cluster, minGroups: 1, maxGroups: 1PropagationPolicyΗ μόνη μορφή που ενεργοποιεί το path
CustomizedClusterResourceModeling=falseschedulerΤα default models δεν έχουν GPU
Ένας karmada-scheduler-estimator ανά memberhost clusterΈλεγχος ανά node αντί για σύνολα του cluster

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

ΥπέρΚατά
Τοποθέτηση ολόκληρου του job, όχι μισό job ανά regionAlpha, τρία components και ένα constraint που πρέπει να είναι ακριβώς έτσι
Αποτέλεσμα ανά component στο API, η βάση για quota και reschedulingΣιωπηλά fallbacks: πλήρη αντίγραφα παντού, πράσινο status
Οκτώ built-in interpreters, καθόλου Lua για Kubeflow v1, Ray, Volcano, Flink, SparkDefaults που δεν βλέπουν GPU, και ένα παραπλανητικό error
Ένα control plane για services και trainingΧωρίς rescheduling και χωρίς Trainer v2 προς το παρόν

Το repository του demo#

Όλα τα παραπάνω βρίσκονται στο srekubecraft-demo/karmada-multi-component, με ένα task ανά αποτυχία:

task up                              # 4 kind clusters, fake GPUs, Karmada, training-operator
task copies                          # Failure 1
task gate:on && task copies:gated    # Failure 2
task one                             # Failure 3
task modeling:off && task one        # fixed
task fragment                        # Failure 4
task estimator && task fragment      # fixed

Το README καλύπτει και τις παγίδες του setup όταν τρέχεις Karmada σε kind πάνω σε macOS, ανάμεσά τους και το ότι τα karmadactl init και join καταγράφουν endpoints που το host cluster δεν μπορεί να φτάσει.

Συμπέρασμα#

Το Karmada v1.19 κάνει αυτό που λένε τα release notes. Το είδα να βάζει ένα PyTorchJob σε ένα cluster και να καταγράφει ποιο component πήρε πόσα replicas. Για να φτάσω εκεί χρειάστηκαν τέσσερις αλλαγές, και όποια έλειπε έμοιαζε είτε με επιτυχία είτε με πρόβλημα capacity. Χρησιμοποίησε τον πίνακα με το configuration παραπάνω, και μη στήνεις alerts μόνο πάνω στα conditions του Karmada: τα Scheduled=True και FullyApplied=True ίσχυαν και τα δύο για ένα job που οι workers του δεν θα ξεκινούσαν ποτέ.

Το feature είναι alpha, και τα defaults του φτιάχτηκαν για stateless replicas, ενώ ο λόγος που υπάρχει είναι οι GPU. Τρέξε το σε staging fleet με το παραπάνω configuration, και παρακολούθησε τη δουλειά για το rescheduling στα επόμενα releases.


Αν σου φάνηκε χρήσιμο, ίσως σε ενδιαφέρουν και τα σχετικά posts μου για scheduling και multi-cluster AI workloads:

Karmada

EOF · 9 min · 1828 words
$ continue exploring
← Red teaming στο δικό μου agent sandbox - 15 τεχνικές exfiltration εναντίον του OpenShell // Τρέχω έναν agent, ένα Telegram bot, μέσα σε sandbox του NVIDIA OpenShell. Οπότε πέρασα ένα απόγευμα χτυπώντας το: 15 τεχνικές exfiltration και απόδρασης πάνω σε ένα αναλώσιμο αντίγραφο, με τα πραγματικά OCSF logs για κάθε αποτέλεσμα. Τι κόβει το κλουβί χωρίς δεύτερη κουβέντα, ποια δύο κενά αφήνει, και το L7 rule που κλείνει το χειρότερο. #sre #security #openshell Πριν κάνεις upgrade: έλεγξε τους proxies και τα meshes σου για το WebSockets transport του kubectl // Το Kubernetes αποσύρει το SPDY streaming transport πίσω από τα kubectl exec, attach, cp και port-forward και περνάει σε WebSockets. Εδώ θα βρεις ένα audit που μπορείς να τρέξεις στα clusters σου αυτή την εβδομάδα, για να εντοπίσεις το hop που θα σπάσει πριν το εντοπίσει το upgrade για λογαριασμό σου. #sre #kubernetes #networking →
$ 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...