Distributed Training with TrainJob
TrainJob is the Kubernetes-native API for submitting distributed training and fine-tuning workloads on OpenShift. You define what to train, how many nodes to use, and which framework to run — TrainJob handles Pod creation, worker coordination, failure recovery, and cleanup.
TrainJob is provided by the KubeFlow Training Operator v2. The operator runs in the background; as a user, you interact only with TrainJob and the training runtimes that configure it.
This module walks through creating TrainJobs for PyTorch and MPI workloads, using training runtimes for reusable configurations, and integrating with Kueue for admission control.
What TrainJob Replaces
Without TrainJob, running distributed training on Kubernetes requires you to:
-
Manually create Pods, Services, and ConfigMaps for each training worker
-
Handle coordination between workers (who is the master? what are the peer addresses?)
-
Detect and handle partial failures (one worker crashes — do you restart or abort?)
-
Manage cleanup after training completes
This is the same problem that traditional HPC schedulers solve with job submission scripts and mpirun.
TrainJob provides an equivalent abstraction for Kubernetes: you submit a job definition, and the system handles the rest.
How TrainJob Works
Submitting a TrainJob
A TrainJob is a short custom resource that describes your training workload:
-
Runtime reference — Which training runtime to use (PyTorch distributed, MPI, etc.).
-
Trainer — Your container image, the number of nodes, resources per node, and any environment variables or hyperparameters.
That is the minimum. You do not need to define individual Pods, configure peer discovery, or set up SSH keys. The system builds all of that from your TrainJob spec and the referenced runtime.
apiVersion: kubeflow.org/v2alpha1
kind: TrainJob
metadata:
name: my-training
spec:
runtimeRef:
name: torch-distributed (1)
trainer:
image: my-registry/my-training:latest
numNodes: 4 (2)
resourcesPerNode: (3)
requests:
cpu: "2"
memory: "4Gi"
| 1 | References a ClusterTrainingRuntime that defines how PyTorch distributed training is set up. |
| 2 | The operator creates 4 worker Pods and configures torchrun with the correct --nnodes and rendezvous endpoint. |
| 3 | Each worker Pod gets these resource requests. |
Training Runtimes
A ClusterTrainingRuntime (cluster-scoped) or TrainingRuntime (namespace-scoped) defines a reusable template for how training jobs run. It specifies:
-
The framework (PyTorch distributed, MPI, etc.)
-
Pod templates for workers (and master/launcher if applicable)
-
Default resource requests and limits
-
Gang scheduling integration
Runtimes are typically created by administrators. As a user, you reference a runtime by name in your TrainJob and override only what you need — image, node count, resources, and environment variables.
This separation keeps TrainJob specs simple and portable. The same TrainJob definition can run on different clusters that each have their own runtime configurations.
Supported Frameworks
TrainJob supports multiple distributed training frameworks through different runtimes:
-
PyTorch Distributed — Uses
torchrun/torch.distributedfor data-parallel and model-parallel training. The system creates workers and configures rendezvous (peer discovery) so thattorch.distributed.init_process_group()works automatically. -
MPI — Uses a launcher/worker pattern. The system creates an MPI launcher Pod that runs
mpirun/mpiexecacross worker Pods. This is the same pattern used in HPC: one launcher, many workers, MPI for communication. -
TensorFlow — Parameter server or multi-worker strategies.
-
Other frameworks — Extensible to additional runtimes via CRDs.
| MPI support in TrainJob v2 is being actively developed. Red Hat Engineering is working on MVP MPI support for customer use cases of MPI on OpenShift. |
Exercise 1: Creating a Distributed PyTorch TrainJob
In this exercise you will create a distributed PyTorch training job using TrainJob.
Prerequisites
Ensure the Training Operator is installed on your cluster.
-
Verify the operator is running:
oc get pods -n kubeflowYou should see the training-operator Pod in
Runningstate. -
Create a namespace for training:
oc new-project training-demo --skip-config-update || oc project training-demo
Create a PyTorch TrainJob
-
Create a simple distributed PyTorch training job:
cat <<EOF | oc apply -f - apiVersion: kubeflow.org/v2alpha1 kind: TrainJob metadata: name: pytorch-distributed-demo namespace: training-demo spec: runtimeRef: name: torch-distributed trainer: image: docker.io/kubeflowkatib/pytorch-mnist:v0.17.0 numNodes: 4 resourcesPerNode: requests: cpu: "1" memory: "2Gi" EOFThis TrainJob tells the system to run
pytorch-mnistacross 4 nodes using thetorch-distributedruntime. Behind the scenes, the operator will:-
Create 4 worker Pods (node-0 through node-3).
-
Configure
torchrunwith the correct--nnodes,--nproc_per_node, and rendezvous endpoint. -
Coordinate startup so all workers connect before training begins.
-
Monitor for completion or failure.
-
-
Watch the training Pods:
oc get pods -n training-demo -l training.kubeflow.org/trainjob-name=pytorch-distributed-demo -wPress
Ctrl+Conce all Pods are Running. -
Check the TrainJob status:
oc get trainjob pytorch-distributed-demo -n training-demo -o yamlThe status section shows the training progress and conditions.
-
View the logs from the workers:
oc logs -n training-demo -l training.kubeflow.org/trainjob-name=pytorch-distributed-demo --tail=20
Exercise 2: Customizing TrainJobs with Runtime Overrides
TrainJobs reference a runtime for defaults, then override specific fields. This lets you change the image, node count, resources, and environment variables without redefining the entire training configuration.
-
Examine the available ClusterTrainingRuntimes:
oc get clustertrainingruntimes -
Look at what a runtime provides:
oc get clustertrainingruntime torch-distributed -o yamlThis shows the Pod templates, framework configuration, and defaults that your TrainJob inherits.
-
Create a TrainJob that overrides the defaults:
cat <<EOF | oc apply -f - apiVersion: kubeflow.org/v2alpha1 kind: TrainJob metadata: name: custom-training namespace: training-demo spec: runtimeRef: name: torch-distributed trainer: image: docker.io/kubeflowkatib/pytorch-mnist:v0.17.0 numNodes: 2 resourcesPerNode: requests: cpu: "2" memory: "4Gi" env: - name: EPOCHS value: "5" - name: BATCH_SIZE value: "64" EOFThis TrainJob uses the same
torch-distributedruntime but changes the node count to 2, increases resources per node, and passes hyperparameters as environment variables. -
Monitor the job:
oc get trainjob -n training-demo oc get pods -n training-demo -l training.kubeflow.org/trainjob-name=custom-training
Exercise 3: MPI TrainJobs
MPI (Message Passing Interface) is the standard communication library for HPC applications.
TrainJob can run MPI workloads using the launcher/worker pattern — identical to how mpirun works on a traditional HPC cluster.
| MPI support in TrainJob v2 may require the MPI runtime to be available. Check your cluster’s available ClusterTrainingRuntimes. |
-
Create an MPI-based TrainJob:
cat <<EOF | oc apply -f - apiVersion: kubeflow.org/v2alpha1 kind: TrainJob metadata: name: mpi-job-demo namespace: training-demo spec: runtimeRef: name: mpi-distributed trainer: image: mpioperator/mpi-pi:latest numNodes: 4 resourcesPerNode: requests: cpu: "1" memory: "1Gi" EOFFrom your perspective, this looks just like the PyTorch TrainJob — a runtime reference, an image, and a node count. The runtime determines the execution model. For MPI, the system creates:
-
One launcher Pod that runs
mpirun(ormpiexec). -
Four worker Pods that the launcher can SSH into or connect to via MPI.
-
The necessary SSH keys and hostfile configuration.
-
-
Watch the MPI job progress:
oc get pods -n training-demo -l training.kubeflow.org/trainjob-name=mpi-job-demo -
View the launcher logs to see MPI output:
oc logs -n training-demo -l training.kubeflow.org/trainjob-name=mpi-job-demo,training.kubeflow.org/replica-type=launcher
Exercise 4: Running TrainJobs with Kueue
TrainJob integrates with Kueue for admission control. When Kueue is installed, TrainJobs are submitted to a LocalQueue just like regular Jobs — add the queue label and Kueue handles the rest.
This is especially important for training workloads because distributed training requires all workers to start together. Kueue ensures that a TrainJob is only admitted when the ClusterQueue has enough quota for all workers simultaneously, providing gang-scheduling-like behavior through admission control.
| This exercise assumes you have already set up Kueue with a ClusterQueue and LocalQueue as described in the Kueue module. If you have not, complete that module first or use the existing queues on your cluster. |
-
Verify that a LocalQueue exists in your namespace:
oc get localqueues -n training-demoIf no LocalQueue exists, create one that points to your ClusterQueue:
cat <<EOF | oc apply -f - apiVersion: kueue.x-k8s.io/v1beta1 kind: LocalQueue metadata: name: training-queue namespace: training-demo spec: clusterQueue: cluster-queue EOF -
Submit a TrainJob to the queue by adding the
kueue.x-k8s.io/queue-namelabel:cat <<EOF | oc apply -f - apiVersion: kubeflow.org/v2alpha1 kind: TrainJob metadata: name: queued-training namespace: training-demo labels: kueue.x-k8s.io/queue-name: training-queue spec: runtimeRef: name: torch-distributed trainer: image: docker.io/kubeflowkatib/pytorch-mnist:v0.17.0 numNodes: 4 resourcesPerNode: requests: cpu: "2" memory: "4Gi" EOFThe only difference from the earlier TrainJobs is the
kueue.x-k8s.io/queue-namelabel. Everything else about TrainJob works the same — Kueue controls when the job is admitted, not how it runs. -
Check the Kueue workload status:
oc get workloads -n training-demoYou should see a workload corresponding to your TrainJob. If the ClusterQueue has sufficient quota, the workload will be admitted and the training Pods will start. If not, it will remain pending until quota is available.
-
Watch the TrainJob and its Pods:
oc get trainjob -n training-demo oc get pods -n training-demo -l training.kubeflow.org/trainjob-name=queued-training -
To see how Kueue manages quota across multiple TrainJobs, submit a second job while the first is still running:
cat <<EOF | oc apply -f - apiVersion: kubeflow.org/v2alpha1 kind: TrainJob metadata: name: queued-training-2 namespace: training-demo labels: kueue.x-k8s.io/queue-name: training-queue spec: runtimeRef: name: torch-distributed trainer: image: docker.io/kubeflowkatib/pytorch-mnist:v0.17.0 numNodes: 4 resourcesPerNode: requests: cpu: "2" memory: "4Gi" EOF -
Check both workloads:
oc get workloads -n training-demo oc get trainjob -n training-demoIf the ClusterQueue does not have enough quota for both jobs at once, the second TrainJob will wait in the queue until the first completes and releases its resources.
Summary
TrainJob provides a simple, consistent API for submitting distributed training workloads on OpenShift.
In this module you:
-
Learned what TrainJob replaces — manual Pod creation, worker coordination, failure handling, and cleanup for distributed training.
-
Created a distributed PyTorch TrainJob that ran across multiple worker nodes.
-
Customized TrainJobs with runtime overrides — changing node counts, resources, and hyperparameters while inheriting runtime defaults.
-
Submitted an MPI TrainJob using the launcher/worker pattern familiar to HPC users.
-
Submitted TrainJobs through Kueue for admission control, and observed how Kueue queues training workloads when quota is constrained.
In the next section, you will learn about Apache Spark on OpenShift for distributed data processing workloads.