Serve ML Models with GPU on Kubernetes

Khimananda Oli 8 min read Virtualization
Serve ML Models with GPU on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

Deploying machine learning workloads requires more than just containerization; you need hardware-aware orchestration to avoid burning budget on idle accelerators. When you serve ML models with GPU on Kubernetes, the cluster must correctly expose hardware resources, manage proprietary drivers, and scale pods based on inference queue depth rather than CPU utilization. This guide covers the production-grade architecture required to run LLMs and computer vision models reliably, drawing from patterns I use daily across AWS EKS and on-premise clusters.

NVIDIA GPU OperatorDrivers • Runtime • ToolkitDevice Plugin DaemonSetInference PodvLLM / TGI Containerrequests: nvidia.com/gpu: 1KEDA ScalerPrometheus AdapterQueue Depth MetricKubernetes Node (GPU Instance)Container RuntimekubeletNVIDIA GPUCUDA • TensorRTModel Cache (PV)
Core infrastructure stack required to serve ML models with GPU on Kubernetes: operator-managed drivers, dedicated inference pods, and metric-driven autoscaling.

How do you configure Kubernetes nodes to expose GPU resources?

Kubernetes does not detect GPUs natively. Without additional configuration, your pods will fail to schedule with Insufficient nvidia.com/gpu errors even if the underlying EC2 or bare-metal instance has four A10Gs installed. The industry standard in 2026 is the NVIDIA GPU Operator, which automates the entire stack lifecycle including drivers, container runtime, device plugin, and monitoring exporters.

Installing the NVIDIA GPU Operator via Helm

Avoid manual driver installation on nodes. Manual installs break during kernel upgrades and create drift between instances. The operator runs as a DaemonSet and handles node labeling, driver compatibility checks, and MIG (Multi-Instance GPU) partitioning automatically.

helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

helm install --wait --generate-name \
  -n gpu-operator --create-namespace \
  nvidia/gpu-operator \
  --set driver.enabled=true \
  --set toolkit.enabled=true \
  --set dcgmExporter.enabled=true

After installation, verify that nodes are advertising GPU capacity. This confirmation step is critical before deploying any inference workload.

kubectl get nodes -o custom-columns=\
NAME:.metadata.name,\
GPU:.status.allocatable.nvidia\.com/gpu,\
READY:.status.conditions[-1].type

# Expected output:
# NAME                     GPU   READY
# ip-10-0-1-45.ec2         4     Ready
# ip-10-0-1-78.ec2         4     Ready

If you operate in Nepal or regions with restricted internet connectivity, pre-pull all operator images into a private registry like ECR or Harbor before installation. The operator attempts to pull ~2GB of driver containers on first launch, which can timeout on slower links. Configure driver.repository and toolkit.repository values in your Helm release to point to your local mirror. For teams managing multiple clusters, see my guide on managing multiple Kubernetes clusters with Rancher to apply GPU operator configurations consistently across environments.

Which inference server should you deploy for LLMs and vision models?

Serving frameworks matter more than raw GPU specs. A naive Flask wrapper around PyTorch will leave 70% of your accelerator idle while waiting for Python GIL locks. In production, you need continuous batching, PagedAttention memory management, and speculative decoding support.

FeaturevLLMTGI (Text Generation Inference)Triton Inference Server
Best ForLLM throughput & chat APIsHugging Face ecosystem integrationMulti-model ensembles & non-LLM
Continuous BatchingYes (PagedAttention)YesYes (dynamic batching)
Quantization SupportAWQ, GPTQ, FP8, GGUFAWQ, GPTQ, bitsandbytesTensorRT-LLM, ONNX, OpenVINO
OpenAI-Compatible APINativeVia adapterVia Triton backend
Startup Time (7B model)~45 seconds~60 seconds~90 seconds
Memory EfficiencyExcellent (KV cache paging)GoodVariable (backend dependent)

Deploying vLLM with Correct Resource Boundaries

Always set both requests and limits to identical values for GPU resources. Unlike CPU, GPUs cannot be oversubscribed safely in most inference scenarios. Fractional GPUs require MIG or time-slicing configuration in the operator.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llama3-8b-instruct
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llama3-8b
  template:
    metadata:
      labels:
        app: llama3-8b
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:v0.8.5
        args:
          - "--model"
          - "meta-llama/Meta-Llama-3-8B-Instruct"
          - "--tensor-parallel-size"
          - "1"
          - "--max-model-len"
          - "4096"
          - "--gpu-memory-utilization"
          - "0.90"
        ports:
        - containerPort: 8000
        resources:
          requests:
            nvidia.com/gpu: 1
            memory: "24Gi"
            cpu: "4"
          limits:
            nvidia.com/gpu: 1
            memory: "24Gi"
            cpu: "4"
        env:
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-token
              key: token
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 60
          periodSeconds: 10

Note the --gpu-memory-utilization flag set to 0.90. Never set this to 1.0. Leave headroom for CUDA context allocation and unexpected KV cache spikes during long-context requests. Setting it too high causes silent OOM kills that manifest as intermittent 500 errors under load, not immediate crashes. Understanding these resource boundaries ties directly into Kubernetes resource limits and requests best practices.

Client AppPOST /v1/chatIngress / LBTLS TerminationPod Replica 1GPU 0 • ActiveBatch Size: 12Pod Replica 2GPU 1 • ActiveBatch Size: 8Model RegistryS3 / PVC Cache
Inference request routing: clients hit an ingress controller that distributes traffic across GPU-backed pods sharing a cached model layer.

How do you autoscale GPU workloads based on inference demand?

Standard Horizontal Pod Autoscaler (HPA) uses CPU or memory metrics. These are useless for GPU inference. A vLLM pod can sit at 5% CPU utilization while its GPU is completely saturated processing a batch of 32 concurrent requests. Conversely, CPU might spike during tokenization while the GPU sits idle. You need queue-aware scaling.

Configuring KEDA for Queue-Depth Scaling

KEDA (Kubernetes Event-Driven Autoscaling) polls Prometheus metrics exposed by your inference server and adjusts replica counts accordingly. vLLM exposes vllm:num_requests_waiting and vllm:gpu_cache_usage_perc natively on the /metrics endpoint.

  1. Install KEDA and the Prometheus adapter in your cluster.
  2. Create a ScaledObject targeting your inference deployment.
  3. Set scale-up thresholds based on acceptable latency SLOs, not arbitrary percentages.
  4. Configure cooldown periods to prevent oscillation during traffic bursts.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: llama3-scaler
spec:
  scaleTargetRef:
    name: llama3-8b-instruct
  minReplicaCount: 1
  maxReplicaCount: 8
  pollingInterval: 10
  cooldownPeriod: 300
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus.monitoring:9090
      metricName: gpu_queue_depth
      query: avg(vllm:num_requests_waiting{app="llama3-8b"})
      threshold: "4"
      activationThreshold: "1"

The cooldownPeriod: 300 is deliberate. GPU pods take 45–90 seconds to start and load weights. Aggressive scale-down causes thrashing where pods terminate before finishing warm-up, then immediately rescale. Five minutes of cool-down absorbs transient dips without sacrificing responsiveness to sustained load. For deeper coverage on defining meaningful thresholds, read defining meaningful SLIs and SLOs for ML services.

What are common pitfalls when running GPU workloads in production?

I have debugged dozens of GPU deployments across AWS, Azure, and on-prem data centers. The same three issues appear repeatedly regardless of cloud provider or model size.

  • Cold start latency dominates user experience. Loading a 70B parameter model takes 3–5 minutes from S3. Pre-warm pods using init containers that fetch weights to a shared PersistentVolume backed by NVMe storage. Use Longhorn distributed storage or AWS FSx for Lustre to achieve sub-second weight mounting across nodes.
  • Driver version mismatches after node rotation. When cluster autoscaler adds new GPU nodes, they may pull different driver versions than existing ones. Pin driver versions explicitly in the GPU Operator Helm values (driver.version: "550.54.15") rather than relying on latest tags.
  • Ignoring DCGM exporter metrics. Without DCGM, you are flying blind. GPU utilization, memory bandwidth, SM occupancy, and power draw are only visible through DCGM. Install the exporter as part of the operator and build Grafana dashboards before your first production incident. Reference the Prometheus and Grafana monitoring stack guide for dashboard templates adapted for GPU metrics.

Security also demands attention. GPU workloads often process sensitive data. Apply Kubernetes network policies to restrict egress from inference pods. Models should only communicate with the model registry and observability backends. Block all other outbound traffic to prevent data exfiltration through compromised inference endpoints.

GPU Utilization: Naive vs Optimized Serving0%25%50%75%100%Naive Flask~30% AvgvLLM + KEDA~85% AvgTGI Batched~75% AvgServing Framework Comparison (Llama-3-8B, 32 Concurrent Requests)
Optimized serving frameworks achieve 2–3× higher GPU utilization compared to naive wrappers, directly reducing cost per token when you serve ML models with GPU on Kubernetes.

Production Checklist for GPU-Accelerated Inference

Before promoting any GPU workload to production, verify these operational requirements. Skipping any single item has caused incidents in environments I have audited.

  • Persistent model caching: Weights stored on ephemeral disk cause 3+ minute cold starts on every pod restart. Use shared PVs with ReadWriteMany access.
  • Explicit tolerations: GPU nodes often carry taints (nvidia.com/gpu=present:NoSchedule). Ensure deployments include matching tolerations or scheduling fails silently.
  • Graceful shutdown hooks: Set terminationGracePeriodSeconds: 120. In-flight generations need time to complete. Default 30s kills active requests mid-stream.
  • Cost allocation tags: Tag GPU node groups with project/team identifiers. Untagged GPU spend becomes untraceable within weeks.
  • Disaster recovery: GPU instances have limited availability zones. Define fallback regions or instance types in your Cluster Autoscaler configuration.

Next Steps for Your GPU Infrastructure

When you serve ML models with GPU on Kubernetes successfully, the infrastructure fades into the background and teams focus on model quality instead of firefighting scheduling errors. Start with the NVIDIA GPU Operator and vLLM as your baseline, instrument everything with DCGM metrics, and implement KEDA scaling before your first traffic spike hits. If your team needs help designing audit-ready GPU infrastructure or optimizing inference costs across multi-cloud environments, reach out to discuss your specific architecture.

Frequently Asked Questions

Kubernetes 1.32 or later is recommended for stable GPU serving. Earlier versions lack improved device plugin stability and newer NVIDIA driver compatibility needed for production ML workloads.

Deploy the official NVIDIA device plugin via Helm using the gpu-operator chart. This automatically configures device plugins, drivers, and container runtime dependencies across all GPU nodes without manual intervention.

Yes, use NVIDIA Multi-Instance GPU (MIG) or time-slicing via the device plugin config. MIG partitions A100/H100 GPUs into isolated instances, while time-slicing allows concurrent pod access with performance trade-offs.

Request exactly one nvidia.com/gpu per pod unless using MIG. Always pair GPU requests with matching CPU and memory limits to prevent scheduling failures and ensure predictable inference latency under load.

KEDA scales based on custom metrics like queue depth or request latency rather than GPU utilization alone. Configure ScaledObjects targeting your inference service metrics to trigger scale-outs before saturation occurs.

Check node labels match your pod nodeSelector, verify the NVIDIA device plugin is running, and confirm sufficient allocatable GPUs exist. Use kubectl describe pod to identify specific scheduling constraints or taints blocking placement.

KServe provides standardized inference APIs and autoscaling but adds complexity. Triton offers superior multi-framework optimization and batching. Many teams deploy Triton as the runtime backend within KServe for combined benefits.

Deploy DCGM Exporter alongside the GPU operator to expose metrics to Prometheus. Track dcgm_gpu_utilization, memory usage, and temperature via Grafana dashboards to detect bottlenecks and right-size inference deployments.

Use high-throughput NVMe-backed storage classes like AWS EFS or GCP Filestore for shared model caches. Local NVMe with hostPath provides lowest latency but requires careful pod affinity rules for weight locality.

Pre-pull model weights using init containers or daemonsets, enable GPU warm-up hooks in your serving framework, and maintain minimum replica counts during peak hours to avoid repeated initialization overhead.

Only for batch or fault-tolerant workloads. Spot interruptions cause dropped requests during model loading. Use on-demand or reserved instances for real-time serving, reserving spot capacity for training or offline evaluation tasks.

Store weights in encrypted object storage with short-lived IAM credentials injected via workload identity. Never embed secrets in container images. Use network policies to restrict pod-to-storage access to authorized inference namespaces only.

Yes, vLLM integrates cleanly via standard GPU resource requests and supports PagedAttention for efficient memory use. Deploy with tensor parallelism flags matching your GPU count for optimal LLM throughput on H100 or A100 hardware.

Model weights plus KV cache exceed allocated GPU memory. Reduce max sequence length, enable quantization, or increase GPU memory requests. Monitor DCGM memory metrics to correlate failures with actual allocation patterns.

Use locust or vegeta against your service endpoint while monitoring DCGM metrics. Measure tokens-per-second, p99 latency, and GPU utilization simultaneously to validate scaling behavior and identify resource contention issues.