KEP-5547: Integrate Workload APIs with Job Controller
KEP-5547: Integrate Workload APIs with Job Controller
- Release Signoff Checklist
- Summary
- Motivation
- Proposal
- Design Details
- Production Readiness Review Questionnaire
- Implementation History
- Drawbacks
- Alternatives
- Infrastructure Needed (Optional)
Release Signoff Checklist
Items marked with (R) are required prior to targeting to a milestone / release.
- (R) Enhancement issue in release milestone, which links to KEP dir in [kubernetes/enhancements] (not the initial KEP PR)
- (R) KEP approvers have approved the KEP status as
implementable - (R) Design details are appropriately documented
- (R) Test plan is in place, giving consideration to SIG Architecture and SIG Testing input (including test refactors)
- e2e Tests for all Beta API Operations (endpoints)
- (R) Ensure GA e2e tests meet requirements for Conformance Tests
- (R) Minimum Two Week Window for GA e2e tests to prove flake free
- (R) Graduation criteria is in place
- (R) all GA Endpoints must be hit by Conformance Tests within one minor version of promotion to GA
- (R) Production readiness review completed
- (R) Production readiness review approved
- “Implementation History” section is up-to-date for milestone
- User-facing documentation has been created in [kubernetes/website], for publication to [kubernetes.io]
- Supporting documentation—e.g., additional design documents, links to mailing list discussions/SIG meetings, relevant PRs/issues, release notes
Summary
This KEP integrates the Workload-aware Scheduling (WAS) APIs (Workload and PodGroup) into the
batch/v1 Job by adding a user-facing spec.scheduling field, allowing users to express explicit
scheduling intent such as gang scheduling1, topology co-location, and disruption policies.
The feature has gone through two alpha releases:
- v1alpha1 (v1.36) intentionally bypassed a user-facing API: the controller inferred a
hardcoded
Gangpolicy from the Job’s type (parallel Jobs with indexed completion mode), withminCountfixed toparallelism. - v1alpha2 (v1.37) replaced that automatic, controller-inferred model with an explicit,
user-driven design that separates scheduling policy from workload structure. It introduced
the
spec.schedulingfield on the Job API, built on the reusable scheduling building blocks and the sharedworkloadbuildertranslation library defined in KEP-6089, keeping the Job integration consistent with the rest of the ecosystem rather than reinventing bespoke logic.
The Job controller acts as a translator, compiling spec.scheduling into the underlying
Workload/PodGroup objects. When spec.scheduling is omitted, it defaults to Basic
scheduling, so the scheduling outcome of existing Jobs is preserved.
For v1.38 the integration is promoted to Beta and the WorkloadWithJob feature gate is enabled
by default. Because the spec.scheduling field is embedded in the GA batch/v1 API, its building
blocks graduate straight into scheduling.k8s.io/v1 (with Go type aliases left in v1alpha3) as
described in KEP-6089. The Beta revision also resolves the design questions the v1alpha2 left
open and the controller exposes metrics for the Workload/PodGroup lifecycle. Default enablement
is conditional on the GenericWorkload gate (KEP-4671) being enabled by default in the same
release, since WorkloadWithJob depends on it.
Motivation
The Kubernetes Job Controller historically created pods independently without workload-aware
scheduling constraints. This is a challenge for parallel applications (i.e., AI/ML training
workloads, MPI jobs) that require all pods to be scheduled and run together or none (gang
scheduling1). The v1.36 alpha brought gang scheduling to the Job controller, but it did so by
inferring a hardcoded Gang policy from the Job’s type rather than from explicit user intent.
Users have diverse use cases and require the ability to express explicit intent, such as opting
in or out of gang scheduling, requesting specific topologies, or configuring disruption policies
for their workloads. KEP-6089 standardizes the reusable scheduling building blocks
(introduced as scheduling.k8s.io/v1alpha3, graduating to scheduling.k8s.io/v1 in v1.38) and a
shared workloadbuilder translation library so that workload controllers can expose these
features consistently. This KEP integrates those building blocks into the core Job API. The same
building blocks have been adopted by out-of-tree controllers (JobSet, LeaderWorkerSet, Kubeflow
TrainJob, kubeRay) and proposed for Deployment and StatefulSet, which is the ecosystem feedback
the alpha was meant to collect.
Goals
- Add a user-facing
spec.scheduling(JobSchedulingConfiguration) field to thebatch/v1Job, embedding thescheduling.k8s.iobuilding blocks (schedulingPolicy,schedulingConstraints,disruptionMode,resourceClaims) so users can express explicit scheduling intent. - Default to
Basicscheduling whenspec.schedulingis omitted, so the observable scheduling outcome of existing Jobs is preserved (no all-or-nothing gate, and any number of schedulable pods proceed to binding). Following the KEP-6089, the controller still materializes aBasicWorkload/PodGroup, which routes these pods through the Workload Scheduling Cycle (batched scheduling and workload-aware preemption) without enforcing minCount. - Let users opt in to
Gangscheduling, withminCountdefaulting toparallelismwhen omitted. - Compile
spec.schedulingintoWorkload/PodGroupobjects via the sharedworkloadbuilderlibrary instead of bespoke controller logic. - Support mutable
spec.scheduling.schedulingPolicy.gang.minCountfor elastic scaling, while keeping all otherspec.schedulingfields immutable after creation. This relies on KEP-4671 that makesminCountinPodGroup/PodGroupTemplatemutable in v1.37 to support workload scaling. - When the Job is not the root of the workload tree (the
OwnerReferencerefers to a parent controller that compiles and owns theWorkload), deferWorkloadmanagement to that parent, preserving the root-controller-as-compiler principle. A parent may own theWorkloadwhile still delegatingPodGroupmanagement to the Job (e.g., aJobrunning under aTrainJobthat does not know about Jobs). The parent signals this split via KEP-6089’s downward-mapping annotations, so a non-root controller can still create and manage thePodGroupfor its own pods. - Support a user pre-created
Workloadfor a standalone Job through the same annotation path. - Ensure proper ordering of
Workload→PodGroup→Podcreation. - Release scheduler-side resources while a Job remains suspended by deleting the runtime
PodGrouptogether with its pods, while keeping theWorkloadtemplate in place.
Non-Goals
- Multi-level / nested composite (
CompositePodGroup) structures, since this KEP covers single-level, flatJobworkloads only. - Implementing the integration in composite controllers (
JobSet,LWS,TrainJob). Those are pursued independently in their own repositories. - Defining the
scheduling.k8s.iobuilding-block API or theworkloadbuilderlibrary itself (owned by KEP-6089 and consumed here).
Proposal
This proposal builds on the recently introduced Workload-aware Scheduling enhancements. We assume the reader is acquainted with the following KEPs:
- KEP-4671: Gang Scheduling.
- KEP-5710: Workload-aware preemption.
- KEP-5732: Topology-aware workload scheduling.
- KEP-6089: WAS Controller APIs.
The Job controller is extended to compile the user’s scheduling intent into Workload and
PodGroup objects as part of its pod-management lifecycle, so that pods belonging to a Job are
scheduled according to the requested policy before they are created. The intent is expressed
through a new spec.scheduling field.
The key design principles are:
- One
Jobmaps to onePodGrouprepresenting a single group of pods. ThePodGroupalways links to aWorkloadvia aPodGroupTemplate:- For a root Job it links to the
Workloadthe controller compiles itself - For a non-root Job it links to the ancestor-owned
Workloadnamed byscheduling.k8s.io/workload-name - The
PodGrouplinks to a parentCompositePodGroupinstance only when the parent supplies thescheduling.k8s.io/parent-compositepodgroupannotation
- For a root Job it links to the
- The scheduling policy comes from the user’s
spec.scheduling, not from the Job’s type. Whenspec.schedulingis omitted, the controller defaults to theBasicpolicy. - Following the KEP-6089, the controller always materializes scheduling objects (a
Workloadand/orPodGroup) for an eligible Job — a Job the controller is responsible for when the gate is on: a standalone/root Job, or a non-root Job whose parent delegates thePodGroup(a Job whose parent owns both objects is skipped). This holds even for theBasicscheduling policy. - For
Gang, an omittedminCountdefaults to the Job’sparallelism.minCountis mutable to support elastic scaling; all otherspec.schedulingfields are immutable after creation. - The Job controller compiles a
Workloadonly for a Job that has no parent workload controller. When a higher-level controller owns the Job, the ancestor (e.g.JobSet,TrainJob) compiles theWorkloadand the Job controller never creates one. Thescheduling.k8s.io/group-template-nameannotation on the Job then decides who creates the runtimePodGroup:- If the annotation is present, the Job controller creates it from the named template in the
Workloadbased on thescheduling.k8s.io/workload-nameannotation. - If the annotation is absent, the ancestor creates it and the Job controller creates nothing.
- If the annotation is present, the Job controller creates it from the named template in the
- The same annotations on a standalone BYO Job point the controller at a user pre-created
Workloadinstead of compiling one. - Jobs created by
CronJobare standalone (no parent-workloadOwnerReference); the Job controller creates oneWorkloadand onePodGroupper Job for them based on each Job’sspec.scheduling. - Discovery and lifecycle are separate:
- In every mode, both the
Workloadand thePodGroupare discovered by name. - ownerReferences govern garbage collection only and play no part in discovery.
- The controller mutates or deletes an object only when its controller
ownerReferenceis the Job.
- In every mode, both the
- Every Job gets its own runtime
PodGroup. Sibling Jobs that select the samePodGroupTemplate(e.g. the replicas of a JobSetReplicatedJob) instantiate distinctPodGroups from it, one per Job.
The spec.scheduling field embeds the building blocks from scheduling.k8s.io/v1, while the
runtime Workload/PodGroup objects the controller creates are served from
scheduling.k8s.io/v1beta1 (KEP-4671). The examples below use those versions. If
GenericWorkload graduates to GA in the same release, the controller uses the v1 objects instead.
The KEP will be updated to reflect this.
Job Integration - API Usage Examples
Example 1: Gang scheduling with zone topology and atomic disruption
A distributed training Job whose 4 pods must schedule together (all-or-nothing), co-locate within
the same availability zone, and be disrupted together (if one pod is preempted, the whole group is):
apiVersion: batch/v1
kind: Job
metadata:
name: <job-name>
namespace: training
spec:
parallelism: 4
completions: 4
completionMode: Indexed
scheduling: # New API field - scheduling intent
schedulingPolicy:
gang: {} # minCount omitted -> defaults to parallelism (4)
schedulingConstraints:
topology:
- level: "topology.kubernetes.io/zone"
disruptionMode:
all: {} # entire group must be disrupted together
template:
spec:
containers:
- name: trainer
image: training-image:latest
resources:
limits:
nvidia.com/gpu: 1
The Job controller compiles this intent into a Workload and its runtime PodGroup:
apiVersion: scheduling.k8s.io/v1beta1
kind: Workload
metadata:
name: <job-name>-<hash>
namespace: training
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: <job-name>
uid: <job-uid>
controller: true
spec:
controllerRef:
apiVersion: batch/v1
kind: Job
name: <job-name>
podGroupTemplates:
- name: <podGroupTemplateName>
schedulingPolicy:
gang:
minCount: 4 # defaulted from Job.spec.parallelism
schedulingConstraints:
topology:
- level: "topology.kubernetes.io/zone"
disruptionMode:
all: {}
---
apiVersion: scheduling.k8s.io/v1beta1
kind: PodGroup
metadata:
name: <workload-name>-<podGroup-template-name>-<hash>
namespace: training
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: <job-name>
uid: <job-uid>
controller: true
- apiVersion: scheduling.k8s.io/v1beta1
kind: Workload
name: <workload-name>
uid: <workload-uid>
spec:
podGroupTemplateRef:
workload:
workloadName: <workload-name>
podGroupTemplateName: <podGroup-template-name>
schedulingPolicy:
gang:
minCount: 4
Example 2: Backward Compatibility and Defaulting (Implicit Opt-Out)
A standard Job that omits the scheduling block. It defaults to Basic scheduling. Per
the KEP-6089, the controller does not impose all-or-nothing, so the scheduling
outcome matches a standard Job (batched scheduling cycle, no minCount enforcement):
apiVersion: batch/v1
kind: Job
metadata:
name: <job-name>
namespace: batch
spec:
parallelism: 10
completions: 10
# The scheduling block is omitted, which defaults to Basic scheduling.
# This acts as an implicit opt-out from gang scheduling.
template:
spec:
containers:
- name: processor
image: processor-image:v1
This compiles into a Basic scheduling policy:
apiVersion: scheduling.k8s.io/v1beta1
kind: Workload
metadata:
name: <job-name>-<hash>
namespace: batch
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: <job-name>
uid: <job-uid>
controller: true
spec:
controllerRef:
apiVersion: batch/v1
kind: Job
name: <job-name>
podGroupTemplates:
- name: <podGroup-template-name>
schedulingPolicy:
basic: {}
---
apiVersion: scheduling.k8s.io/v1beta1
kind: PodGroup
metadata:
name: <workload-name>-<podGroup-template-name>-<hash>
namespace: batch
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: <job-name>
uid: <job-uid>
controller: true
- apiVersion: scheduling.k8s.io/v1beta1
kind: Workload
name: <workload-name>
uid: <workload-uid>
spec:
podGroupTemplateRef:
workload:
workloadName: <workload-name>
podGroupTemplateName: <podGroup-template-name>
schedulingPolicy:
basic: {}
Example 3: CronJob with Gang Scheduling
A CronJob that periodically runs a gang-scheduled training Job. Each Job created by the
CronJob is treated as standalone. CronJob does not create or manage Workload objects,
the Job controller creates a separate Workload and PodGroup per Job. These objects are
garbage-collected when each Job completes or is deleted.
apiVersion: batch/v1
kind: CronJob
metadata:
name: periodic-training
namespace: training
spec:
schedule: "0 */6 * * *"
jobTemplate:
spec:
parallelism: 4
completions: 4
completionMode: Indexed
scheduling:
schedulingPolicy:
gang: {} # minCount defaults to parallelism (4) per Job
template:
spec:
containers:
- name: trainer
image: training-image:latest
resources:
limits:
nvidia.com/gpu: 1
Each Job created by this CronJob produces its own Workload and PodGroup, compiled from
the Job’s spec.scheduling.
If the CronJob’s jobTemplate omits the scheduling block, each Job defaults to Basic.
In all cases, the Job controller then creates the pods and sets the schedulingGroup field so the
scheduler can associate each pod with its PodGroup:
apiVersion: v1
kind: Pod
metadata:
name: <job-name>-<random-suffix>
namespace: <namespace>
ownerReferences:
- apiVersion: batch/v1
kind: Job
name: <job-name>
uid: <job-uid>
controller: true
- apiVersion: scheduling.k8s.io/v1beta1
kind: PodGroup
name: <podGroup-name>
uid: <podGroup-uid>
spec:
schedulingGroup:
podGroupName: <workload-name>-<podGroup-template-name>-<hash>
containers:
- name: ...
User Stories
ML Training Job with Gang Scheduling
As a machine learning engineer, I want to run a distributed training job with 8 workers that must
all be scheduled together. I set spec.scheduling.schedulingPolicy.gang on the Job (optionally with a
topology constraint to co-locate the workers), so that if only 7 workers can be scheduled, no pods
start and no resources are wasted. I do not have to set parallelism and completions
in a specific way to “qualify” for gang scheduling; I declare my intent explicitly.
Backward-Compatible Standard Batch Job
As a data engineer, I want to run a batch processing job that processes files independently without
gang scheduling requirements. I omit spec.scheduling entirely (or set spec.scheduling.schedulingPolicy.basic
explicitly for the same effect), so the Job defaults to Basic scheduling. The observable scheduling
outcome matches a standard Job, while a Basic Workload/PodGroup is still materialized, giving me consistent objects to observe its scheduling state.
Notes/Constraints/Caveats
Constraints
- The integration targets single-level
Jobworkloads: oneJobmaps to onePodGroup, and all pods in theJobshare a single scheduling policy. A Job-ownedWorkloadtherefore has exactly onePodGroupTemplate, while heterogeneous groups belong in a composite controller. Instantiating severalPodGroups from a single template within one Job (e.g. one gang per TPU slice) is a known use case deferred to a follow-up KEP, see Future Extensions. - Sibling Jobs whose annotations name the same
PodGroupTemplate(e.g. the replicas of a JobSetReplicatedJob, see JobSet KEP-969) each get their own runtimePodGroup, named after the Job, and therefore form independent gangs. If the parent wants several Jobs in one gang, it must create thatPodGroupitself and setspec.template.spec.schedulingGroupon the child Jobs. spec.scheduling.schedulingPolicy.gang.minCountis mutable to support elastic scaling (KEP-4671); all otherspec.schedulingfields are immutable after creation.- The Job controller creates
Workload/PodGroupobjects for every eligible Job, includingBasicones. The only way to avoid the objects entirely is to disable the feature gate. By default, an end user gets the original scheduling outcome even though aBasicWorkload/PodGroupis still created. - The controller manages only objects whose controller
ownerReferenceis the Job. Pre-created objects are used as-is and never adopted.
Risks and Mitigations
Split-brain configuration. A composite wrapper controller (such as
JobSetorTrainJob) may expose its own scheduling fields while the childJobnow also has nativespec.schedulingfields, letting a user configure scheduling in two conflicting places.- Mitigation: the parent controller remains the sole compiler of the workload tree and can map
its own fields onto the compiled
Workload, strip/ignore the child’s nested scheduling fields, or reject requests that populate both. The parent controller selects the authoritativePodGroupTemplateand decides whether the Job creates its ownPodGroupor uses theschedulingGroupalready placed on its pod template. The Job controller never reconciles the child Job’sspec.schedulinginto a parent-ownedWorkload.
- Mitigation: the parent controller remains the sole compiler of the workload tree and can map
its own fields onto the compiled
Increased object count. Because the controller now materializes a
Workload/PodGroupfor every eligible Job, the number of scheduling objects grows relative to the v1.36 alpha, which only created objects for inferred gang Jobs. With the gate enabled by default from v1.38, this applies to every cluster, not only to those that opted in.- Mitigation: objects are small and garbage-collected with the Job. The Scalability section
quantifies the impact, the feature can still be disabled with the
WorkloadWithJobgate, and performance tests covering Job creation throughput with the gate on are a Beta requirement.
- Mitigation: objects are small and garbage-collected with the Job. The Scalability section
quantifies the impact, the feature can still be disabled with the
Behavior change between alphas. Jobs that were automatically gang-scheduled in v1.36 default to
Basicin v1.37 unless the user setsspec.scheduling.schedulingPolicy.gang.- Mitigation: gang is now an explicit opt-in; this is documented in the Upgrade / Downgrade Strategy and release notes.
Suspended Jobs and resource release. In v1alpha2 the controller relied only on GC, which does not release resources associated with a runtime
PodGroup(e.g. DRA claims) while a Job is suspended.- Mitigation: from Beta the controller deletes the runtime
PodGrouptogether with the suspended Job’s pods and recreates it on resume, while retaining theWorkload. See Suspend and Resume.
- Mitigation: from Beta the controller deletes the runtime
Job blocked on unsupported scheduling objects. The controller cannot safely determine which scheduling constraints to apply when a root Job finds more than one
Workloadreferencing it, when aWorkloadhas an unsupported shape, or when the object at the Job’sPodGroupname does not reference the selected template. This can result from a change to controller-owned objects, a name collision, or a bug in a higher-level controller. The Job should not run in this case and must wait until the issue is resolved.- Mitigation: the controller blocks new pod creation and reports the problem through a
Warning event and the
SchedulingBlockedJob condition until an operator repairs the objects.
- Mitigation: the controller blocks new pod creation and reports the problem through a
Warning event and the
Pod creation latency for gangs. The Job controller creates pods in slow-start batches (1, 2, 4, …), so a gang of
minCountpods is fully created only afterceil(log2(minCount))rounds and the scheduler cannot admit the gang before then.- Mitigation: the scheduler, not the controller, enforces all-or-nothing, so the batching adds bounded latency rather than incorrect behavior. Batching stays unchanged for Beta. Performance tests measure time-to-first-schedule for gang Jobs so the trade-off can be revisited with data.
Design Details
Core Principles & Assumptions
The integration follows the Workload-aware Scheduling design principles from KEP-6089 for the
single-level Job:
- The Root Controller is the Compiler. By default, the Job controller compiles and manages the
scheduler-facing
Workloadfor a scheduling-root Job, including Jobs created byCronJob.- When
scheduling.k8s.io/workload-nameandscheduling.k8s.io/group-template-nameselect a template in an externalWorkload(ancestor-owned or user pre-created), theWorkloadis the authoritative compiler output. - The Job controller may still create and manage the runtime
PodGroupwithout owning theWorkload, but it never adopts or mutates the externalWorkload.
- When
- Universal Representation. Standard pod-by-pod scheduling is a first-class policy (
Basic). The controller always emits aWorkload/PodGroupfor an eligible Job, usingBasicas the backward-compatible default.Basickeeps the standard scheduling outcome, while still participating in the Workload Scheduling Cycle, without enforcing minCount. - Sane Defaults and Escape Hatches. A
Jobdefaults toBasic.
Job API Changes
To deliver native, typed Workload-aware Scheduling on core Kubernetes, we add a new Scheduling
field to JobSpec. This integration is the foundational, single-level implementation that
demonstrates the building blocks before out-of-tree controllers adopt them.
We introduce a new optional Scheduling field in JobSpec that embeds a curated composition of
the standardized building blocks:
// API Group: batch/v1
// JobSpec defines the desired state of a Job.
type JobSpec struct {
// ... existing fields ...
// Scheduling defines the Workload-aware Scheduling configuration for this Job.
// This field is beta-gated by the WorkloadWithJob feature gate (enabled by
// default since v1.38).
// +optional
Scheduling *JobSchedulingConfiguration `json:"scheduling,omitempty"`
}
// JobSchedulingConfiguration composes the reusable WAS building blocks. The
// field names mirror the scheduler-facing PodGroupSpec 1:1 rather than being
// shortened, so the Job surface reads the same as the compiled PodGroup.
type JobSchedulingConfiguration struct {
// SchedulingPolicy defines the gang or basic scheduling rules for this Job.
// Exactly one of Basic or Gang must be set. Immutable after creation: the
// policy may not be added or removed, and the basic/gang variant may not be
// switched. Only gang.minCount may change.
// +optional
SchedulingPolicy *schedulingv1.WorkloadPodGroupSchedulingPolicy `json:"schedulingPolicy,omitempty"`
// SchedulingConstraints defines topology co-location constraints for the
// Job's pods. Immutable after creation.
// +optional
SchedulingConstraints *schedulingv1.WorkloadPodGroupSchedulingConstraints `json:"schedulingConstraints,omitempty"`
// DisruptionMode specifies how the pods in this Job should be disrupted
// (Single vs All). Immutable after creation.
// +optional
DisruptionMode *schedulingv1.WorkloadPodGroupDisruptionMode `json:"disruptionMode,omitempty"`
// ResourceClaims specifies dynamic resource claims shared across the Job's
// pods. Immutable after creation.
// +optional
ResourceClaims []schedulingv1.WorkloadPodGroupResourceClaim `json:"resourceClaims,omitempty"`
}
// SchedulingBlocked indicates that the controller cannot safely create pods
// because the scheduling objects for the Job are missing, ambiguous, or have
// an unsupported structure. This is a non-terminal condition and is cleared
// after the scheduling objects are repaired.
const JobSchedulingBlocked JobConditionType = "SchedulingBlocked"
The building blocks are the versioned scheduling.k8s.io/v1 types, embedded directly by
both the internal and versioned batch API. There are no internal batch-owned copies
of the building blocks; the internal JobSchedulingConfiguration references the schedulingv1
types as-is, so a single workloadbuilder mapping serves both the API validation and the
controller.
The Scheduling field is gated by the existing WorkloadWithJob feature gate. Standard
field-gating semantics apply: when the gate is disabled, the API server clears spec.scheduling on
create and ignores it on update (preserving an already-set value on the stored object), and the
field’s validation and the controller’s compilation (including its compile-time resolution of unset
fields; the api-server does not default spec.scheduling) only run when the gate is enabled. From
v1.38 the gate is enabled by default, so this path is only taken by clusters that opt out.
This typed, user-facing field replaces the v1.36 alpha’s implicit mechanisms: the type-based
automatic policy inference and the spec.template.spec.schedulingGroup-based opt-out are no longer
how users express or suppress scheduling intent.
Go Package Placement & Graduation
Embedding a pre-stable scheduling.k8s.io/v1alpha3 type inside the GA batch/v1.JobSpec was
permitted while the Scheduling field itself remained alpha-gated. Promoting the field to
default-enabled commits its wire format at the v1 resource level, and API conventions draw no
distinction between Beta and GA for a struct embedded in a v1 resource. Following the transition
pattern in KEP-6089, v1.38 therefore:
- updates the
batch/v1.JobSpecfield to reference thev1types - leaves Go type aliases (
type WorkloadPodGroupSchedulingPolicy = schedulingv1.WorkloadPodGroupSchedulingPolicy, etc.) ink8s.io/api/scheduling/v1alpha3so third-party controllers that still import the alpha package continue to compile and serialize identically.
Integration with the workloadbuilder Library
The Job controller compiles spec.scheduling into a Workload using the workloadbuilder library.
Library Dependency and Packaging
The building-block types live in the API staging repo (k8s.io/api/scheduling/v1). The workloadbuilder
library lives separately in k8s.io/component-helpers/scheduling/schedulingv1/workloadbuilder, so it can
be vendored by both in-tree and out-of-tree controllers. The Job controller consumes these entrypoints:
NewBuilder(root *WorkloadItem, opts BuildOptions)and(*Builder).BuildWorkload()to compile theWorkloadfromspec.schedulingNewBuilderFromExistingWorkload(workload, opts)to recompile against a persistedWorkload(elastic resize, delegatedPodGroup, resume after suspend)(*Builder).NewPodGroup(podGroupName, templateName)to instantiate the runtimePodGroupfrom a template(*Builder).Validate(ctx, ValidationInput)from API validation
If the library API shifts, the Job integration tracks those changes through the shared dependency rather than maintaining its own copy.
Building the Logical Tree and Compiling the Workload
The controller assembles the logical tree and compiles it in these steps:
- Build a
workloadbuilder.WorkloadInputfromspec.schedulingviajobutil.WorkloadInput, mappingschedulingPolicy/schedulingConstraints/disruptionMode/resourceClaimsonto the library’s per-block inputs (with their field paths for error reporting). - Assemble a single-node
WorkloadItemviajobutil.WorkloadItemForJob, whoseDefaultConfigresolves an absent policy toBasicand whose callback defaults an unset gangminCounttospec.parallelism(clamped to>= 1). - Invoke
NewBuilder(item, BuildOptions{Owner: <controllerRef to Job>, AllowedPolicies: ..., AllowedDisruptionModes: ...}).BuildWorkload(). The controller-ownerOwnerReferencemakes the emittedWorkloadGC with the Job.
For a PodGroup created from an external Workload, an update against a controller-owned Workload, or a
PodGroup recreated on resume, the controller uses NewBuilderFromExistingWorkload(workload, BuildOptions{...})
so it recompiles against the already-persisted template instead of re-deriving it from scratch.
API Validation via the workloadbuilder Library
spec.scheduling is validated in three complementary layers, all self-contained:
- Declarative validation (DV) on the building blocks owns the structural rules and most of the
immutability. Because the
batchAPI embeds the versionedscheduling.k8s.io/v1building blocks directly, their DV markers apply unchanged. - Hand-written
batchvalidation covers the cross-cutting rules DV cannot express:validateGangMinCountrejects a gangminCountgreater thanspec.parallelism. A gang larger than the pod count can never be satisfied and the Job would stall with pending pods, so it is rejected at admission rather than surfacing only at runtime. It runs on create and update and for Jobs embedded in aCronJobjobTemplate. An elastic scale-up that raisesspec.parallelismandgang.minCountin the same request is validated against the final state.validateJobSchedulingUpdatefreezes the basic/gang policy after creation. DV keeps the policy present but cannot forbid an in-place switch betweenbasicandgang, so onlygang.minCountmay change.spec.schedulingis rejected whenspec.template.spec.schedulingGroupis set, because the user’sPodGroupis authoritative andspec.schedulingwould have no effect. This applies on create and on update, unless the old object already had both fields (v1.37 alpha).
workloadbuildersemantic validation owns the consistency rules that must stay identical to what the controller compiles. Validation builds the sameWorkloadItemtree the controller does and callsNewBuilder(...).Validate(). This runs the builder’s allow-list checks. In-tree it is constructed withBuildOptions{DisableDeclarativeValidation: true}because the api-server already ran DV on the versioned building blocks, soValidatehere neither re-runs DV, compiles theWorkload, nor needs an owner.
Instantiating the runtime PodGroup
BuildWorkload returns only the Workload (the scheduling template); it does not create the runtime
PodGroup. After the Workload exists on the API server, the Job controller instantiates the
PodGroup from the Workload’s single PodGroupTemplate. For a Job there is exactly one template,
so the controller creates one PodGroup that references the template and carries two
ownerReferences — a controller ref to the Job (so it is GC’d with the Job) and a non-controller
ref to the Workload. The same NewPodGroup path is used when the PodGroup is recreated on
resume, with the retained Workload as input.
Pods are then created by the existing Job pod-management logic with
pod.Spec.SchedulingGroup.PodGroupName set to the PodGroup’s name, which is what the scheduler
keys on for gang/topology behavior.
Reconcile Integration and Error Handling
The scheduling reconcile path is invoked from the Job reconcile loop, gated on the
WorkloadWithJob feature gate, before pod management runs. Creation eligibility depends on the
management mode and lifecycle phase, as described in Controller Workflow.
The integration is designed to be idempotent and crash-safe:
- Discovery first: the controller looks up an existing
Workload/PodGroupbefore compiling. A restart between creating theWorkloadand thePodGrouptherefore does not produce duplicates, and a create that races with informer lag fails withAlreadyExistsand is retried against the now-visible object. - Ordering:
Workloadis created (or found) before thePodGroup, and both before pods, so references always resolve. - Invalid
spec.scheduling: a compilation error fromBuildWorkloadis terminal for that spec. Becausespec.schedulingis immutable, the controller fails the Job without creating pods, with reasonFailedWorkloadCompilationon both theFailedcondition and the Warning event. The user recreates the Job with a validspec.scheduling. API validation runs the sameworkloadbuilderchecks, so this is not expected after admission. - API errors are retryable: an API error creating the
Workload/PodGroupemitsFailedWorkloadCreateand requeues the Job with backoff, blocking pod creation until the write succeeds. - Blocking: two situations stop pod creation without failing the Job. In both, the controller
sets the non-terminal
SchedulingBlocked=Truecondition through a dedicated status update (sostatus.startTimeand other lifecycle fields are untouched), emits a Warning event, incrementsjob_scheduling_object_syncs_total{result="error"}, and returns an error so the Job is requeued with backoff. No new pods are created until the next successful reconciliation clears the condition.- Missing external dependency (
managePodGroupOnlyonly): eventSchedulingDependencyNotFound, reasonWorkloadNotFoundorPodGroupTemplateNotFound. The creation window stays open so thePodGroupis created once the dependency appears. - Unsupported or ambiguous objects: event and reason
UnsupportedWorkloadStructure. An operator must remove the duplicate or repair the object.
- Missing external dependency (
- Updates: on a
gang.minCount(orparallelism-driven) change the controller recompiles the desiredWorkload(NewBuilderFromExistingWorkload(...).BuildWorkload) and patches the delta to the existing object, then patches the size onto the runtimePodGroup. - Suspend/resume: the runtime
PodGroupis the only object the controller deletes explicitly.
Job Controller Changes
The Job controller reconciliation loop that processes each Job will be extended to ensure Workload
and PodGroup objects exist before creating pods.
Before doing any work, the controller classifies each Job into one of three management modes, which determines how much of the scheduling tree it owns. The supported scenarios are:
manageBoth: The controller compiles and owns both theWorkloadand runtimePodGroupfromspec.scheduling. Jobs created byCronJobare scheduling roots even though they have a controllerownerReference.managePodGroupOnly: the Job carries thescheduling.k8s.io/group-template-nameannotation. The controller gets the externalWorkloadfrom thescheduling.k8s.io/workload-nameannotation, uses the namedPodGroupTemplate, and creates the runtimePodGrouponly.manageNone: the controller creates nothing and only stamps pods with whateverschedulingGroupthe pod template already carries. This mode is selected when any of the following holds:spec.template.spec.schedulingGroupis set (bring-your-ownPodGroup): the user manages the objects.- A higher-level workload controller (e.g. JobSet) owns the Job and no
scheduling.k8s.io/group-template-nameannotation is present. The parent controller owns both scheduling objects and has setschedulingGroupon the pod template itself. - The
WorkloadWithJobfeature gate is disabled.
flowchart TD
gate{WorkloadWithJob on?} -->|no| none[manageNone]
gate -->|yes| sg{Pod template schedulingGroup set?}
sg -->|yes| none
sg -->|no| ann{group-template-name set?}
ann -->|yes| pgOnly[managePodGroupOnly]
ann -->|no| parent{Higher-level workload controller owns Job?}
parent -->|yes| none
parent -->|no, including CronJob| both[manageBoth]Workload/PodGroup Management and Discovery
In every mode, the controller discovers both the Workload and the PodGroup by name. Discovery
never uses ownerReferences, because a higher-level controller may create objects before the Job
exists. ownerReferences only govern garbage collection and whether the Job controller may mutate
or delete an object.
A Workload is considered the Workload for this Job object if:
- it is in the Job’s namespace
- its name is:
- in
manageBoth,<job-name>-<hash>(Naming Conventions). - in
managePodGroupOnly, the value of thescheduling.k8s.io/workload-nameannotation (KEP-6089). TheWorkloadmay be compiled by any ancestor of the Job (e.g. TrainJob -> JobSet -> Job) or pre-created by the user.
- in
A PodGroup is considered the PodGroup for this Job if:
- it is in the Job’s namespace
- its name is
GeneratePodGroupName(workloadName, templateName, job.UID), see Naming Conventions. Sibling Jobs selecting the same template (e.g. JobSetReplicatedJobreplicas) have identicalspec.podGroupTemplateRefs, so only the name tells them apart.
The Job is blocked with UnsupportedWorkloadStructure if:
- a root Job’s
Workloaddoes not have exactly one template - a root Job’s
Workloadhas aspec.controllerRefthat does not point at this Job - the Job’s
PodGroupname does not reference the selectedWorkloadand template
In managePodGroupOnly, the Job is blocked and retried while any of these is true:
WorkloadNotFound: thescheduling.k8s.io/workload-nameannotation is missing, or noWorkloadwith that name exists in the Job’s namespace yet.PodGroupTemplateNotFound: theWorkloadhas no template named byscheduling.k8s.io/group-template-name.
When spec.template.spec.schedulingGroup.podGroupName is set (manageNone), the controller uses
that name as-is and performs no discovery.
Controller Workflow
The Job controller attempts to create the Workload and PodGroup only when the Job has no pods
associated with it (no active or terminal pods owned by the Job). If the Job already has one or
more pods, the controller only discovers and uses the existing Workload/PodGroup, if any, and
does not create new ones. This rule is important for correctness when the controller restarts or
is upgraded in the middle of the workflow (i.e., after creating the Workload but before creating
the PodGroup or pods). On the next sync, the controller finds the existing objects via the
listers and continues.
A suspended Job follows the same workflow and gets its Workload created. Its PodGroup is
created only on resume.
The controller discovers or creates Workload and PodGroup as follows:
- Determine the management mode:
- In
manageNone, the controller stops here and pods keep whateverschedulingGroupthe pod template carries. - In
managePodGroupOnly(skip step 4), theWorkloadis external and is never created, adopted, recompiled, mutated, or deleted.
- In
- If the Job already has pods (active or terminal pods owned by this Job), skip creation and only discover existing objects.
- Get the
Workloadby name:- If none found in
manageBothcompile aWorkloadfrom the Job’sspec.schedulingand create it, with a controllerownerReferenceandspec.controllerRefpointing to this Job. - In
managePodGroupOnly, get theWorkloadnamed byscheduling.k8s.io/workload-nameand select the template named byscheduling.k8s.io/group-template-name. If either is missing, setSchedulingBlocked=True(reasonWorkloadNotFoundorPodGroupTemplateNotFound) and retry. - If one of the
UnsupportedWorkloadStructurecases above applies, block. - Otherwise, that is the
Workloadfor this Job. ItsownerReferencesare not changed.
- If none found in
- When creating a new
Workload, the controller derives the scheduling policy from the Job’sspec.schedulingrather than from the Job’s type. It mapsspec.schedulinginto theworkloadbuilderlibrary, which applies the defaulting rules (defaulting toBasic, defaultingGang.minCounttoparallelism) and compiles theWorkload. This happens for every eligible Job, including those that default toBasic. - Look up the
PodGroupfor the targetPodGroupTemplateby name.- If none found, instantiate a
PodGroupfrom thatWorkloadand template and create it under that name with a controllerownerReferenceto theJob. When thescheduling.k8s.io/parent-compositepodgroupannotation is present, link it to that parentCompositePodGroupinstance. AnAlreadyExistserror (informer lag) requeues the Job and the next sync finds the object. - If the object at that name does not reference the selected
Workloadand template, block (UnsupportedWorkloadStructure). - Otherwise, that is the
PodGroupfor this Job. ItsownerReferencesare not changed.gang.minCountis reconciled only when the Job is the controller owner, which is the case for everyPodGroupthe Job controller created.
- If none found, instantiate a
- Execute the existing pod-management logic to create pods, including
schedulingGroup.podGroupNamein the pod spec to associate pods with thePodGroup. This step is not reached while the Job is blocked. If the Job already has active pods withoutschedulingGroup(created while the gate was off), new pods are created without it too and aMixedSchedulingGroupWarning event is emitted once, so a gang is never half-grouped.
The controller requires informers, listers and indexers for Workload and PodGroup objects.
Both Workload and PodGroup are automatically garbage collected when they were created by the Job
controller and the corresponding Job is deleted.
OwnerReferences Relationship
The ownerReferences relationship between Job, Workload, PodGroup, and Pod depends on the
management mode.
Root Job (manageBoth):
flowchart BT
Pod[Pod]
PodGroup[PodGroup]
Workload[Workload]
Job[Job]
Pod -->|"controller ownerRef"| Job
Pod -->|"ownerRef"| PodGroup
PodGroup -->|"controller ownerRef"| Job
PodGroup -->|"ownerRef"| Workload
Workload -->|"controller ownerRef"| Job
Workload -.->|"spec.controllerRef"| Job
PodGroup -.->|"spec.podGroupTemplateRef"| Workload
Pod -.->|"spec.schedulingGroup.podGroupName"| PodGroupDelegated Job (managePodGroupOnly under a parent workload controller):
flowchart BT
Pod[Pod]
PodGroup["PodGroup"]
Workload["Workload, compiled by parent"]
Job[Job]
Parent[Parent workload controller]
Pod -->|"controller ownerRef"| Job
Pod -->|"ownerRef"| PodGroup
Job -->|"controller ownerRef"| Parent
PodGroup -->|"controller ownerRef"| Job
Workload -->|"controller ownerRef"| Parent
Workload -.->|"spec.controllerRef"| Parent
PodGroup -.->|"spec.podGroupTemplateRef"| Workload
Pod -.->|"spec.schedulingGroup.podGroupName"| PodGroup- The
Workloadhas a controller ownerReference to theJobonly when the Job controller created it. - The
PodGrouplinks to itsWorkloadviaspec.podGroupTemplateRef. When the Job controller creates it, the controller ownerReference points to the Job in both modes, so a delegatedPodGroupis garbage collected with its Job. A parent-ownedWorkloadis never given an ownerReference from thePodGroup. - The
Podhas a controller ownerReference to theJoband a non-controller one to thePodGroup.
A user pre-created Workload follows the delegated diagram without the parent.
The Job controller never adds ownerReferences to objects it did not create. Garbage collection
follows the solid edges, so no Pod is left with a stale PodGroup reference.
Defaulting Rules
These rules are applied by the controller when it compiles the
Workload/PodGroup; they are not api-server field defaulting.
This is a controller-side resolution that is required to resolve the unset case anyway.
Schedulingunset →Basic. Existing and non-WAS Jobs carry nospec.scheduling, the controller resolves the absent policy toBasic, preserving their behavior.Schedulingset butSchedulingPolicynil →Basic.WorkloadPodGroupSchedulingPolicyis a discriminated union for which the compiledPodGroupmust carry exactly one concrete policy, so a nilSchedulingPolicyis resolved toBasic.GangwithMinCountunset →MinCount = parallelism. Done controller-side only without persisting the derived value back onto the Job spec because writing it back would make a user-setminCountindistinguishable from the default on later updates, whereminCountis mutable. The controller clamps this default to a minimum of 1 (a suspended Job may haveparallelism = 0, but a gangminCountmust be positive).
Optional modifiers (DisruptionMode, SchedulingConstraints, ResourceClaims) are deliberately not
defaulted. Unlike SchedulingPolicy, these are optional fields whose absence is a defined state. A nil DisruptionMode resolves to standard per-pod (Single) disruption, a nil SchedulingConstraints
means no topology co-location, and a nil ResourceClaims means no shared claims.
Object Creation Order
The Job controller creates objects in the following order so that references point to existing objects and to satisfy any API validation that Workload exists before PodGroup is created. The order is as follows:
Workloadobject which will reference theJob.PodGroupobject which will reference theWorkloadand theJob.Podobjects which will referencePodGroup.
The kube-scheduler waits for PodGroup when Pods have schedulingGroup, so scheduling does not depend on this order, the order is for consistency and API validity.
Handling Updates and Mutability
To support dynamic scaling of gang-scheduled workloads (Elastic Jobs), the Job API allows in-flight
updates to spec.scheduling.schedulingPolicy.gang.minCount; all other spec.scheduling fields are immutable
after Job creation, and updates that change them are rejected by API validation. This replaces the
v1.36 validation that rejected spec.parallelism updates for gang Jobs: because the gang size is
now driven by the mutable minCount (KEP-4671), spec.parallelism is no longer frozen for gang
Jobs, restoring support for Elastic Indexed
Jobs. API
validation reuses the workloadbuilder library where possible so the accepted configurations stay
consistent with what the controller actually compiles. The update-validation rules change as follows:
spec.parallelismbecomes mutable again: the v1.36 rule that rejectedspec.parallelismupdates for gang Jobs is removed, restoring Elastic Indexed Jobs.spec.scheduling.schedulingPolicy.gang.minCountis mutable in-flight: on change the controller recompiles theWorkloadand re-syncs thePodGroupsize.- All other
spec.schedulingfields remain immutable after creation, enforced by api-server validation, since changing the policy, topology, disruption mode, or resourceClaims would require recreating theWorkload/PodGroup.
When minCount is omitted it follows spec.parallelism, so a parallelism update is a valid way to
scale the gang without ever touching spec.scheduling.
Reconciliation Flow upon Updates
A user can change the target gang size in one of two ways:
- by setting
spec.scheduling.schedulingPolicy.gang.minCountdirectly, when it is set explicitly - by setting
spec.parallelism, whenminCountis unset.
In either case, the Job controller reconciles the change as follows:
- Detection: the Job controller’s reconcile loop detects the change and fetches the existing
Workloadresource from the API server. - Workload Compilation: it recompiles the desired
Workloadfrom the updatedspec.parallelism/minCountviaNewBuilderFromExistingWorkload(...).BuildWorkload, reusing the persisted template so only the gang size changes. - Workload Update: the controller applies the delta to the existing resource with a
strategic-merge
Patchrather than a full replace. - PodGroup Sync: the controller patches the updated size onto the runtime
PodGroupso the scheduler targets the newly scaled size. InmanagePodGroupOnlythe externalWorkloadis never recompiled, and the controller syncs thePodGroupit owns with the selected external template instead.
minCount is enforced only during scheduling: per KEP-4671, updates do not affect
already-scheduled pods and apply only to pods evaluated in future scheduling cycles. The scheduler
also operates on an eventually consistent view, so an update may not take effect until the next
scheduling cycle.
Suspend and Resume
Suspending a Job deletes its pods and is expected to release the resources they held. In the
v1.37 alpha, the Workload and PodGroup were left in place during suspension. That is a
problem for the PodGroup, because scheduler state and DRA claims can remain associated with
the PodGroup after its members terminate. A suspended Job should not keep those resources.
For Beta, the controller handles the two objects differently:
- The
Workloadis the persisted scheduling template. Keeping it preserves the selected template and lets the controller instantiate a replacementPodGroupwithNewBuilderFromExistingWorkload. A controller-owned Workload remains eligible for supportedgang.minCountreconciliation while suspended. - The runtime
PodGroupis the object used for scheduling and DRA integration. Deleting it releases those resources. A newPodGroupis created on resume and gets a fresh placement decision under the selected policy.
The sequence on suspend is:
- The existing suspend path deletes the Job’s active pods and sets the
JobSuspendedcondition. - In the same reconciliation, the controller deletes the
PodGroupit owns, including a delegatedPodGroup, and emits aPodGroupDeletedevent. The finalizer keeps the object terminating until its pods are gone, so there is no ordering to enforce here. APodGroupthe controller did not create is untouched. - The
Workloadstays.gang.minCountchanges made while suspended are still patched onto the controller-ownedWorkloadso a newly createdPodGrouppicks them up.
On resume, the controller acts on the observed PodGroup state:
- If the
PodGroupis absent, create a replacement from the retainedWorkload, then create pods. The replacement has a new UID. - If the
PodGroupis terminating, wait until it is absent, then proceed as above. - If the
PodGroupis present and not terminating (an earlier delete failed or was never issued), reuse it.
For a Job that is created suspended (spec.suspend: true at creation) the controller creates the
Workload immediately but defers the PodGroup to the first resume, so a suspended Job never owns
a runtime PodGroup.
Bring-your-own Workload and PodGroup
A user can pre-create a Workload and select one of its templates by placing
scheduling.k8s.io/workload-name and scheduling.k8s.io/group-template-name on the Job. The
controller finds the Workload by that name, not by its spec.controllerRef:
apiVersion: scheduling.k8s.io/v1beta1
kind: Workload
metadata:
name: training-template
namespace: training
spec:
controllerRef:
apiGroup: batch
kind: Job
name: training
podGroupTemplates:
- name: workers
schedulingPolicy:
gang:
minCount: 4
---
apiVersion: batch/v1
kind: Job
metadata:
name: training
namespace: training
annotations:
scheduling.k8s.io/workload-name: training<hash>
scheduling.k8s.io/group-template-name: <workload-name>-<podGroup-template-name>-<hash>
spec:
parallelism: 4
# ... pod template omitted
The controller enters managePodGroupOnly, discovers the external Workload, and creates only the
runtime PodGroup. It does not add an ownerReference to the external Workload. A missing or
unsupported template blocks pod creation, as described in Reconcile Integration and Error Handling.
This is not a new mechanism. This path and the delegated path both use KEP-6089’s
downward-mapping annotations, which the Job controller already honors today. Beta adds no other way
to bring your own Workload, and none is planned.
The Job controller cannot tell whether an external Workload was compiled by a higher-level
controller or pre-created by an end user, and it does not try to. When the annotations select a
template, both are honored identically. The Workload is never adopted or reconciled against
spec.scheduling, and a structure that does not match the supported shape blocks the Job rather
than being repaired.
A user or higher-level controller can instead manage the PodGroup and wire the Job’s pods to it
by setting spec.template.spec.schedulingGroup.podGroupName. In this case the controller enters
manageNone:
- It creates no
Workloadand noPodGroup, and does not add anownerReferenceto, mutate, or delete the user’sPodGroup. - The user’s
PodGroupis authoritative: validation rejectsspec.schedulingtogether withschedulingGroup, so there is nothing for the controller to translate or sync into it. - Pods are created with the user-provided
schedulingGroupas-is.
Naming Conventions
Names are derived deterministically from the Job. The PodGroup name is how the controller
discovers the PodGroup in every mode, and the Workload name is how a root Job discovers its
Workload. Neither pattern can be changed in later releases without orphaning existing objects.
Following prior-art in Deployment, the naming convention is as follows:
1. Workload
- Pattern:
<(truncated-if-needed)job-name>-<hash> - Truncation of the Job name is applied when necessary to respect object name length limits.
- The hash is derived from the Job UID, so a recreated Job with the same name never matches the
old
Workload.
2. PodGroup
- Pattern:
<(truncated-if-needed)workload-name>-<(truncated-if-needed)podGroup-template-name>-<hash> - Truncation of the workload name and podGroup template name is applied when necessary to respect name length limits.
- The hash is derived from the Job UID, so sibling Jobs that select the same
PodGroupTemplateget onePodGroupeach. The parent is not part of the name, because the name describes which Job the object serves, not who owns theWorkload. - The controller creates a single
PodGroupper Job at a time. Recreating it from the same Workload and template can reuse the name after the old object is gone. The new API object is distinguished by its UID. - This replaces the alpha
GeneratePodGroupName(workloadName, templateName), which had no Job identity.
Deletion and Garbage Collection
Deletion of the scheduling objects happens on two paths:
- Job deletion relies on garbage collection:
- The controller-created
WorkloadandPodGroupobjects carry a controllerownerReferenceto the Job, so they are deleted when the Job is deleted. - A
PodGroupDELETEexecutes immediately, but the object stays in terminating state until the finalizer observes all referencing pods terminal. TheblockOwnerDeletionis unset on the Pod ownerRef so the garbage collector adds no further dependency.
- The controller-created
- Job suspension is the one case where the controller deletes explicitly, and only a runtime
PodGroupwhose controllerownerReferenceis the Job. TheWorkloadis never deleted by the Job controller.
Future Extensions
The following requirements (out of scope for Beta) shape the design so they can be added without
breaking the spec.scheduling surface:
- Multiple
PodGroups per Job from onePodGroupTemplate. Some accelerator topologies (e.g. TPU slices) require splitting a Job’s pods into homogeneous pieces where each piece is gang-scheduled on its own slice. The pieces share one scheduling policy, so a singlePodGroupTemplatesuffices, but each piece needs its own runtimePodGroup. Today the controller instantiates exactly onePodGroupper Job. Supporting NPodGroups per template requires a way to express the split on the Job and a pod-to-group assignment rule, which will be proposed in a separate KEP. Since this changes the Job-to-PodGroup mapping, it must be designed beforespec.schedulinggraduates to GA, see Graduation Criteria. - Gang-aware slow-start batching. The Job controller creates pods in slow-start batches
starting at 1, which delays the moment a gang of
minCountpods is fully created. Once the Beta performance tests quantify time-to-first-schedule for gang Jobs, the initial batch size for aGangJob may be raised (e.g. to a fraction ofminCount) if the data shows a meaningful gain.
Test Plan
[x] I/we understand the owners of the involved components may require updates to existing tests to make this code solid enough prior to committing the changes necessary to implement this enhancement.
Prerequisite testing updates
Unit tests
k8s.io/kubernetes/pkg/controller/job:2026-09-10-91.2%k8s.io/kubernetes/pkg/apis/batch/validation:2026-09-10-88.8%k8s.io/kubernetes/pkg/registry/batch/job:2026-09-10-95.4%- Add tests that verify:
- An omitted
spec.schedulingis resolved by the controller to theBasicpolicy, and aGangpolicy with a nilMinCountis resolved toMinCount = parallelismwithout the api-server writing these values back into the Job’sspec.scheduling. workloadbuildercompilation:BasicvsGangpolicy, and that topology constraints, disruption mode (single/all), and resourceClaims are mapped into the generatedWorkload/PodGroup; that aJobbuilds a flat single-node tree via the sharedWorkloadInput/WorkloadItemForJobhelpers. A compilation error fails the Job with reasonFailedWorkloadCompilationand creates no pods.- A
BasicWorkload/PodGroupis created for a Job withspec.schedulingomitted. - pod creation includes the correct
schedulingGroup. - Mutability/validation: updates to
spec.scheduling.schedulingPolicy.gang.minCountare allowed; updates to any otherspec.schedulingfield are rejected.spec.schedulingwithspec.template.spec.schedulingGroupis rejected on create and on update, unless the old object already had both. gang.minCount > spec.parallelismis rejected on both create and update. A single request that raisesspec.parallelismandgang.minCounttogether is accepted.- Feature gate disabled:
spec.schedulingis dropped on create and noWorkload/PodGroupis created. - Mixed-gang guard: a Job with an existing
PodGroupand an active pod withoutschedulingGroupcreates its next pod withoutschedulingGroupand emitsMixedSchedulingGrouponce. Once no such pod is active, new pods carryschedulingGroup. - Parent-owned
Workload, both delegated: a Job with anOwnerReferenceto a parent workload and no annotation creates neitherWorkloadnorPodGroup. - Parent-owned
Workload,PodGroupdelegated: a Job with anOwnerReferenceto a parent workload and both annotations present does not create aWorkload, but does create aPodGrouplinked to theWorkloadnamed byscheduling.k8s.io/workload-name, including when thatWorkloadis owned by a higher-level controller. - Shared delegated template: two sibling Jobs whose annotations name the same
PodGroupTemplateeach create and own their ownPodGroup. Deleting one sibling deletes only itsPodGroup, and agang.minCountchange on the template is synced onto both. PodGroupname collision: an unrelatedPodGroupat the Job’sPodGroupname blocks withUnsupportedWorkloadStructureuntil it is removed, for both root and delegated Jobs.- Root
Workloadname collision: aWorkloadat the root Job’s<job-name>-<hash>name whosespec.controllerRefis not the Job blocks withUnsupportedWorkloadStructure. - Missing delegated dependency: a delegated Job with no
scheduling.k8s.io/workload-nameannotation, or whose namedWorkloadorPodGroupTemplatedoes not exist yet, getsSchedulingBlocked=True(reasonWorkloadNotFoundorPodGroupTemplateNotFound) and no pods, also while suspended. - BYO
Workload: a standalone Job with both annotations uses theWorkloadnamed byscheduling.k8s.io/workload-name. - Job deletion cascades to
WorkloadandPodGroupdeletion. - ownerReferences on controller-created objects match the expected structure:
- Root Job:
Workloadhas a controller ownerRef to the Job;PodGrouphas a controller ownerRef to the Job and a non-controller ownerRef to theWorkload. - Delegated Job:
PodGrouphas a controller ownerRef to the Job and links to the parent-ownedWorkload/CompositePodGroup. - Standalone BYO
WorkloadJob:PodGrouphas a controller ownerRef to the Job.
- Root Job:
- Naming abbreviations for
WorkloadandPodGroup, and differentPodGroupnames for two Jobs with the same name but different UIDs. - Discovery and management are independent: a
PodGroupis discovered regardless of its ownerReferences, but is mutated or deleted only if its controller ownerReference is the Job. - Drift: a controller-owned
WorkloadwhosePodGroupTemplatescount is not 1. Repairing the object clears the condition and pod creation resumes withschedulingGroup. - Suspend/resume: the
PodGroupdelete is issued in the suspend sync and theWorkloadis retained. Resume reuses a non-terminatingPodGroup, waits out a terminating one, and recreates an absent one with a new UID. A Job created withspec.suspend: truegets aWorkloadbut noPodGroupuntil first resume.
- An omitted
Integration tests
Existing tests in test/integration/job/job_test.go (v1.37):
TestJobGangScheduling: lifecycle forBasicandGangJobs (create, discover, delete;WorkloadandPodGroupare materialized, pods carryschedulingGroup, deletion cascades), passthrough of topology constraints, disruption mode and resourceClaims, and blocking on multiple objects.TestJobGangSchedulingElasticScaling: updatingspec.scheduling.schedulingPolicy.gang.minCount(orspec.parallelismwhenminCountis unset) updates theWorkloadand the runtimePodGroup.TestJobGangSchedulingSuspendResume: suspend/resume behavior (updated for Beta, see above).TestJobDelegatedPodGroup: a Job owned by a parent workload skipsWorkloadcreation, skipsPodGroupcreation when no annotation is set, and creates aPodGroupmapped to the parent’sPodGroupTemplatewhen the annotation is present.- Feature gate disabled:
spec.schedulingis dropped and noWorkload/PodGroupis created.
Tests added for Beta, exercising the unit-tested behaviors above against a real API server with informer lag, pod finalizer removal, and controller restarts:
- Suspend/resume: resume while the
PodGroupis terminating and after it is gone, pods created after resume reference the replacementPodGroup, and completed Pod objects left behind do not block the finalizer. - Bring-your-own
PodGroupviaspec.template.spec.schedulingGroup: noWorkload/PodGroupis created and noownerReferenceis added to the user’sPodGroup. - BYO
Workloadviascheduling.k8s.io/workload-nameandscheduling.k8s.io/group-template-name: only the runtimePodGroupis created,spec.schedulingis not reconciled into the external object, and a missing or unsupported template blocks. - Delegated dependency retry: a delegated Job created before its parent
Workload(and, separately, before the named template exists) is blocked, then recovers without being recreated once the dependency is added. Repeated with the Job created suspended: the condition is set while suspended, clears once theWorkloadexists, and thePodGroupappears on resume. - Ambiguous and drifted scheduling objects: blocked, then recovers after the objects are repaired.
- Sibling delegated Jobs: two Jobs with the same parent and template each get their own
PodGroup, and deleting one Job leaves the other’sPodGroupin place. - Gate enable -> disable -> re-enable: a gang Job created with the gate on keeps its
Workload/PodGroupwhile the gate is off and creates ungrouped pods. After re-enable, new pods stay ungrouped with aMixedSchedulingGroupevent until the ungrouped pods finish, then carryschedulingGroupagain.
e2e tests
Existing tests (test/e2e/apps/job.go):
should compile a Workload and PodGroup for a gang-scheduled Job and wire its pods to the PodGroupshould propagate an elastic gang minCount change to the Workload and PodGroupshould create Workload and PodGroup for gang-eligible Job(also gated onGenericWorkload; verifiesPodGroupowner references and podschedulingGroupwiring)
Tests added for Beta:
- Gang scheduling behavior: with insufficient capacity for
minCountpods, no pod of the gang is bound; once capacity is available all are bound. Basicscheduling policy: pods of a Job withoutspec.schedulingare scheduled pod-by-pod and the Job completes, with aBasicWorkload/PodGrouppresent.- Suspend/resume: a suspended gang Job gets a replacement
PodGroupafter resume and completes under theGangpolicy. ABasicJob resumes with pod-by-pod placement under its own policy. - CronJob with gang scheduling: each Job created by the CronJob gets its own
Workload/PodGroup, and completed Jobs clean up their scheduling objects via GC.
Performance: a scale test in kubernetes/perf-tests compares Job creation throughput and
job_sync_duration_seconds with the gate on and off, and records time-to-first-bind for gang Jobs.
Graduation Criteria
Alpha (v1.36)
The first alpha (the automatic, type-based model) delivered:
- Feature implemented behind the
WorkloadWithJobfeature gate (default: disabled). - Job controller creates
Workload/PodGroupobjects when the feature gate is enabled. - Gang scheduling policy applied to indexed parallel Jobs (
parallelism > 1,completions = parallelism,completionMode: Indexed). - Non-gang scheduling Jobs do not have
Workload/PodGroupobjects created. - Jobs managed by higher-level controllers skip
Workload/PodGroupcreation. - API validation rejects updates that change
spec.parallelismfor gang scheduling Jobs. - Unit and integration tests for the
Workload/PodGroupcreation flow.
Alpha (v1.37)
The second alpha replaced the automatic model with the user-facing API:
- New
spec.scheduling(JobSchedulingConfiguration) field added tobatch/v1, gated by the existingWorkloadWithJobfeature gate (still default-disabled). - The Job controller compiles
spec.schedulingintoWorkload/PodGroupvia the sharedworkloadbuilderlibrary, defaulting toBasicand materializing aWorkload/PodGroupfor every eligible Job. Gangopt-in withminCountdefaulting toparallelism, plus support for mutableminCount(elastic scaling) and passthrough of topology constraints, disruption mode, and resourceClaims.- API validation makes
spec.schedulingfields immutable exceptgang.minCount; the v1.36spec.parallelism-rejection validation is removed. - Jobs owned by a higher-level controller (via
OwnerReference) deferWorkloadownership to the parent; they manage their ownPodGroupwhen the parent delegates it via the annotation, and skip both objects otherwise. - Unit and integration tests for the new API, defaulting, mutability, and
workloadbuildercompilation; user-facing documentation for the new API.
Beta
WorkloadWithJobenabled by default, together withGenericWorkload(KEP-4671). IfGenericWorkloaddoes not ship default-on in v1.38,WorkloadWithJobgraduates to Beta as default-disabled.batch/v1.JobSpec.Schedulingrepointed to thescheduling.k8s.io/v1building blocks (KEP-6089).- A suspended Job deletes its controller-owned runtime
PodGrouptogether with its pods, and resume recreates it once the old object is gone. - Unsupported or ambiguous scheduling objects emit an
UnsupportedWorkloadStructureWarning event, set the non-terminalSchedulingBlockedcondition, and block new pod creation until repaired. - Missing delegated
WorkloadandPodGroupTemplatedependencies are retried without closing the PodGroup creation window or creating ungrouped pods. - The
Workloadand thePodGroupare discovered by name in every mode, using thescheduling.k8s.io/workload-nameannotation for an externalWorkload. EveryPodGroupthe controller creates is owned by the Job. - Validation rejects
spec.schedulingif it is set together withspec.template.spec.schedulingGroup, on create and on update, unless the old object already had both. - Re-enabling the gate never produces a half-grouped gang (
MixedSchedulingGroup). - Metrics
job_scheduling_object_syncs_totalandjob_scheduling_object_sync_duration_seconds. - E2E tests passed as designed in the Test Plan and linked in Testgrid.
- Performance test in
kubernetes/perf-testsshowing no regression in Job creation throughput. - Address all issues reported by users during alpha.
- User-facing documentation updated.
GA
GenericWorkload(KEP-4671) is GA.- Two releases of Beta with no changes to the
spec.schedulingshape or defaulting. - Address all issues reported by users during beta, including feedback on the blocking and recovery behavior for ambiguous or drifted objects.
- Multiple
PodGroups per Job from a singlePodGroupTemplatein a follow-up KEP, see Future Extensions. - e2e tests for
spec.schedulingpromoted to conformance. WorkloadWithJoblocked to enabled; the gate is removed after the deprecation window.
Deprecation
N/A. Promotion to Beta deprecates nothing; the v1alpha3 building-block package remains
importable through type aliases.
Upgrade / Downgrade Strategy
This KEP is additive and falls back to the original behavior on downgrade.
Upgrade
kube-apiserver is upgraded first, then kube-controller-manager. From that point every eligible Job
that has not started gets a Workload/PodGroup compiled from its spec.scheduling (defaulting
to Basic). Jobs that already started before the upgrade are left alone. Operators who do not want
the scheduling objects can set WorkloadWithJob=false on kube-apiserver and kube-controller-manager;
disabling GenericWorkload requires disabling WorkloadWithJob as well.
The v1.37 PodGroup name has no Job identity (<workload>-<template>), so a Job that already has
pods may not find its existing PodGroup after the upgrade, and its replacement pods are created
without schedulingGroup. In-flight gang Jobs should finish or be recreated before upgrading.
Clusters upgrading from the v1.36 alpha should note that indexed fully-parallel Jobs are no longer
gang-scheduled automatically; since v1.37 gang scheduling requires spec.scheduling.schedulingPolicy.gang.
Downgrade
Downgrading kube-controller-manager (or disabling the gate) stops compiling Workload/PodGroup,
stops setting schedulingGroup on new pods, and stops deleting PodGroups on suspend. In-flight gang
Jobs and suspended Jobs need operator attention, see
Rollout, Upgrade and Rollback Planning. Downgrading
kube-apiserver clears spec.scheduling on create and ignores it on update; values already stored on
a Job are preserved and served again once the gate is re-enabled. Existing Workload/PodGroup
objects remain in etcd, are still honored by the scheduler for pods that reference them while
GenericWorkload is enabled there, and are garbage collected with their Job.
Version Skew Strategy
This feature is limited to the control plane, so version skew with kubelets does not matter.
kube-apiserver can be one minor version ahead of kube-controller-manager and kube-scheduler. A
kube-controller-manager with WorkloadWithJob disabled ignores spec.scheduling and compiles no
Workload/PodGroup, and a kube-scheduler with GenericWorkload disabled ignores
schedulingGroup on pods. In both cases Jobs schedule pod-by-pod as if the feature were absent
until the gate is enabled on that component. Users should not rely on gang scheduling until all
control-plane instances run with both gates enabled.
In HA control planes only one kube-controller-manager and one kube-scheduler are leader at a time, so replica skew does not matter.
Production Readiness Review Questionnaire
Feature Enablement and Rollback
How can this feature be enabled / disabled in a live cluster?
- Feature gate (also fill in values in
kep.yaml)- Feature gate name:
WorkloadWithJob - Components depending on the feature gate:
- kube-controller-manager
- kube-apiserver
- Feature gate name:
- Other
- Describe the mechanism:
- Will enabling / disabling the feature require downtime of the control plane?
- Will enabling / disabling the feature require downtime or reprovisioning of a node?
Does enabling the feature change any default behavior?
Yes. With the gate enabled, the Job controller creates and manages a Workload and a PodGroup
for every eligible Job that has not started, including Jobs without spec.scheduling, and sets
spec.schedulingGroup.podGroupName on the pods it creates.
Note this differs from the v1.36 alpha, where indexed fully-parallel Jobs were gang-scheduled
automatically. Since v1.37 gang scheduling is an explicit opt-in via spec.scheduling.
Can the feature be disabled once it has been enabled (i.e. can we roll back the enablement)?
Yes. With the gate disabled on kube-apiserver, it clears spec.scheduling on create and ignores it
on update. With the gate disabled on kube-controller-manager, the Job controller stops compiling
Workload/PodGroup, stops setting schedulingGroup on new pods, and stops deleting PodGroups
on suspend, so suspended Jobs no longer release scheduler-side resources.
What happens if we reenable the feature if it was previously rolled back?
Jobs that have not started get Workload/PodGroup compiled from their stored spec.scheduling
(defaulting to Basic) on their next sync. Jobs that already have these objects from before the
rollback reuse them; a partial set (e.g. Workload without PodGroup) is completed if the Job has
no pods yet. A Job that created ungrouped pods during the rollback keeps creating ungrouped pods
until those are gone.
Are there any tests for feature enablement/disablement?
Yes.
Rollout, Upgrade and Rollback Planning
How can a rollout or rollback fail? Can it impact already running workloads?
Already running pods are not affected. The Job controller does not touch existing pods and the scheduler does not revisit bound pods.
If WorkloadWithJob is enabled on kube-controller-manager while GenericWorkload is still
disabled on kube-apiserver (e.g. kube-controller-manager upgraded first), Workload/PodGroup
creates fail and new Jobs are requeued with backoff without creating pods until the API is served.
Upgrading kube-apiserver first avoids this.
Rollback and re-enable affect in-flight Jobs in three ways. Terminal and not-yet-started Jobs are unaffected.
WorkloadWithJobdisabled on kube-controller-manager,GenericWorkloadstill enabled on the scheduler: new pods of an in-flight gang Job are created withoutschedulingGroup, while its pending grouped pods stay gated waiting for aminCountthe Job will never reach. The Job is stuck until the operator deletes itsPodGroupalong with its pending pods that carryschedulingGroup, so the Job controller recreates them without it or recreates the Job. Deleting thePodGroupalone is not enough, because the pods still reference it. DisablingGenericWorkloadon the scheduler as well avoids this.WorkloadWithJobdisabled on kube-controller-manager: suspended Jobs stop releasing theirPodGroups. Operators who need the resources back delete thosePodGroups. Otherwise, they will be garbage collected with the Job.WorkloadWithJobre-enabled: a Job whosePodGroupsurvived the rollback and that created ungrouped pods in the meantime would otherwise become a mixed gang. The controller detects active pods withoutschedulingGroup, keeps creating ungrouped pods, and emits aMixedSchedulingGroupWarning event once, so the Job finishes pod-by-pod. Gang semantics return for new pods once the ungrouped pods are gone, or the operator recreates the Job.
What specific metrics should inform a rollback?
job_scheduling_object_syncs_total{result="error"}increasing: the controller cannot writeWorkload/PodGroupobjects and Jobs are stuck before pod creation.job_syncs_total{result="error"}or p99job_sync_duration_secondsincreasing after enablement compared to the pre-upgrade baseline.scheduler_pending_pods{queue="gated"}growing for Jobs that used to schedule (KEP-4671).
Were upgrade and rollback tested? Was the upgrade->downgrade->upgrade path tested?
The gate enable -> disable -> re-enable sequence is covered by the integration test added for Beta. The upgrade -> downgrade -> upgrade path will be tested manually before the release using the following sequence:
- Start a v1.37 cluster with
WorkloadWithJobandGenericWorkloaddisabled (default). Create a Job withspec.scheduling.schedulingPolicy.gang; verify the field is dropped and the Job runs pod-by-pod with noWorkload/PodGroup. - Upgrade to v1.38 (gates enabled by default). Create a gang Job; verify a
WorkloadandPodGroupare created, pods carryschedulingGroup, and pods are bound together. Suspend the Job; verify thePodGroupis deleted and theWorkloadretained. Resume; verify a newPodGroupis created and the Job completes. - Downgrade to v1.37. Verify existing
Workload/PodGroupobjects remain, a new Job gets no scheduling objects, and its pods schedule pod-by-pod. - Upgrade to v1.38 again. Verify the Jobs created in step 2 reuse their existing objects and a new gang Job is gang-scheduled.
Is the rollout accompanied by any deprecations and/or removals of features, APIs, fields of API types, flags, etc.?
No.
Monitoring Requirements
How can an operator determine if the feature is in use by workloads?
Check the job_scheduling_object_syncs_total{kind="Workload",action="create",result="success"}
metric. If it is non-zero, the Job controller has created scheduling objects and the feature is
in use. For a specific Job, kubectl get workloads,podgroups -n <ns> lists the objects owned by it.
How can someone using this feature know that it is working for their instance?
- Events
- Event Reason:
WorkloadCreated,PodGroupCreated,PodGroupDeleted(Normal);FailedWorkloadCompilation,FailedWorkloadCreate,SchedulingDependencyNotFound,UnsupportedWorkloadStructure(Warning)
- Event Reason:
- API .spec
- Other field:
.spec.schedulingGroup.podGroupNameon the Job’s pods- a
Workloadand aPodGroupwith a controllerownerReferenceto the Job
- Other field:
- API .status
- Condition:
SchedulingBlocked=Truewith reasonUnsupportedWorkloadStructurewhen the controller cannot safely select or use the scheduling objects.SchedulingBlocked=Truewith reasonWorkloadNotFoundorPodGroupTemplateNotFoundwhile a delegated scheduling dependency is unavailable. The condition clears after recovery.Failed=Truewith reasonFailedWorkloadCompilationwhenspec.schedulingcannot be compiled into aWorkload.
- Condition:
What are the reasonable SLOs (Service Level Objectives) for the enhancement?
99th percentile over a day of job_sync_duration_seconds with the feature enabled stays within 10%
of the value with the feature disabled for the same workload. 99% of Workload/PodGroup writes
made by the Job controller per day succeed.
What are the SLIs (Service Level Indicators) an operator can use to determine the health of the service?
- Metrics
- Metric name:
job_scheduling_object_syncs_total(new), with labelskind,actionandresult. Incremented for eachWorkload/PodGroupcreate/update/delete made by the Job controller.job_scheduling_object_sync_duration_seconds(new), with labelskindandaction. Latency of those calls.job_sync_duration_secondsandjob_syncs_total(existing), compared against the pre-enablement baseline.
- Metric name:
Are there any missing metrics that would be useful to have to improve observability of this feature?
A gauge of Jobs per scheduling policy (basic/gang) would show adoption directly. However, it is not
added because the same information is available by listing PodGroup objects.
Dependencies
Does this feature depend on any specific services running in the cluster?
No dependencies other than the components where the feature is implemented (kube-apiserver and
kube-controller-manager) and the GenericWorkload feature gate being enabled on kube-apiserver
and kube-scheduler.
Scalability
Will enabling / using this feature result in any new API calls?
Yes. The Job controller adds two informers (Workload, PodGroup), i.e. two LIST+WATCH streams
against kube-apiserver. Per Job, with the gate on (the default from v1.38), for every root Job
including Basic Jobs:
CREATE Workload: 1 per Job creation.CREATE PodGroup: 1 per Job creation, plus 1 per suspension cycle in which thePodGroupwas deleted.DELETE PodGroup: at most 1 per suspension cycle (none if the Job is resumed before its pods are gone).PATCH WorkloadandPATCH PodGroup: 1 each pergang.minCount(orparallelism-driven) elastic resize.
A non-root Job whose parent owns the Workload but delegates the PodGroup, or a standalone Job
using a BYO Workload, makes only the PodGroup calls. A non-root Job where the parent owns both
objects, or a Job with a BYO PodGroup, makes none of these calls.
Will enabling / using this feature result in introducing new API types?
No new top-level types. spec.scheduling (JobSchedulingConfiguration) is a new field on the
existing batch/v1 Job that embeds the scheduling.k8s.io/v1 building blocks from KEP-6089.
The Workload/PodGroup resources are defined by KEP-4671.
Will enabling / using this feature result in any new calls to the cloud provider?
No.
Will enabling / using this feature result in increasing size or count of the existing API objects?
Yes. Because of Universal Representation, every root Job (both Gang and Basic) creates 1
Workload (~500 bytes) and 1 PodGroup (~500 bytes), and each Pod gains a schedulingGroup
field (~100 bytes) plus one ownerReference (~150 bytes). A delegated Job adds one PodGroup,
and a fully-delegated Job adds none. Job objects themselves grow only when the user sets
spec.scheduling (a few hundred bytes at most).
For a cluster with 10,000 live root Jobs, this adds approximately:
- 10,000
Workloadobjects - 10,000
PodGroupobjects - ~10MB additional etcd storage for the objects, plus ~250 bytes per Job pod
Will enabling / using this feature result in increasing time taken by any operations covered by existing SLIs/SLOs?
No. No existing SLI/SLO covers Job sync latency, so none is affected.
The first sync of a new Job does gain two sequential API writes (Workload, then PodGroup) before
pod creation. This is measured by job_sync_duration_seconds.
Will enabling / using this feature result in non-negligible increase of resource usage (CPU, RAM, disk, IO, …) in any components?
Yes, proportional to the number of live Jobs.
- kube-controller-manager:
Workload/PodGroupinformer caches (~1-2KB per Job, about 20MB for 10,000 Jobs). - kube-scheduler: memory for the same
Workload/PodGroupcaches, accounted for in KEP-4671. - etcd: storage for
Workload/PodGroupobjects (~1KB per Job, about 10MB for 10,000 Jobs). - kube-apiserver: two additional watch streams and two extra writes per Job creation. Minimal CPU impact.
Can enabling / using this feature result in resource exhaustion of some node resources (PIDs, sockets, inodes, etc.)?
No. This feature is purely control-plane and does not affect node resources.
Troubleshooting
How does this feature react if the API server and/or etcd is unavailable?
The Job controller cannot create or delete Workload/PodGroup objects and requeues the affected
Jobs with backoff, as it does for any other write. New Jobs make no progress until the API server
recovers, the same as for pod creation. Running pods are unaffected.
What are other known failure modes?
Workload/PodGroupcreate rejected by the API server (RBAC, admission webhook,GenericWorkloaddisabled).- Detection:
FailedWorkloadCreateWarning event on the Job;job_scheduling_object_syncs_total{action="create",result="error"}increasing. - Mitigations: fix the rejecting component; the controller retries with backoff. If the resources
are intentionally unavailable, disable
WorkloadWithJobon kube-controller-manager. - Diagnostics: kube-controller-manager logs include the API error.
- Testing: unit tests with a fake-client reactor.
- Detection:
- External
Workloador selectedPodGroupTemplateformanagePodGroupOnlyis not available.- Detection:
SchedulingDependencyNotFoundWarning event,SchedulingBlocked=Truewith reasonWorkloadNotFoundorPodGroupTemplateNotFound, and repeated Job sync errors. No pods are created while the dependency is missing. - Mitigations: create or repair the external
Workloadand its selected template. The controller retries with backoff and creates thePodGrouponce the dependency resolves; the Job does not need to be recreated. - Diagnostics: inspect the Job’s
scheduling.k8s.io/workload-nameandscheduling.k8s.io/group-template-nameannotations, and the namedWorkloadandPodGroupTemplate. - Testing: unit and integration tests cover delayed Workload discovery and a missing template.
- Detection:
- Duplicate, colliding, or externally mutated controller-owned objects.
- Detection:
UnsupportedWorkloadStructureWarning event,SchedulingBlocked=Trueon the Job, andjob_scheduling_object_syncs_total{result="error"}increasing. No new pods are created while the condition is true. - Mitigations: remove an unintended duplicate or colliding object, or repair the authoritative object. The controller retries with backoff and clears the condition after the objects validate.
- Diagnostics: audit log for writers on
workloads/podgroups. - Testing: unit and integration tests for blocking, condition persistence, and recovery.
- Detection:
What steps should be taken if SLOs are not being met to determine the problem?
- Confirm
WorkloadWithJobandGenericWorkloadare enabled consistently on kube-apiserver, kube-controller-manager and kube-scheduler. - Check
job_scheduling_object_syncs_total{result="error"}, and the Warning events on the affected Jobs. - Compare
job_scheduling_object_sync_duration_secondswithapiserver_request_duration_secondsforworkloads/podgroupsto tell controller-side from API-server-side latency. - For gang Jobs whose pods stay
Pendingwith objects present, follow the scheduler troubleshooting steps in KEP-4671.
Implementation History
- 2026-01-29: KEP created
- 2026-02-10: KEP updated according to final API design for
WorkloadandPodGroup - 2026-06-03: KEP reworked for a second alpha (v1.37) to replace the automatic, type-based gang
selection with the explicit user-facing
spec.schedulingAPI from KEP-6089, adopting the sharedworkloadbuilderlibrary, Universal Representation, and mutablegang.minCountfor elastic scaling. - 2026-09-10: KEP updated for Beta (v1.38).
- 2026-09-18: Beta design clarified blocking on invalid scheduling objects, retry-safe suspend/resume, BYO clarification, and delayed delegated dependencies.
- 2026-09-25: Delegated
PodGroupdiscovered by name and controller-owned by the Job, - 2026-09-28:
PodGroupdiscovered by name in every mode, so sibling Jobs sharing aPodGroupTemplateget onePodGroupeach.
Drawbacks
Alternatives
Fallback to pod-by-pod scheduling
For v1.38 Beta, the controller could have continued the alpha behavior of creating pods without
schedulingGroup when it found ambiguous or unsupported scheduling objects. This keeps the Job
making progress, but it can silently discard gang, topology, disruption, or resource-claim
constraints. A bug in a higher-level controller could therefore run a workload with placement
semantics different from those the user requested.
This alternative is rejected for Beta. The controller instead blocks new pod creation, reports the
offending objects through the non-terminal SchedulingBlocked condition and a Warning event, and
retries until the objects are repaired. This favors preserving requested scheduling semantics over
unconstrained progress.
Bring-your-own Workload
An earlier variant proposed discovering an external Workload without an explicit template
selection signal, then using a separate managed-by marker to decide whether the Job controller
should reconcile it. During the v1.37 implementation review this variant was
identified as lacking an unambiguous template mapping.
Beta does not adopt ownership-agnostic reconciliation or a new managed-by marker. Instead it uses
the scheduling.k8s.io/workload-name and scheduling.k8s.io/group-template-name annotations to
select the external template. That Workload remains authoritative and is never reconciled against
spec.scheduling. The ownership continues to govern mutation and deletion. This is the supported
BYO Workload path described in Bring-your-own Workload and PodGroup.
This is not a new entry point, the annotation path is the one KEP-6089 already defines and the Job controller already honors, and no additional BYOW mechanism is planned.
Reference-based discovery
The alpha discovered every Workload through spec.controllerRef and every PodGroup through
spec.podGroupTemplateRef, both filtered by a controller ownerReference to the Job. The following
variants were considered during the Beta review:
- Filter by ownerReference. A higher-level controller may create the
PodGroupbefore the Job exists, so it cannot reference the Job. - Filter by
spec.podGroupTemplateRefalone. Sibling Jobs that select the same template carry identical references, so they would all share onePodGroup. Each replica must be able to form its own gang. - Match
Workload.spec.controllerRefto the Job’s parent. TheWorkloadmay be compiled by any ancestor, and walking the ownerReference chain would use ownerReferences for discovery. Thescheduling.k8s.io/workload-nameannotation works at any depth with a single listerGet.
A name derived from the Job is unique per Job and available before any object exists. Root Jobs
could keep reference-based discovery, since they own the Workload, but one rule for every mode
is simpler and has fewer conditional paths.
Deleting the Workload on suspend
Two options were considered for the controller-owned objects of a suspended Job:
Option A (chosen): Keep the Workload, delete only the PodGroup
- Pros:
- One object to restore on resume.
NewBuilderFromExistingWorkloadre-instantiates thePodGroupfrom the persisted template with the same name, template reference andminCount. - Consistent with a Job created suspended, which already gets a
Workloadand noPodGroup. gang.minCountchanges made while suspended are patched onto the retained template with the existing update path; no suspend-specific handling.- No extra API writes per suspension cycle and a stable
Workloadidentity for the Job’s lifetime.
- One object to restore on resume.
- Cons:
- If fields beyond
gang.minCountbecome mutable while suspended (e.g. structure changes for multiplePodGroups per template, or resource-shape changes from Dynamic Containers), the retained template must be patched in place or the design must switch to Option B at that point.
- If fields beyond
Option B: delete both Workload and PodGroup, recompile from spec.scheduling on resume.
- Pros:
- A suspended Job owns no scheduling objects, which is a simpler invariant.
- Any future mutability while suspended is handled for free: resume always compiles a fresh
Workloadfrom the currentspec.scheduling, so the template can never drift from the Job.
- Cons:
- One extra DELETE and CREATE of the
Workloadper completed suspension cycle, and a newWorkloadidentity each time. - A second dependency has to be restored before the
PodGroupand pods on resume, so the resume path has one more state to keep retry-safe. - Breaks the symmetry with a Job created suspended unless that case also stops creating the
Workloadup front. - Does not remove the controller-ordering concern: scheduling objects are reconciled before pod management in either option, so both need suspension-aware creation rules and a level-triggered resume path.
- One extra DELETE and CREATE of the
Beta adopts Option A because nothing in the current API needs the template to change while
suspended, and the future-extension cases that would are not yet designed. The decision is
revisited when a follow-up KEP makes scheduling structure mutable while suspended. External
Workloads are never deleted by the Job controller under either option.
Infrastructure Needed (Optional)
The Kubernetes community uses the term “gang scheduling” to mean “all-or-nothing scheduling of a set of pods” [1,2,3,4,5,6,7,8,9,10,11,12,13]. In the Kubernetes context, it does not imply time-multiplexing (in contrast to prior academic work such as Feitelson and Rudolph, and in contrast to Slurm Gang Scheduling. ↩︎ ↩︎