Run Jobs Using Python
This guide is for batch users that have a basic understanding of interacting with Kubernetes from Python. For more information, see Kueue’s overview.
Before you begin
Check administer cluster quotas for details on the initial cluster setup.
You’ll also need uv installed. Each example script includes
inline script metadata (PEP 723) so uv run will
automatically create an isolated environment and install the required dependencies.
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
No manual pip install or virtual environment setup is needed — just run the scripts
with uv run.
You can either follow the install instructions for Kueue, or use the install example, below.
Kueue in Python
Kueue at the core is a controller for a Custom Resource, and so to interact with it from Python we don’t need a custom SDK, but rather we can use the generic functions provided by the Kubernetes Python library. In this guide, we provide several examples for interacting with Kueue in this fashion. If you would like to request a new example or would like help for a specific use case, please open an issue.
Examples
The following examples demonstrate different use cases for using Kueue in Python.
Install Kueue
This example demonstrates installing Kueue to an existing cluster. You can save this
script to your local machine as install-kueue-queues.py.
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "kubernetes",
# "requests",
# ]
# ///
from kubernetes import utils, config, client
import tempfile
import requests
import argparse
# install-kueue-queues.py will:
# 1. install queue from the latest or a specific version on GitHub
# This example will demonstrate installing Kueue and applying a YAML file (local) to install Kueue
# Make sure your cluster is running!
def get_parser():
parser = argparse.ArgumentParser(
description="Submit Kueue Job Example",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--version",
help="Version of Kueue to install (if undefined, will install from master branch)",
default=None,
)
return parser
def main():
"""
Install Kueue and the Queue components.
This will error if they are already installed.
"""
config.load_kube_config()
parser = get_parser()
args, _ = parser.parse_known_args()
install_kueue(args.version)
def get_install_url(version):
"""
Get the install version.
If a version is specified, use it. Otherwise install
from the main branch.
"""
if version is not None:
return f"https://github.com/kubernetes-sigs/kueue/releases/download/v{version}/manifests.yaml"
return "https://github.com/kubernetes-sigs/kueue/config/default?ref=main"
def install_kueue(version):
"""
Install Kueue of a particular version.
"""
crd_api = client.CustomObjectsApi()
api_client = crd_api.api_client
print("⭐️ Installing Kueue...")
url = get_install_url(version)
with tempfile.NamedTemporaryFile(delete=True) as install_yaml:
res = requests.get(url)
assert res.status_code == 200
install_yaml.write(res.content)
utils.create_from_yaml(api_client, install_yaml.name)
if __name__ == "__main__":
main()
And then run as follows:
uv run install-kueue-queues.py
⭐️ Installing Kueue...
⭐️ Applying queues from single-clusterqueue-setup.yaml...
You can also target a specific version:
uv run install-kueue-queues.py --version v0.19.0
Sample Job
For the next example, let’s start with a cluster with Kueue installed, and first create our queues:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "kubernetes",
# ]
# ///
import argparse
from kubernetes import config, client
# create_job.py
# This example will demonstrate full steps to submit a Job.
# Make sure your cluster is running!
def get_parser():
parser = argparse.ArgumentParser(
description="Submit Kueue Job Example",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--job-name",
help="generateName field to set for job",
default="sample-job-",
)
parser.add_argument(
"--image",
help="container image to use",
default="registry.k8s.io/e2e-test-images/agnhost:2.53",
)
parser.add_argument(
"--args",
nargs="+",
help="args for container",
default=["entrypoint-tester", "hello", "world"],
)
return parser
def generate_job_crd(job_name, image, args):
"""
Generate an equivalent job CRD to sample-job.yaml
"""
metadata = client.V1ObjectMeta(
generate_name=job_name, labels={"kueue.x-k8s.io/queue-name": "user-queue"}
)
# Job container
container = client.V1Container(
image=image,
name="dummy-job",
args=args,
resources={
"requests": {
"cpu": 1,
"memory": "200Mi",
}
},
)
# Job template
template = {"spec": {"containers": [container], "restartPolicy": "Never"}}
return client.V1Job(
api_version="batch/v1",
kind="Job",
metadata=metadata,
spec=client.V1JobSpec(
parallelism=1, completions=3, suspend=True, template=template
),
)
def main():
"""
Run a job.
"""
config.load_kube_config()
parser = get_parser()
args, _ = parser.parse_known_args()
# Generate a CRD spec
crd = generate_job_crd(args.job_name, args.image, args.args)
batch_api = client.BatchV1Api()
print(f"📦️ Container image selected is {args.image}...")
print(f"⭐️ Creating sample job with prefix {args.job_name}...")
batch_api.create_namespaced_job("default", crd)
print(
'Use:\n"kubectl get queue" to see queue assignment\n"kubectl get jobs" to see jobs'
)
if __name__ == "__main__":
main()
And run as follows:
uv run sample-job.py
📦️ Container image selected is registry.k8s.io/e2e-test-images/agnhost:2.53...
⭐️ Creating sample job with prefix sample-job-...
Use:
"kubectl get queue" to see queue assignment
"kubectl get jobs" to see jobs
or try changing the name (generateName) of the job:
uv run sample-job.py --job-name sleep-job-
📦️ Container image selected is registry.k8s.io/e2e-test-images/agnhost:2.53...
⭐️ Creating sample job with prefix sleep-job-...
Use:
"kubectl get queue" to see queue assignment
"kubectl get jobs" to see jobs
You can also change the container image with --image and args with --args.
For more customization, you can edit the example script.
Interact with Queues and Jobs
If you are developing an application that submits jobs and needs to interact
with and check on them, you likely want to interact with queues or jobs directly.
After running the example above, you can test the following example to interact
with the results. Write the following to a script called sample-queue-control.py.
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "kubernetes",
# ]
# ///
import argparse
from kubernetes import config, client
# sample-queue-control.py
# This will show how to interact with queues
# Make sure your cluster is running!
def get_parser():
parser = argparse.ArgumentParser(
description="Interact with Queues e",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--namespace",
help="namespace to list for",
default="default",
)
return parser
def main():
"""
Get a listing of jobs in the queue
"""
config.load_kube_config()
crd_api = client.CustomObjectsApi()
parser = get_parser()
args, _ = parser.parse_known_args()
listing = crd_api.list_namespaced_custom_object(
group="kueue.x-k8s.io",
version="v1beta2",
namespace=args.namespace,
plural="localqueues",
)
list_queues(listing)
listing = crd_api.list_namespaced_custom_object(
group="batch",
version="v1",
namespace=args.namespace,
plural="jobs",
)
list_jobs(listing)
def list_jobs(listing):
"""
Iterate and show job metadata.
"""
if not listing:
print("💼️ There are no jobs.")
return
print("\n💼️ Jobs")
for job in listing["items"]:
jobname = job["metadata"]["name"]
status = (
"TBA" if "succeeded" not in job["status"] else job["status"]["succeeded"]
)
ready = job["status"]["ready"]
print(f"Found job {jobname}")
print(f" Succeeded: {status}")
print(f" Ready: {ready}")
def list_queues(listing):
"""
Helper function to iterate over and list queues.
"""
if not listing:
print("⛑️ There are no queues.")
return
print("\n⛑️ Local Queues")
# This is listing queues
for q in listing["items"]:
print(f'Found queue {q["metadata"]["name"]}')
print(f" Admitted workloads: {q['status']['admittedWorkloads']}")
print(f" Pending workloads: {q['status']['pendingWorkloads']}")
# And flavors with resources
for f in q["status"]["flavorUsage"]:
print(f' Flavor {f["name"]} has resources {f["resources"]}')
if __name__ == "__main__":
main()
To make the output more interesting, we can run a few random jobs first:
uv run sample-job.py
uv run sample-job.py
uv run sample-job.py --job-name tacos
And then run the script to see your queue and sample job that you submit previously.
uv run sample-queue-control.py
⛑️ Local Queues
Found queue user-queue
Admitted workloads: 3
Pending workloads: 0
Flavor default-flavor has resources [{'name': 'cpu', 'total': '3'}, {'name': 'memory', 'total': '600Mi'}]
💼️ Jobs
Found job sample-job-8n5sb
Succeeded: 3
Ready: 0
Found job sample-job-gnxtl
Succeeded: 1
Ready: 0
Found job tacos46bqw
Succeeded: 1
Ready: 1
If you wanted to filter jobs to a specific queue, you can do this via the job labels under `job[“metadata”][“labels”][“kueue.x-k8s.io/queue-name”]’. To list a specific job by name, you can do:
from kubernetes import client, config
# Interact with batch
config.load_kube_config()
batch_api = client.BatchV1Api()
# This is providing the name, and namespace
job = batch_api.read_namespaced_job("tacos46bqw", "default")
print(job)
See the BatchV1 API documentation for more calls.
Flux Operator Job
For this example, we will be using the Flux Operator to submit a job, and specifically using the Python SDK to do this easily. Given our Python environment created in the setup, we can install this Python SDK directly to it as follows:
The fluxoperator dependency is declared in the script’s inline metadata and will be
automatically installed by uv run.
We will also need to install the Flux operator.
kubectl apply -f https://raw.githubusercontent.com/flux-framework/flux-operator/main/examples/dist/flux-operator.yaml
Write the following script to sample-flux-operator-job.py:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "kubernetes",
# "fluxoperator",
# ]
# ///
import argparse
from kubernetes import config, client
import fluxoperator.models as models
# sample-flux-operator.py
# This example will demonstrate full steps to submit a Job via the Flux Operator.
# Make sure your cluster is running!
def get_parser():
parser = argparse.ArgumentParser(
description="Submit Kueue Flux Operator Job Example",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--job-name",
help="generateName field to set for job (job prefix does not work here)",
default="hello-world",
)
parser.add_argument(
"--image",
help="container image to use",
default="ghcr.io/flux-framework/flux-restful-api",
)
parser.add_argument(
"--tasks",
help="Number of tasks",
default=1,
type=int,
)
parser.add_argument(
"--quiet",
help="Do not show extra flux output (only hello worlds!)",
action="store_true",
default=False,
)
parser.add_argument(
"--command",
help="command to run",
default="echo",
)
parser.add_argument(
"--args", nargs="+", help="args for container", default=["hello", "world"]
)
return parser
def generate_minicluster_crd(job_name, image, command, args, quiet=False, tasks=1):
"""
Generate a minicluster CRD
"""
container = models.MiniClusterContainer(
command=command + " " + " ".join(args),
resources={
"limits": {
"cpu": 1,
"memory": "2Gi",
}
},
)
# 4 pods and 4 tasks will echo hello-world x 4
spec = models.MiniClusterSpec(
job_labels={"kueue.x-k8s.io/queue-name": "user-queue"},
containers=[container],
size=4,
tasks=tasks,
logging={"quiet": quiet},
)
return models.MiniCluster(
kind="MiniCluster",
api_version="flux-framework.org/v1alpha1",
metadata=client.V1ObjectMeta(
generate_name=job_name,
namespace="default",
),
spec=spec,
)
def main():
"""
Run an example job using the Flux Operator.
"""
config.load_kube_config()
parser = get_parser()
args, _ = parser.parse_known_args()
# Generate a CRD spec
minicluster = generate_minicluster_crd(
args.job_name, args.image, args.command, args.args, args.quiet, args.tasks
)
crd_api = client.CustomObjectsApi()
print(f"📦️ Container image selected is {args.image}...")
print(f"⭐️ Creating sample job with prefix {args.job_name}...")
crd_api.create_namespaced_custom_object(
group="flux-framework.org",
version="v1alpha1",
namespace="default",
plural="miniclusters",
body=minicluster,
)
print(
'Use:\n"kubectl get queue" to see queue assignment\n"kubectl get pods" to see pods'
)
if __name__ == "__main__":
main()
Now try running the example:
uv run sample-flux-operator-job.py
📦️ Container image selected is ghcr.io/flux-framework/flux-restful-api...
⭐️ Creating sample job with prefix hello-world...
Use:
"kubectl get queue" to see queue assignment
"kubectl get pods" to see pods
You’ll be able to almost immediately see the MiniCluster job admitted to the local queue:
kubectl get queue
NAME CLUSTERQUEUE PENDING WORKLOADS ADMITTED WORKLOADS
user-queue cluster-queue 0 1
And the 4 pods running (we are creating a networked cluster with 4 nodes):
kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-world7qgqd-0-wp596 1/1 Running 0 7s
hello-world7qgqd-1-d7r87 1/1 Running 0 7s
hello-world7qgqd-2-rfn4t 1/1 Running 0 7s
hello-world7qgqd-3-blvtn 1/1 Running 0 7s
If you look at logs of the main broker pod (index 0 of the job above), there is a lot of output for debugging, and you can see “hello world” running at the end:
kubectl logs hello-world7qgqd-0-wp596
Flux Operator Lead Broker Output
🌀 Submit Mode: flux start -o --config /etc/flux/config -Scron.directory=/etc/flux/system/cron.d -Stbon.fanout=256 -Srundir=/run/flux -Sstatedir=/var/lib/flux -Slocal-uri=local:///run/flux/local -Slog-stderr-level=6 -Slog-stderr-mode=local flux submit -n 1 --quiet --watch echo hello world
broker.info[0]: start: none->join 0.399725ms
broker.info[0]: parent-none: join->init 0.030894ms
cron.info[0]: synchronizing cron tasks to event heartbeat.pulse
job-manager.info[0]: restart: 0 jobs
job-manager.info[0]: restart: 0 running jobs
job-manager.info[0]: restart: checkpoint.job-manager not found
broker.info[0]: rc1.0: running /etc/flux/rc1.d/01-sched-fluxion
sched-fluxion-resource.info[0]: version 0.27.0-15-gc90fbcc2
sched-fluxion-resource.warning[0]: create_reader: allowlist unsupported
sched-fluxion-resource.info[0]: populate_resource_db: loaded resources from core's resource.acquire
sched-fluxion-qmanager.info[0]: version 0.27.0-15-gc90fbcc2
broker.info[0]: rc1.0: running /etc/flux/rc1.d/02-cron
broker.info[0]: rc1.0: /etc/flux/rc1 Exited (rc=0) 0.5s
broker.info[0]: rc1-success: init->quorum 0.485239s
broker.info[0]: online: hello-world7qgqd-0 (ranks 0)
broker.info[0]: online: hello-world7qgqd-[0-3] (ranks 0-3)
broker.info[0]: quorum-full: quorum->run 0.354587s
hello world
broker.info[0]: rc2.0: flux submit -n 1 --quiet --watch echo hello world Exited (rc=0) 0.3s
broker.info[0]: rc2-success: run->cleanup 0.308392s
broker.info[0]: cleanup.0: flux queue stop --quiet --all --nocheckpoint Exited (rc=0) 0.1s
broker.info[0]: cleanup.1: flux cancel --user=all --quiet --states RUN Exited (rc=0) 0.1s
broker.info[0]: cleanup.2: flux queue idle --quiet Exited (rc=0) 0.1s
broker.info[0]: cleanup-success: cleanup->shutdown 0.252899s
broker.info[0]: children-complete: shutdown->finalize 47.6699ms
broker.info[0]: rc3.0: running /etc/flux/rc3.d/01-sched-fluxion
broker.info[0]: rc3.0: /etc/flux/rc3 Exited (rc=0) 0.2s
broker.info[0]: rc3-success: finalize->goodbye 0.212425s
broker.info[0]: goodbye: goodbye->exit 0.06917ms
If you submit and ask for four tasks, you’ll see “hello world” four times:
uv run sample-flux-operator-job.py --tasks 4
...
broker.info[0]: quorum-full: quorum->run 23.5812s
hello world
hello world
hello world
hello world
You can further customize the job, and can ask questions on the Flux Operator issues board. Finally, for instructions for how to do this with YAML outside of Python, see Run A Flux MiniCluster.
Kubeflow Trainer Job
For this example, we will use Kubeflow Trainer
to submit a TrainJob (the Kubeflow Trainer v2 API), and specifically the
Kubeflow SDK to do this easily. Unlike the previous
examples, the SDK builds and submits the resource for us, so we don’t construct the CRD by
hand. The kubeflow dependency is declared in the script’s inline metadata and will be
automatically installed by uv run.
This example assumes Kubeflow Trainer v2 is installed, the built-in torch-distributed
ClusterTrainingRuntime is available, and Kueue is configured to manage TrainJobs. For these
prerequisites (installing Kubeflow Trainer, the runtime, and enabling the TrainJob
integration in Kueue), follow the Run A TrainJob guide. You’ll
also need the queues created:
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/kueue/main/site/static/examples/admin/single-clusterqueue-setup.yaml
Write the following script to sample-trainjob.py:
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "kubeflow",
# ]
# ///
import argparse
from kubeflow.trainer import TrainerClient, CustomTrainer
from kubeflow.trainer.options import Labels
# sample-trainjob.py
# This example demonstrates submitting a Kubeflow Trainer TrainJob, managed by
# Kueue, using the Kubeflow SDK. It runs a small distributed PyTorch training
# job (Fashion-MNIST) against the built-in "torch-distributed" runtime.
def train_pytorch():
import os
import torch
from torch import nn
import torch.nn.functional as F
from torchvision import datasets, transforms
import torch.distributed as dist
from torch.utils.data import DataLoader, DistributedSampler
# Kubeflow Trainer configures the distributed environment automatically.
device, backend = ("cuda", "nccl") if torch.cuda.is_available() else ("cpu", "gloo")
dist.init_process_group(backend=backend)
local_rank = int(os.getenv("LOCAL_RANK", 0))
print(
"Distributed Training with WORLD_SIZE: {}, RANK: {}, LOCAL_RANK: {}.".format(
dist.get_world_size(), dist.get_rank(), local_rank
)
)
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, 5, 1)
self.conv2 = nn.Conv2d(20, 50, 5, 1)
self.fc1 = nn.Linear(4 * 4 * 50, 500)
self.fc2 = nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4 * 4 * 50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
device = torch.device(f"{device}:{local_rank}")
model = nn.parallel.DistributedDataParallel(Net().to(device))
model.train()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# Download the dataset on the rank-0 process first, then load it on every rank.
if local_rank == 0:
datasets.FashionMNIST(
"./data", train=True, download=True, transform=transforms.ToTensor()
)
dist.barrier()
dataset = datasets.FashionMNIST(
"./data", train=True, download=False, transform=transforms.ToTensor()
)
train_loader = DataLoader(
dataset, batch_size=100, sampler=DistributedSampler(dataset)
)
for epoch in range(3):
for batch_idx, (inputs, labels) in enumerate(train_loader):
inputs, labels = inputs.to(device), labels.to(device)
loss = F.nll_loss(model(inputs), labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if batch_idx % 10 == 0 and dist.get_rank() == 0:
print(
"Train Epoch: {} [{}/{}]\tLoss: {:.6f}".format(
epoch,
batch_idx * len(inputs),
len(train_loader.dataset),
loss.item(),
)
)
dist.barrier()
if dist.get_rank() == 0:
print("Training is finished")
dist.destroy_process_group()
def get_parser():
parser = argparse.ArgumentParser(
description="Submit Kueue Kubeflow Trainer TrainJob Example",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--runtime",
help="Kubeflow Trainer runtime to use",
default="torch-distributed",
)
parser.add_argument(
"--queue-name",
help="Kueue local queue name",
default="user-queue",
)
parser.add_argument(
"--num-nodes",
help="Number of PyTorch training nodes",
type=int,
default=1,
)
return parser
def main():
"""
Submit a TrainJob via the Kubeflow SDK. This requires Kubeflow Trainer to be
installed, the selected runtime to exist, and Kueue with TrainJob integration.
"""
parser = get_parser()
args, _ = parser.parse_known_args()
client = TrainerClient()
print(f"⭐️ Submitting TrainJob to runtime {args.runtime}...")
job_name = client.train(
runtime=args.runtime,
trainer=CustomTrainer(
func=train_pytorch,
num_nodes=args.num_nodes,
resources_per_node={"cpu": 1, "memory": "2Gi"},
),
options=[Labels({"kueue.x-k8s.io/queue-name": args.queue_name})],
)
print(f"⭐️ Created TrainJob {job_name}")
print(
'Use:\n"kubectl get queue" to see queue assignment\n'
'"kubectl get trainjobs" to see TrainJobs'
)
if __name__ == "__main__":
main()
The Kueue integration is the Labels option, which sets the kueue.x-k8s.io/queue-name
label on the TrainJob so Kueue can admit it to the right local queue (the same label the
other examples set on their job metadata).
Now try running the example:
uv run sample-trainjob.py
⭐️ Submitting TrainJob to runtime torch-distributed...
⭐️ Created TrainJob md3b82e943a5
Use:
"kubectl get queue" to see queue assignment
"kubectl get trainjobs" to see TrainJobs
Using the TrainJob name printed above, you can watch the TrainJob and stream its logs with the Kubeflow SDK:
from kubeflow.trainer import TrainerClient
client = TrainerClient()
job_name = "md3b82e943a5"
# Wait until Kueue admits the TrainJob and it starts running, then stream its logs.
client.wait_for_job_status(name=job_name, status={"Running"})
for logline in client.get_job_logs(job_name, follow=True):
print(logline)
[Gloo] Rank 0 is connected to 0 peer ranks. Expected number of connected peer ranks is : 0
Distributed Training with WORLD_SIZE: 1, RANK: 0, LOCAL_RANK: 0.
Train Epoch: 0 [0/60000] Loss: 2.310645
Train Epoch: 0 [1000/60000] Loss: 1.935180
Train Epoch: 0 [2000/60000] Loss: 2.096165
...
Train Epoch: 2 [58000/60000] Loss: 0.372064
Train Epoch: 2 [59000/60000] Loss: 0.226857
Training is finished
You can further customize the job, and ask questions on the Kubeflow Trainer issues board. Finally, for instructions for how to do this with YAML outside of Python, see Run A TrainJob.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.