Ecstaticloud
Initializing0%
Ecstaticloud Logo
Back to Insights
Cloud & AIAugust 29, 2026

Optimizing LLM Inference at Scale: Leveraging AWS EKS, vLLM, and Custom KEDA Metrics for 60% Cost Reduction

Scaling large language models in production often leads to skyrocketing cloud expenses and erratic request latency. This technical deep dive demonstrates how to orchestrate vLLM on AWS EKS using KEDA and GPU partitioning to drastically cut compute costs while maintaining sub-second inference times.

Deploying Large Language Models (LLMs) into production is a double-edged sword. On one hand, generative AI capabilities unlock immense business value; on the other, running models like Llama 3 70B, Mistral, or Qwen at scale can devastate your cloud budget if unoptimized.

In classical web workloads, scaling is straightforward: CPU and Memory utilization correlate directly with incoming HTTP traffic. LLM inference, however, breaks traditional horizontal pod autoscaling (HPA). GPU memory is locked upfront by model weights and KV caches, rendering standard CPU/RAM metrics completely useless. Furthermore, static GPU node allocation often results in an idle cluster during off-peak hours—costing tens of thousands of dollars per month for unused compute.

In this deep dive, we will architect a production-ready, ultra-efficient LLM inference platform on AWS EKS. By combining vLLM for high-throughput serving, KEDA for dynamic event-driven pod scaling via custom metrics, NVIDIA GPU Partitioning for hardware density, and Karpenter for fast node auto-provisioning, we achieved a 60% reduction in compute spend while maintaining sub-second Time To First Token (TTFT).


Architectural Overview

To solve both performance and cost inefficiencies, we must decouple request ingestion, pod autoscaling, and node provisioning into a tight, event-driven feedback loop.

                         [ User Requests ]
                                 │
                                 ▼
                     [ AWS Application LB ]
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │  AWS EKS Cluster        │
                    │                         │
┌──────────────┐    │   ┌─────────────────┐   │
│ Prometheus   │◄───┼───┤ vLLM Pods       │   │
└──────┬───────┘    │   │ (PagedAttention)│   │
       │            │   └────────┬────────┘   │
       │ Metrics    │            │            │
       ▼            │            │ Metrics    │
┌──────────────┐    │            ▼            │
│ KEDA Operator│───►│   ┌─────────────────┐   │
└──────┬───────┘    │   │ Pod ScaledObject│   │
       │            │   └─────────────────┘   │
       │ Pod Specs  └────────────┬────────────┘
       ▼                         │
┌──────────────┐                 │ Unschedulable Pods
│ Karpenter    │◄────────────────┘
└──────┬───────┘
       │ Provisioning
       ▼
┌─────────────────────────────────────────────┐
│ AWS EC2 Spot / On-Demand Instances (G5/P4)  │
└─────────────────────────────────────────────┘

The Optimization Stack

  1. Inference Engine: vLLM utilizes PagedAttention to eliminate memory fragmentation in the Key-Value (KV) cache, enabling high-throughput continuous batching.
  2. GPU Virtualization: NVIDIA MIG (Multi-Instance GPU) or Time-Slicing to run multiple inference processes per physical GPU when handling smaller models or lower QPS.
  3. Pod Autoscaling: KEDA scrapes native vLLM Prometheus metrics (such as request queue length and KV cache saturation) to scale pods responsively before latency spikes.
  4. Node Autoscaling: Karpenter reacts in seconds to pending pod requirements, launching optimal EC2 GPU Spot and On-Demand instances without relying on rigid AWS Auto Scaling Groups (ASGs).

Phase 1: High-Efficiency Serving with vLLM

Standard PyTorch or naive Hugging Face deployments allocate KV cache contiguously. This leads to virtual memory fragmentation of up to 60-80%, drastically bottlenecking concurrency.

vLLM solves this by managing KV cache keys in non-contiguous memory blocks—similar to virtual memory in operating systems.

Here is a production-hardened Deployment manifest running Llama-3-8B-Instruct on AWS EKS using an NVIDIA A10G GPU (g5.2xlarge instance):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama3-8b
  namespace: llm-inference
  labels:
    app: vllm-llama3-8b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-llama3-8b
  template:
    metadata:
      labels:
        app: vllm-llama3-8b
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8000"
        prometheus.io/path: "/metrics"
    spec:
      containers:
      - name: vllm-container
        image: vllm/vllm-openai:v0.6.2
        args:
        - "--model"
        - "meta-llama/Meta-Llama-3-8B-Instruct"
        - "--port"
        - "8000"
        - "--max-model-len"
        - "8192"
        - "--gpu-memory-utilization"
        - "0.90"
        - "--tensor-parallel-size"
        - "1"
        - "--enable-chunked-prefill"
        env:
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-token-secret
              key: token
        ports:
        - containerPort: 8000
          name: http
        resources:
          limits:
            cpu: "6"
            memory: "24Gi"
            nvidia.com/gpu: "1"
          requests:
            cpu: "4"
            memory: "16Gi"
            nvidia.com/gpu: "1"
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 60
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 15

Key Parameters Explained

  • --gpu-memory-utilization 0.90: Reserves 90% of available GPU VRAM for model weights and KV cache, leaving 10% safety margin for dynamic execution graphs.
  • --enable-chunked-prefill: Splits large prompt prefills into smaller chunks and batches them with decode requests, smoothing out spikey TTFT latencies.

Phase 2: Maximizing Hardware Density with GPU Partitioning

Not all models require an entire dedicated physical GPU. For smaller tasks (e.g., embedding generation, classification, or small parameters fine-tunes), dedicating an entire NVIDIA A10G (24GB) or A100 (80GB) per pod results in low compute utilization and high cost.

We can split physical GPUs into smaller logical hardware interfaces using NVIDIA Time-Slicing (for shared compute workloads) or MIG (Multi-Instance GPU) (for strict hardware partition isolation on A100/H100 instances).

Configuring Time-Slicing in EKS

To implement Time-Slicing across g5 instance families, apply the following ConfigMap to the gpu-operator namespace:

apiVersion: v1
kind: ConfigMap
metadata:
  name: device-plugin-config
  namespace: gpu-operator
data:
  g5-timeslice: |
    version: v1
    sharing:
      timeSlicing:
        resources:
        - name: nvidia.com/gpu
          replicas: 2

This configuration tells the Kubernetes device plugin that every single physical A10G GPU can be advertised as 2 virtual GPU resources. This effectively doubles the pod density for lightweight inference workloads on the same underlying node hardware.


Phase 3: Event-Driven Scaling with KEDA & Custom Metrics

Standard Horizontal Pod Autoscalers (HPA) rely on CPU and RAM usage. In LLM workloads, CPU remains flat while GPU VRAM allocation is statically set at container initialization (--gpu-memory-utilization 0.90). Therefore, CPU/Memory triggers fail completely.

Instead, we must scale dynamically based on real-time inference telemetry exposed by vLLM's /metrics endpoint:

  1. vllm:num_requests_waiting: The number of requests sitting in the queue waiting for KV cache allocation. (Primary Scaling Indicator)
  2. vllm:gpu_cache_usage_perc: KV Cache memory saturation percentage.

If vllm:num_requests_waiting > 0, your current pod fleet is at capacity, and request latency is degrading exponentially.

Setting up KEDA ScaledObject

First, configure Prometheus to scrape the vLLM pods. Then, deploy a KEDA ScaledObject that scales our EKS deployment based on queuing activity:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: vllm-keda-autoscaler
  namespace: llm-inference
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: vllm-llama3-8b
  minReplicaCount: 1
  maxReplicaCount: 10
  cooldownPeriod: 300
  pollingInterval: 15
  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleUp:
          stabilizationWindowSeconds: 0
          policies:
          - type: Percent
            value: 100
            periodSeconds: 15
        scaleDown:
          stabilizationWindowSeconds: 300
          policies:
          - type: Percent
            value: 10
            periodSeconds: 60
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-k8s.monitoring.svc.cluster.local:9090
      metricName: vllm_num_requests_waiting
      query: |
        sum(vllm:num_requests_waiting{namespace="llm-inference"})
      threshold: '3'

Why This Strategy Works

  • Zero Aggressiveness on Scale Down: The 300-second cooldownPeriod and controlled scaleDown policy prevent thrashing (scaling down while a secondary batch arrives).
  • Instant Scale Up: Sets stabilizationWindowSeconds: 0. As soon as the queue aggregate crosses 3 waiting requests, KEDA instantly triggers extra pod creation.

Phase 4: Fast Node Provisioning with Karpenter

Autoscaling pods is only useful if the underlying Kubernetes cluster can expand hardware capacity in seconds. Traditional AWS Auto Scaling Groups (ASGs) take 3–5 minutes to launch GPU nodes.

Karpenter bypasses ASGs entirely, directly evaluating pod unschedulable constraints and calling AWS EC2 APIs to provision nodes in under 45 seconds.

Here is our production Karpenter configuration leveraging mixed g5 instance families with an aggressive Spot strategy:

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: gpu-node-pool
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: instance-family
          operator: In
          values: ["g5"]
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind: EC2NodeClass
        name: gpu-node-class
      limits:
        nvidia.com/gpu: "32"
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 2m
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: gpu-node-class
spec:
  amiFamily: AL2
  amiSelectorTerms:
    - alias: al2@latest
  role: "KarpenterNodeRole-EKSCluster"
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "eks-llm-cluster"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "eks-llm-cluster"
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 150Gi
        volumeType: gp3
        iops: 3000

Cost Optimization Mechanics

  1. Spot Fallback Strategy: Karpenter prioritizes GPU Spot Instances (which offer up to 70% discounts over On-Demand). If Spot capacity for g5.2xlarge dries up, it smoothly falls back to On-Demand instances.
  2. Fast Consolidation: Unused GPU nodes are terminated after 2m of emptiness, ensuring you stop paying for idle GPU hardware instantly.

Benchmark Results: Before vs. After

By combining vLLM, GPU Time-Slicing, KEDA metric scaling, and Karpenter Spot provisioning, we conducted load tests mimicking enterprise production traffic over a 30-day billing cycle.

Performance Metrics Comparison

| Metric | Legacy Setup (TGI + Static ASG) | Optimized Setup (vLLM + KEDA + Karpenter) | Improvement | | :--- | :--- | :--- | :--- | | P99 TTFT (Time To First Token) | 2.45 seconds | 0.38 seconds | 84% reduction | | Average GPU Utilization | 18% | 74% | 4.1x density | | Scale-up Reaction Time | 6.5 minutes | 45 seconds | 88% faster | | Monthly Compute Cost | $14,200 | $5,680 | 60% Savings |

Latency Profile Under Dynamic Load

When hit with a sudden spike of 200 concurrent requests:

  • Legacy Stack: Requests sat in TCP buffer queues; HTTP client timeouts spiked to 12%. Nodes took over 6 minutes to spin up via cluster-autoscaler.
  • Optimized Stack: vLLM’s continuous batching handled initial prefill spikes. Within 15 seconds, KEDA evaluated vllm:num_requests_waiting > 3 and spawned 4 additional pods. Karpenter provisioned two dual-GPU g5.12xlarge spot instances simultaneously, absorbing the load spike effortlessly without a single dropped HTTP connection.

Key Takeaways for Production Deployments

  1. Abandon Native HPA for LLMs: Standard CPU/RAM metrics are a trap. You must expose engine queue dynamics (vllm:num_requests_waiting) to scale proactively.
  2. Optimize Memory Engine First: Running inefficient serving frameworks on top of dynamic autoscalers just scales waste. Implement PagedAttention (vLLM, TensorRT-LLM) before tuning infrastructure.
  3. Hardware Densification: Utilize GPU Time-Slicing or MIG for smaller models or internal enterprise micro-services to squeeze full throughput out of underlying silicon.
  4. Use Dynamic Provisioners: Traditional ASGs are too slow for high-variance LLM workloads. Dynamic provisioners like Karpenter cut cold-start delays from minutes to seconds, making GPU Spot strategies viable in production.

By treating LLM infrastructure as a deeply coupled stack—from CUDA memory layout all the way up to cloud node autoprovisioning—you can deliver low-latency AI experiences without ballooning operational infrastructure spend.