GPU instances for AI are expensive: an A100 hour costs a lot, and idle time eats your budget. Cold start of a new pod takes 3–10 minutes (model loading, CUDA initialization), and standard CPU autoscaling does not account for GPU specifics. Without proper autoscaling, you pay for idle time or lose requests during peaks. We solve this: we configure scaling that actually meets SLA and reduces costs by 30–40%.
Problems We Solve
Cold start: GPU pod startup time 3–10 minutes. During this, the request queue overflows. Solutions: keepalive pods (minimum 1), pre-warming (start at 70% queue load), and buffering via request queue. Using spot instances worsens the problem — they can be interrupted anytime, so we combine spot with on-demand for critical services.
GPU utilization vs queue depth: GPU load during a long request is 100%, but new requests wait. Relying on utilization is a mistake. The correct metric is vllm_num_requests_waiting or queue_depth. We use a combination: scale-up by queue depth, scale-down by utilization.
Thrashing pods: Frequent spin-ups and downs due to sharp spikes. Configuring stabilization windows prevents this: scale-down after 10 minutes, scale-up within 30 seconds.
Why Queue Depth Is the Main Metric for LLM?
GPU utilization does not reflect queue latency. When a model processes a long request, GPU is 100% busy, but new requests are queued. Queue depth shows the real resource need. It should be the primary trigger for scale-up.
How We Configure GPU Autoscaling
Kubernetes HPA with Custom Metrics
Example HPA Configuration
# Prometheus Adapter for custom metrics
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-autoscaler
namespace: ai-serving
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-llama3
minReplicas: 1
maxReplicas: 8
metrics:
# Main metric: queue of waiting requests
- type: Pods
pods:
metric:
name: vllm_pending_requests
target:
type: AverageValue
averageValue: "5" # scale when > 5 requests in queue per pod
# Additional: GPU utilization (for scale-down)
- type: Pods
pods:
metric:
name: nvidia_gpu_duty_cycle
target:
type: AverageValue
averageValue: "70" # scale-down when < 70% utilization
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # fast scale-up
policies:
- type: Pods
value: 2
periodSeconds: 60 # +2 pods every minute
scaleDown:
stabilizationWindowSeconds: 600 # slow scale-down (10 minutes)
policies:
- type: Pods
value: 1
periodSeconds: 300 # -1 pod every 5 minutes
KEDA for Event-Driven Autoscaling
KEDA is more flexible than HPA: scaling by Prometheus, Kafka, RabbitMQ, SQS. Below is an example configuration.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: vllm-keda-scaler
namespace: ai-serving
spec:
scaleTargetRef:
name: vllm-llama3
minReplicaCount: 1
maxReplicaCount: 10
cooldownPeriod: 300
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
metricName: vllm_queue_size
query: sum(vllm_num_requests_waiting{namespace="ai-serving"})
threshold: "10" # 1 replica per 10 waiting requests
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
metricName: request_rate
query: rate(http_requests_total{job="vllm"}[2m])
threshold: "20" # additional trigger by RPS
Cloud-Native Autoscaling
For cloud GPU instances, we use Auto Scaling Groups with custom metrics:
import boto3
autoscaling = boto3.client('autoscaling', region_name='us-east-1')
autoscaling.put_scaling_policy(
AutoScalingGroupName='llm-gpu-asg',
PolicyName='scale-on-queue-depth',
PolicyType='TargetTrackingScaling',
TargetTrackingConfiguration={
'CustomizedMetricSpecification': {
'MetricName': 'LLMQueueDepth',
'Namespace': 'Custom/LLMMetrics',
'Statistic': 'Average',
},
'TargetValue': 5.0,
'ScaleInCooldown': 300,
'ScaleOutCooldown': 60,
'DisableScaleIn': False,
}
)
Publishing Custom Metrics from vLLM
We collect metrics directly from the inference server:
from prometheus_client import Gauge, start_http_server
import requests
import time
QUEUE_SIZE = Gauge('llm_queue_depth', 'Number of pending requests')
GPU_MEMORY = Gauge('llm_gpu_memory_used_gb', 'GPU memory usage in GB', ['gpu_id'])
def collect_metrics():
response = requests.get("http://localhost:8000/metrics").text
for line in response.split('\n'):
if 'vllm:num_requests_waiting' in line and not line.startswith('#'):
queue_size = float(line.split()[-1])
QUEUE_SIZE.set(queue_size)
import subprocess
result = subprocess.run(
['nvidia-smi', '--query-gpu=memory.used', '--format=csv,noheader,nounits'],
capture_output=True, text=True
)
for i, mem_mb in enumerate(result.stdout.strip().split('\n')):
GPU_MEMORY.labels(gpu_id=str(i)).set(float(mem_mb) / 1024)
start_http_server(9091)
while True:
collect_metrics()
time.sleep(15)
How Pre-Warming Prevents Cold Start?
We prevent cold start by forecasting load. If queue depth exceeds 70% of the maximum, we launch an additional pod in advance. Strategy code:
class PreWarmingStrategy:
def __init__(self, warmup_threshold: float = 0.7, warmup_lead_time: int = 180):
self.warmup_threshold = warmup_threshold
self.warmup_lead_time = warmup_lead_time
def should_scale_up(self, current_queue: int, max_queue: int, forecast: list) -> bool:
if current_queue / max_queue >= self.warmup_threshold:
return True
future_queue = forecast[self.warmup_lead_time // 15]
return future_queue / max_queue >= self.warmup_threshold
Why GPU Autoscaling Is Harder Than CPU?
Key differences: GPU instances cannot be "split" between services, cold start is much longer (model loading ~5 GB), and load metrics are misleading. On CPU you rely on CPU utilization, on GPU — only on queue depth and request arrival rate. Also, the cost of error is higher: an extra GPU pod means extra monthly expenses.
How to Choose a Metric for Scaling?
| Metric | Description | Suitable for |
|---|---|---|
| GPU utilization | Fraction of time cores are busy, easy to collect | Scale-down, baseline monitoring |
| Queue depth (pending requests) | Number of requests in queue, requires custom exporter | Scale-up, primary metric |
| Request rate (RPS) | Request arrival rate, good for forecasting | Pre-warming, additional trigger |
| GPU memory usage | Video memory usage, low variability | Overload notifications |
The best solution is a combination: queue depth for scale-up and GPU utilization for scale-down.
What Our Work Includes
We offer turnkey implementation:
- Infrastructure audit — load analysis, bottleneck identification, instance selection.
- Scaling scheme design — metric selection, HPA/KEDA configuration, behavior optimization (stabilization windows, policies).
- Implementation — Prometheus stack deployment, vLLM/TGI integration, cloud autoscaling group setup.
- Testing — load testing with peak simulation, threshold calibration.
- Documentation and training — runbook for on-call, metric and alert description.
- Post-release support — monitoring for first weeks, policy adjustments based on actuals.
Timeline and Savings
Estimated timeline:
- Basic setup (metrics + HPA) — from 5 days.
- Extended configuration (KEDA + pre-warming) — from 10 days.
- Full cycle with load testing — from 3 weeks.
Cost is calculated individually based on stack complexity and workload. Our clients save 30–40% monthly GPU costs after implementation.
Order GPU autoscaling implementation and start saving within a week. We have been working with AI infrastructure for 5+ years, completed 20+ projects. We guarantee stable operation — we include SLA on scaling response time in the contract. Kubernetes HPA and KEDA are proven tools. Contact us for a consultation on your project — we will analyze your load and offer the optimal scheme.







