Ringkasan Utama
Untuk autoscale vLLM di Kubernetes dengan KEDA, scale berdasarkan queue depth dan p95 latency, bukan GPU utilisation. Walkthrough EKS dari AWS memakai vllm:num_requests_waiting dengan threshold 25 per pod, activation threshold 1, dan guardrail p95 5 detik, scale up cepat dan scale down pelan. Anggap angka itu contoh dan ukur ulang untuk model dan GPU Anda sendiri.
Hal pertama yang saya cek di autoscaler inference adalah apa yang dipantau. Grafik GPU yang angkanya terlihat nyaman tidak memberi tahu apa pun tentang user yang request-nya sudah menunggu sepuluh detik, dan replica yang butuh beberapa menit untuk siap dipakai tidak bisa ditambah setelah keluhan datang.
Post ini adalah pembacaan dekat atas panduan AWS EKS tentang autoscaling AI inference dengan HPA dan KEDA, halaman pendampingnya tentang mencari threshold, halaman referensi KEDA dan Kubernetes yang menjadi dasarnya, serta release notes llm-d v0.9.0. Saya belum menjalankannya di cluster. Setiap angka di bawah dibaca dari sebuah halaman dan disebutkan sumbernya, dan bila saya sedang bernalar dan bukan mengutip, saya katakan.
Mengapa queue depth dan p95 latency, bukan GPU utilisation
AWS men-scale Deployment vllm-inference-app berdasarkan dua sinyal Prometheus. Queue depth disebut sinyal permintaan utama dan p95 end-to-end latency disebut guardrail SLO. GPU utilisation sama sekali tidak muncul di manifest. Halaman desain metrics vLLM membingkai masalah dasarnya: metrics sering dipakai untuk automated scaling, tujuannya mengekspos apa yang dibutuhkan untuk mendeteksi saturation point, dan topik ini tidak sederhana.
Penalaran saya, bukan klaim dari kedua halaman: GPU melaporkan seberapa sibuk ia, bukan berapa request yang menunggu. Pada load test AWS sendiri, jumlah request menunggu tetap mendekati nol sampai replica saturasi lalu melonjak ke ratusan, sebuah lompatan yang jauh lebih bersih daripada yang dijanjikan grafik utilisation. Tabel berikut mendaftar arti tiap kandidat sinyal menurut halaman yang saya baca.
| Sinyal | Kata sumber | Peran di walkthrough AWS | Catatan saya |
|---|---|---|---|
| vllm:num_requests_waiting | Request yang menunggu diproses setelah replica saturasi (halaman thresholds AWS) | Trigger utama: 25 per pod, activation 1 | Dikeluarkan oleh pod vLLM, jadi tidak berbicara apa pun saat pod berjumlah nol |
| vllm:e2e_request_latency_seconds | Histogram end-to-end request latency (halaman metrics vLLM); AWS menyebut ia bisa naik sebelum queue menumpuk | Trigger guardrail: p95 di atas 5 detik | Bergantung pada bentuk request seperti panjang output, jadi threshold dari satu workload tidak bisa dipindah |
| vllm:kv_cache_usage_perc | Fraksi blok KV cache yang terpakai, 0 sampai 1 (halaman metrics vLLM) | Tidak dipakai | Layak digrafikkan di samping queue; saya tidak menemukan threshold bersumber, jadi tidak saya berikan |
| GPU utilisation | Tidak dibahas di halaman mana pun yang saya baca | Tidak dipakai | Apakah ia mengikuti saturasi untuk workload Anda adalah hasil pengukuran, bukan asumsi |
Dari mana 25 dan 5 berasal: load test-nya
Halaman thresholds memakai Ministral-3-8B-Instruct-2512 di g6e.4xlarge dan k6 dengan model constant-arrival-rate pada 10, 20, 30, 40, 50, 60 dan 70 request per detik, 60 detik per rate dengan jeda 30 detik di antaranya. GPU di-warm-up dulu dengan 100 request berurutan, karena vLLM menangkap CUDA graph, meng-compile dan autotune kernel, serta mengalokasikan pool KV cache pada inference pertama sementara GPU menaikkan clock-nya. Jadi replica yang Ready belum tentu hangat.
Hasilnya, dengan kata-kata halaman itu: queue depth tetap mendekati nol sampai 50 concurrent request, lalu naik tajam di 60, mencapai sekitar 150 sampai 350 request antre, sementara p95 end-to-end latency naik dari sekitar 2,5 detik yang stabil menjadi 6 sampai 10 detik. Halaman itu memberi label langkah dalam request per detik tetapi menyebutnya concurrent request, jadi saya anggap lompatan 50 ke 60 sebagai knee dan tidak membaca presisi lebih dari itu.
Dari knee itu diturunkan tiga titik awal: scale up bila queue depth melebihi 25 selama 30 sampai 60 detik, scale up sebagai sinyal sekunder bila p95 melebihi 5 detik, dan scale down hanya bila queue 0 dan p95 di bawah 3 detik selama 5 menit. Halaman itu menyatakan terang-terangan bahwa ini titik awal dan latihannya harus diulang untuk model, GPU dan pola request Anda. Berikut load generator yang menghasilkan kurvanya.
// k6 script from the AWS thresholds page (stored in the vllm-loadtest-script ConfigMap).
// constant-arrival-rate holds the request RATE fixed, so when the GPU cannot keep up
// the excess shows up as vllm:num_requests_waiting instead of slowing the client down.
import http from 'k6/http';
// Random leading word varies the prompt so prefix caching does not flatter the result.
const words = ['revolution', 'technology', 'javascript', 'kubernetes', 'basketball'];
export const options = {
noConnectionReuse: true,
scenarios: {
constant_request_rate: {
executor: 'constant-arrival-rate',
rate: __ENV.TARGET_RPS || 10, // AWS steps this 10, 20, ... 70
timeUnit: '1s',
duration: __ENV.DURATION || '60s',
preAllocatedVUs: 50,
maxVUs: 500,
},
},
};
export default function () {
const word = words[Math.floor(Math.random() * words.length)];
const payload = JSON.stringify({
model: __ENV.MODEL,
prompt: 'Starting with the word "' + word + '", write a brief technical explanation about cloud computing concepts.',
max_tokens: 100,
temperature: 0.7,
});
http.post('http://vllm-inference-svc:8000/v1/completions', payload,
{ headers: { 'Content-Type': 'application/json' }, timeout: '240s' });
}ScaledObject, baris demi baris
KEDA di-install dengan Helm ke namespace sendiri, dan ScaledObject di bawah adalah manifest AWS dengan komentar saya. KEDA membuat dan mengelola HPA pendukung dengan nama yang sama, jadi Anda tidak menulis HPA itu sendiri. Komentar menandai pilihan yang penting.
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
kubectl get pods -n keda # operator, metrics-apiserver, admission-webhooks
kubectl apply -f - <<'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: vllm-inference-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-inference-app
minReplicaCount: 1 # 1 means the scaler is always "active" (see activation section)
maxReplicaCount: 5
advanced:
horizontalPodAutoscalerConfig:
name: vllm-inference-app # the HPA KEDA creates and owns; do not edit it by hand
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300 # GPU pods are slow to start; do not shed fast
policies:
- type: Pods
value: 1
periodSeconds: 120
selectPolicy: Min
triggers:
# Primary: demand exceeds capacity. AverageValue divides by replicas => 25 PER POD.
- type: prometheus
metricType: AverageValue
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring.svc:9090
# Wrong: sum(vllm:num_requests_waiting) -> empty result when nothing is waiting
# Right: add "or vector(0)" so the trigger always reports a number
query: sum(vllm:num_requests_waiting) or vector(0)
threshold: "25"
activationThreshold: "1"
# Guardrail: p95 end-to-end latency in seconds over a 1-minute rate window.
- type: prometheus
metricType: AverageValue
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring.svc:9090
query: histogram_quantile(0.95, sum(rate(vllm:e2e_request_latency_seconds_bucket[1m])) by (le)) or vector(0)
threshold: "5"
EOF
kubectl get scaledobject vllm-inference-app # READY True, ACTIVE False while idle
kubectl get hpa vllm-inference-app # TARGETS shows both: x/25 (avg), y/5 (avg)Tiga detail menopang desainnya. metricType AverageValue membuat KEDA membagi tiap metric ke seluruh replica, jadi 25 berarti 25 request menunggu per pod walau query menjumlahkan semuanya. Klausa or vector(0) mengembalikan 0 alih-alih hasil kosong saat tidak ada yang menunggu, yang menurut AWS mencegah trigger menjadi inactive. Dan KEDA melakukan scale ke trigger yang meminta replica terbanyak, jadi salah satu sinyal saja sudah bisa menambah kapasitas.
Karpenter adalah separuh cerita lainnya di cluster ini: menurut AWS ia menyediakan node GPU baru bila tidak ada yang tersedia, dan replica baru baru Ready setelah model termuat ke memori GPU. KEDA memutuskan berapa pod yang diinginkan; ia tidak membuat pod muncul cepat.
Activation threshold versus scaling threshold
Halaman scaling-deployments KEDA menjelaskan dua fase. Fase activation memutuskan antara 0 dan 1 replica berdasarkan apakah scaler aktif, dan fase scaling menyerahkan 1 sampai N ke HPA memakai nilai target. Activation membutuhkan nilai yang benar-benar lebih besar dari activation threshold, defaultnya 0, dan ia berprioritas: dengan threshold 10 dan activationThreshold 50, metric 40 membuat scaler inactive dan scale ke nol walau HPA sebenarnya menginginkan 4 instance.
Ada jebakan yang saya temukan dengan menaruh dua halaman berdampingan. Halaman KEDA yang sama menyebut bahwa bila minReplicaCount 1 atau lebih, scaler selalu aktif dan nilai activation diabaikan. Manifest AWS menetapkan minReplicaCount 1, tetapi teksnya mengatakan activationThreshold 1 menahan deployment di minReplicaCount sampai ada request menunggu. Dibaca bersama, activationThreshold di manifest itu mendokumentasikan maksud tetapi tidak menahan apa pun. Saya belum menguji pembacaan mana yang berlaku di cluster nyata. Perhatikan juga kata benar-benar lebih besar: activation 1 membutuhkan sedikitnya 2 request menunggu.
Halaman spec KEDA menyebut cooldownPeriod (default 300 detik) hanya berlaku saat scale ke 0, dan idleReplicaCount hanya menerima 0. Jika ingin scale to zero sungguhan, set minReplicaCount ke 0 dan perlakukan activation threshold sebagai aktif. Lalu tanyakan apa yang membangunkan pod pertama, karena masalah di paragraf berikutnya muncul seketika.
Inferensi saya: vllm:num_requests_waiting dikeluarkan oleh pod vLLM, jadi dengan nol pod tidak ada series yang bisa melewati activation threshold, dan request ke Service tanpa endpoint tidak pernah sampai ke queue vLLM untuk dihitung. Scale from zero membutuhkan sinyal dari sesuatu yang berada di depan pod, seperti gateway atau message queue, dan annotation force-activation di KEDA ada untuk memaksa scale up secara manual. Untuk GPU serving yang sensitif latency, saya akan menjaga minReplicaCount di 1 dan menerima biaya idle, yang juga dilakukan manifest AWS.
Cepat naik, pelan turun: behaviour window
HPA mengambil jumlah replica yang diinginkan terbesar di antara semua metric, itulah sebabnya pelanggaran latency bisa menambah pod saat queue masih tampak baik. Kubernetes mendokumentasikan stabilisasi scale-down memakai rekomendasi tertinggi dalam window, default 5 menit, jadi 300 detik di AWS sama dengan default itu. Sisi scale-up diatur terpisah: window 30 detik dan policy hingga 2 pod per 60 detik.
| Pengaturan | Scale up | Scale down | Alasan menurut AWS |
|---|---|---|---|
| stabilizationWindowSeconds | 30 | 300 | Queuing berarti user sudah menunggu; pod GPU lambat start |
| policies | Pods, 2 per 60 detik | Pods, 1 per 120 detik | Hindari membuang kapasitas yang dibutuhkan lagi sesaat kemudian |
| selectPolicy | Max | Min | Kubernetes: Max memilih policy dengan perubahan terbesar, Min yang terkecil |
Sedikit hitungan saya sendiri, bukan dari kedua halaman: dari 1 ke maxReplicaCount 5 butuh empat pod tambahan, dan pada 2 per 60 detik itu sedikitnya dua periode policy sebelum provisioning node atau model load. Saat turun, empat penghapusan pada satu per 120 detik tersebar sedikitnya sekitar enam menit. Asimetri ini disengaja, tetapi biayanya nyata: sisi yang lambat dibayar dalam jam GPU.
Satu celah lagi: halaman thresholds AWS mengusulkan scale down saat queue 0 dan p95 di bawah 3 detik selama 5 menit, tetapi ScaledObject hanya meng-encode window 5 menit terhadap target 25 dan 5. Kondisi 3 detik itu saran, bukan konfigurasi.
Apa yang rusak lebih dulu
Berikut titik kegagalan yang ditunjuk sumber, diurutkan menurut seberapa cepat saya perkirakan akan bertemu.
- Replica yang Ready belum tentu hangat. Halaman warm-up menunjukkan latency request pertama turun dari sekitar 2,6 detik ke baseline stabil, sehingga traffic ke replica baru bisa memicu guardrail p95 Anda sendiri dan memicu scale up lagi.
- Tidak ada node GPU yang bebas. Karpenter harus menyediakannya sebelum pod bisa mulai memuat weights, jadi waktu nyata menuju kapasitas adalah node ditambah model load, jauh di atas window scale-up 30 detik.
- Scale-in di tengah request. Halaman KEDA memperingatkan bahwa HPA bisa menghentikan replica di tengah tugas panjang dan menyarankan penanganan SIGTERM, hook preStop atau menjalankan pekerjaan sebagai Job. Untuk streaming completion itu perhatian nyata.
- Prometheus senyap. ignoreNullValues pada scaler default-nya true, jadi hasil kosong diabaikan; menetapkannya false membuat scaler error saat target hilang. Bagian fallback KEDA mendefinisikan jumlah replica yang dipertahankan saat scaler berstatus error, dan tidak didukung untuk trigger CPU dan memory.
- Threshold hasil salin. 25 dan 5 berasal dari satu model di satu tipe instance dengan satu bentuk prompt. Model lebih besar atau max_tokens lebih panjang menggeser knee, dan guardrail lalu menyala terus atau tidak pernah.
Semua isi post ini berasal dari dokumentasi yang saya baca, bukan dari cluster yang saya jalankan. Anggap 25 dan 5 sebagai nilai contoh AWS sendiri dan jalankan load test di hardware Anda sebelum angka itu mengendalikan sesuatu yang berbiaya.
llm-d memindahkan panduannya ke KEDA
Release notes llm-d v0.9.0 tidak memuat prosa tentang autoscaling, hanya judul pull request, jadi saya kutip itu saja: convert EPP autoscaling guide to KEDA; migrate nightly dan guide dari Prometheus Adapter ke KEDA; add KEDA support dan shared-cluster setup; serta authenticate KEDA ke Thanos dan assert the metric path. Notes itu juga mencantumkan workload variant autoscaler di v0.9.0.
Yang saya tarik darinya, sebagai inferensi dari judul: argumen AWS bahwa KEDA membaca Prometheus langsung tanpa metrics adapter terpisah untuk dikelola adalah argumen yang sama dengan yang diambil proyek serving saat meninggalkan Prometheus Adapter. Perubahan auth Thanos mengingatkan bahwa di shared cluster endpoint metric yang di-query KEDA biasanya tidak terbuka, jadi siapkan autentikasi di trigger.
Yang dikerjakan hari Senin
Kerjakan berurutan. Tiap langkah menggantikan angka yang kalau tidak akan Anda salin.
- Jalankan warm-up, lalu load test bertahap ke satu replica model Anda sendiri dan catat queue depth dan p95 di tiap rate.
- Tetapkan threshold queue di atas titik saat queue pertama kali tetap positif, dan threshold latency di bawah SLO Anda.
- Putuskan minReplicaCount dengan sengaja, 1 atau 0, dan bila 0, sebutkan sinyal yang membangunkan pod pertama.
- Pastikan kubectl get hpa menampilkan kedua metric di TARGETS dan baca event-nya dengan kubectl describe.
- Latih hari buruknya: hentikan load dan ukur waktu scale-down terhadap window Anda, lalu rusak alamat Prometheus dan lihat apa yang dilakukan fallback.
# My modification of the AWS ScaledObject (spec fields from KEDA's ScaledObject
# reference). NOT applied on a cluster by me. Only the changed parts are shown.
spec:
minReplicaCount: 1 # set 0 only if something in front of vLLM can wake it
maxReplicaCount: 5
pollingInterval: 30 # KEDA default; how often each trigger is checked
cooldownPeriod: 300 # default; applies ONLY to the 1 -> 0 step
fallback: # replicas to hold when a scaler is in an error state
failureThreshold: 3
replicas: 2 # pick from your own load test; not a sourced value
triggers:
- type: prometheus
metricType: AverageValue
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring.svc:9090
query: sum(vllm:num_requests_waiting) or vector(0)
threshold: "25" # REPLACE with the knee from YOUR load test
activationThreshold: "1" # strictly greater than: needs 2+ waiting to activate
ignoreNullValues: "false" # error (and fall back) when Prometheus is lost
# Rehearsal commands
kubectl get hpa vllm-inference-app -w
kubectl describe hpa vllm-inference-app | sed -n '/Events/,$p'
kubectl get pods -l app=vllm-inference-app -wFragmen di atas adalah modifikasi saya atas manifest AWS memakai field dari halaman spec KEDA, dan belum saya terapkan. Triggernya sama, ditambah fallback agar Prometheus yang mati mempertahankan kapasitas alih-alih menebak, dan menunjukkan di mana scale to zero akan dinyalakan.
Scale inference server berdasarkan apa yang dirasakan user, yaitu queue dan tail latency, scale up dalam detik dan turun dalam menit, dan anggap setiap threshold yang tidak Anda ukur sendiri sebagai placeholder. Halaman yang saya baca sepakat soal mekanismenya; celahnya ada di detail yang diserahkan kepada Anda, seperti apa yang sebenarnya digerbang activation dan berapa lama GPU menjadi kapasitas.
Sumber
- Dokumen AWS EKS: Autoscale AI inference with HPA and KEDA
- Dokumen AWS EKS: Identify scaling metric thresholds for AI inference
- Dokumen KEDA: Prometheus scaler
- Dokumen KEDA: Scaling Deployments, activating and scaling thresholds
- Dokumen KEDA: spesifikasi ScaledObject
- Dokumen Kubernetes: Horizontal Pod Autoscaling
- Dokumen vLLM: Metrics design
- GitHub: llm-d releases, catatan v0.9.0

