Skip to main content
BlogAI/ML

Panduan KServe LLMInferenceService: llm-d, WVA dan vLLM

Pembacaan teliti LLMInferenceService KServe v0.17 dan v0.18: routing llm-d, autoscaling WVA, LeaderWorkerSet, dan kapan ia mengungguli Deployment vLLM biasa.

Matthews Wong

Software Engineer

Oktober 202612 menit baca
Panduan KServe LLMInferenceService: llm-d, WVA dan vLLM

Ringkasan Utama

LLMInferenceService dari KServe menggantikan Deployment vLLM, Service dan autoscaler buatan tangan dengan satu custom resource yang menyiapkan Gateway, endpoint picker llm-d yang me-route berdasarkan status KV-cache, pool prefill dan decode opsional, serta autoscaling WVA. v0.17 membuatnya siap produksi dan v0.18 menambah multi-node tanpa Ray, tetapi API-nya masih alpha, jadi kunci versinya.

Saya mencari jawaban singkat untuk pertanyaan sederhana: kalau Anda sudah menjalankan vLLM di Kubernetes dengan Deployment dan Service, apa yang ditawarkan LLMInferenceService dari KServe sehingga layak untuk migrasi? Release post v0.17 dan v0.18 berisi daftar pull request yang padat, dan sample di repository tidak semuanya konsisten satu sama lain.

Ini adalah pembacaan teliti, bukan laporan lapangan. Saya membaca release post v0.17 dan v0.18, tipe Go untuk CRD pada tag v0.18.0, manifest sample di tag yang sama, panduan Kubernetes dari vLLM, dan release notes v0.21.0. Saya tidak menjalankan satu pun di cluster GPU, dan setiap manifest di bawah yang merupakan hasil edit saya sendiri akan disebutkan.

Apa yang digantikan LLMInferenceService, dan apa yang tidak

LLMInferenceService adalah custom resource terpisah dari InferenceService. Post v0.17 menyebutnya GenAI-first, berbeda dari InferenceService yang menyasar model prediktif, dan menyatakan dukungan untuk distributed inference, KV-cache-aware scheduling, disaggregated prefill dan decode, integrasi Gateway API Inference Extension, serta token-based rate limiting. Contoh minimalnya membuat Deployment, Service, Gateway, HTTPRoute, InferencePool, InferenceModel dan endpoint picker pod dari satu objek.

Bandingkan dengan jalur biasa. Panduan vLLM untuk Kubernetes menampilkan PVC, Secret untuk token Hugging Face, Deployment, Service ClusterIP yang memetakan port 80 ke port container 8000, emptyDir in-memory di /dev/shm untuk tensor parallelism, dan probe HTTP di /health. Panduan itu juga mencantumkan KServe, llm-d, LeaderWorkerSet, KubeRay dan beberapa stack lain sebagai alternatif, yang menunjukkan di mana para maintainer mengharapkan manifest biasa berhenti.

AspekManifest vLLM biasaLLMInferenceService
Objek yang Anda tulisPVC, Secret, Deployment, Service, probe, shared memorySatu resource berisi model, replicas, router dan template
Routing requestService ClusterIP di depan podGateway, HTTPRoute dan endpoint picker yang menilai pod berdasarkan queue depth dan status KV-cache
Pemisahan prefill dan decodeTidak ada di panduan; dua Deployment dan KV connector yang Anda pasang sendirispec.prefill adalah pool kedua dengan replicas dan scaling sendiri
Serving multi-nodeDisebut sebagai integrasi (LeaderWorkerSet, KubeRay), bukan manifestWorker spec menghasilkan LeaderWorkerSet; field parallelism diteruskan ke vLLM
AutoscalingTidak dibahas di panduanspec.scaling dengan WVA dan aktuator KEDA atau HPA
Stabilitas APIObjek inti Kubernetes, stabilserving.kserve.io v1alpha1 di v0.18.0; notes v0.21.0 memindahkan sample ke v1alpha2

Pembacaan jujur dari tabel ini: Anda menukar objek yang stabil dan membosankan dengan controller yang mengerjakan banyak hal untuk Anda. Jika Anda menjalankan satu model di beberapa GPU di balik sebuah Service, kolom kiri sudah cukup dan kolom kanan hanya menjadi beban. Kolom kanan mulai berguna ketika routing, autoscaling, atau penempatan multi-node adalah hal yang terus Anda bangun ulang.

Yang dibangun v0.17: komponen llm-d di dalam KServe

Post v0.17, terbit pada 2026-03-13, adalah rilis yang membuat resource ini siap produksi. Isinya, dalam urutan yang menurut saya paling penting:

  • KV-cache-aware scheduling: endpoint picker melacak status cache di seluruh instance vLLM lewat event ZMQ BlockStored dan BlockRemoved, lalu menilai pod melalui pipeline plugin EndpointPickerConfig, bukan round-robin.
  • Disaggregated prefill dan decode: prefill yang berat di compute dan decode yang berat di memory menjadi workload terpisah yang di-scale sendiri-sendiri, dengan blok KV dipindahkan antar pod lewat NixlConnector di atas RoCE berbasis RDMA.
  • Parallelism lewat LeaderWorkerSet: tensor, data, data-local dan expert parallelism, dikonfigurasi dengan worker spec.
  • Integrasi Envoy AI Gateway: AIGatewayRoute mendefinisikan llmRequestCosts untuk token input, output dan total, dan BackendTrafficPolicy membatasi rate per header x-user-id.
  • Autoscaling dengan Workload Variant Autoscaler (WVA), yang memantau utilisasi KV-cache dan queue depth lewat Prometheus dan mengeluarkan wva_desired_replicas untuk dieksekusi KEDA atau HPA.

Rilis yang sama memecah Helm chart dari satu menjadi sepuluh dan menyatakan jelas bahwa upgrade dari v0.16 memerlukan upgrade guide, bukan sekadar helm upgrade. Rilis ini melaporkan 167 pull request yang di-merge. Perubahan struktur lebih penting daripada angkanya: kserve, llmisvc dan localmodel kini komponen independen, sehingga Anda bisa memasang stack generatif tanpa stack prediktif.

Manifest terkecil yang sudah berguna

Sample di bawah adalah contoh GPU single-node dari repository di tag v0.18.0, dipangkas, dengan komentar saya pada bagian yang mengejutkan. Field router yang kosong adalah keputusan desain yang menarik: objek kosong berarti meminta default yang dikelola, bukan mematikan komponen.

# From docs/samples/llmisvc/single-node-gpu at the v0.18.0 tag (trimmed).
# One object. The controller derives the Deployment, Service, Gateway,
# HTTPRoute, InferencePool and the endpoint picker from it.
apiVersion: serving.kserve.io/v1alpha1
kind: LLMInferenceService
metadata:
  name: qwen2-7b-instruct-single
spec:
  model:
    uri: hf://Qwen/Qwen2.5-7B-Instruct   # where weights come from
    name: Qwen/Qwen2.5-7B-Instruct       # the name clients put in "model"
  replicas: 3
  router:
    scheduler: {}   # {} means "use the managed default", not "disabled"
    route: {}
    gateway: {}
  template:
    containers:
      - name: main
        resources:
          limits:   { cpu: "4", memory: 32Gi, nvidia.com/gpu: "1" }
          requests: { cpu: "2", memory: 16Gi, nvidia.com/gpu: "1" }
        livenessProbe:
          httpGet: { path: /health, port: 8000, scheme: HTTPS }  # HTTPS, not HTTP
          initialDelaySeconds: 120   # model load time, not a typo
          periodSeconds: 30
          timeoutSeconds: 30
          failureThreshold: 5

Halaman overview mendaftar field top-level lainnya. spec.template adalah workload single-node atau decode, spec.worker adalah pod worker multi-node yang memicu LeaderWorkerSet, spec.prefill adalah workload khusus prefill, dan spec.parallelism membawa ukuran tensor, data dan expert. Tipe Go menambahkan pipeline dan dataLocal ke daftar itu.

Perhatikan apa yang tidak ada: tidak ada Service, Deployment, atau HTTPRoute. Jika Anda apply ini lalu mendaftar objek di namespace, perbandingan di bagian pertama menjadi nyata, karena semua yang pada jalur biasa harus Anda tulis muncul sebagai output controller, bukan manifest Anda.

Routing berdasarkan status KV-cache, dan konfigurasi yang harus tetap sinkron

Sample routing di v0.18.0 adalah tempat desain ini terbayar dan juga tempat ia menggigit. Konfigurasi scheduler adalah EndpointPickerConfig inline. Beberapa nilai harus sama dengan pod vLLM: block size dan hash seed, yang ditandai komentar sample, ditambah hashing algorithm dan endpoint event ZMQ yang diatur sample di sisi vLLM. Jika bergeser, gambaran scheduler tentang pod mana menyimpan prefix mana bisa salah, dan ia me-route berdasarkan gambaran yang salah itu.

# Based on precise-prefix-cache-routing/llm-inference-service-qwen2-7b-gpu-kv-cache-routing.yaml
# at v0.18.0. Values marked "must equal" have to match the vLLM pods.
spec:
  router:
    scheduler:
      config:
        inline:
          apiVersion: inference.networking.x-k8s.io/v1alpha1
          kind: EndpointPickerConfig
          plugins:
            - type: single-profile-handler
            - type: precise-prefix-cache-scorer
              parameters:
                tokenProcessorConfig:
                  blockSize: 64      # must equal vLLM --block-size (default 16)
                  hashSeed: "42"     # must equal PYTHONHASHSEED in the vLLM pods
                kvEventsConfig:
                  zmqEndpoint: "tcp://*:5557"   # vLLM publishes block events here
            - type: queue-scorer
            - type: kv-cache-utilization-scorer
            - type: max-score-picker
          schedulingProfiles:
            - name: default
              plugins:
                - pluginRef: queue-scorer
                  weight: 2
                - pluginRef: kv-cache-utilization-scorer
                  weight: 2
                # Wrong (as shipped in the sample): pluginRef: prefix-cache-scorer
                # Right: reference the plugin you actually declared above.
                - pluginRef: precise-prefix-cache-scorer
                  weight: 3
                - pluginRef: max-score-picker
  template:
    containers:
      - name: main
        env:
          - name: PYTHONHASHSEED
            value: "42"
          - name: VLLM_ADDITIONAL_ARGS   # the full sample also adds --kv-events-config (zmq publisher)
            value: "--prefix-caching-hash-algo sha256 --block-size 64"

Komentar sample menyatakan langsung dua batasannya: blockSize harus sama dengan --block-size vLLM yang defaultnya 16, dan hashSeed harus sama dengan PYTHONHASHSEED di pod vLLM. Manifest mengatur block size 64, hash seed 42 dan hashing prefix sha256 di sisi vLLM. Bobot di sample adalah queue-scorer 2, kv-cache-utilization-scorer 2 dan prefix scorer 3, sehingga prefix affinity memegang bobot tertinggi.

Jangan tempel sample routing apa adanya. Di tag v0.18.0 file itu mendeklarasikan plugin bertipe precise-prefix-cache-scorer, tetapi scheduling profile merujuk prefix-cache-scorer yang tidak dideklarasikan di file tersebut. Blok saya di atas memakai nama yang dideklarasikan. Saya belum menjalankannya, jadi anggap ini pembacaan atas file, dan biarkan kubectl apply dengan --dry-run=server serta log scheduler yang menjadi hakimnya.

Post v0.17 menampilkan kosakata plugin yang berbeda lagi: prefix-cache-scorer dengan bobot 2.0, load-aware-scorer dengan bobot 1.0 dan max-score-picker. Nama-namanya berubah antara blog dan sample, dan v0.21.0 mencantumkan perubahan yang memigrasikan apiVersion EndpointPickerConfig. Nama scorer adalah konfigurasi yang terikat versi, bukan kosakata yang stabil.

Pool prefill dan decode, multi-node, dan tambahan di v0.18

Disaggregation di API ini adalah dua pool dalam satu objek. template dan replicas tingkat atas menjelaskan decode, dan spec.prefill menjelaskan prefill dengan jumlah replica dan pod template sendiri. Sample memindahkan blok KV dengan NixlConnector dan meminta jaringan RoCE lewat annotation CNI dan resource limit rdma/roce_gdr. Itu hardware sungguhan: tanpa jaringan yang mendukung RDMA, pool-nya bisa ada tetapi jalur transfer yang diasumsikan sample tidak.

# Disaggregated prefill/decode, from single-node-gpu/...-pd-qwen2-7b-gpu.yaml
# at v0.18.0 (trimmed). Top level is the decode pool; spec.prefill is its own pool.
metadata:
  annotations:
    k8s.v1.cni.cncf.io/networks: roce-p2   # RoCE network for the KV transfer
spec:
  replicas: 1          # decode
  prefill:
    replicas: 2        # prefill scales independently
    template:
      containers:
        - name: main
          env:
            - name: KSERVE_INFER_ROCE
              value: "true"
            - name: VLLM_NIXL_SIDE_CHANNEL_HOST
              valueFrom: { fieldRef: { fieldPath: status.podIP } }
            - name: VLLM_ADDITIONAL_ARGS
              value: "--kv_transfer_config '{\"kv_connector\":\"NixlConnector\",\"kv_role\":\"kv_both\"}'"
            - name: UCX_TLS
              value: "rc,sm,self,cuda_copy,cuda_ipc"
          resources:
            limits: { nvidia.com/gpu: "1", rdma/roce_gdr: 1 }

# Multi-node: spec.parallelism plus a worker pod spec becomes a LeaderWorkerSet.
# From the dp-ep sample: 16-way data parallel, 8 per node, expert parallel on.
spec:
  parallelism: { data: 16, dataLocal: 8, expert: true, tensor: 1 }

Post v0.18, terbit pada 2026-04-29, mendaftar tambahan berikut yang relevan untuk resource ini:

  • Inference multi-node tanpa Ray: annotation multinode/executor-backend pada ServingRuntime memilih ray, yang menjadi default, atau mp. Pada mode mp jumlah node berasal dari pipeline parallelism dan GPU per node dari tensor parallelism, dan controller menyuntikkan PIPELINE_PARALLEL_SIZE, TENSOR_PARALLEL_SIZE dan WORKER_SVC ke container worker.
  • LeaderWorkerSet sebagai scale target: LLMInferenceService bisa meng-autoscale workload multi-node lewat WVA v0.6.0, padahal sebelumnya validasi memblokir kombinasi spec.worker dengan spec.scaling. Rilis ini juga meng-upgrade llm-d ke v0.6.
  • OpenAI Responses API: /v1/responses kini di-route lewat HTTPRoute.
  • Profil restricted dari Pod Security Standards diberlakukan di default template, dan saya perkirakan ini item yang akan merusak pod spec lama yang Anda bawa.
  • CRD LocalModelNamespaceCache untuk model cache per namespace, vLLM di-upgrade ke v0.19.0, dan 130 pull request yang di-merge dari 27 kontributor, 10 di antaranya baru pertama kali berkontribusi.

Satu batas yang perlu dihormati: annotation tanpa Ray dijelaskan terhadap ServingRuntime, jadi tampaknya ia milik jalur multi-node InferenceService. Untuk LLMInferenceService, mekanisme multi-node di post yang sama adalah LeaderWorkerSet. Saya tidak akan berasumsi satu flag mencakup keduanya.

Autoscaling dengan WVA, dibaca dari schema

Tipe Go di v0.18.0 adalah deskripsi autoscaling paling presisi yang saya temukan, dan aturan validasinya ada sebagai komentar. Bentuk di bawah diambil langsung dari sana. Angka di blok saya adalah nilai yang legal, bukan yang sudah di-tuning, dan saya tidak mengukur latensi scale-up untuk model apa pun.

# Field names and rules read from the v0.18.0 Go types
# (pkg/apis/serving/v1alpha1/llm_inference_service_types.go).
# The numbers are mine, chosen to be legal, not tuned.
spec:
  # replicas and scaling are mutually exclusive (CEL rule on the workload).
  scaling:
    minReplicas: 2        # defaults to 1, minimum 1
    maxReplicas: 8
    wva:                  # required whenever scaling is set
      variantCost: "10.0" # default; relative cost per replica of this variant
      keda:               # exactly one of keda or hpa
        pollingInterval: 15          # seconds
        cooldownPeriod: 300          # seconds after the last active trigger
        initialCooldownPeriod: 600   # let the model load before KEDA judges it
        idleReplicaCount: 1          # must be below minReplicas, and minimum is 1
  # prefill.scaling takes the same shape, so each pool scales on its own.

Tiga aturan muncul dari validasi CEL. replicas dan scaling saling eksklusif pada sebuah workload. wva wajib ada setiap kali scaling diatur, dan tepat satu dari hpa atau keda harus berada di bawahnya. Lalu idleReplicaCount harus lebih kecil dari minReplicas sementara keduanya bernilai minimum 1, jadi memakai idleReplicaCount berarti minReplicas minimal 2.

Aturan terakhir itu bertentangan dengan post v0.17, yang menyebut aktuator KEDA mendukung idle scale-to-zero lewat idleReplicaCount. Di tag v0.18.0 field itu bernilai minimum 1, jadi ia adalah idle floor di atas nol. Klaim v0.17 mungkin menggambarkan niat atau versi lain; CRD di tag adalah yang ditegakkan admission webhook, jadi saya memercayai itu. Menurut tipenya, controller juga membuat resource VariantAutoscaling dan ServiceMonitor di samping aktuator.

initialCooldownPeriod ada persis untuk kasus LLM: tipenya menjelaskan ia sebagai waktu tunggu setelah ScaledObject dibuat agar KEDA tidak menilai pod yang masih memuat weights. Atur di atas waktu load model yang Anda ukur, dan ukur waktu itu terlebih dahulu.

Di mana ia rusak: API alpha dan bagian yang bergerak

Semua di atas bisa benar dan tetap menghabiskan seminggu Anda, karena permukaannya terus bergerak. Buktinya ada di rilis itu sendiri.

  • Versi API berpindah. Sample di v0.18.0 memakai serving.kserve.io/v1alpha1; notes v0.21.0 mencantumkan perubahan yang memperbarui sample llmisvc dari v1alpha1 ke v1alpha2. Manifest dari sebuah blog post mungkin tidak bisa di-apply ke cluster Anda.
  • Wiring autoscaling berpindah. Tipe v0.18.0 menjelaskan resource VariantAutoscaling yang dibuat controller; v0.21.0 mencantumkan migrasi WVA dari VA CRD ke discovery berbasis annotation. Dashboard atau policy yang merujuk objek lama akan rusak.
  • Jalur tokenizer berpindah. v0.21.0 mencantumkan penggantian UDS tokenizer sidecar dengan vLLM render deployment. Apa pun yang men-scrape atau mem-patch sidecar itu perlu dicek ulang saat upgrade.
  • Instalasi berpindah. v0.17 memecah satu Helm chart menjadi sepuluh dan mewajibkan upgrade guide dari v0.16, dan v0.21.0 mencantumkan restrukturisasi manajemen CRD agar bisa dipasang independen.

Rilis terbaru yang bisa saya konfirmasi adalah v0.21.0, terbit pada 2026-09-25, dengan v0.19.0 pada 2026-06-14 dan v0.20.0 pada 2026-08-06 di antara rilis itu dan post v0.18. Jadi panduan yang dibangun di atas v0.18 sudah tiga minor version yang lalu. Saya memeriksa tag dan release notes, tetapi tidak membaca notes v0.19 dan v0.20 secara penuh, jadi saya tidak membuat klaim tentang apa yang berubah di sana.

Yang dilakukan hari Senin

Perlakukan ini sebagai evaluasi, bukan migrasi. Urutan di bawah dimulai dari yang termurah, dan setiap langkah entah mengakhiri evaluasi lebih awal atau layak untuk langkah berikutnya.

  1. Putuskan apakah Anda membutuhkannya. Satu model, beberapa replica, tanpa multi-node: tetap di Deployment vLLM. Butuh KV-cache-aware routing, pemisahan prefill dan decode, multi-node, atau autoscaling berdasarkan metrik inference adalah sinyal untuk lanjut.
  2. Kunci versinya dan baca CRD yang disajikan cluster Anda dengan perintah di bawah, sebelum menyalin YAML dari post mana pun, termasuk yang ini.
  3. Mulai dari sample single-node dengan router kosong. Pastikan controller menghasilkan objek yang Anda harapkan sebelum menyentuh konfigurasi scheduler.
  4. Tambahkan konfigurasi routing hanya dengan pengukuran di tangan: workload dengan prefix sharing milik Anda sendiri, block size, hash seed dan hashing algorithm yang diatur identik di kedua sisi, dan log scheduler terbuka saat Anda menguji.
  5. Tambahkan scaling terakhir, dengan initialCooldownPeriod di atas waktu load model dan minReplicas minimal 2 jika Anda ingin idle floor. Jalankan ulang apply dengan dry-run setelah setiap upgrade KServe.
# 1. Which release is actually installed? Do not trust the blog post's version.
kubectl get deploy -A -o wide | grep -i kserve

# 2. Which API version does the CRD serve here? v1alpha1 on v0.18, v1alpha2 in newer samples.
kubectl get crd llminferenceservices.serving.kserve.io \
  -o jsonpath='{.spec.versions[*].name}{"\n"}'

# 3. Read the schema your cluster enforces, not the one you remember.
kubectl explain llminferenceservice.spec.scaling --recursive

# 4. Let the admission webhook judge the manifest before anything is created.
kubectl apply --dry-run=server -f my-llmisvc.yaml

# 5. After applying, see what the controller generated from your one object.
kubectl get llminferenceservice,deploy,svc,httproute,inferencepool -n my-ns

Perintahnya kubectl biasa. kubectl explain membaca schema dari CRD yang terpasang, sehingga ia satu-satunya sumber yang tidak mungkin usang untuk cluster Anda.

Aturan saya setelah membaca semuanya: pakai LLMInferenceService untuk routing, disaggregation dan autoscaling yang kalau tidak harus Anda bangun sendiri, pertahankan Deployment vLLM biasa untuk semua yang lebih sederhana, dan perlakukan setiap potongan YAML, termasuk milik saya, sebagai klaim tentang satu versi yang harus dikonfirmasi oleh CRD Anda sendiri.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa itu KServe LLMInferenceService?

Ini adalah custom resource KServe untuk model generatif, terpisah dari InferenceService. Dari satu objek, controller membuat workload, Gateway, HTTPRoute, InferencePool dan endpoint picker, serta mendukung pool prefill dan decode, serving multi-node dan autoscaling WVA. Release post v0.17 menyebutnya siap produksi.

Kapan sebaiknya memakai LLMInferenceService dibanding Deployment vLLM biasa?

Pakai saat routing berdasarkan status KV-cache, pemisahan prefill dan decode, serving multi-node, atau autoscaling berdasarkan metrik inference adalah hal yang kalau tidak akan Anda bangun sendiri. Untuk satu model di beberapa GPU di balik Service, Deployment biasa lebih sederhana dan objeknya stabil. API-nya masih alpha, dan itu bagian dari biayanya.

Apa yang ditambahkan KServe v0.18 ke LLMInferenceService?

Post v0.18 mencantumkan LeaderWorkerSet sebagai target autoscaling dengan WVA v0.6.0, llm-d v0.6, routing OpenAI Responses API di /v1/responses, dan profil restricted Pod Security Standards di default template. Ada juga opsi multi-node tanpa Ray dan CRD LocalModelNamespaceCache.

Apakah LLMInferenceService bisa scale to zero?

Post v0.17 menyebut aktuator KEDA mendukung idle scale-to-zero lewat idleReplicaCount. Di tipe Go v0.18.0, idleReplicaCount bernilai minimum 1 dan harus lebih kecil dari minReplicas, jadi ia berperilaku sebagai idle floor. Cek CRD yang disajikan cluster Anda dengan kubectl explain sebelum mengandalkan nol.

Apakah API LLMInferenceService sudah stabil?

Belum. Sample di v0.18.0 memakai serving.kserve.io/v1alpha1, dan release notes v0.21.0 mencantumkan pemindahan sample ke v1alpha2, migrasi discovery WVA ke annotation, dan penggantian tokenizer sidecar. Kunci versi KServe Anda dan jalankan ulang apply dry-run server-side setelah setiap upgrade.