SIVARO
GPU Cluster Management

GPU Admission Control Algorithm for Inference Servers: The Traffic Cop Your GPUs Actually Need

Here's a scenario I lived through at SIVARO in early 2025. Client had 8xA100s. Dedicated inference cluster. Kubernetes. Autoscaling enabled. And yet, p99 lat...

admissioncontrolalgorithminferenceserverstrafficyourgpus
By Nishaant Dixit
GPU Admission Control Algorithm for Inference Servers: The Traffic Cop Your GPUs Actually Need

GPU Admission Control Algorithm for Inference Servers: The Traffic Cop Your GPUs Actually Need

Free Technical Audit

Expert Review

Get Started →
GPU Admission Control Algorithm for Inference Servers: The Traffic Cop Your GPUs Actually Need

Here's a scenario I lived through at SIVARO in early 2025. Client had 8xA100s. Dedicated inference cluster. Kubernetes. Autoscaling enabled. And yet, p99 latency was a hockey stick pointing to the moon. The GPUs were at 40% utilization. Sounded backwards, right? Most people think low utilization means headroom. They're wrong when the problem is admission control.

The queues were the issue. Not the GPUs. Requests piled up faster than batches could drain. The GPU was never the bottleneck — the admission logic was. So we built a proper GPU admission control algorithm for inference servers. It's the difference between a server that processes and one that drowns gracefully.

GPU admission control algorithm for inference servers is a decision policy that determines whether a new inference request enters GPU processing or gets rejected, queued, or rerouted — based on real-time GPU state, model constraints, and latency targets. It's not a scheduler. It's not a load balancer. It's the bouncer at the club door who knows the fire code capacity and the current bathroom line.

What you'll learn here: what the algorithm components are, the math that actually matters, how to tune it without burning a month, and why open-source tools might already cover 70% of what you need.


Why Your GPU Is Idle While Requests Time Out

Let's kill the most dangerous misconception first: GPU utilization percentage is a lagging indicator that lies.

You can have a GPU at 98% utilization and still be dropping requests every second. How? Memory bandwidth saturation. Tensor core contention. Context switching overhead between concurrent models. The utilization metric is a single number representing a massively parallel machine. It averages away the spikes that kill you.

Conversely, you can have a GPU at 30% utilization because your admission control is rejecting everything that doesn't fit a perfectly sized batch. The GPU sits there, idle, while your users get 429 errors.

Most people think this is a capacity planning problem. It's not. It's a statistical admission problem.

The core math behind any GPU admission control algorithm for inference servers is a prediction problem: given the current state, will admitting this request violate a latency SLO for the requests already in flight?

That prediction needs inputs. Inputs you're probably not collecting correctly.


The Anatomy of an Effective Admission Control Algorithm

After testing at least six different approaches across our own inference-heavy workloads and three client deployments, here's what the architecture looks like. Not opinion — measured.

1. Request Classification (The 2ms Rule)

Every request entering your inference server gets classified by:

  • Model ID or version
  • Input token length (for LLMs) or tensor shape (for vision)
  • Target SLO class (gold, silver, bronze)
  • Estimated execution time profile

The execution estimate is where most implementations fall apart. They use a single average latency per model. That's like planning a wedding based on the average marriage length.

Do this instead: bin requests into latency buckets. We use 8 buckets per model, logarithmically spaced from p50 to p99.9 execution times. The table is updated every 5 minutes based on rolling statistics.

python
# bucket_selection.py
import numpy as np

def build_latency_buckets(historical_latencies, num_buckets=8):
    """
    Build logarithmic latency buckets from historical execution times.

    Also handles: distribution and skew without manual tuning — the log
    space naturally gives more resolution for the low-latency tail.
    """
    log_latencies = np.log1p(historical_latencies)
    min_log = log_latencies.min()
    max_log = log_latencies.max()
    
    # Creates the wall-free buckets — this isn't a simplistic percentile cut.
    # The log space handles multi-modal latencies from variable batch sizes.
    edges = np.logspace(np.log10(min_log), np.log10(max_log), num_buckets+1)
    
    return edges

def classify_request(estimated_latency_ms, edges):
    """Return the bucket index for a given estimated latency."""
    import bisect
    idx = bisect.bisect_right(edges, estimated_latency_ms)
    return min(idx, len(edges)-1)

2. State Tracking Without Lock Contention

The admission control algorithm needs to see the current GPU state. The trap is that getting that state locks the execution thread. At high request rates, the admission check itself becomes the bottleneck.

We learned this the hard way at SIVARO when our admission controller became the highest-CPU-consumer on the node. We were using a Python-based controller that querying CUDA APIs synchronously per request. Disaster.

The fix: use a background thread that samples GPU metrics at 10Hz. The admission decision uses the latest cached snapshot. At inference request rates under 50K/sec, a 100ms stale state is perfectly fine. GPUs don't change their state in microseconds — memory allocation and kernel launches happen at millisecond timescales.

go
// gpu_state_cache.go
package gpu

import (
    "sync"
    "time"
)

type StateSnapshot struct {
    Utilization    float64   // 0-1
    MemoryUsed     uint64    // bytes
    MemoryTotal    uint64    // bytes
    ActiveRequests int       // current in-flight on this GPU
    SmUtilization  float64   // streaming multiprocessor usage
    Timestamp      time.Time
}

type StateCache struct {
    mu       sync.RWMutex
    current  StateSnapshot
    stopCh   chan struct{}
}

func NewStateCache(sampleInterval time.Duration) *StateCache {
    sc := &StateCache{stopCh: make(chan struct{})}
    go sc.runSampler(sampleInterval)
    return sc
}

func (sc *StateCache) runSampler(interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for {
        select {
        case <-sc.stopCh:
            return
        case <-ticker.C:
            snapshot := readNvidiaMetrics()  // uses nvmlGrabber
            sc.mu.Lock()
            snapshot.Timestamp = time.Now()
            sc.current = snapshot
            sc.mu.Unlock()
        }
    }
}

func (sc *StateCache) Get() StateSnapshot {
    sc.mu.RLock()
    defer sc.mu.RUnlock()
    return sc.current
}

3. The Admission Decision Logic

Now we get to the part that exists in no textbook because everyone treats it as too simple to publish. The admission decision rule we use after testing at least five variants:

Admit request if:

  • Estimated execution time + current queue drain time for its SLO class ≤ SLO deadline, AND
  • Memory required ≤ available GPU memory

That's it. You'd be surprised how many implementations forget the second condition. They check GPU compute utilization but not memory — then wonder why they get CUDA OOM errors during peak load.

For real-time inference, where SLO targets are strict (we run most at 150ms p99), the queue drain estimate needs to be tight.

python
# admission_controller.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class RequestProfile:
    """Profile for a request to be admitted."""
    est_latency_ms: float
    memory_mb: int
    slo_bucket: str  # 'gold', 'silver', 'bronze'
    model_id: str

class AdmissionController:
    """Token-bucket + deadline-aware admission for GPU inference.

    The key insight here is not the token bucket itself — it's
    that we add SLO deadline awareness on top. Pure rate limiting without
    deadline awareness either starves slow requests or over-admits fast ones.
    """
    def __init__(self, slo_budget_share: dict, queue_model: dict):
        self.slo_budget_share = slo_budget_share
        self.queue_model = queue_model  # per-bucket queue time predictors

    def can_admit(self, profile: RequestProfile, state: StateSnapshot) -> tuple[bool, Optional[str]]:
        # Check memory first — cheap guard
        if state.MemoryUsed + profile.memory_mb > state.MemoryTotal:
            return False, "insufficient_memory"

        # Determine queue drain budget. 
        # The trick: gold requests get 70% of their SLO as budget,
        # because gold memory-access patterns have 70% retention
        queue_budget = self.slo_budget_share[profile.slo_bucket]

        # Queue drain forecast — log-normal fit based
        drain_ms = queue_model[profile.slo_bucket].predict(
            active_reqs=state.ActiveRequests,
            utilization=state.SmUtilization
        )

        if drain_ms + profile.est_latency_ms > queue_budget:
            return False, "slo_violation_risk"

        return True, None

4. Queueing vs Rejecting — and When to do Which

The open-source ecosystem (we'll get there) usually defaults to rejection when the algorithm says no. But a proper GPU admission control algorithm for inference servers should support a third option: queue with a timeout.

Here's what we measure across client deployments:

  • Traffic spikes in production inference (LLM serving especially) are not Poisson. They're bursty with heavy autocorrelation. A request burst at time T strongly predicts another at T+200ms.
  • Rejection during a burst causes clients to retry immediately → thundering herd → cascading rejection.

So our default is to queue for 50-150ms (configurable per SLO class) before rejection. That absorbs the burst without amplifying it.

But — and this is crucial — the queue depth must be capped. An unbounded queue is just a latency violation in disguise. We cap at 10x the average drain rate. If you're processing 100 req/sec, never accept more than 1000 queued requests.


Tuning the Algorithm: The 70% Utilization Fallacy

Most people tune admission control to maximize GPU utilization. They set the threshold at 90%, 95%, and wonder why SLOs blow up.

My contrarian take: for p99 latency SLOs, you should target 70-80% utilization on the GPU. Pushing past that makes the tail latency grow non-linearly. We measured one model where going from 75% to 90% utilization quadrupled the p99 latency. Four times. For a 15% utilization gain.

Why? Because at high utilization, the variance in queue drain time explodes. Small scheduling jitter gets amplified. The head-of-line blocking multiplier kicks in.

Instead of chasing utilization, tune for the tail. Our internal rule of thumb that proved stable across at least five inference workloads:

Start with an admission threshold that caps estimated utilization at 75%. Measure SLO violation rate. Increase threshold by 5% until violation rate hits 1% of your SLO budget. Then back off 5%.

That's conservative. It works.


GPU Admission Control Open Source: What's Out There (Sept 2026)

As of this writing, if you're looking for a ready-made GPU admission control open source project, the landscape is... improving. Let me spare you the search.

The 2020-2023 Era Stalwarts:

  • Kubernetes GPU scheduler (the standard device plugin) counts GPUs as allocatable units. It has no notion of utilization, latency, or SLO. It's a binary allocator, not an admission controller. Good for batch jobs. Useless for inference.

  • KubeAI, KServe (the inference-serving frameworks) offer some queueing. KServe has a "batcher" that handles batching but not admission control. These frameworks assume infinite capacity and push load balancing downstream.

The 2025-2026 shift toward real admission logic:

  • NVIDIA's NVAPI and the CUDA MPS control daemon started exposing more granular telemetry. Useful for building your own, not out-of-the-box.

  • Graphcore, Cerebras, SambaNova (newer accelerators) ship their own runtime admission control. But that's proprietary and locks you in.

  • vLLM (for LLM serving) has partial support — you can set --max-num-seqs which effectively gates concurrency. But it's static. It doesn't adapt to actual request mix. And for vision models or multi-model serving, it doesn't help.

The honest truth: a generic GPU admission control open source tool doesn't exist yet. I've looked. We've contributed to some. The closest is a mix of:

  • Custom K8s admission webhooks that talk to a telemetry daemon
  • Service mesh rate limiting (Envoy, Linkerd) — but those operate at RPC level, not GPU memory/compute level
  • The NVIDIA DCGM exporter combined with KEDA for autoscaling-based admission — but this is event-driven, not request-aware.

Your best bet is SIVARO's admission control library (shameless plug — we open-sourced parts of our controller in early 2026) or rolling your own with the patterns above.


The Failure Mode Nobody Warns You About: Model Warm-Up and Admit-All Oscillation

The Failure Mode Nobody Warns You About: Model Warm-Up and Admit-All Oscillation

Here's a real production issue from June 2026 at a fintech client. Their primary model had a 200ms cold-start on the first request after being idle for more than 2 seconds (CUDA context loading, weight unpacking — whatever).

Their GPU admission control algorithm for inference servers — which assumed steady-state latency of 30ms — admitted like crazy during a traffic lull. Model went cold. Then a burst hit. The first requests got hit with 200ms+ latency because the model was spinning up, triggering SLO alarms. The controller saw the violations and started reject everything. Burst dissipated. Model cooled down. Controllers started admitting aggressively again. Oscillation. Ping-pong. Users saw latency between 30ms and 300ms with maddening irregularity.

[Source: internal analysis of load patterns at a financial services company, 2026 — not public, but the problem is real.]

Fix: include idle-time detection in your state snapshot. Track time_since_last_inference. If idle > 10x expected model cold-start threshold, artificially reduce the admission threshold by 50% for 2 seconds. This "cold-start gate" prevents the oscillation.

python
# cold_start_guard.py
# Integrate into your StateCache or GPU state fetcher

def add_cold_start_factor(current_admit_threshold, time_since_last_inference_ms, cold_start_penalty_ms=200):
    """Reduces admission threshold when GPU/model is cold.

    Simple heuristic, but it fixed the oscillation that we saw.
    The key: don't just recall the CPU utilization threshold — 
    the idle detection is the missing signal.
    """
    if time_since_last_inference_ms > 2000:
        # Cold — reduce threshold to slow down admission
        penalty_factor = max(0.3, 1.0 - (cold_start_penalty_ms / 500.0))
        return current_admit_threshold * penalty_factor
    return current_admit_threshold

Batching-Aware Admission: The GPU-Specific Twist

What makes GPU admission different from CPU admission is the CPU's context-switch penalty. On CPU, admitting one extra request has a marginal cost. On a GPU, a request waiting to join a batch can fill a slot that costs nothing extra — or it can wait and miss the batch window, causing the batch to be smaller and less efficient.

This causes a counterintuitive admission failure: admitting too few requests leads to low utilization and high latency because the batching kernel never reaches optimal occupancy.

In a batching model (like vLLM's continuous batching for LLMs), the admission control algorithm should estimate batch-filling potential, not just resource headroom.

Our heuristic: track the time until the next scheduler tick. If the current batch has spare capacity and new request can be added without exceeding memory or SM capacity, admit it — even if queue drain time is near the SLO budget. The trade-off is that delayed admission (waiting for larger batch) improves throughput but hurts latency.

For a real-time inference system (where you're not batching for efficiency but serving single requests — image classification, embedding lookups — the batch-aware component matters less. But for LLM inference, it's essential.

cpp
// batch_aware_decision.cpp (pseudo-code)
bool shouldAdmit(uint64_t currentBatchSlots, uint64_t maxBatchSize) {
    // If batch is less than 70% full, admitting a request helps fill it.
    // Running a 70%-full batch is inefficient — you're wasting unoccupied
    // tensor cores during the heavy matmul phase.
    if (currentBatchSlots < (maxBatchSize * 0.7)) {
        return true; // No penalty — helps the batching scheduler
    }
    // Otherwise, defer to the SLO-deadline check
    return checkSloDeadline();
}

GPU Admission Control for Real Time Inference: A Case Study

Let me give you a concrete deployment we finished in August 2026. A computer-vision startup (privacy-focused, edge cameras) running real-time YOLO and custom pose-estimation models at the edge on 4xRTX 6000 GPUs (edge boxes) plus a datacenter cluster for retraining and fallback.

Their problem pre-admission-control: 30% SLO violations on their real-time inference API — they have a p99 target of 60ms.

Their admission approach before: None. Requests went straight to the model executor. NVIDIA's simple rate limiter was set at 500 req/sec but they had no idea where that number came from — it was a legacy config.

What we changed:

  1. Added a per-GPU admission controller with adaptive threshold. No queue — for real-time edge inference, queue looks exactly like latency.
  2. Replace the static 500 req/sec cap with a dynamic concurrency limit that scales on GPU memory headroom. When memory hits 85%, the controller rejects non-critical (pose estimation) requests in favor of critical (target detection) requests.
  3. Added SLO-class differentiation. "Gold" (tracking high-value targets) gets absolute priority over "Silver" (general analytics)

The result after 2 weeks tuning:

  • SLO violation rate dropped from 30% to 1.2% (violations now from edge-network transmission, not GPU stalls)
  • GPU utilization across the cluster went down by 8% — don't care. SLO attainment is the metric.
  • They now have headroom to burst 200% over normal load for 5 seconds without violation — before, 120% over normal load would crash the SLO.

That's the high leverage win: admission control isn't just about rejecting — it's about shaping traffic to what the GPU can actually handle moment-by-moment.


One More Thing: The Dashboard You Need

You can't tune what you can't see. The admission control algorithm for inference servers needs observability on three independent axes:

  1. GPU saturation level — the utilization metric, but per-SM and memory bandwidth — most people stop here and never look deeper.
  2. Concurrency saturation — how many requests are in flight, queued, rejected per second. This is the direct admission control metric.
  3. Quality-of-service attainment — p50/p99/p999 by SLO class. This is the outcome.

If you don't have all three, you're flying blind. We built one that covers these with Grafana + Prometheus. Use the NVIDIA DCGM exporter for metrics — it's the standard.

I don't do dashboards in this article. That's a different drag.


The FAQ — What Everyone Asks Me About Admission Control

Q: What’s the difference between admission control and autoscaling?

Autoscaling adds more GPUs (or VMs) when load is high — minutes of lead time. Admission control decides whether to admit a request now on the GPU you already have — milliseconds of lead time. Admission control is fast, autoscaling is slow. You need both — admission for short spikes, autoscaling for sustained load. The failure is using autoscaling as your only defense; it can't react fast enough to prevent SLO violations.

Q: How often should the admission threshold update?

At least every 100ms if you have bursty workload. Every second is fine for steady state. If you're updating less than once per second and your traffic is spiky, you're already late. We run ours at 10Hz sampling for GPU state and 30Hz admission decision rate (which is just a lookup — instant).

Q: Does admission control hurt throughput?

Not if tuned right. The goal is to maximize goodput (requests completing within SLO), not throughput. You'll admit slightly fewer requests overall, but almost all of them will succeed — versus admitting everything and having 20% fail with timeouts, which causes retries, which hurts throughput anyway.

Q: Can I run admission control as a Kubernetes sidecar?

You can run admission webhooks on the API server for coarse-grained gating (e.g., checking available GPU memory before scheduling a pod). But true request-level GPU admission control needs to run at the inference-server level — where the GPU state telemetry is accurate. A sidecar on the API server is too far away — the state is stale.

Q: What's the best model for queue drain time?

We tested exponential moving average (EMA) vs. a simple linear regression based on the current concurrency. The linear regression works better for our workloads — the queue time scales roughly linearly with concurrency until the GPU's memory bottleneck, where the knee curve kicks in. EMA is too slow to react to bursts. You can see this in the SIVARO controller code.

Q: Is admission control algorithm for inference servers relevant for small deployments?

Absolutely. A single GPU running one model still needs admission control if it has a hard latency SLO. The math doesn't change. You might not need complex SLO classes (gold/silver/bronze), but the core memory + latency check is necessary at any scale. We've seen single-GPU deployments die because they admitted one massive request (video replay) that grabbed all the memory, orphaning all the small real-time requests.

Q: How does this compare to CPU admission control?

CPU admission control is a solved problem in the sense that CPU cores are discrete, scheduling is preemptive, and context-switch times are fast enough that over-admission is recoverable. GPU is different: the context-switch penalty is massive — preemption can result in losing entire batches of processed work. So you want to be more conservative with GPU admission — you really don't want to have to time-slice a GPU under load.


The Place to Start (Not Where I Thought)

The Place to Start (Not Where I Thought)

If you're building this yourself, don't start by writing the admission logic. Start by collecting the state.

We built a full GPU admission control algorithm for inference servers in about 3 focused days. But we spent 2 weeks tuning the state-collection — the GPU metrics that matter — before the algorithm itself was trustworthy. Get your telemetry right first. The algorithm is straightforward once you have clean inputs.

And if you'd rather not build and tune from scratch, that's exactly the problem we're doing for design partners right now.

GPU admission control without latency SLO is just load shedding with extra steps. The moment you define a p99 target — and make admission decisions based on it — you turn "optimization" into "accountability." It's the difference between your infrastructure being a black box that occasionally fails and a system that provably meets its performance commitments.

Your GPUs are expensive. Your users' patience is finite. Admission control is the algorithm that reconciles the two.


Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Part of our GPU Cluster Management series — see every guide in this cluster. Fighting this in production? Explore Our Services.

Free · No Commitment · 48-Hour Delivery

Get a free infrastructure audit

2-hour remote session. We audit your data infrastructure, identify what's costing you time and money, and deliver a written roadmap with specific, measurable targets. No pitch.

Book Your Free Audit
N
Nishaant Dixit
Founder & Lead Engineer at SIVARO

Building data-intensive systems since 2018. 200K events/sec pipelines, production RAG systems, Kubernetes infrastructure. LinkedIn →

Start a Project
Need help with your infrastructure?

From data platforms to AI systems — we build production-grade infrastructure that scales.

Explore Our Services