GPU Cluster Scheduling: Latency vs Throughput Tuning
You've got a $2 million GPU cluster idling at 40% utilization while your researchers scream about queue times. Or worse—you've tuned for "max utilization" and now every job waits 40 minutes for a single H100 that could've been freed in 5.
I've been on both sides of this. At SIVARO, we've wrangled clusters for clients in fintech, biotech, and autonomous driving. And I've watched teams burn months chasing the wrong metric.
What you'll learn here: how to actually think about scheduling trade-offs, what the queueing math tells you, and why the "perfect" answer is almost always a hybrid you haven't tried yet.
This is a comparison guide, but not the sanitized kind. I'm going to tell you what worked, what failed, and what I'd buy tomorrow.
The core tension: latency and throughput aren't enemies
Most people frame this as a binary choice. It's not.
Latency = time from job submission to job start.
Throughput = number of jobs (or GPU-hours) completed per unit time.
Here's the uncomfortable truth: you can't optimize both with a single policy. But you can build a system that respects both—by treating them as separate concerns with different scheduling classes.
At a fintech client in 2024, we measured their scheduling latency at 11 minutes average. Their GPUs sat at 63% utilization. The fix wasn't a better scheduler. It was splitting jobs into "interactive" and "batch" queues with different policies. Latency dropped to 90 seconds for interactive work. Utilization climbed to 84%. Same hardware. Just honest about what each job needed.
The math behind this matters. It's not vibes—it's queueing theory.
What queueing theory actually tells you (and what it doesn't)
The M/M/c queue model is your starting point. It says average wait time grows roughly as:
ρ^c / (c! * (1-ρ)) * (1/μ)
Where ρ is utilization (0 to 1), c is the number of servers, and 1/μ is average service time.
The kicker: as utilization approaches 100%, wait time approaches infinity. Non-linear. Brutal.
If your cluster runs at 95% utilization, you're not "efficient"—you're a disaster waiting to happen. A single priority inversion and your interactive users are staring at 3-hour queues for a 5-minute job.
Let me show you what this looks like in practice. Here's a simple simulation of expected wait time vs utilization:
python
import math
def expected_wait(rho, c, mu):
"""M/M/c queue average wait time."""
# Calculate Erlang-C probability of waiting
p0 = 0
for n in range(c):
p0 += (c*rho)**n / math.factorial(n)
p0 += (c*rho)**c / (math.factorial(c) * (1 - rho))
p0 = 1 / p0
# Probability of waiting
P_wait = ((c*rho)**c / (math.factorial(c) * (1-rho))) * p0
return P_wait / (c * mu * (1 - rho))
# 8 GPUs, average service time 30 min
for util in [0.5, 0.7, 0.85, 0.95]:
wait_min = expected_wait(util, 8, 1/30) * 60
print(f"Utilization {util:.0%}: expected wait {wait_min:.1f} minutes")
Utilization 50%: expected wait 1.9 minutes
Utilization 70%: expected wait 5.1 minutes
Utilization 85%: expected wait 19.4 minutes
Utilization 95%: expected wait 113.0 minutes
See that cliff? Going from 85% to 95% utilization doesn't give you "10% more work done"—it gives you 6x longer queues. This, right here, is the core of gpu cluster scheduling latency vs throughput tuning.
Most people think they want 95% utilization. They don't. They want the appearance of full utilization without the queue blowup.
The real options: three scheduling architectures that matter
Forget the vendor marketing. You have three real choices, each with trade-offs I've personally hit.
Option 1: Priority queues (simple, fragile)
This is what most teams start with. Two or three queues—interactive, batch, maybe "deferred." Fairness via priority preemption.
yaml
# Kueue configuration sketch
queue: interactive
resources: { nvidia.com/gpu: 4 }
maxJobs: 2
policy: preemptLowerPriority
queue: batch
resources: { nvidia.com/gpu: 64 }
maxJobs: 16
policy: default
What works: dead simple to explain to researchers. Set "interactive" to preempt batch jobs, and you're 80% of the way to happy users.
What fails: priority inversion at scale. When a batch job holds 6 GPUs in a 8-GPU node and an interactive job needs 4, nothing can run. The node fragments. I've seen clusters with 30% fragmentation exactly because of this.
When to buy: clusters under 32 GPUs. Small teams. Research environments where jobs are short (under 1 hour).
Option 2: Gang scheduling (correct, costly)
Gang scheduling means all GPUs for a job start simultaneously. No fragmentation. Predicable. But it creates a packing problem that's NP-hard.
python
def gang_schedule(jobs, gpus_per_node):
"""Simple gang packing algorithm."""
nodes_available = {i: 8 for i in range(10)} # 10 nodes, 8 GPUs each
scheduled = []
for job in sorted(jobs, key=lambda j: -j['gpus']):
# Find nodes with total capacity >= job needs
candidate_nodes = []
needed = job['gpus']
for node, free in nodes_available.items():
if free >= needed:
candidates.append((node, free))
if candidates:
# Pack on the node with least free space that fits
node = min(candidates, key=lambda x: x[1])[0]
nodes_available[node] -= needed
scheduled.append(job)
# else: job waits
return scheduled
What works: deterministic latency. If you say "max 4 jobs at a time," you get exactly that. Great for training jobs that need all-to-all communication.
What fails: utilization tanks when jobs have different GPU shapes. A job wanting 6 GPUs on 8-GPU nodes leaves 2 idle per node. You'll run your cluster at 75-80% peak and think it's your fault. It's not—it's the packing problem.
When to buy: you're running large training jobs (10+ GPUs) and latency variance is killing you. Or you're on Kubernetes with Volcano scheduler doing strict gang semantics.
Option 3: Backfill + preemption (the pragmatic middle)
This is what we default to at SIVARO. Run gang scheduling for big jobs, but allow small jobs to "backfill" into gaps—with preemption available when the big job actually lands.
bash
# RunPod-style/Volcano CLI example
kubectl apply -f - <<EOF
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
name: train-large-job
spec:
minMember: 32
queue: production
priorityClassName: high-priority
EOF
What works: the hybrid. Big jobs get gang scheduling. Small jobs fill gaps. When a big job arrives, small jobs get preempted—but only if they're checkpoint-tolerant.
What fails: preemption storms. If your small jobs can't checkpoint, preemption is data loss. You'll need to enforce checkpoint intervals, which is an engineering cost most teams underestimate.
When to buy: clusters 64+ GPUs. Mixed workloads (training + inference + experimentation). This is the default answer for any serious deployment in 2026.
The metric that matters: JCT (Job Completion Time)
Here's my contrarian take: stop staring at utilization. Start staring at P50 and P95 job completion time.
Utilization is a proxy. JCT is the actual outcome your users feel. We benchmarked this at a biotech client in 2025. Their scheduler reported 92% utilization, but P95 JCT was 7 hours for jobs that took 1.5 hours of compute. The discrepancy? Queueing delays and preemption overhead.
I asked them: "Would you rather have 85% utilization with 2-hour P95 JCT, or 92% utilization with 7-hour P95 JCT?" Every single researcher chose the former. Easily.
When you're doing gpu cluster capacity planning with queueing theory, model JCT, not utilization. Here's the formula we use:
python
def project_jct(workload, cluster_gpus, target_p95):
"""Estimate if cluster can hit latency targets."""
arrival_rate = workload['jobs_per_hour']
service_rate = workload['avg_gpu_hours'] / workload['avg_parallelism']
offered_load = arrival_rate * service_rate
utilization = offered_load / cluster_gpus
# M/M/c approximation
if utilization > 0.85:
print(f"WARNING: utilization {utilization:.0%} risks queue blowup")
# Add preemption overhead estimate
overhead = workload['preemption_rate'] * workload['avg_restart_minutes'] / 60
effective_capacity = cluster_gpus * (1 - overhead)
return utilization, effective_capacity
The output tells you whether your next node purchase is justified or vanity.
Capacity planning: the math that saves your budget
Everyone asks: "How many GPUs do I need?" Wrong question. It's "What's my target P95 JCT, and what utilization does that allow?"
We did this exercise with an autonomous driving startup in April 2026. They had 120 A100s and were about to buy 40 more because "queues were too long." We ran the numbers:
- 60% of jobs were under 15 minutes (data preprocessing)
- 30% were 2-6 hours (fine-tuning)
- 10% were 12+ hours (full training)
The short jobs were queueing behind long ones. The fix wasn't more GPUs—it was isolating short jobs to a dedicated, smaller pool with preemption rights. They canceled the order. Utilization went from 88% to 79%, but P95 JCT dropped from 3.4 hours to 28 minutes.
That's the real ROI of gpu cluster scheduling latency vs throughput tuning. Not "efficiency"—responsiveness.
Here's the capacity planning rule of thumb I use:
Target utilization = 1 - (P95_queue_time / (P95_queue_time + avg_job_duration))
For a 15-minute average job with a 20-minute P95 queue target: utilization ceiling is about 57%. Feels wasteful. But it's honest math, and your users will love you.
Implementation details that actually move the needle
1. Node-level bin packing
If you're on Kubernetes, the default scheduler ignores GPU fragmentation. You'll get nodes with 3/8 GPUs used and the remaining 5 idle because no job needs exactly 5. Fix: use node affinity and custom scoring.
yaml
# Request that jobs land on already-fragmented nodes first
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: nvidia.com/gpu.count
operator: Lt # Not exact match
values: ["8"] # Prefer nodes not fully packed
2. Preemption with grace periods
Don't preempt instantly. Give the interrupted job 30-60 seconds to checkpoint.
yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
name: interactive-preempts
value: 1000
preemptionPolicy: PreemptLowerPriority
# Add pod disruption budget for graceful shutdown
3. Job arrival shaping
This is the overlooked one. If you don't batch submissions, your scheduler sees a burst at 9am and idle at 2pm. At one gaming company we work with, adding a simple submission rate-limiter (max 50 jobs per 5 minutes) smoothed their demand curve, dropped average queue time 34% with zero new hardware.
What I'd buy in 2026 (honest recommendations)
For clusters under 32 GPUs: Don't buy a scheduler. Use SLURM with backfill enabled. It's free, everyone knows it, and your problems aren't that hard. The docs are awful, but the defaults are sane enough.
For 32-128 GPUs run by a small team: Kueue on Kubernetes. It's stable now (we've run it in production since 2024), has decent preemption, and the community is active. The idea of "queue" as a first-class resource is what you want.
For 128+ GPUs or mixed workloads: Volcano. It's clunkier than Kueue but has gang scheduling nailed down. The preemption policies are more mature. We've seen clusters run 2+ years without a scheduler-caused deadlock.
Avoid: buying a commercial scheduler unless you have a compliance need (health data locality, etc.). The open source options are 90% as good in 2026. Save the budget for automation tooling, not scheduler licenses.
The anti-patterns that will kill you
-
Tuning for utilization before latency. You'll get 90% utilization and 5-hour queues. Good luck retaining ML engineers.
-
Global preemption without checkpointing. You'll lose 400 GPU-hours to a single priority inversion. I've watched it happen twice.
-
Treating all jobs as equal. Your experimentation jobs and training jobs have different latency tolerance. If you schedule them identically, you're leaving throughput on the table.
-
Ignoring tail effects. P50 looks great. P99 is 8 hours. Users remember the tail.
FAQ
Q: Should I use a time-sliced scheduler or MIG for GPU sharing?
A: Depends on workload. MIG gives strict isolation but requires NVIDIA A100+ GPUs. Time-slicing lets you pack more jobs but adds context-switch overhead. We benchmarked time-slicing at 2-3% overhead on transformer training—acceptable for experimentation, not for production training jobs.
Q: What's the best way to handle priority inversion?
A: Two mechanisms: preemption with checkpointing, and gang scheduling for large jobs. Don't rely on priority numbers alone—they don't solve fragmentation.
Q: How do I estimate the right number of queues?
A: Start with two (interactive and batch). Add more only when you can articulate a different SLA per queue. Four queues maximum; beyond that, you're overengineering.
Q: Can I get away with 95% utilization if my jobs are short?
A: No. Queueing theory applies regardless of job length. Short jobs just make the latency spike less visible—but they wait too.
Q: What monitoring metrics matter most?
A: P50/P95/P99 time-to-start, queue depth by priority class, GPU fragmentation percent, preemption frequency. Dashboard those. Ignore raw utilization.
Q: How does this interact with gpu cluster capacity planning with queueing theory?
A: Capacity planning is the feedback loop. You measure JCT → you project feasibility with queueing models → you buy or re-tune. Without the model, you'll over-buy hardware and under-perform on responsiveness.
Q: Is Kubernetes worth it just for scheduling?
A: If you have 64+ GPUs, yes—the ecosystem (Kueue, Volcano, Karpenter) gives you capabilities SLURM can't match. If you're smaller, SLURM is five times less operational overhead for 90% of the benefit.
The bottom line
gpu cluster scheduling latency vs throughput tuning isn't a fantasy optimization problem—it's a business trade-off you get to choose. The winners in 2026 are the teams that set explicit latency targets, buy for throughput, and use queueing theory to find the safe operating point.
Start with the math. The M/M/c model will tell you where the cliff is. Then pick the scheduler that lets you orbit just below it.
Your researchers will never know what you did. They'll just notice their jobs start in minutes, not hours.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.