Kueue GPU Topology Aware Scheduling: Install, Configure, Run AI Jobs

Updated on Sep 17, 2026
Mila H
8 MINS READ
Table of Contents
Rack-Aware GPU Scheduling on Kubernetes with Kueue

If you run multi-GPU AI jobs on Kubernetes, you may have seen a job stay stuck in Pending even when your cluster has plenty of free GPUs. This usually happens because the free GPUs are spread across many nodes instead of sitting together on one node or rack. This guide shows you how to fix this with Kueue GPU topology-aware scheduling. 

Why GPU Jobs Get Stuck on Kubernetes

The default Kubernetes scheduler places pods one by one. It does not know that an AI job's pods need to sit close together, so GPUs can talk to each other fast. A basic Kueue setup has the same problem. It only checks if there are enough free GPUs in total, not where those free GPUs are.

This is where Kueue GPU topology-aware scheduling helps. It teaches Kueue your real layout, such as which node sits in which rack. This way, Kueue waits until one node or rack has enough free GPUs together. It prevents pods from spreading across nodes and prevents jobs from getting stuck half-started.

What Is Kueue

Kueue is an open-source tool that adds a queue in front of Kubernetes. Instead of every job fighting for pods at once, Kueue holds jobs until there is enough quota, then lets them run in a fair order. 

Kueue solves "too many jobs, not enough GPUs" through quotas. But quotas only count numbers, not location. Topology-Aware Scheduling fixes that. This is why Kueue GPU topology-aware scheduling matters for real AI clusters with more than a few nodes.

Before You Start

This guide is for anyone running GPU servers on Kubernetes who wants queueing and smart pod placement for training or inference jobs. We assume you already have:

  • A Kubernetes cluster, such as K3s, kubeadm, or managed.
  • At least two or three nodes with NVIDIA GPUs, with the NVIDIA GPU Operator already installed so nvidia.com/gpu shows up as an allocatable resource. If you have not done this yet, follow our guide on how to install the NVIDIA GPU Operator on a bare-metal K3s cluster.
  • kubectl and helm installed and pointed at your cluster.
  • jq installed on your workstation.

Note: If you plan to build a real multi-GPU cluster, PerLod's GPU dedicated servers give you full root access and predictable hardware, which makes rack-level labeling and topology testing much easier than on shared cloud instances.

Step 1: Check Your Cluster Is Ready

Confirm your nodes are visible and that GPUs show up as allocatable resources:

Bash
kubectl get nodeskubectl get nodes -o json | jq '.items[] | {name: .metadata.name, gpus: .status.allocatable["nvidia.com/gpu"]}'

You should see each GPU node listed with a gpus count matching its physical cards. If any node shows null, its GPU Operator setup is not complete, and you should fix that before continuing.

Step 2: Install Kueue

At this point, you should install the latest stable release of Kueue. Always check the Kueue releases page for the newest tag before you install, and change the version number below if a newer one is available.

You can install Kueue with the following kubectl command:

Bash
kubectl apply --server-side -f https://github.com/kubernetes-sigs/kueue/releases/download/v0.19.4/manifests.yaml

Wait for the controller to become ready:

Bash
kubectl wait deploy/kueue-controller-manager -n kueue-system --for=condition=available --timeout=5m

If you prefer Helm, you can use this Helm command to install Kueue:

Bash
helm install kueue oci://registry.k8s.io/kueue/charts/kueue \  --version=0.19.4 \  --namespace kueue-system \  --create-namespace \  --wait --timeout 300s

Check that the controller pod is running:

Bash
kubectl get pods -n kueue-system

You should see a kueue-controller-manager pod with a Running status.

Step 3: Confirm Topology-Aware Scheduling Is Enabled

The TopologyAwareScheduling feature is what makes Kueue GPU topology-aware scheduling work. If you installed v0.19.4 above, it is already on; no extra steps needed. You can check it with this command:

Bash
kubectl get deploy kueue-controller-manager -n kueue-system -o jsonpath='{.spec.template.spec.containers[0].args}'

If you do not see TopologyAwareScheduling=false anywhere in the output, the feature is active by default, and you can move to the next step.

Step 4: Label Your GPU Nodes by Rack

Cloud providers usually add rack or zone labels to nodes automatically. On a bare-metal or dedicated GPU cluster, you must add these labels yourself so Kueue knows which physical group each node belongs to.

For this guide, consider three GPU nodes split across two racks:

Bash
kubectl label node gpu-node-1 topology.perlod.com/rack=rack-a nvidia.com/gpu.present=true --overwritekubectl label node gpu-node-2 topology.perlod.com/rack=rack-a nvidia.com/gpu.present=true --overwritekubectl label node gpu-node-3 topology.perlod.com/rack=rack-b nvidia.com/gpu.present=true --overwrite

Replace gpu-node-1, gpu-node-2, and gpu-node-3 with your real node names from kubectl get nodes. The nvidia.com/gpu.present label is added automatically by GPU Feature Discovery once the GPU Operator is installed. So you only need to add the rack label. Confirm the labels are set up correctly:

Bash
kubectl get nodes -L topology.perlod.com/rack,nvidia.com/gpu.present

Step 5: Create the Topology Object

The Topology object tells Kueue how your data center is organized, from the biggest group down to every single node. Create the file with your desired text editor:

Bash
nano gpu-topology.yaml

Add this content into the file:

YAML
apiVersion: kueue.x-k8s.io/v1beta2kind: Topologymetadata:  name: "gpu-rack-topology"spec:  levels:    - nodeLabel: "topology.perlod.com/rack"    - nodeLabel: "kubernetes.io/hostname"

With this, group nodes first by rack, then by individual hostname. The kubernetes.io/hostname label must always be the last level in the list. Apply it:

Bash
kubectl apply -f gpu-topology.yaml

Step 6: Create the GPU ResourceFlavor

A ResourceFlavor tells Kueue which nodes to use for a given quota, and links those nodes to the Topology object you just made. Create the file with:

Bash
nano gpu-flavor.yaml

Paste this content:

YAML
apiVersion: kueue.x-k8s.io/v1beta2kind: ResourceFlavormetadata:  name: "gpu-flavor"spec:  nodeLabels:    nvidia.com/gpu.present: "true"  topologyName: "gpu-rack-topology"

Apply it with:

Bash
kubectl apply -f gpu-flavor.yaml

This is the key link for Kueue GPU topology-aware scheduling. The topologyName field turns a normal ResourceFlavor into a topology-aware one. Without it, Kueue only tracks quota, not where pods land.

Step 7: Create the ClusterQueue

The ClusterQueue sets the total GPU quota that all teams or jobs share. Create the file with:

Bash
nano gpu-cluster-queue.yaml

Paste this content and change nominalQuota to match your real total GPU count:

YAML
apiVersion: kueue.x-k8s.io/v1beta2kind: ClusterQueuemetadata:  name: "gpu-cluster-queue"spec:  namespaceSelector: {}  queueingStrategy: BestEffortFIFO  resourceGroups:    - coveredResources: ["nvidia.com/gpu"]      flavors:        - name: "gpu-flavor"          resources:            - name: "nvidia.com/gpu"              nominalQuota: 12

Apply it:

Bash
kubectl apply -f gpu-cluster-queue.yaml

Step 8: Create the LocalQueue

The LocalQueue is the entry point your team submits jobs to inside a namespace. Create a namespace for AI jobs first, then the queue:

Bash
kubectl create namespace ai-jobsnano gpu-local-queue.yaml

Add this content:

YAML
apiVersion: kueue.x-k8s.io/v1beta2kind: LocalQueuemetadata:  namespace: "ai-jobs"  name: "gpu-user-queue"spec:  clusterQueue: "gpu-cluster-queue"

Apply and check that everything is registered:

Bash
kubectl apply -f gpu-local-queue.yamlkubectl get topology,resourceflavor,clusterqueue,localqueue -A

At this point, you have a complete queueing setup for Kueue GPU topology-aware scheduling.

Step 9: Run the Job That Was Stuck Before

Now create a job file that asks for 4 GPUs, all on the same host:

Bash
nano gpu-training-job.yaml

Paste this content:

YAML
apiVersion: batch/v1kind: Jobmetadata:  generateName: gpu-training-required-  namespace: ai-jobs  labels:    kueue.x-k8s.io/queue-name: gpu-user-queuespec:  parallelism: 4  completions: 4  completionMode: Indexed  template:    metadata:      annotations:        kueue.x-k8s.io/podset-required-topology: "kubernetes.io/hostname"    spec:      containers:        - name: gpu-worker          image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04          resources:            requests:              nvidia.com/gpu: "1"            limits:              nvidia.com/gpu: "1"      restartPolicy: Never

Apply it with:

Bash
kubectl create -f gpu-training-job.yaml

In the fragmented state, only 2 free GPUs per node; this job stays Pending instead of splitting across two nodes. That's not a failure; it's Kueue protecting your job from bad placement. Once gpu-node-3 with 4 free GPUs together is free, Kueue puts the whole job there automatically.

Step 10: Check Where Kueue Placed Your Job

Now check the Workload object Kueue created for your job:

Bash
kubectl get workloads -n ai-jobs

Get the exact node assignment:

Bash
WORKLOAD=$(kubectl get workload -n ai-jobs -o name --sort-by='.metadata.creationTimestamp' | tail -n 1)kubectl get $WORKLOAD -n ai-jobs -o jsonpath='{.status.admission.podSetAssignments[0].topologyAssignment}' | jq

The output lists the exact node Kueue chose. If your job is still pending, check the reason:

Bash
kubectl describe $WORKLOAD -n ai-jobs

Check the Events section and the QuotaReserved message. It will tell you if no rack or node has enough free GPUs yet. This confirms Kueue GPU topology-aware scheduling is checking real placement, not just totals.

Other Topology Modes: Preferred vs Unconstrained Scheduling

To try softer modes, you can just change the annotation in your job file and submit it again:

Bash
kueue.x-k8s.io/podset-preferred-topology: "topology.perlod.com/rack"

With preferred, Kueue first tries to fit all pods in one rack. It only spreads them across racks if it really has to. This mode is a good default for jobs that run faster together but can still finish if split.

For background jobs that don't need speed, use this instead:

Bash
kueue.x-k8s.io/podset-unconstrained-topology: "true"

This tells Kueue to fill any free GPU slots it can find. Over time, this makes the small gaps across your cluster smaller instead of leaving them spread out everywhere.

Conclusion

Kueue GPU topology-aware scheduling knows where your free GPUs sit, not just how many there are. You've now installed Kueue, labeled your GPU nodes, built the Topology object, and set up quota with a ClusterQueue and LocalQueue. Pick required for tight training jobs, preferred when locality helps, and unconstrained for background work. That's what keeps your GPU cluster fast and fair instead of stuck.

We hope you enjoy this guide. For more detailed information, you can check the official Kueue Topology-Aware Scheduling Docs.

No. Kueue only decides when a job can start and which nodes it should use. The default Kubernetes scheduler still places the actual pods, following the node selector Kueue adds.

You can install it, but it will not show real value with one node. The benefit appears once you have multiple nodes or racks and want to control how pods spread across them.

No, it works for CPU and memory too. It matters most for GPU and AI jobs because network locality has the biggest effect on training and inference speed.