Phase 2 — Capstone: Autoscaling Inference Platform¶
Goal: build a small but real inference platform on Kubernetes that autoscales on inference-aware signals. This is the heart of the project. It mirrors, at homelab scale, what a production inference platform does at global scale: serve open-weights models on GPUs, scale them on the signals that actually predict saturation, and make the whole thing observable, declarative, and reproducible.
Status: the core platform is built and validated on real GPUs. The autoscaling control plane was first validated locally with no GPU on a kind cluster, where KEDA scaled the deployment from 1 to 5 replicas on the queue-depth signal, with Grafana dashboards, an SLO, and alerts. The build then moved onto real GPUs: the DIY two-GPU cluster is live and serving real vLLM, with cross-node GPU and inference metrics flowing into a single Prometheus. The one core layer still pending live bring-up is the Envoy AI Gateway; the reinforcement items (canary/shadow, a game-day, disaggregated prefill/decode) are tracked as next steps in the checklist below.
Real-GPU progress on the DIY two-GPU cluster (RTX 3060 Ti + RTX 4070, 8 GB each):
- Real vLLM serving on the 8 GB cards (Qwen2.5-1.5B and 3B), with TTFT, throughput, KV-cache, and cost numbers and a 1.5B-vs-3B comparison (real-gpu-results.md). The 3B run reproduces the autoscaler's trigger conditions on real hardware: KV-cache 99.5%, queue depth 12, TTFT p95 4.1s.
- Cross-node networking root-caused and fixed. The failure was a WSL2 mirrored-mode and kernel-VXLAN-socket limitation, written up as a debugging case study (troubleshooting log).
- Cluster-wide GPU and vLLM telemetry in Prometheus (hostNetwork exporters plus a vLLM PodMonitor, working around the overlay).
- Two-GPU load-balanced scale-out. One replica per card behind an external nginx LB, so both GPUs serve in parallel at roughly 2,360 tok/s aggregate.
- KServe InferenceService variant and comparison. The same model served the platform way (RawDeployment plus the huggingface/vLLM runtime). Serving numbers are identical to the plain Deployment, so KServe's cost is operational, not runtime (results, architecture decisions).
- KEDA wired on the real cluster. A
ScaledObjectautoscales vLLM on queue depth plus KV-cache (the HPA reads0/3, 0/700m), not CPU. Prometheus is exposed via hostNetwork so KEDA can reach it across the broken overlay. - Custom Go KEDA external scaler that combines KV-cache and queue-depth into one composite trigger (
keda-inference-scaler/). - CI and GitOps. GitHub Actions validates manifests and builds and tests the Go scaler. Argo CD
Applications declare the platform (gitops/). - One-command bootstrap and teardown (
bootstrap/) that reconstitutes the whole stack, including the WSL2 workarounds, for demos. Argo CD is live on the cluster syncing the platform. - Envoy AI Gateway. Automated in the bootstrap with route manifests written. Live bring-up is the one layer still pending, since it needs helm and is the most overlay-sensitive component.
-
Tech-stack rationale. Every tool and language is justified with its purpose, trade-off, and benefit in tech-stack.md.
-
local/is the runnable local stack (mock vLLM plus KEDA plus Prometheus/Grafana), one command:make up gpu-node/is the DIY two-GPU cluster (two 8 GB consumer GPUs as k3s GPU workers) serving real vLLM at $0, including the runbookgpu-node/real-gpu-results.mdhas the real-GPU serving numbers: 1.5B vs 3B and the two-GPU load-balanced scale-outWRITEUP.mdis the portfolio write-up (local and real-GPU results)loadtest/is the load and capture harness (gpu-loadtest.py,scale-demo.sh,incluster-load.yaml)runbook.mdis the operational runbook (draft)
Architecture¶
┌──────────────────────┐
client traffic → │ Envoy AI Gateway │ token-aware rate limiting, model routing
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ KServe │ LLMInferenceService
│ InferenceService │ (vLLM runtime, OpenAI-compatible)
└──────────┬───────────┘
│ exposes /metrics (TTFT, KV-cache %, queue depth)
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌─────────────┐ ┌──────────────┐
│Prometheus│───▶│ KEDA │ │ Grafana │
│ │ │ ScaledObject │ │ dashboards │
└──────────┘ └──────┬──────┘ └──────────────┘
│ scales replicas on TTFT p95 / KV-cache util
▼
GPU-backed pods
Checklist¶
Build¶
- [x] Deploy an open-weights model on Kubernetes via KServe with vLLM as the engine. Done both ways: a plain Deployment and a KServe
InferenceService(RawDeployment plus the huggingface/vLLM runtime). Numbers and the operational comparison are in real-gpu-results.md, and the trade-offs are in architecture-decisions.md. - [x] Autoscale with KEDA driven by TTFT p95 or KV-cache utilization from Prometheus, not CPU. Validated locally from 1 to 5 replicas, now live on the GPU cluster as a ScaledObject autoscaling vLLM on queue-depth plus KV-cache (HPA reading
0/3, 0/700m). Seek8s/keda-scaledobject.yamlandgpu-node/keda-scaledobject-gpu.yaml. This is the platform's core capability. - [ ] Add an inference gateway (Envoy AI Gateway) for token-aware rate limiting and model routing. Automated in the bootstrap with route manifests written; live bring-up is pending because it needs helm and is the most overlay-sensitive layer.
- [x] Instrument everything. Prometheus scrape plus Grafana dashboards for TTFT, inter-token latency, throughput, queue depth, and GPU utilization. One SLO defined and one alert wired. Done locally (dashboard plus TTFT-p95 SLO plus alerts), and now live on the DIY cluster with cross-node GPU and vLLM metrics in one Prometheus.
- [x] Load test and capture saturation behavior. See
loadtest/. Captured locally, plus real-GPU load tests on the 2-GPU cluster (real-gpu-results.md).
Ship real code and treat it like a product platform¶
- [x] Build at least one genuine code component in a named language: a custom KEDA external scaler in Go (
keda-inference-scaler/) that scales on a composite KV-cache plus queue-depth signal, which is one trigger the built-in scaler cannot express, plus thegpu-loadtest.pyharness. Not just YAML. - [ ] Land one small PR to an OSS inference project (vLLM / KServe / KEDA) and link it from the write-up. Submitted to KEDA in external-scalers#34 (draft, DCO green), adding the standalone scaler repo to the community list.
- [x] GitOps and CI. GitHub Actions (
ci.yml) lints and schema-validates every manifest and builds and tests the Go scaler. Argo CDApplications (gitops/) declare the platform for continuous reconciliation. A Terraform/Helm IaC pass remains. - [x] The stack is CNCF end to end. KServe, KEDA, Prometheus, and Argo CD are all CNCF projects (Argo CD, KEDA, and Prometheus are graduated).
Reinforce alongside the build¶
- [ ] Read disaggregated prefill/decode (NVIDIA Dynamo, llm-d). If budget allows, run prefill and decode on separate pools.
- [ ] Add a canary or shadow deployment path so a new model version takes a slice of traffic before full rollout.
- [ ] Write a short
runbook.mdand run one game-day failure (kill a pod mid-load). Pair it with a postmortem.
Milestone¶
- [ ] A working demo I can screen-share: model serving, autoscaling on an inference signal, live dashboards.
- [ ] A public write-up (
WRITEUP.md): architecture, load-test results, cost-per-million-tokens, design decisions, next steps. - [ ] At least one real code component in the repo plus one OSS PR linked, and the platform deploys via GitOps/CI/CD reproducibly from a clean clone.
Getting a cluster and GPU cheaply¶
- GPU access on demand: RunPod, Lambda, or Modal. Budget a few hundred dollars for the phase.
- Shut instances down between sessions. Idle GPUs are the fastest way to burn the budget, so I treat start/stop discipline as part of the platform's cost model.
- For the control plane without GPU cost, prototype manifests on a local
kind/k3dcluster, then move the workload to a GPU node pool.
The manifests in
k8s/are annotated starting points, not turnkey. Expect to adjust image tags, resource requests, model names, and metric names to match your engine version and cluster. Read the comments.