AI Runtime Platform
AI workloads often start as API calls but become production systems that need identity, policy, scaling, telemetry, and clear ownership
Every model call: policy + trace + budget
Andrey Lesnikov · Senior DevOps Platform Engineer · 2026-10-22 · sources checked 2026-10-22
Part of LLM inference on Kubernetes
CPU-based Horizontal Pod Autoscaler rules are a poor fit for LLM inference. A GPU pod can be saturated on KV-cache and queue depth while CPU looks calm. The search phrase that usually follows is some mix of kserve autoscaling vllm keda: use an event-driven scaler on signals the engine actually exposes.
This guide walks through that pattern with three anchors:
ScaledObject checked into justrunme/ai-runtime-platform at deploy/keda/vllm-scaledobject.yaml (maturity: Reference).vllm:num_requests_waiting) and KServe's documented KEDA path for generative InferenceServices.There is no public repository named justrunme/ai-runtime (GitHub returns 404 as of this check). The case study and the ScaledObject live under ai-runtime-platform. I do not invent scale-out latency or tokens-per-second numbers here; the Reference sample is a wiring example, not a load-test report.
vLLM's OpenAI-compatible server exposes Prometheus metrics on /metrics. The production metrics docs include gauges such as:
vllm:num_requests_waiting — requests waiting to be processedvllm:num_requests_running — requests in model execution batchesvllm:kv_cache_usage_perc — KV-cache usage (1 means 100%)Queue depth answers "how many requests are already waiting for capacity?". That is closer to user-visible backlog than CPU. KV-cache usage is another strong signal when memory, not admission queue, is the bottleneck. Pick the metric that matches the failure mode you care about; do not scale on four signals at once without a clear primary.
In docs/maturity-matrix.md and docs/vllm-production.md, KEDA ScaledObject is listed as Reference ("usable examples"), while the vLLM Helm chart is Supported. That wording matters: the chart is the primary path; the ScaledObject is a sample you must validate on your Prometheus, GPU pool and model.
The committed object (comments included):
# KEDA and Prometheus must be installed before applying this resource.
# This governs the Helm release's Deployment; do not combine it with an HPA for the same target.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: qwen-vllm-queue
namespace: ai-runtime
spec:
scaleTargetRef:
name: qwen-vllm-runtime
pollingInterval: 15
cooldownPeriod: 180
minReplicaCount: 1
maxReplicaCount: 4
triggers:
- type: prometheus
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090
metricName: vllm_requests_waiting
query: sum(vllm:num_requests_waiting{namespace="ai-runtime"})
threshold: "4"
activationThreshold: "1"
ignoreNullValues: "false"
What this says, without extra claims:
qwen-vllm-runtime in namespace ai-runtime (the Helm release's Deployment).prometheus scaler, query sum(vllm:num_requests_waiting{namespace="ai-runtime"}), threshold 4, activation threshold 1.ignoreNullValues: "false" — if Prometheus returns empty, the scaler errors instead of pretending the queue is fine (KEDA default for this field is true).The file comments warn not to attach a second HPA to the same Deployment. KEDA creates and owns the HPA for the ScaledObject.
Threshold 4 is a starting value in this sample, not a tuned production result. On this setup you still need to confirm scrape labels, ServiceMonitor targets and that the metric name your vLLM build emits matches the query (colon vs underscore variants show up in dashboards; the query here uses the vllm:num_requests_waiting form from the vLLM docs).
From the KEDA Prometheus scaler docs (checked against the v2.17 doc set; confirm against the version you install):
| Field | Role in this sample |
|---|---|
serverAddress |
In-cluster Prometheus HTTP API |
query |
Must return a single vector/scalar element |
threshold |
Scale-out target for the query value |
activationThreshold |
When the scaler becomes active (here 1) |
ignoreNullValues |
Whether a lost scrape is ignored (true) or errors (false) |
Authentication (bearer, basic, TLS, cloud-managed Prometheus) is documented separately via TriggerAuthentication. The sample assumes an open in-cluster Prometheus Service DNS name from kube-prometheus-stack.
If you serve through KServe in Standard mode, the KServe generative-inference autoscaling docs describe setting serving.kserve.io/autoscalerClass: "keda" and driving scale from Prometheus or OpenTelemetry on LLM metrics (examples use vllm:num_requests_running). KEDA remains the engine; KServe owns the InferenceService and can create the ScaledObject for you.
Two honest boundaries from those docs and from my runtime:
So "kserve autoscaling vllm keda" and "plain vLLM Deployment + KEDA" are sibling patterns: same metric family, different owner of the Pod template.
maxReplicaCount: 4 matches a particular GPU node pool.Cases
AI workloads often start as API calls but become production systems that need identity, policy, scaling, telemetry, and clear ownership
Every model call: policy + trace + budget
LLM workloads move faster than traditional platform controls and can quickly become expensive, opaque, and hard to operate
LLM serving with operability signals