K8s 1.30+ LLM Inference Autoscaling: vLLM Deployment and KEDA Auto-Scaling Deep Guide
Summary
- K8s 1.30+ features like SidecarContainers GA and ReadWriteOncePod PVC provide finer resource control for LLM inference services
- vLLM's PagedAttention + ContinuousBatching is the de facto standard for LLM inference, delivering 3-5x throughput over traditional approaches
- KEDA custom metrics autoscaling (based on request queue depth, GPU utilization, TTFT latency) matches inference load more precisely than HPA CPU metrics
- Three-layer elastic strategy: Pod-level HPA → Deployment-level rolling update → Cluster-level autoscaling
- This article provides a complete solution from vLLM K8s deployment to KEDA autoscaling, with production-grade YAML and performance tuning parameters
Table of Contents
- K8s 1.30+ and LLM Inference: New Synergy Points
- vLLM Inference Engine K8s Deployment
- KEDA Custom Metrics Autoscaling
- GPU Resource Optimization and Multi-Tenant Isolation
- Three-Layer Elastic Strategy and Self-Healing
- Production-Grade Inference Cluster Observability
- Summary and Further Reading
K8s 1.30+ and LLM Inference: New Synergy Points
Kubernetes 1.30 (Ubykah) introduced several features critical for AI inference workloads. SidecarContainers reached GA, meaning log collection and metrics-exporting sidecars no longer block Pod termination, accelerating rolling updates by 60%. The ReadWriteOncePod PVC access mode makes model weight file storage safer and more efficient, preventing write conflicts from multiple Pods mounting the same volume.
┌──────────────────────────────────────────────────────────────────┐
│ K8s 1.30+ LLM Inference Architecture │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Inference Gateway (Nginx/Traefik) │ │
│ │ - Load balancing: Least connections algorithm │ │
│ │ - Rate limiting & circuit breaking: Token bucket │ │
│ │ - A/B routing: Model version canary deployment │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ vLLM Pod (x N) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ vLLM Server│ │ Log Sidecar│ │ Metrics │ │ │
│ │ │ (Main) │ │(1.30 GA) │ │ Sidecar │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ │ GPU: NVIDIA A100/H100 │ │
│ │ Model: RWX Pod PVC mount │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ KEDA Scaler │ │
│ │ - Metrics: Request queue depth / GPU memory / TTFT │ │
│ │ - Min replicas: 2 (HA) │ │
│ │ - Max replicas: 20 (elastic ceiling) │ │
│ │ - Cooldown: 300s (prevent flapping) │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
K8s 1.30+ Key Features Impact on LLM Inference
| K8s Feature | Version | Impact on LLM Inference |
|---|---|---|
| SidecarContainers GA | 1.30 | 60% faster rolling updates; sidecars no longer block termination |
| ReadWriteOncePod PVC | 1.30 | Exclusive model weight file mounting, preventing write conflicts |
| PodReadyToStartContainers | 1.31 | More precise readiness detection, avoiding traffic to uninitialized Pods |
| AppArmor GA | 1.31 | Pod security hardening, preventing model weight leaks |
| Structured Authorization | 1.32 | Fine-grained RBAC for multi-tenant inference clusters |
vLLM Inference Engine K8s Deployment
vLLM Core Optimization Principles
vLLM boosts GPU memory utilization from 30-40% in traditional approaches to over 90% through two core technologies: PagedAttention and ContinuousBatching. PagedAttention borrows the paging mechanism from OS virtual memory, managing KV Cache in blocks to eliminate memory fragmentation. ContinuousBatching schedules at the request level, allowing new requests to join execution without waiting for the current batch to complete.
┌──────────────────────────────────────────────────────────────┐
│ vLLM PagedAttention + ContinuousBatching │
│ │
│ Traditional (Static Batching): │
│ ┌────┬────┬────┬────┐ │
│ │ R1 │ R2 │ R3 │ R4 │ Wait for longest request to finish │
│ └────┴────┴────┴────┘ │
│ ████████████████████ GPU idle waiting │
│ │
│ vLLM (Continuous Batching): │
│ ┌────┬────┬────┬────┐ │
│ │ R1 │ R2 │ R3 │ R4 │ Insert R5 immediately after R1 done │
│ └────┴────┴────┴────┘ │
│ ┌────┬────┬────┬────┐ │
│ │ R5 │ R2 │ R3 │ R4 │ Insert R6 immediately after R2 done │
│ └────┴────┴────┴────┘ │
│ ████████████████ GPU consistently high utilization │
│ │
│ PagedAttention KV Cache: │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │P0 │P1 │P2 │P3 │P4 │P5 │P6 │P7 │ On-demand physical blocks│
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ R1→[P0,P1,P2] R2→[P3,P4] R3→[P5,P6,P7] │
│ No fragmentation, 90%+ memory utilization │
└──────────────────────────────────────────────────────────────┘
vLLM K8s Deployment YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen2-72b
namespace: llm-inference
labels:
app: vllm
model: qwen2-72b-instruct
spec:
replicas: 2
selector:
matchLabels:
app: vllm
model: qwen2-72b-instruct
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: vllm
model: qwen2-72b-instruct
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 120
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: vllm
model: qwen2-72b-instruct
topologyKey: kubernetes.io/hostname
containers:
- name: vllm-server
image: vllm/vllm-openai:v0.6.0
command:
- python
- -m
- vllm.entrypoints.openai.api_server
args:
- --model
- /models/Qwen2.5-72B-Instruct
- --tensor-parallel-size
- "2"
- --max-model-len
- "32768"
- --gpu-memory-utilization
- "0.92"
- --max-num-seqs
- "256"
- --enable-prefix-caching
- --enable-chunked-prefill
- --port
- "8000"
ports:
- containerPort: 8000
resources:
requests:
nvidia.com/gpu: "2"
cpu: "8"
memory: 32Gi
limits:
nvidia.com/gpu: "2"
cpu: "16"
memory: 64Gi
env:
- name: MODEL_NAME
value: "qwen2-72b-instruct"
- name: VLLM_WORKER_MULTIPROC_METHOD
value: "ray"
volumeMounts:
- name: model-storage
mountPath: /models
- name: shm
mountPath: /dev/shm
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 180
periodSeconds: 15
failureThreshold: 5
- name: metrics-exporter
image: prometheus-exporter/vllm-metrics:v1.0
ports:
- containerPort: 9090
resources:
requests:
cpu: "100m"
memory: 128Mi
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: model-weights-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: 16Gi
Model Weights PVC (ReadWriteOncePod)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-weights-pvc
namespace: llm-inference
spec:
accessModes:
- ReadWriteOncePod
storageClassName: local-ssd
resources:
requests:
storage: 200Gi
vLLM Performance Tuning Parameters
| Parameter | Default | Recommended | Description |
|---|---|---|---|
--gpu-memory-utilization |
0.9 | 0.92 | GPU memory utilization cap |
--max-model-len |
Model default | 32768 | Max sequence length, affects KV Cache size |
--max-num-seqs |
256 | 256 | Max concurrent sequences |
--enable-prefix-caching |
false | true | Prefix caching, reuse KV Cache for same prompts |
--enable-chunked-prefill |
false | true | Chunked prefill, reduces first-token latency |
--swap-space |
4GB | 8GB | CPU swap space for long sequence overflow |
KEDA Custom Metrics Autoscaling
Why HPA Is Not Enough
K8s native HPA scales based on CPU/memory utilization, but LLM inference is GPU-intensive — CPU utilization cannot reflect actual load. An inference Pod might show 30% CPU while GPU memory is full and the request queue is backlogged. KEDA (Kubernetes Event-Driven Autoscaling) supports custom metric scaling based on vLLM-exposed metrics like request queue depth, GPU utilization, and TTFT (Time To First Token).
KEDA ScaledObject
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: vllm-scaler
namespace: llm-inference
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-qwen2-72b
minReplicaCount: 2
maxReplicaCount: 20
cooldownPeriod: 300
pollingInterval: 15
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 600
policies:
- type: Pods
value: 1
periodSeconds: 120
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 3
periodSeconds: 60
- type: Percent
value: 50
periodSeconds: 60
selectPolicy: Max
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus:9090
metricName: vllm_num_requests_waiting
threshold: "10"
query: |
avg_over_time(vllm_num_requests_waiting{model="qwen2-72b-instruct"}[1m])
- type: prometheus
metadata:
serverAddress: http://prometheus:9090
metricName: vllm_gpu_cache_usage_perc
threshold: "80"
query: |
avg_over_time(vllm_gpu_cache_usage_perc{model="qwen2-72b-instruct"}[2m])
- type: prometheus
metadata:
serverAddress: http://prometheus:9090
metricName: vllm_e2e_request_latency_seconds
threshold: "5"
query: |
histogram_quantile(0.95, sum(rate(vllm_e2e_request_latency_seconds_bucket{model="qwen2-72b-instruct"}[2m])) by (le))
Multi-Metric Scaling Strategy
┌──────────────────────────────────────────────────────────────┐
│ KEDA Multi-Metric Scaling Decision Flow │
│ │
│ Metric 1: Request Queue Depth │
│ vllm_num_requests_waiting > 10 → Scale up │
│ │
│ Metric 2: GPU KV Cache Utilization │
│ vllm_gpu_cache_usage_perc > 80% → Scale up │
│ │
│ Metric 3: P95 End-to-End Latency │
│ vllm_e2e_request_latency_seconds > 5s → Scale up │
│ │
│ Decision: Scale up if ANY metric triggers; scale down only │
│ when ALL metrics are normal │
│ Cooldown: Scale up 30s / Scale down 600s (prevent flapping) │
│ Step: Scale up max(3 Pods, 50%) / Scale down 1 Pod/2min │
└──────────────────────────────────────────────────────────────┘
GPU Resource Optimization and Multi-Tenant Isolation
NVIDIA MIG Partitioning
Use MIG (Multi-Instance GPU) on A100/H100 to partition a single GPU into multiple instances for multi-tenant inference isolation.
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaim
metadata:
name: mig-instance
namespace: llm-inference
spec:
devices:
- name: gpu
driver: nvidia.com/gpu
requests:
- name: mig-instance
deviceClassName: mig.nvidia.com/1g.10gb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-small-model
namespace: llm-inference
spec:
replicas: 4
selector:
matchLabels:
app: vllm-small
template:
spec:
containers:
- name: vllm
image: vllm/vllm-openai:v0.6.0
args:
- --model
- /models/Qwen2.5-7B-Instruct
- --gpu-memory-utilization
- "0.90"
- --max-model-len
- "8192"
resources:
claims:
- name: mig-instance
resourceClaims:
- name: mig-instance
claimName: mig-instance
GPU Time-Slicing
For low-priority inference tasks, use GPU time-slicing to share a single GPU across multiple Pods.
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-batch-inference
namespace: llm-inference
spec:
replicas: 2
selector:
matchLabels:
app: vllm-batch
template:
spec:
containers:
- name: vllm
image: vllm/vllm-openai:v0.6.0
args:
- --model
- /models/Qwen2.5-7B-Instruct
- --gpu-memory-utilization
- "0.45"
resources:
requests:
nvidia.com/gpu: "1"
limits:
nvidia.com/gpu: "1"
env:
- name: NVIDIA_VISIBLE_DEVICES
value: "0"
- name: CUDA_MPS_PIPE_DIRECTORY
value: "/tmp/nvidia-mps"
- name: CUDA_MPS_LOG_DIRECTORY
value: "/tmp/nvidia-log"
Multi-Tenant GPU Resource Quotas
apiVersion: v1
kind: ResourceQuota
metadata:
name: gpu-quota-team-a
namespace: team-a-inference
spec:
hard:
requests.nvidia.com/gpu: "8"
limits.nvidia.com/gpu: "8"
requests.cpu: "32"
limits.cpu: "64"
requests.memory: 128Gi
limits.memory: 256Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: gpu-quota-team-b
namespace: team-b-inference
spec:
hard:
requests.nvidia.com/gpu: "4"
limits.nvidia.com/gpu: "4"
Three-Layer Elastic Strategy and Self-Healing
Three-Layer Elastic Architecture
┌──────────────────────────────────────────────────────────────┐
│ Three-Layer Elastic Strategy Architecture │
│ │
│ Layer 1: Pod-Level (KEDA HPA) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Custom metrics-based Pod replica scaling │ │
│ │ Response time: 30-60s │ │
│ │ Granularity: Single Deployment │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ Still insufficient │
│ Layer 2: Deployment-Level (Rolling Update) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Model version switch, parameter adjustment │ │
│ │ Response time: 5-15min │ │
│ │ Granularity: Single model service │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ Cluster-level │
│ Layer 3: Cluster-Level (Cluster Autoscaler) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Node pool scaling, adding GPU nodes │ │
│ │ Response time: 3-10min (including GPU driver init) │ │
│ │ Granularity: Entire cluster │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Self-Healing: Pod Anomaly Detection and Recovery
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen2-72b
namespace: llm-inference
spec:
replicas: 2
selector:
matchLabels:
app: vllm
template:
spec:
containers:
- name: vllm-server
image: vllm/vllm-openai:v0.6.0
startupProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 180
periodSeconds: 15
failureThreshold: 5
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: vllm-pdb
namespace: llm-inference
spec:
minAvailable: 1
selector:
matchLabels:
app: vllm
model: qwen2-72b-instruct
Inference Service Warmup Strategy
import httpx
import asyncio
import logging
logger = logging.getLogger(__name__)
class InferenceWarmer:
def __init__(self, base_url: str, model: str, warmup_prompts: list[str]):
self.base_url = base_url
self.model = model
self.warmup_prompts = warmup_prompts
async def warmup(self, rounds: int = 3):
for round_num in range(rounds):
tasks = [self._send_request(prompt) for prompt in self.warmup_prompts]
results = await asyncio.gather(*tasks, return_exceptions=True)
success_count = sum(1 for r in results if not isinstance(r, Exception))
logger.info(f"Warmup round {round_num + 1}: {success_count}/{len(tasks)} succeeded")
async def _send_request(self, prompt: str) -> dict:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{self.base_url}/v1/completions",
json={
"model": self.model,
"prompt": prompt,
"max_tokens": 32,
"temperature": 0.0,
},
)
return response.json()
async def warmup_new_pod(pod_ip: str, model: str):
warmer = InferenceWarmer(
base_url=f"http://{pod_ip}:8000",
model=model,
warmup_prompts=[
"Hello, how are you?",
"What is the capital of France?",
"Explain quantum computing briefly.",
],
)
await warmer.warmup(rounds=3)
Production-Grade Inference Cluster Observability
vLLM Prometheus Alerts
apiVersion: v1
kind: ConfigMap
metadata:
name: vllm-alerts
namespace: llm-inference
data:
alerts.yml: |
groups:
- name: vllm.rules
rules:
- alert: VLLMHighQueueDepth
expr: vllm_num_requests_waiting > 20
for: 2m
labels:
severity: warning
annotations:
summary: "vLLM request queue depth is high"
description: "Queue depth {{ $value }} exceeds threshold 20"
- alert: VLLMHighTTFT
expr: histogram_quantile(0.95, rate(vllm_time_to_first_token_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "vLLM TTFT P95 is high"
description: "TTFT P95 {{ $value }}s exceeds 2s threshold"
- alert: VLLMGPUOOM
expr: vllm_gpu_cache_usage_perc > 0.95
for: 1m
labels:
severity: critical
annotations:
summary: "vLLM GPU cache near OOM"
description: "GPU cache usage {{ $value }}% exceeds 95%"
- alert: VLLMHighErrorRate
expr: rate(vllm_request_errors_total[5m]) / rate(vllm_request_total[5m]) > 0.05
for: 3m
labels:
severity: critical
annotations:
summary: "vLLM error rate is high"
description: "Error rate {{ $value | humanizePercentage }} exceeds 5%"
Grafana Dashboard Key Panels
| Panel | Metric | Description |
|---|---|---|
| Request Throughput | rate(vllm_request_total[1m]) |
Completed requests per second |
| Queue Depth | vllm_num_requests_waiting |
Pending requests |
| TTFT P95 | histogram_quantile(0.95, rate(vllm_time_to_first_token_seconds_bucket[5m])) |
First-token latency |
| TPOT P95 | histogram_quantile(0.95, rate(vllm_time_per_output_token_seconds_bucket[5m])) |
Per-token latency |
| GPU Cache Utilization | vllm_gpu_cache_usage_perc |
KV Cache memory usage |
| Running Sequences | vllm_num_requests_running |
Currently executing sequences |
| Error Rate | rate(vllm_request_errors_total[5m]) / rate(vllm_request_total[5m]) |
Request failure rate |
Inference Performance Benchmarks (Qwen2.5-72B, 2xA100)
| Scenario | Concurrency | TTFT P50 | TTFT P95 | TPOT | Throughput (tokens/s) |
|---|---|---|---|---|---|
| Short (128 in, 256 out) | 16 | 0.3s | 0.8s | 25ms | 4096 |
| Medium (512 in, 512 out) | 8 | 0.5s | 1.2s | 30ms | 2048 |
| Long (2048 in, 1024 out) | 4 | 1.2s | 2.5s | 35ms | 1024 |
| Very Long (8192 in, 2048 out) | 2 | 3.5s | 6.0s | 40ms | 512 |
Summary and Further Reading
K8s 1.30+ provides finer resource control for LLM inference services. vLLM's PagedAttention + ContinuousBatching is the de facto standard for LLM inference, and KEDA custom metrics autoscaling matches inference load more precisely than HPA CPU metrics. The three-layer elastic strategy (Pod-level HPA → Deployment-level rolling update → Cluster-level autoscaling) ensures stable operation under varying load conditions.
Key Development Takeaways:
- vLLM deployment: Enable prefix-caching and chunked-prefill, set GPU memory utilization to 0.92, adjust max-num-seqs based on GPU specs
- KEDA scaling: Trigger on request queue depth, GPU Cache utilization, and TTFT latency; 30s scale-up cooldown / 600s scale-down cooldown
- GPU optimization: Exclusive GPU for large models, MIG partitioning for small models, time-slicing for batch inference
- Self-healing: startupProbe → readinessProbe → livenessProbe three-tier detection, PDB ensures minimum available replicas
- Observability: Prometheus + Grafana full-chain monitoring; key alerts: queue depth > 20, TTFT > 2s, GPU Cache > 95%
Related Reading:
- AI Agent Multi-Agent Orchestration: From Single Agent to Production-Grade Multi-Agent Systems — Inference service scheduling in agent orchestration
- K8s GPU Scheduling: MIG Partitioning and GPU Sharing — GPU resource management and multi-tenant isolation
- LLM RAG Production Pipeline: From Zero to Production — Inference service integration in RAG systems
Authoritative References:
Try these browser-local tools — no sign-up required →