👋 Hi! I’m Bibin Wilson. In each edition, I share practical tips, guides, and the latest trends in DevOps and MLOps. If someone forwarded this email to you, you can subscribe here to never miss out!

✉️ In Today’s MLOps Edition

In the last edition, we learned Kubeflow Pipelines. Today we cover the second Kubeflow subproject: Kubeflow Trainer.

It covers,

  • A look into distributed training

  • Kubeflow Trainer architecture (TrainJob, Runtimes, JobSet)

  • Hands-on: Install Trainer and run your first distributed TrainJob

  • How to run ML Training Jobs on GPU Nodes

  • Which Models Actually Need GPUs

By the end, you will know exactly how enterprise teams run distributed training on Kubernetes and what your job is as the MLOps engineer behind it.

Sponsored by Udacity

Upskill with Udacity: Cloud and AI skills are becoming important across engineering roles. Udacity provides hands-on programs where you can learn through practical projects and real-world scenarios. Browse Udacity Courses

In the last edition, I explained how Kubeflow Pipelines turns an ML workflow into a series of containerized steps.

Now, let’s look at one important step in that pipeline. The training step. In that step, the pipeline delegates the training job to Kubeflow Trainer as shown in the image below.

Before You Continue: This is part of an ongoing MLOps series. You can check this repo to go through all the previous editions in order.

To understand the need for the Kubeflow trainer, first you need to understand why we need Distributed Training.

A Look Into Distributed Training

When we train a machine learning model, the model has to process huge amounts of data, run mathematical operations, calculate errors, and update millions or even billions of parameters again and again.

For small models, a CPU is enough. For example, our employee attrition model, training runs comfortably in a single pod. Because a scikit-learn model on 500,000 rows of data does not need much.

However, for deep learning models and LLMs, CPUs become too slow because the amount of computation is huge. This is where GPUs come in.

Unlike CPUs, GPUs are designed to perform thousands of calculations in parallel. This makes them ideal for training large models.

But there is another challenge that many engineers discover when working with large models. Adding a GPU does not magically solve every training problem. Even GPUs have limits.

For example, when training a large model, the model and data may not even fit inside one GPU memory. So ML teams distribute the training workload across multiple GPUs and sometimes across multiple servers.

This is called distributed training. The following image illustrates it better.

Here is what happens at a high level.

  • The training job is split across multiple worker processes.

  • Each worker runs on a different GPU.

  • The workers process different parts of the data.

  • They calculate updates and communicate with each other to keep the model synchronized.

  • Together, all workers behave like one large training system.

And this is where things become interesting from an infrastructure point of view because distributed training is not just about running ML code on multiple GPUs. Behind the scenes, you need to handle scheduling, networking between workers, shared storage, GPU allocation, etc..

This is the complexity Kubeflow Trainer helps solve.

What is Kubeflow Trainer?

When ML engineers train large models or fine-tune LLMs, the training process often needs multiple GPUs running across multiple Kubernetes nodes.

For this, you need the following.

  • Creating multiple worker pods

  • Assigning GPU resources

  • Configuring communication between workers

  • Tracking training job status and more..

Kubeflow Trainer helps in doing all these in Kubernetes-native way and abstracts away all the complexities.

In simple terms, with Kubeflow Trainer, ML engineers define a TrainJob custom resource, and Kubeflow takes care of creating and managing the required infrastructure for distributed training.

Kubeflow Trainer Custom Resources

Kubeflow Trainer is basically a Kubernetes Operator implementation.

So it comes with specific custom resource definitions for ML engineers, Platform engineers and MLOps engineers to manage and deploy Training Jobs.

It has three key CRD’s. Let's look at each custom resource in detail and who uses it.

1. ClusterTrainingRuntime (cluster-scoped)

It is managed by Platform engineers / AI infrastructure teams. It defines the following

  • Which container image?

  • Which ML framework? (PyTorch, XGBoost, etc.)

  • How many nodes?

  • CPU/GPU requirements? and more..

In short, it defines cluster-scoped reusable default training runtime shared across teams with standard training environments and infrastructure policies.

Here is an example CRD.

Think of it like a golden template created by the platform team. ML engineers focus on training code, while infrastructure standards remain consistent.

2. TrainingRuntimes (namespace-scoped)

It is managed by MLOps engineers or individual teams.

TrainingRuntime is the same concept as ClusterTrainingRuntime. The main difference is scope. Namespace-scoped runtime template.

In the CRD, only change is the addition of namespace parameter as compared to ClusterTrainingRuntime

3. TrainJob

It is used by ML engineers / AI engineers. It defines the actual training workload, including training code, dataset configuration, model parameters, and a reference to a TrainingRuntime or ClusterTrainingRuntime.

Here is an example TrainJob CRD.

Using this CRD, ML engineers can customize workload-specific configurations such as container image, CPU, memory, and GPU requirements without modifying the default TrainingRuntime or ClusterTrainingRuntime managed by the platform team.

How Kubeflow Trainer Works Internally

The following image illustrates the high-level overview of how Kubeflow Trainer works.

When a user creates a TrainJob, Kubeflow Trainer does not directly create training pods.

Instead, it converts the TrainJob into a JobSet. A JobSet is a Kubernetes-native API designed for distributed workloads. It manages a group of related Kubernetes Jobs that need to run together.

For example, in distributed AI training, you may need,

  • 1 coordinator node

  • multiple worker nodes

  • GPU-based training pods

JobSet ensures these components are created and managed as a single training workload.

Now lets look at the three key components of Kubeflow trainer.

kubeflow-trainer-controller-manager

This is the main controller of Kubeflow Trainer. It watches for TrainJob resources created by users. When a TrainJob is submitted, the controller combines TrainJob, TrainingRuntime & ClusterTrainingRuntime and generates a JobSet resource.

jobset-controller

The JobSet controller takes care of the actual distributed job orchestration. It watches JobSet resources and creates the required Kubernetes Jobs and training pods.

It also manages the lifecycle of these workloads.

lws-controller-manager

LeaderWorkerSet (LWS) controller is an optional component used for Kubeflow Trainer’s Distributed Data Cache feature. It is not required for normal TrainJob execution.

When Distributed Data Cache is enabled, the LWS controller manages the leader-worker cache workloads that help stream large datasets efficiently to GPU nodes during training.

💡 Key Insight

Kubeflow trainer worker pods can communicate with each other through NCCL or Gloo protocols for effective gradient synchronization in distributed training, whether they are running on CPU or GPU nodes.

Kubeflow Trainer (Must Do Hands-on)

Now let’s put everything we learned into practice with a hands-on setup. Here is what we will do.

  • We will set up Kubeflow Trainer on Kubernetes.

  • We will run a distributed PyTorch training job using the torch-distributed ClusterTrainingRuntime that works on CPU.

  • We will understand how Kubeflow Trainer creates and manages distributed training workloads.

  • We will validate the training job and verify the results.

Kubeflow Trainer Project Files

Important Note: Fork or sync your forked mlops repo to get the latest code changes. Check out this guide to learn how to keep your fork updated.

Inside the repo, the phase-2-enterprise-setup/kubeflow-trainer folder contains the YAML manifest used to create the TrainJob custom resource.

.
└── phase-2-enterprise-setup
    └── kubeflow-trainer
        └── trainjob.yaml
        └── train.py

Setup Kubeflow Trainer on Kubernetes

We will set up a standalone Kubeflow Trainer using the official Helm chart.

Run the following command to deploy Kubeflow Trainer.

helm install kubeflow-trainer oci://ghcr.io/kubeflow/charts/kubeflow-trainer \
    --namespace kubeflow-system \
    --create-namespace \
    --version 2.2.1 \
    --set runtimes.defaultEnabled=true

In the above command, I have added the --set runtimes.defaultEnabled=true flag to enable all the default training runtimes.

💡 Key Insight

Default runtimes are preconfigured training templates provided by Kubeflow Trainer. It typically includes ready-to-use configurations for ML frameworks such as PyTorch, MPI, JAX etc..

After the installation, ensure the trainer pod and jobset controller are in running state.

$ kubectl get po -n kubeflow-system

NAME                                                  READY   STATUS    RESTARTS   AGE
jobset-controller-67f6757844-4gffm                    1/1     Running   0          3m34s
kubeflow-trainer-controller-manager-d68b55dd4-5fbkm   1/1     Running   0          3m34s

To verify the available default runtimes, run the following command.

$ kubectl get clustertrainingruntimes

NAME                     AGE

deepspeed-distributed    6m30s
jax-distributed          6m30s
mlx-distributed          6m30s
torch-distributed        6m30s
torchtune-llama3.2-1b    6m30s
torchtune-llama3.2-3b    6m30s
torchtune-qwen2.5-1.5b   6m30s
xgboost-distributed      6m30s

In the above list, torch-distributed, jax-distributed, and xgboost-distributed can run training on CPU nodes.

Create A TrainJob

For testing, we will use the torch-distributed runtime, which runs the training workload on CPUs.

In this example, we will perform distributed training with two worker pods. Each pod processes a portion of the training data and synchronizes with the other worker to train the model, as shown below.

You can find the trainjob.yaml manifest under phase-2-enterprise-setup/kubeflow-trainer folder

💡 About Training Python Script:

The trainjob.yaml manifest has the Python training script defined inside the args section. The script creates sample data and a simple neural network model, runs distributed training across workers, and saves the final trained model inside the worker pod (specifically the rank 0 worker pod).

Lets deploy the TrainJob.

$ kubectl apply -f trainjob.yaml

trainjob.trainer.kubeflow.org/distributed-training created

Once it is applied, use the following command to list the training pods. You will see two pods created by TrainJobs in two different nodes for distributed training.

$ kubectl get po -n kubeflow-system -o wide

NAME                                                  READY   STATUS              RESTARTS        AGE     IP              NODE                                         NOMINATED NODE   READINESS GATES
distributed-training-node-0-0-p57fq                   0/1     ContainerCreating   0               36s     <none>          ip-172-31-45-62.us-west-2.compute.internal   <none>           <none>
distributed-training-node-0-1-7lh46                   0/1     ContainerCreating   0               36s     <none>          ip-172-31-5-41.us-west-2.compute.internal    <none>           <none>

Since the deployed pods are created as Kubernetes Jobs, they will move to the completed state once the training script executes successfully.

Since there is no PersistentVolume (PV/PVC) or external storage configured, the saved model in the pod will be lost when the pod gets deleted after the training job completes.

For this hands-on example, /tmp/model.pt is mainly used to validate that the distributed training completed successfully.

💡 Key Production Insight

In production environments, the trained model and training artifacts are typically stored in model registry like MLflow or similar model registry tools.

These tools help track experiments, manage model versions, and maintain the lifecycle of trained models.

We will explore MLflow in detail in the next edition.

Triggering Training From SDK

When ML engineers work on training workflows, they can use the Kubeflow SDK to trigger training jobs directly from their workstation.

You can try this using the train.py script available in the kubeflow-trainer folder.

Note: When running the script, the SDK uses the ~/.kube/config file to connect to the Kubernetes cluster and create the TrainJob.

Let’s set up the Python environment and install the Kubeflow SDK.

python3 -m venv venv                                    
source venv/bin/activate
pip install kubeflow

Then, run the train.py script to create the TrainJob.

$ python3 train.py

The script will submit the training job to Kubernetes and print the training details in the terminal output.

Clean Up

If you no longer need Kubeflow Trainer, run the following command to uninstall it.

helm uninstall kubeflow-trainer -n kubeflow-system

Running ML Training Jobs on GPU Nodes

You might be wondering how we can run training jobs on GPU nodes, since we used a simple CPU-based training example.

To run training workloads on GPUs, the Kubernetes cluster should have GPU-enabled nodes with the required GPU drivers and device plugins configured.

Also, default runtimes may not work because GPU nodes usually have taints, and the default runtimes do not include the required tolerations.

To solve this, you need to create a custom runtime with GPU tolerations and node selectors, then use it in your TrainJob to schedule workloads on GPU nodes.

For example, the following ClusterTrainingRuntime spec configures GPU scheduling by adding the required tolerations, node selector, and GPU-enabled training image.

spec:
  tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
  nodeSelector:
    doks.digitalocean.com/gpu-model: h200
  containers:
    - name: node
      image: pytorch/pytorch:2.5.1-cuda12.1-cudnn9-runtime

Now, any TrainJob that references this runtime will be scheduled on the GPU nodes and can access the GPUs based on the configured resource requests.

What's Coming Next?

You now know both Kubeflow subprojects (pipelines & trainer) our final project uses.

Now, we need a way to track every training run, log parameters and metrics, and properly version trained models. That is done using MLflow.

In the next edition, I will cover MLflow.

The TrainJobs you learned today create the training outputs, and MLflow helps track, version, and manage them.

🧱Which Models Actually Need GPUs?

When I started learning MLOps, one misconception I had was: ML workload = GPU. But that is not true.

Not every model needs a GPU.

Model type

Example

Hardware

Traditional ML

Our attrition model (Random Forest, 500K rows)

CPU. Trains in minutes on a laptop

Deep learning

Image recognition, text classification

1 GPU. Hours to days

LLM fine-tuning

Adapting a 7B parameter model to your data

Multiple GPUs, often multiple nodes

LLM pretraining

Training a foundation model from scratch

Thousands of GPUs, weeks to months

Reply

Avatar

or to participate

Keep Reading