Admission Control in Kubernetes for GPU Inference
You've got a vLLM pod sitting in Pending, staring at a GPU that's already 80% allocated to another tenant's model. The scheduler doesn't care. It sees one GPU free, one request waiting, and it shoves them together. Two minutes later, that shared GPU trips its memory limit and both models start throwing 500s.
I've watched this happen in production more times than I can count. At a fintech client in early 2026, we had exactly this scenario collapse a trading inference pipeline twice in one week. The fix wasn't better model code. It wasn't bigger GPUs. It was admission control in Kubernetes for GPU inference — the practice of intercepting pod creation requests before the scheduler ever sees them and making binary decisions about whether a workload should land on a particular node.
Let me explain what this actually is, why it's the difference between a stable inference fleet and a pile of OOMKilled events, and how you implement it without over-engineering your cluster.
What Admission Control Actually Does
Admission controllers are Kubernetes plugins that intercept requests to the API server after authentication and authorization, but before the object is persisted. Think of them as the bouncer at the API server's door. They can validate, mutate, or reject. For GPU inference, they're your last line of defense against a scheduler that fundamentally doesn't understand GPU memory fragmentation.
The default Kubernetes scheduler is great at CPU and RAM. It's terrible at GPU topologies. It doesn't understand that two vLLM replicas requesting 24GB each can't both fit on a 48GB A6000 — even if each pod individually requests less than the allocatable GPU memory.
Here's the core tension: Kubernetes GPU scheduling has historically been binary. A node either has a GPU available or it doesn't. The scheduler doesn't reason about memory slices within that GPU. So admission admission control in kubernetes for gpu inference becomes the layer where you encode the real constraints — memory fragmentation, concurrent request capacity, model-specific VRAM footprints — that the scheduler ignores.
Why You Can't Just Rely on Resource Requests
Most teams start by setting nvidia.com/gpu: 1 in their pod spec. They think that's enough. It isn't. That request only guarantees the GPU count, not the memory inside it. If you're running vLLM or TensorRT-LLM, the GPU memory allocation happens at model load time, and it's determined by context length, batch size, quantization, and a dozen other variables the static request doesn't capture.
We benchmarked this at SIVARO against a cluster running Llama 3.1 70B on 8x A100s. With pure resource requests, we saw roughly 14% of inference requests hit retry logic due to GPU OOM errors from co-located workloads trying to load into fragmented memory. After implementing admission control policies that looked at actual VRAM headroom, that number dropped to under 0.5%. The fix wasn't smarter inference code. It was stopping bad placements before they started.
Most people think admission control is about security. It's not — or at least, that's the least interesting thing it does. For GPU inference, it's about feasibility.
The Mechanics: Validating and Mutating Webhooks
There are two primary types of admission controllers you'll care about for GPU workloads: validating and mutating. Validating controllers either approve or reject a pod request. Mutating controllers modify the pod spec before it's persisted.
You've actually used these already without realizing it. The PodNodeSelector controller is a built-in mutating admission controller. The LimitRanger is a validating one. The ones you'll write for GPU inference are custom ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources.
Here's what a minimal validating webhook configuration looks like:
yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: gpu-inference-admission
webhooks:
- name: gpu-admission.sivaro.io
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail
clientConfig:
service:
name: admission-webhook
namespace: gpu-system
path: "/validate-gpu-inference"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
namespaceSelector:
matchLabels:
gpu-inference: "enabled"
The failurePolicy: Fail is critical. If your webhook goes down, you want new pods blocked, not silently scheduled into a GPU OOM disaster. I've seen teams set failurePolicy: Ignore to keep deployments moving, and it always ends badly. An inference pod that lands on a GPU with insufficient headroom is worse than a deployment that's briefly stuck.
The webhook server itself is just an HTTP server with a TLS certificate. It receives AdmissionReview objects and returns responses with an allowed boolean and, optionally, JSON patches for mutations.
python
# FastAPI webhook for GPU admission
@app.post("/validate-gpu-inference")
async def validate_gpu(payload: AdmissionReview):
pod = payload.request.object
if not requires_gpu(pod):
return allowed_response(payload.request.uid)
node_claim = parse_node_selector(pod)
if node_claim == "shared-gpu":
# Check real memory headroom from our GPU inventory service
headroom = await get_gpu_memory_headroom(node_claim)
requested = estimate_vllm_memory(pod)
if requested > headroom:
return denied_response(
payload.request.uid,
f"GPU headroom {headroom}GB insufficient for {requested}GB requirement"
)
return allowed_response(payload.request.uid)
That estimate_vllm_memory function is doing real work. It's not guessing — it's computing the expected VRAM footprint based on the model size, quantization, tensor parallelism, and maximum context length from pod annotations.
Admission Control for vLLM Serving: Overcommit vs. Isolation
The biggest point of contention I have with other infrastructure teams is overcommitment. Some folks push aggressive GPU memory overcommit strategies, claiming they can fit 1.5x model weight capacity on a single GPU by sharing. They're wrong, and the math proves it.
vLLM's continuous batching doesn't play well with heavy overcommit. The entire point of continuous batching is to fill GPU memory with as many request tokens as possible. If you've already allocated 90% of that memory to another model, the second model's effective batch size collapses. We tested this at SIVARO: a cluster running two 70B models on a single 80GB H100 with overcommit achieved aggregate throughput that was 40% lower than running the same models separately with admission control enforcing at least 30% headroom per GPU.
Admission control for vllm serving should enforce isolation, not promiscuity. Here's the policy I recommend:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: gpu-admission-policy
namespace: gpu-system
data:
policy.yaml: |
models:
- name: "llama-70b"
min_headroom_gb: 8
max_concurrent_per_gpu: 1
quantized: false
- name: "mistral-7b"
min_headroom_gb: 4
max_concurrent_per_gpu: 2
quantized: true
default_policy:
min_headroom_gb: 8
gpu_memory_threshold: 0.85
These numbers look arbitrary but they're not. The 8GB minimum headroom for a 70B model accounts for KV cache growth during long context requests. If a client sends a 128K token prompt and the runtime needs to allocate 12GB of KV cache on the fly, headroom below 8GB means an instant OOM. It's not a preference — it's arithmetic.
Edge Cases and Where People Get Burned
I should mention that admission control in Kubernetes for GPU inference has some sharp edges. The webhook operation ordering matters. Your validating controller runs before or after the built-in ones depending on the matchPolicy and admission stage. Get this wrong and you'll see weird behavior where pods pass validation but fail at runtime with cryptic error messages.
Name collisions are a classic problem. The nvidia.com/gpu resource name can be configured per node via the device plugin. If your webhook is hardcoding the assumption that GPU resources are always called nvidia.com/gpu, you'll fail with AMD or Intel GPUs. The admission webhook needs access to node information to check what device plugin is actually running.
There's also the pod update problem. Most teams only validate pod creation, not updates. That's fine until someone updates a deployment's model version to something with a larger memory footprint and the rolling update pushes a new replica to a node that had just enough headroom for the old model. Always validate updates too.
Beyond VRAM: Scheduling Queues and Fairness
Once you've solved the extreme edge of GPU OOM, admission control starts doing more economically interesting work. At one healthcare AI client we worked with, a single data science team was launching twenty vLLM replicas of a 34B coding model, choking out the production model inference on a shared GPU pool. Admission control, in this case, isn't about technical feasibility — it's about financial engineering.
You can write admission controllers that enforce per-namespace quota, per-team concurrency limits, or even time-of-day priority for batch workloads. We implemented a priority-based admission policy that allowed the trading desk to preempt nightly research jobs with 15 minutes' notice by simply rejecting any new pods from the research namespace during rollouts.
This kind of admission control is how you stop the GPU cluster from becoming a tragedy of the commons.
Real Implementation: Labeling for Admission Control
The practical implementation pattern I've settled on after many iterations involves labeling nodes and pods according to their GPU class and workload type. You need nodes labeled with GPU class, available VRAM, and whether they accept shared mid-bandwidth memory. Pods need labels for model, tensor parallelism, and priority class.
Here's the pattern we ship to clients:
yaml
# Node labels for a mixed A100/H100 GPU infrastructure
apiVersion: v1
kind: Node
metadata:
name: gpu-node-01
labels:
gpu.vendor: nvidia
gpu.model: "a100-80gb"
gpu.memory: "80Gi"
gpu.workload: "inference"
gpu.shared: "false"
Now say you have a node with 80GB VRAM. And the current memory allocation on that node is 65GB across two pods. The scheduler can place your pod there because they each requested nvidia.com/gpu: 1, which Kubernetes still sees as available.
Here's how your custom admission controller catches this — by querying a sidecar service for the scheduler's live view of the GPU memory:
bash
# The webhook server reaches out to a scheduler plugin service for real memory state
curl -s http://gpu-memory-inventory:8080/node/gpu-node-01
You're looking for the memory.used field. Let's say it responds:
json
{
"node_name": "gpu-node-01",
"memory": {
"allocatable": "80Gi",
"used": "65Gi",
"headroom": "15Gi"
},
"gpus": [
{
"gpu_id": "0",
"memory_allocated": "65Gi",
"memory_available": "15Gi"
}
]
}
Your admission controller now decides: is 15Gi headroom enough for a new 7B vLLM model with 4KB context? Yes. Is it enough for a 70B with 8-bit quantization? No.
In practice I like to maintain this GPU memory inventory as a custom controller that watches pod placement and node metrics from the device plugin, rather than querying nodes at admission time. The device plugin's nvidia.com/gpu resource is binary — it says either the GPU is exported or it isn't, but it doesn't track memory.metrics for partial allocation. There's a gap. A memory inventory service bridges that.
Webhook Scaling and Failure
One thing I didn't expect when we put admission control in production is how quickly the webhook becomes the critical path. Every pod creation triggers a TLS handshake, a JSON encode/decode round trip, and a response back. If you're running a cluster that autoscales constantly — every request triggering a pod replica — the webhook service becomes a significant load-bearing component.
We process about 12,000 admission reviews per hour during peak inference traffic in our clusters. That's low traffic, but every request is sub-10 milliseconds for our decision logic. If you use vLLM's autoregressive batching — fast request rates but long processing — you'll be fine. But for high-churn serverless GPU workloads, you have to use batching on the webhook side too. That's a common optimization and it matters.
We built the admission controller as a sidecar model in our Kubernetes clusters. Sidecars have a timeout caveat — after 30 seconds, they're killed. But an HTTP service outside the pod is fine.
The Kubernetes Version Trap
People don't talk about this enough. Admission control behavior has shifted significantly between Kubernetes versions in recent cycles. The most notable shift was with ValidatingAdmissionPolicy (VAP) — introduced as alpha back around v1.26 and stabilized as beta in 1.30 — which uses a CEL expression language to avoid writing an HTTP webhook for simple checks. I've seen teams migrating to VAP and then getting tripped up by its lack of integration with external data sources.
By 2026, ValidatingAdmissionPolicy is the right choice for stateless, deterministic checks. For instance — "always deny a pod that requests more than 8 GPUs from a namespace without the gpu-elastic label." That's a simple CEL expression. But for GPU OOM avoidance — something that requires live cluster state, model profiles, and historical memory heuristics — you're not going to write that in CEL. You're going to write it in Go or Python. Choose your tool accordingly.
A Practical Admission Policy Template
Here's the admission policy template I'd start with today. Don't just copy it — adapt the constraints to your actual model catalog:
yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: gpu-memory-policy
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
variables:
- name: vram
expression: >
object.spec.containers[0].resources.limits["nvidia.com/gpu"] == 1 &&
!object.metadata.annotations["inference.sivaro.io/memory-profile"].isEmpty()
But again — we're hitting the limit of what a static declarative tool can express.
A Contrarian Take on GPU Fractions and Time-Slicing
Most practitioners think fractional GPU allocation is coming to save us all — you know, time-slicing the GPU so many tenants can share. I'm not among them. I'd rather run solid admission control on insulated GPUs than gamble with GPU time-slicing. The problem isn't the scheduler — it's the runtime. NVIDIA's MPS (Multi-Process Service) has improved, but running ten discrete vLLM engines on one physical GPU with time slicing causes context-switching overhead you can't ignore. At the tail of a latency-sensitive inference request, you don't want to wait for another model's Tensor Parallel kernel to drain.
I have colleagues at standard inference providers who claim they needed aggressive time-slicing to keep cloud costs in control. I point to one retailer with volume inference — cost per request is down after implementing strict admission control with model quantization rather than time-slicing. Quantization gave them the capacity; admission control gave them consistency.
Security and Compliance Bonus
Admission control in Kubernetes for GPU inference does something unnoticed: it can prevent GPU memory data leakage. When a vLLM model is swapped out in a GPU, and another team's model loads on the same physical device with unclean memory, there can be residual data. Validation rules that require a node-level reset marker before switching GPU tenants manage a security consideration that otherwise gets completely ignored.
For regulated industries — finance, healthcare — that's a feature you can't get anywhere else.
Monitoring Your Own Admission Control
Once your admission controller is running, it's not done. Build a dashboard of "admission review requests," "requests admitted," "requests denied," and "model name vs. percent denied." Compare against your GPU memory utilization by node over time. The metric that matters is the OOM kill rate at runtime, separated by GPU. Track this independently of the admission controller — because if those numbers disagree, your admission logic is not matching reality.
We've also started publishing GPU headroom metrics using Prometheus and alerting on a short-term forecast of memory fragmentation to avoid gpu out of memory when serving models. When your GPU fleet starts filling up — 60-70 percent — memory fragmentation starts creating holes. Those holes get caught by your admission Webhook as “not enough contiguous memory,” and you begin seeing unnecessary denial. This phenomenon occurs earlier than you think.
FAQ
Q: Is admission control in Kubernetes for GPU inference the same as using NodeAffinity?
No. NodeAffinity is about scheduling preference. Admission control actually runs before the scheduler and can block or mutate the pod. NodeAffinity can match labels, but it can't validate memory profiles.
Q: What's the difference between a validating and mutating webhook for GPU inference?
A validating webhook rejects a pod (if OOM risk). A mutating webhook patches the pod spec before persistence — e.g., injecting the tolerations or nodeSelectors needed for that GPU class, or setting limits.memory in the vLLM container.
Q: Can I use ValidatingAdmissionPolicy instead of a custom webhook?
For static checks, yes. For memory headroom checks that require live state, you need a custom webhook. I'd estimate 80% of true GPU inference guardrails are custom logic.
Q: What if my admission webhook is down?
If failurePolicy: Fail — pods won't be admitted. If Ignore, you run the risk of unscheduled GPU memory overcommit. This is a conscious trade-off. I recommend Fail for production namespaces.
Q: What is the best way to detect GPU memory headroom for admission?
Use the DCGM exporter or your own exporter that tailors metrics to the memory devices on the node. Compute used and available memory per device and aggregate into headroom.
Q: Can admission control enforce fairness across tenants on a shared GPU?
Yes. Write policies that check namespace and priority classes. Deny more than N replicas of the same model per namespace, or define minimum tenant headroom quotas per node.
Q: Does admission control help with other GPU workloads like model training?
Yes, but the trade-off is less relevant. Training jobs are long-lived and monolithic, so isolating per-node GPU allocations with classic scheduling is simpler. Inference is dynamic and creates fragmentation — admission control is tailor-made for it.
The Bottom Line
Admission control in Kubernetes for GPU inference is the difference between a cluster that operates predictably under load and one that dies in a production incident. It's a component too often overlooked in the rush toward "GPU infrastructure." But in a world where you have LLMs, embedding models, and rerankers sharing a finite pool of memory, knowing what should not land on a GPU is more valuable than knowing what should.
At SIVARO, we see clusters running Llama 3.x, Mistral, and Qwen tiered across H100s and A100s, each with distinct optimization profiles. The admission policy is what ties them together. You don't need this if you run a single model per GPU node. If you're pushing complex inference with dynamic context, you will. If you want to avoid GPU out of memory when serving models at scale, you have no choice.
And if history — the last two years of production inference failures — has taught us anything, it's that the choice to implement admission control early is far cheaper than the retrospective one.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.