Moving AI workloads from development notebooks to production environments changes the infrastructure equation. Traditional web applications are generally I/O bound, spending most of their lifecycle waiting for database queries or network calls. AI workloads are different. They are compute-bound, memory-bound, and highly sensitive to hardware latency.
If you run large language models (LLMs) or diffusion models using the same patterns as traditional HTTP endpoints, your cluster will face runaway costs—often because teams fail to budget for the real cost structure of AI agents—GPU starvation, and cascading timeouts. As teams shift to engineering for the agentic era, managing these pipelines at scale requires restructuring your compute, storage, and networking layers.
The Compute Bottleneck: GPU Scheduling and Resource Allocation
GPUs are scarce resources. Leaving them idle is a waste of capital. Kubernetes is the standard for orchestration, but its default scheduler treats GPUs as binary resources. A container gets one GPU or zero.
This approach fails for mixed workloads. A lightweight embedding model does not need an entire physical GPU. It needs a fraction of it.
To share GPU resources, you have three primary options:
First, time-slicing. Multiple containers share the GPU by taking turns. The driver switches context between them. This is simple to set up but introduces latency spikes. If one container starts a heavy computation, other containers must wait.
Second, Multi-Instance GPU (MIG). This technology splits a single physical GPU into independent instances at the hardware level. Each instance has its own dedicated memory and compute cores. If one instance runs out of memory, it does not crash the others. This provides clean hardware-level isolation.
Third, virtual GPUs (vGPU). This is software-based sharing. It allows flexible allocations, but it lacks strict memory isolation. A bad query on one container can degrade performance for everyone else on the same physical card.
For production APIs with strict SLAs, MIG is the safest choice.
Here is a Kubernetes pod spec using fractional GPU resources via the NVIDIA device plugin:
apiVersion: v1
kind: Pod
metadata:
name: embedding-service
spec:
containers:
- name: predictor
image: embedding-model:v1
resources:
limits:
nvidia.com/mig-1g.10gb: 1This requests a specific MIG profile instead of the whole GPU.
Handling GPU Starvation and Queuing
When traffic spikes, you cannot spin up ten more GPU nodes instantly. Cold starts for GPU instances take minutes. You need a queue.
Instead of letting HTTP requests pile up at the API gateway, route them to a message broker like RabbitMQ or Redis. The worker pool pulls jobs from the queue based on GPU availability.
If your workers process jobs faster than the queue grows, the system remains stable. If the queue builds up, you trigger autoscaling. You scale the queue consumers, not the HTTP endpoints.
This pattern decouples client requests from GPU execution. If a GPU node dies, the request remains in the queue. The client gets a pending status instead of a gateway timeout.
Mitigating the Storage Bottleneck
GPUs process data at extreme speeds. If your storage layer cannot feed data to the GPU fast enough, the processor sits idle. This state is called GPU starvation. It is a common cause of wasted cloud spend.
Standard cloud object storage is too slow for direct access within the execution loop. Downloading a 50GB dataset or model checkpoint over the network at runtime introduces massive latency.
Implement a multi-tiered caching strategy. Use fast local NVMe drives attached to the GPU instances as scratch space. Before starting a training job or serving a model, run a pre-start script to pull the weights or data blocks from object storage to the local NVMe disk.
If you scale across multiple nodes, look at distributed file systems like JuiceFS or SeaweedFS. These platforms cache metadata and frequently accessed data blocks on local SSDs while using object storage as the source of truth.
Here is an initialization script for a container to pull model weights locally before starting the main process:
#!/usr/bin/env bash
set -euo pipefail
WEIGHTS_DIR="/mnt/nvme/models/llama-3-8b"
S3_URI="s3://my-model-registry/llama-3-8b"
if [ ! -d "$WEIGHTS_DIR" ]; then
echo "Local cache miss. Downloading weights from storage bucket..."
mkdir -p "$WEIGHTS_DIR"
aws s3 sync "`S3_URI" "`WEIGHTS_DIR" -quiet
else
echo "Local cache hit. Model weights are ready."
fi
exec python3 -m vllm.entrypoints.openai.api_server -model "$WEIGHTS_DIR"This script ensures the application server only starts once the weights are locally cached, avoiding runtime network latency.
LLM Serving: Prefill, Decode, and Dynamic Batching
Serving LLMs requires a different approach than traditional REST APIs. An LLM request has two distinct phases.
The prefill phase processes the input prompt. This is compute-bound. The engine processes all input tokens in parallel.
The decode phase generates the output tokens. This is memory-bandwidth bound. The engine generates one token at a time. To generate token N, the engine must load the entire model weights and the key-value (KV) cache of all previous tokens into SRAM.
Because of this, processing requests one by one is highly inefficient. The GPU spends most of its time waiting for memory transfers.
To solve this, use dynamic batching. Instead of waiting for a batch to fill, the inference engine groups incoming requests on the fly. If Request A is mid-generation and Request B arrives, the engine inserts Request B into the next iteration of the decode loop. This is known as continuous batching or iteration-level scheduling.
Engines like vLLM, TensorRT-LLM, and Triton implement this pattern.
Dynamic batching increases the time-to-first-token (TTFT) for incoming requests if the queue is full. You must balance throughput and latency. Set a maximum batch size and a timeout threshold to prevent requests from waiting too long in the scheduler.
Model Quantization and VRAM Optimization
Model size directly dictates your infrastructure requirements. A 70B parameter model stored in FP16 (16-bit floating-point) requires roughly 140 GB of VRAM just to load the weights. Add the KV cache for concurrent users, and you need multiple high-end GPUs just to run a single replica.
Quantization reduces the precision of the model weights, shrinking the memory footprint. Common formats include INT8, INT4, AWQ, and GPTQ.
Quantizing a model to 4-bit precision reduces the memory requirement by nearly 75%. The 70B model now fits into a single 40GB or 48GB GPU. This reduction lowers your hardware costs.
Quantization can degrade model accuracy slightly. Test your models against evaluation benchmarks after quantization to ensure the performance drop is acceptable for your use case.
Event-Driven Autoscaling with KEDA
Traditional autoscalers use CPU or memory utilization to scale instances. This does not work for GPU workloads. A GPU running inference will show high VRAM usage even when idle because the runtime reserves memory beforehand. CPU usage remains low because the heavy lifting happens on the GPU cores.
To scale accurately, you must monitor queue depth. If requests are piling up in your message broker, you need more workers.
Use Kubernetes Event-driven Autoscaling (KEDA) to scale your deployments based on external metrics. Here is a KEDA ScaledObject configuration that scales a GPU worker pool based on the number of pending tasks in a Redis queue:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: gpu-inference-scaler
namespace: ai-workloads
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gpu-worker
minReplicaCount: 1
maxReplicaCount: 10
cooldownPeriod: 300
triggers:
- type: redis
metadata:
address: redis-master.default.svc.cluster.local:6379
listName: inference_jobs
listLength: "5"This configuration adds worker pods when the number of jobs in the inference_jobs list exceeds 5 per active pod. The cooldownPeriod is set to 300 seconds (5 minutes) to prevent thrashing, where nodes are repeatedly created and destroyed due to brief traffic spikes.
Observability: Metrics that Matter
Monitoring AI infrastructure requires looking beyond basic system metrics. You must collect metrics from the GPU driver level and the inference engine itself.
Key infrastructure metrics:
GPU Compute Utilization: The percentage of time the GPU kernels were active.VRAM Allocation: The amount of memory reserved vs. actually used.PCIe Bandwidth: The data transfer rate between the CPU host and the GPU. High PCIe utilization indicates that data transfers are bottlenecking your pipeline.
Key application metrics:
Time to First Token (TTFT): The latency between sending the request and receiving the first generated token. This measures how fast your system handles the prefill phase.Time Per Output Token (TPOT): The average time to generate each subsequent token. This measures the efficiency of your decode phase.KV Cache Usage: The percentage of memory allocated for storing token history. If this hits 100%, the engine will pause requests or drop them.
Use the NVIDIA DCGM Exporter to scrape GPU metrics and expose them to Prometheus. Combine these with engine-level metrics to build a complete dashboard.
Multi-Node Networking: Infiniband and RoCE
When scaling large training runs or running massive models that do not fit on a single node, you must distribute the workload across multiple servers. This setup requires high-speed communication between GPUs on different nodes.
Standard Gigabit Ethernet is too slow and introduces too much latency. You need specialized networking hardware.
Infiniband is the industry standard for low-latency, high-throughput clustering. It bypasses the host operating system's network stack using Remote Direct Memory Access (RDMA), allowing GPUs to read and write directly to the memory of remote GPUs.
RDMA over Converged Ethernet (RoCE) is an alternative that runs RDMA over standard Ethernet infrastructure. Setting up RoCE requires configuring your network switches to support lossless Ethernet, which is complex but cheaper than deploying dedicated Infiniband hardware.
Without these protocols, multi-node scaling efficiency drops rapidly. The GPUs spend more time waiting for network packets than running computations.
Cost Containment and Spot Instance Lifecycle Management
Running GPU clusters 24/7 is financially draining. To keep costs manageable, you must implement strict lifecycle policies.
Use spot instances (or preemptible VMs) for non-critical workloads like batch processing, model training, and offline evaluation. Spot instances offer discounts up to 80% compared to on-demand pricing.
But they can be reclaimed by the cloud provider at any time, often with only a 30-second to 2-minute warning. To handle this, your applications must be resilient to sudden termination.
For training jobs, implement automated checkpointing. Save the model state to shared storage or object storage at regular intervals.
Use a node termination handler in your Kubernetes cluster. This agent listens for the cloud provider's termination signal and immediately drains the node, allowing the scheduler to reschedule the pod on a warm node before the hard shutdown occurs.
For serving workloads, you cannot rely entirely on spot instances due to latency and availability risks. Instead, use a hybrid node pool. Keep a baseline of on-demand instances to handle minimum traffic, and scale up using spot instances during peak hours. If the spot instances are reclaimed, the on-demand instances keep the service running while the system provisions new nodes.



