JobSet: Multi-Job Workloads on Kubernetes
JobSet is a Kubernetes-native API for managing groups of related Jobs as a single unit. It creates one or more Kubernetes Jobs from different or identical templates, controlling their lifecycle together — all succeed or all fail. This makes it ideal for distributed workloads like multi-node training, MPI applications, and HPC simulations where multiple Job groups must coordinate.
This module introduces JobSet concepts, walks you through creating multi-job workloads, and shows how to integrate JobSet with Kueue for admission control.
Why JobSet?
Running a distributed workload on Kubernetes often requires multiple groups of Pods with different roles — workers, parameter servers, drivers. Without JobSet you must:
-
Create and manage each Job independently
-
Coordinate lifecycle across Jobs manually — if one Job fails, you must detect and clean up the others
-
Handle DNS and network discovery between Pods across different Jobs
-
Implement restart logic that restarts all Jobs together, not just the one that failed
JobSet solves these problems by treating a set of Jobs as a single resource with coordinated lifecycle, automatic DNS setup, and unified failure handling.
Core Concepts
ReplicatedJobs
The .spec.replicatedJobs list is the heart of a JobSet.
Each entry defines a Job template and a replica count:
-
template — The Job spec (parallelism, completions, Pod template).
-
replicas — How many copies of this Job to create (defaults to 1).
Each Job gets a deterministic name: <jobSetName>-<replicatedJobName>-<jobIndex>, where jobIndex runs from 0 to replicas - 1.
You can define multiple entries in replicatedJobs to create Jobs with different templates — for example, a leader Job and a set of worker Jobs.
Completion Policy
A JobSet is marked as successful when all of its child Jobs complete successfully. If any Job is still running, the JobSet remains active.
Failure Policy
A JobSet failure is counted when any child Job fails.
The spec.failurePolicy.maxRestarts field controls how many times JobSet will automatically restart all child Jobs before declaring terminal failure.
A restart recreates all Jobs, not just the one that failed — this is critical for distributed workloads where partial state is invalid.
Network and DNS
JobSet automatically creates a headless Service and configures DNS so that Pods can discover each other by hostname.
The subdomain defaults to the JobSet name but can be overridden with spec.network.subdomain.
-
Pod hostname:
<jobSetName>-<replicatedJobName>-<replicaIndex>-<podIndex> -
FQDN:
<jobSetName>-<replicatedJobName>-<replicaIndex>-<podIndex>.<subdomain>
This deterministic naming is essential for distributed frameworks that need to know peer addresses at startup (e.g., setting MASTER_ADDR in PyTorch distributed training).
Coordinator
JobSet can designate a coordinator Pod using spec.coordinator.
The coordinator’s stable network endpoint is automatically injected as a label (jobset.sigs.k8s.io/coordinator) on all Jobs and Pods, so workers can discover the coordinator without hardcoding addresses.
Labels
JobSet applies these labels to all child Jobs and Pods:
| Label | Value |
|---|---|
|
The JobSet’s name |
|
The replicatedJob entry name |
|
The replica count |
|
Ordinal index of the Job (0, 1, 2, …) |
Exercise 1: Creating a Basic JobSet
In this exercise you will create a JobSet with a single replicated Job group to understand the basic mechanics.
Prerequisites
Ensure the JobSet controller is installed on your cluster.
-
Verify the JobSet controller is running:
oc get pods -n jobset-systemYou should see the jobset-controller-manager Pod in
Runningstate. -
Create a namespace for the exercises:
oc new-project jobset-demo --skip-config-update || oc project jobset-demo
Create a simple JobSet
-
Create a JobSet with 3 worker replicas, each running 2 parallel Pods:
cat <<EOF | oc apply -f - apiVersion: jobset.x-k8s.io/v1alpha2 kind: JobSet metadata: name: simple-jobset namespace: jobset-demo spec: replicatedJobs: - name: workers replicas: 3 template: spec: parallelism: 2 completions: 2 backoffLimit: 0 template: spec: containers: - name: worker image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: - sh - -c - | echo "JobSet worker started" echo "Hostname: $(hostname)" sleep 30 echo "Worker complete" resources: requests: cpu: "250m" memory: "128Mi" restartPolicy: OnFailure EOFThis creates 3 Jobs (
simple-jobset-workers-0,simple-jobset-workers-1,simple-jobset-workers-2), each running 2 Pods. -
Watch the Jobs and Pods:
oc get jobs -n jobset-demo -l jobset.sigs.k8s.io/jobset-name=simple-jobset -
View the Pods across all Jobs:
oc get pods -n jobset-demo -l jobset.sigs.k8s.io/jobset-name=simple-jobsetYou should see 6 Pods total (3 Jobs x 2 Pods each).
-
Check the JobSet status:
oc get jobsets -n jobset-demo
Exercise 2: Multi-Role JobSet with Leader and Workers
A common pattern in distributed computing is having distinct roles — a leader (or driver) and workers.
JobSet supports this by allowing multiple entries in replicatedJobs with different templates.
-
Create a JobSet with a leader and workers:
cat <<EOF | oc apply -f - apiVersion: jobset.x-k8s.io/v1alpha2 kind: JobSet metadata: name: leader-workers namespace: jobset-demo spec: network: enableDNSHostnames: true coordinator: replicatedJob: leader jobIndex: 0 podIndex: 0 replicatedJobs: - name: leader replicas: 1 template: spec: parallelism: 1 completions: 1 backoffLimit: 0 template: spec: containers: - name: leader image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: - sh - -c - | echo "Leader started at $(hostname)" echo "Waiting for workers to connect..." sleep 45 echo "Leader work complete" resources: requests: cpu: "500m" memory: "256Mi" restartPolicy: OnFailure - name: workers replicas: 1 template: spec: parallelism: 4 completions: 4 backoffLimit: 0 template: spec: containers: - name: worker image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: - sh - -c - | LEADER=$(cat /etc/hostname | sed 's/-workers-.*/-leader-0-0/') echo "Worker $(hostname) connecting to leader at ${LEADER}" sleep 40 echo "Worker $(hostname) complete" resources: requests: cpu: "250m" memory: "128Mi" restartPolicy: OnFailure EOFKey elements:
-
The
coordinatorfield designates the leader Pod as the coordinator. -
enableDNSHostnames: trueensures all Pods get DNS entries for peer discovery. -
The leader and workers are defined as separate
replicatedJobsentries with different resource requirements.
-
-
Verify both Job groups are running:
oc get jobs -n jobset-demo -l jobset.sigs.k8s.io/jobset-name=leader-workersYou should see two Jobs:
leader-workers-leader-0andleader-workers-workers-0. -
Check the Pods and their roles:
oc get pods -n jobset-demo -l jobset.sigs.k8s.io/jobset-name=leader-workers \ -o custom-columns=NAME:.metadata.name,ROLE:.metadata.labels.jobset\\.sigs\\.k8s\\.io/replicatedjob-name,STATUS:.status.phase
Exercise 3: Failure Policy and Automatic Restarts
JobSet can automatically restart all Jobs when any Job fails. This is essential for distributed workloads where partial state from a failed run is invalid.
-
Create a JobSet with a failure policy that allows 2 restarts:
cat <<EOF | oc apply -f - apiVersion: jobset.x-k8s.io/v1alpha2 kind: JobSet metadata: name: restart-demo namespace: jobset-demo spec: failurePolicy: maxRestarts: 2 replicatedJobs: - name: workers replicas: 2 template: spec: parallelism: 1 completions: 1 backoffLimit: 0 template: spec: containers: - name: worker image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: - sh - -c - | echo "Worker $(hostname) attempt" if [ "$((RANDOM % 3))" -eq "0" ]; then echo "Simulated failure!" exit 1 fi sleep 20 echo "Worker complete" resources: requests: cpu: "100m" memory: "64Mi" restartPolicy: Never EOFIf any worker fails, JobSet recreates all Jobs (not just the failed one) up to
maxRestartstimes. -
Watch the JobSet events to see restart behavior:
oc get events -n jobset-demo --field-selector involvedObject.name=restart-demo --sort-by=.lastTimestamp -
Check the JobSet status for restart count:
oc get jobset restart-demo -n jobset-demo -o yaml | grep -A 5 "status:"
Kueue Integration
JobSet integrates with Kueue for admission control and quota management. When Kueue is installed, JobSets are held in a queue until the ClusterQueue has enough capacity to run all Jobs simultaneously — providing gang-scheduling-like behavior through admission control.
Prerequisites
This section assumes you have Kueue installed and a LocalQueue configured. If you completed the Kueue module, you can reuse the same queue setup.
-
Set up the Kueue resources (skip if already created):
cat <<EOF | oc apply -f - apiVersion: kueue.x-k8s.io/v1beta1 kind: ResourceFlavor metadata: name: default-flavor --- apiVersion: kueue.x-k8s.io/v1beta1 kind: ClusterQueue metadata: name: cluster-queue spec: namespaceSelector: {} resourceGroups: - coveredResources: ["cpu", "memory"] flavors: - name: default-flavor resources: - name: "cpu" nominalQuota: 9 - name: "memory" nominalQuota: 36Gi --- apiVersion: kueue.x-k8s.io/v1beta1 kind: LocalQueue metadata: namespace: jobset-demo name: user-queue spec: clusterQueue: cluster-queue EOF
Submit a JobSet to a Kueue Queue
The integration point is the kueue.x-k8s.io/queue-name label in the JobSet’s metadata.
Kueue calculates the total resource requirements across all replicated Jobs, considering replicas, parallelism, and per-container requests.
-
Create a JobSet managed by Kueue:
cat <<EOF | oc apply -f - apiVersion: jobset.x-k8s.io/v1alpha2 kind: JobSet metadata: generateName: queued-jobset- namespace: jobset-demo labels: kueue.x-k8s.io/queue-name: user-queue spec: network: enableDNSHostnames: false subdomain: queued-jobset replicatedJobs: - name: workers replicas: 1 template: spec: parallelism: 2 completions: 2 backoffLimit: 0 template: spec: containers: - name: worker image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: - sh - -c - | echo "Kueue-managed worker $(hostname)" sleep 30 echo "Done" resources: requests: cpu: "1" memory: "512Mi" restartPolicy: Never - name: driver template: spec: parallelism: 1 completions: 1 backoffLimit: 0 template: spec: containers: - name: driver image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: - sh - -c - | echo "Kueue-managed driver $(hostname)" sleep 30 echo "Done" resources: requests: cpu: "2" memory: "512Mi" restartPolicy: Never EOFThe
kueue.x-k8s.io/queue-name: user-queuelabel tells Kueue to manage this JobSet. Kueue will calculate the total resource need (2 worker Pods x 1 CPU + 1 driver Pod x 2 CPU = 4 CPUs) and admit the JobSet only when the ClusterQueue has enough quota. -
Check the Kueue workload status:
oc get workloads -n jobset-demoThe workload should show as
ADMITTEDif there is enough quota available. -
Verify the Jobs are running:
oc get jobs -n jobset-demo oc get pods -n jobset-demo
Observe Queuing Behavior
Submit a second JobSet that will exceed the available quota to see Kueue hold it in the queue.
-
Submit a larger JobSet:
cat <<EOF | oc apply -f - apiVersion: jobset.x-k8s.io/v1alpha2 kind: JobSet metadata: generateName: large-jobset- namespace: jobset-demo labels: kueue.x-k8s.io/queue-name: user-queue spec: replicatedJobs: - name: workers replicas: 2 template: spec: parallelism: 4 completions: 4 backoffLimit: 0 template: spec: containers: - name: worker image: registry.access.redhat.com/ubi9/ubi-minimal:latest command: ["sh", "-c", "sleep 60"] resources: requests: cpu: "1" memory: "512Mi" restartPolicy: Never EOF -
Check the workload queue — the larger JobSet should be waiting:
oc get workloads -n jobset-demoThe second workload needs 8 CPUs (2 replicas x 4 parallel Pods x 1 CPU), which exceeds the remaining quota. It will be admitted once the first JobSet completes and releases its resources.
Priority with Kueue
You can set workload priority by adding a priorityClassName to the Pod spec in any of the replicatedJobs entries.
Kueue uses the first non-empty priorityClassName it finds across the replicated Jobs to determine the workload priority:
spec:
replicatedJobs:
- name: workers
template:
spec:
template:
spec:
priorityClassName: high-priority
containers:
- name: worker
...
JobSet vs Traditional HPC Job Arrays
If you come from an HPC background, JobSet maps to several familiar concepts:
| HPC Concept | JobSet Equivalent |
|---|---|
Job array ( |
|
Multiple job steps |
Multiple entries in |
All-or-nothing restart |
|
Hostfile / peer discovery |
Automatic DNS via headless Service |
Master/worker pattern |
|
Clean Up
-
Remove all resources created in these exercises:
oc delete jobsets --all -n jobset-demo oc delete localqueues user-queue -n jobset-demo oc delete clusterqueues cluster-queue oc delete resourceflavors default-flavor
Summary
JobSet provides a Kubernetes-native API for managing groups of related Jobs as a single coordinated unit.
In this module you:
-
Understood why JobSet is needed — Coordinated lifecycle, automatic DNS, and unified failure handling for multi-job workloads.
-
Created a basic JobSet with replicated workers.
-
Built a multi-role JobSet with a leader and workers using the coordinator feature.
-
Explored failure policies and automatic restart behavior.
-
Integrated JobSet with Kueue for admission control and quota-based scheduling.
-
Compared JobSet concepts to traditional HPC job arrays and steps.
In the next section, you will learn about Distributed Training with TrainJob for distributed training workloads.