, ,

How to schedule GPU jobs fairly in Kubernetes

The default Kubernetes scheduler places pods one at a time, so multi-GPU jobs half-start and hold idle GPUs, and one team can take everything. How Kueue fixes it with queues, quotas, borrowing and preemption, with a live demo.

How to schedule GPU jobs fairly: three team quota bars, one team borrowing two GPUs

AI infrastructure · Kubernetes · Kueue · Quotas · Gang scheduling

Once several teams share a GPU cluster, the question changes from “is the GPU healthy?” to “who gets it?”. The default Kubernetes scheduler was built for web services: it places pods one at a time, first come first served, with no idea that eight pods belong to one training job. This post shows the three ways that goes wrong with GPUs and how Kueue fixes them with queues, quotas, borrowing and preemption. The live demo lets you watch both side by side.

Try it: GPU Queue Lab

Three teams, 16 GPUs. Submit two 8-GPU training jobs in plain Kubernetes and watch them deadlock. Switch to Kueue and do it again, then let a team reclaim its quota.

Open the demo →

The problemThree ways the default scheduler fails GPU work

Half-placed jobs

Two training jobs each need two 4-GPU pods. Each gets one pod placed, neither can start, and 8 GPUs sit held and idle. Nothing in Kubernetes notices.

No fair share

One busy team can take every GPU. A ResourceQuota stops that, but it is a hard wall: idle quota in one namespace cannot be lent to another.

No real queue

Pending pods are just retried. Small jobs keep slipping into free gaps, and the big job that needs a whole node can wait forever.

The fixHow Kueue works

Kueue is a Kubernetes project that sits in front of the scheduler. It does not place pods itself. It decides when a whole job may start, then lets the normal scheduler place its pods.

Jobcreated suspendedqueue-name label LocalQueueone per namespacepoints to a team ClusterQueue quota: 8 × nvidia.com/gpu queue order and priority borrow from cohort preempt to reclaim ALL OR NOTHING Admittedjob unsuspendedpods created Schedulerplaces podson nodes

The job’s pods do not exist until Kueue admits it, so a waiting job never holds GPUs.

ResourceFlavor

A kind of hardware, such as “h100” or “l40s”, usually matched to node labels.

ClusterQueue

A team’s share: how many GPUs of each flavor it is guaranteed, and its rules for queueing and preemption.

LocalQueue

The queue a team submits to from its own namespace. It points at a ClusterQueue.

Cohort

A group of ClusterQueues that can lend each other unused quota.

Quota you can borrow, and take back

This is the part a ResourceQuota cannot do. Each team has a guaranteed share, called its nominal quota. When a team is quiet, others may borrow its share. When it needs its GPUs back, Kueue preempts the borrowers.

Research
8 own + 2 borrowed
Product
6 of 6 used
Platform
0 of 2 used, lent out

The red line on each bar is the team’s own quota. When Platform submits a 2-GPU job, it is within its quota, so Kueue preempts Research’s borrowed work to give the GPUs back. The demo shows exactly this.

Side by sideSame jobs, two outcomes

What happensPlain KubernetesKueue
Two 8-GPU jobs, room for oneBoth half-placed, 8 GPUs idleOne admitted, one waits holding nothing
A quiet team’s GPUsFree for whoever asks firstLent out, taken back when needed
Team over its shareNo limit, or a hard wall with ResourceQuotaMay borrow, gets preempted first
Urgent jobNeeds PriorityClass and pod preemptionWorkload priority, preempts lower priority in its queue
Why is my job waiting?“Insufficient nvidia.com/gpu”“Waiting for quota: would use 18 of 12”
What Kueue does not do on its own. It admits on quota, then the normal scheduler places pods one by one. If the GPUs are free in total but split across nodes, pods can still be half-placed. Turn on Kueue’s waitForPodsReady so a job that cannot get all its pods running in time is evicted and requeued, and look at its topology-aware scheduling for jobs that need GPUs close together.

AlternativesOther ways to queue GPU work

Start hereKueue

Kubernetes SIG project. Works with the normal scheduler and with Job, JobSet, Kubeflow, Ray and plain pods. Quotas, borrowing, priorities.

Batch schedulerVolcano

A replacement scheduler built for batch and AI. Strong gang scheduling and queues. More to run, since it replaces the default scheduler for those pods.

Batch schedulerApache YuniKorn

Hierarchical queues and fair sharing, popular with teams coming from Hadoop and Spark.

CommercialNVIDIA Run:ai

GPU-focused scheduling with fractional GPUs, quotas and dashboards. A product you buy, not a project you run.

The setupThree teams, one cohort

  1. Install Kueue

    # pick the current release from github.com/kubernetes-sigs/kueue/releases
    kubectl apply --server-side -f \
      https://github.com/kubernetes-sigs/kueue/releases/download/<version>/manifests.yaml
  2. Describe the hardware and each team’s share

    apiVersion: kueue.x-k8s.io/v1beta1
    kind: ResourceFlavor
    metadata:
      name: h100
    spec:
      nodeLabels:
        nvidia.com/gpu.product: NVIDIA-H100-80GB-HBM3
    ---
    apiVersion: kueue.x-k8s.io/v1beta1
    kind: ClusterQueue
    metadata:
      name: research
    spec:
      cohort: gpu-pool
      namespaceSelector: {}
      queueingStrategy: BestEffortFIFO
      preemption:
        reclaimWithinCohort: Any
        withinClusterQueue: LowerPriority
      resourceGroups:
      - coveredResources: ["nvidia.com/gpu"]
        flavors:
        - name: h100
          resources:
          - name: nvidia.com/gpu
            nominalQuota: 8        # product: 6, platform: 2

    Repeat the ClusterQueue for each team with its own quota and the same cohort.

  3. Give each namespace a queue

    apiVersion: kueue.x-k8s.io/v1beta1
    kind: LocalQueue
    metadata:
      name: research
      namespace: research
    spec:
      clusterQueue: research
  4. Point jobs at the queue

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: llama-train-1
      namespace: research
      labels:
        kueue.x-k8s.io/queue-name: research
    spec:
      parallelism: 2
      completions: 2
      template:
        spec:
          containers:
          - name: trainer
            resources:
              limits:
                nvidia.com/gpu: 4

    Kueue suspends the job until it is admitted. kubectl get workloads -n research shows why it is waiting.

Before you call it done
  • Every GPU namespace submits through a LocalQueue, and GPU pods without a queue are blocked by policy.
  • Team quotas add up to the GPUs you actually have.
  • Teams are in one cohort so idle GPUs get lent, and they know borrowed work can be preempted.
  • Training jobs checkpoint, so preemption costs minutes, not days.
  • waitForPodsReady is on, so a half-placed job gets requeued instead of holding GPUs.
  • Your dashboards show quota used and borrowed per team, next to GPU utilisation.
Deadlock it, then fix it

Open GPU Queue Lab, submit the two training jobs in plain Kubernetes, and watch the idle GPU-seconds climb. Then switch to Kueue and try the Platform job.

Open the demo →

The series so far: monitoring, logging, node lifecycle, sharing and this post on scheduling. Next: what all of this costs.

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading