llm-d - Kubernetes-native distributed LLM inference σε κλίμακα
// Μια hands-on περιήγηση στο llm-d, το CNCF Sandbox framework για distributed LLM inference σε Kubernetes: inference-aware routing, prefill/decode disaggregation και KV-cache offload. Με demo χωρίς GPU σε Kind, με το vLLM simulator και Flux GitOps.
Πριν από τρεις μήνες έγραψα για το KServe και για το πώς το InferenceService CRD έχει γίνει ό,τι πιο κοντινό σε standard διαθέτει το cloud-native για να βάλεις ένα εκπαιδευμένο μοντέλο πίσω από ένα API. Εκείνο το post έκλεινε με ένα σκόπιμο cliffhanger: το KServe σου δίνει ένα πολύ καλό primitive για serving ενός μοντέλου, αλλά δεν λύνει το GPU sharing, το fractional scheduling ή το πώς κάνεις load balancing στο inference traffic ανάμεσα σε πολλά replicas ενός μεγάλου μοντέλου. Έδειξα προς το Volcano και το Kueue και προχώρησα.
Αυτό το post είναι το άλλο μισό της ιστορίας. Όταν τα μοντέλα σου μεγαλώσουν αρκετά και το traffic ανέβει αρκετά, το ενδιαφέρον πρόβλημα δεν είναι πια «πώς σερβίρω ένα μοντέλο», αλλά «πώς κάνω routing, disaggregation και caching του inference σε ένα fleet από GPU, ώστε το tail latency να μένει σταθερό υπό φορτίο». Ένα απλό Kubernetes Service μπροστά από οκτώ vLLM pods δεν βλέπει τίποτα από το inference: μοιράζει τα requests round-robin, λες και κάθε token κοστίζει το ίδιο. Δεν κοστίζει. Αυτό ακριβώς το κενό καλύπτει το llm-d.
Το llm-d μπήκε στο CNCF ως Sandbox project στο KubeCon EU 2026, κοινή δωρεά των IBM Research, Red Hat και Google Cloud, με ιδρυτική υποστήριξη από NVIDIA, AMD, CoreWeave, Hugging Face, Intel, Lambda και Mistral AI. Είναι ένα Kubernetes-native framework για distributed inference, χτισμένο πάνω στο vLLM, στο Gateway API Inference Extension και στο LeaderWorkerSet. Εδώ θα δούμε τι είναι, γιατί το απλό load balancing του Kubernetes δεν φτάνει για LLMs, και πώς τρέχεις ολόκληρο το orchestration layer σε ένα Kind cluster στο laptop σου, χωρίς GPU, με το vLLM simulator και Flux GitOps. Ολόκληρο το demo βρίσκεται στο srekubecraft-demo/llm-d/.
Για ποιον είναι αυτό το post;#
Το post απευθύνεται σε:
- Platform Engineers που χτίζουν εσωτερική πλατφόρμα LLM inference και τους έχει πέσει μικρό το «ένα vLLM Deployment πίσω από ένα Service»
- SREs που πρέπει να λειτουργούν LLM serving με προβλέψιμο tail latency, autoscaling και GitOps, όπως κάθε άλλο production workload
- ML Platform Teams που τρέχουν ήδη KServe ή σκέτο vLLM και βλέπουν cache thrash και άνιση χρήση των GPU με πραγματικό traffic
- Engineers που αξιολογούν το inference stack του 2026 (vLLM, KServe, llm-d, KAITO, Kueue) και θέλουν να ξέρουν πού κολλάει το llm-d
- Όποιον είδε την ανακοίνωση για το CNCF Sandbox και θέλει να καταλάβει τι κάνει πραγματικά το llm-d πριν του δώσει GPU
TL;DR#
Πρόβλημα: Για να σερβίρεις ένα μεγάλο μοντέλο σε κλίμακα, τρέχεις πολλά replicas σε πολλές GPU. Ένα τυπικό Kubernetes Service τα κάνει load balance με round-robin ή least-connections, χωρίς να βλέπει το prompt cache locality, την πίεση στο KV-cache και το ότι οι φάσεις prefill και decode ενός request έχουν εντελώς διαφορετικό προφίλ πόρων. Το αποτέλεσμα: cache thrash, άνιση χρήση των GPU και απρόβλεπτο p99 latency.
Λύση: Το llm-d είναι ένα Kubernetes-native framework για distributed inference. Αντικαθιστά τον χαζό load balancer με έναν inference-aware scheduler (χτισμένο πάνω στο Gateway API Inference Extension), χωρίζει το prefill από το decode σε pods που κάνουν scale ανεξάρτητα, και προσθέτει ιεραρχικό KV-cache offload σε tiers από GPU, CPU και storage. Το engine από κάτω είναι το vLLM.
Αποτέλεσμα: Κάθε request πηγαίνει στο replica που είναι πιθανότερο να έχει ήδη στο cache το prefix του prompt, το prefill και το decode κάνουν scale ανεξάρτητα, και η μνήμη των GPU αξιοποιείται πολύ πιο αποδοτικά. Σε αυτό το post τρέχω ολόκληρο το orchestration layer (scheduler, gateway, endpoint picker, model servers) σε ένα Kind cluster με μηδέν GPU, βάζοντας στη θέση του πραγματικού vLLM το simulator llm-d-inference-sim, και όλα παραδίδονται μέσω Flux. Ολόκληρο το demo repo.
Το πρόβλημα του load balancing στα LLMs#
Βάλε οκτώ vLLM replicas πίσω από ένα Kubernetes Service και στείλε του production traffic. Δουλεύει. Μέχρι να κοιτάξεις τα metrics.
Γιατί το round-robin είναι λάθος για LLMs:
- Το prompt cache locality είναι αόρατο. Το vLLM κρατάει ένα prefix cache: αν δύο requests μοιράζονται ένα μεγάλο system prompt, το δεύτερο μπορεί να ξαναχρησιμοποιήσει το KV-cache του πρώτου και να γλιτώσει τον επανυπολογισμό. Ένα round-robin
Serviceσκορπάει τα δύο requests σε διαφορετικά pods, οπότε το δεύτερο τα υπολογίζει όλα από την αρχή. Σε κοινό prefix 4k tokens, αυτή είναι η διαφορά ανάμεσα σε time-to-first-token 40ms και 2000ms. - Το prefill και το decode είναι διαφορετικά workloads. Η φάση του prefill επεξεργάζεται ολόκληρο το prompt σε ένα υπολογιστικά βαρύ forward pass: το όριό της είναι τα FLOPs της GPU. Η φάση του decode παράγει tokens ένα ένα: το όριό της είναι το memory bandwidth και το μέγεθος του KV-cache. Αν τα στριμώξεις και τα δύο στο ίδιο pod, ένα μεγάλο prefill request κάνει head-of-line blocking σε κάθε decode stream που μοιράζεται εκείνη τη GPU.
- Η πίεση στο KV-cache είναι ανά pod και αλλάζει συνεχώς. Ένα pod που τρέχει δώδεκα συνομιλίες με μεγάλο context είναι κοντά στο όριο του KV-cache του και θα αρχίσει να κάνει evict ή να βάζει requests σε ουρά. Ένας round-robin balancer συνεχίζει να του στέλνει νέα requests, γιατί μετράει μόνο connections, όχι μνήμη GPU.
- Το queue depth διαφέρει τεράστια από pod σε pod. Ένα in-flight request που παράγει 2000 tokens κρατάει ένα slot για 30 δευτερόλεπτα. Ο balancer βλέπει «ένα connection» και θεωρεί το pod σχεδόν άδειο.
Τίποτα από αυτά δεν το βλέπει ένας τυπικός L4/L7 load balancer, γιατί τίποτα από αυτά δεν εκφράζεται σε HTTP. Η απόφαση του routing πρέπει να βλέπει το inference state (τι έχει το cache, πόσο γεμάτο είναι το KV-cache, πόσο βαθιά είναι η ουρά), και αυτό ζει μέσα στον model server. Αυτή είναι όλη η ιδέα πίσω από το llm-d.
Τι είναι το llm-d;#
Το llm-d είναι ένα Kubernetes-native, high-performance framework για distributed LLM inference. Δεν είναι καινούριο inference engine: χρησιμοποιεί ως engine το vLLM (και μπορεί να μπει μπροστά και από SGLang και TensorRT-LLM). Αυτό που προσθέτει το llm-d είναι το orchestration γύρω από το engine: inference-aware routing, prefill/decode disaggregation και διαχείριση του KV-cache σε tiers, όλα εκφρασμένα με τα standard primitives του Kubernetes.
Έγινε δεκτό στο CNCF Sandbox τον Μάρτιο του 2026. Το πιο πρόσφατο release τη στιγμή που γράφω είναι το v0.8.1 (Ιούνιος 2026). Το project δεν δίνει ένα μονολιθικό install, δίνει «well-lit paths»: τεκμηριωμένες, δοκιμασμένες και benchmarked συνταγές για deployment.
Το llm-d στηρίζεται σε τρεις πυλώνες.
Πυλώνας 1: Inference-aware routing (Gateway API Inference Extension)#
Ο εγκέφαλος του routing είναι ένας Endpoint Picker (EPP), χτισμένος πάνω στο Gateway API Inference Extension (GAIE), ένα project των Kubernetes SIGs που επεκτείνει το Gateway API με ένα resource InferencePool και έναν pluggable scheduler. Αντί για round-robin, ο EPP βαθμολογεί κάθε υποψήφιο endpoint με μια αλυσίδα από scorers:
- prefix-cache scorer - εκτιμά πόσο από το prefix του εισερχόμενου prompt έχει ήδη κάθε pod στο cache, και προτιμά το καλύτερο hit
- no-hit LRU scorer - τα cold requests, αυτά με μηδέν cache hits, τα μοιράζει ομοιόμορφα για να ισορροπήσει το φορτίο του prefill
- kv-cache utilization scorer - ρίχνει το score στα pods που πλησιάζουν το όριο του KV-cache τους
- queue-depth scorer - ρίχνει το score στα pods με βαθιές ουρές από requests
Το αποτέλεσμα είναι μια απόφαση routing που μεγιστοποιεί το cache reuse και αποφεύγει τα hotspots, το ακριβώς αντίθετο του round-robin.
Πυλώνας 2: Prefill/decode disaggregation#
Το llm-d μπορεί να χωρίσει τις δύο φάσεις του inference σε ξεχωριστά pods που κάνουν scale ανεξάρτητα. Τα prefill pods τρέχουν σε compute-optimized GPU και αναλαμβάνουν το βαρύ πέρασμα της επεξεργασίας του prompt, ενώ τα decode pods τρέχουν σε bandwidth-optimized GPU και κάνουν stream τα tokens. Το KV-cache που υπολογίστηκε στο prefill μεταφέρεται στο decode pod μέσω ενός γρήγορου connector (NIXL/NCCL). Έτσι κάνεις scale το capacity του prefill και του decode με βάση την πραγματική αναλογία μήκους prompt προς μήκος output, αντί να κάνεις over-provision και τα δύο. Τα multi-node replicas ενός μοντέλου τα ενορχηστρώνει το LeaderWorkerSet (LWS).
Πυλώνας 3: Ιεραρχικό KV-cache offload#
Το KV-cache είναι η μνήμη μιας συνομιλίας, και το HBM της GPU είναι λιγοστό. Το llm-d ενσωματώνει το LMCache για να κάνει offload το KV-cache σε μια ιεραρχία από tiers (GPU HBM, μετά CPU RAM, μετά τοπικός δίσκος ή shared storage), ώστε μια warm συνομιλία να συνεχίζει χωρίς επανυπολογισμό ακόμα και αφού έχει γίνει evict από τη GPU. Αυτό είναι που κάνει τα μεγάλα, multi-turn sessions οικονομικά εφικτά.
flowchart TB
Client["Client / OpenAI SDK"] --> GW["Inference Gateway<br/>(Gateway API + GAIE)"]
subgraph LLMD["llm-d control + data plane"]
GW --> EPP["Endpoint Picker (EPP)<br/>prefix · kv-util · queue scorers"]
EPP -->|routing decision| POOL
subgraph POOL["InferencePool"]
direction LR
subgraph Prefill["Prefill pods (compute-bound)"]
P1["vLLM prefill"]
P2["vLLM prefill"]
end
subgraph Decode["Decode pods (bandwidth-bound)"]
D1["vLLM decode"]
D2["vLLM decode"]
end
Prefill -->|KV transfer<br/>NIXL/NCCL| Decode
end
Decode <--> KV["LMCache tiers<br/>GPU HBM → CPU → disk"]
end
style LLMD fill:#0d1117,stroke:#30363d,color:#c9d1d9
style POOL fill:#161b22,stroke:#30363d,color:#c9d1d9
Τι ΔΕΝ είναι το llm-d#
- Δεν είναι inference engine. Ενορχηστρώνει vLLM/SGLang/TensorRT-LLM, δεν τα αντικαθιστά. Αν θέλεις σκέτο serving σε ένα pod, τρέξε το vLLM απευθείας.
- Δεν είναι CRD για model serving όπως το KServe. Δεν υπάρχει abstraction τύπου
InferenceServiceπου να τυλίγει predictive ML, ONNX και sklearn. Το llm-d ασχολείται αποκλειστικά με μεγάλα generative μοντέλα σε κλίμακα. Δες τη σύγκριση παρακάτω. - Δεν είναι πλατφόρμα για training ή fine-tuning. Αυτό είναι δουλειά του Kubeflow, του Ray ή των δικών σου pipelines. Το llm-d κάνει μόνο inference.
- Δεν είναι GPU scheduler. Θεωρεί δεδομένο ότι οι accelerators υπάρχουν και είναι schedulable. Συνδύασέ το με τον NVIDIA GPU Operator, το Kueue ή το Volcano για το πραγματικό GPU plumbing.
- Δεν κάνει μαγικά σε μία GPU. Αν σερβίρεις ένα μικρό μοντέλο με χαμηλό QPS σε ένα replica, το routing και το disaggregation του llm-d δεν σου προσφέρουν τίποτα. Η αξία του φαίνεται όταν έχεις πολλά replicas σε πολλά nodes.
Το llm-d απέναντι στις εναλλακτικές#
| Κριτήριο | Σκέτο vLLM + Service | KServe | llm-d |
|---|---|---|---|
| Βασικός στόχος | Serving ενός μοντέλου | Multi-framework model serving (CRD) | Distributed LLM inference σε κλίμακα |
| Routing | Round-robin / least-conn | Knative (με βάση το concurrency) | Inference-aware (cache + KV + queue) |
| Διαχωρισμός prefill/decode | Όχι | Όχι | Ναι, με ανεξάρτητο scaling |
| KV-cache offload | Μόνο μέσα στη GPU | Μόνο μέσα στη GPU | Σε tiers (GPU → CPU → disk) μέσω LMCache |
| Multi-node replicas | Χειροκίνητα | Περιορισμένα | Native με LeaderWorkerSet |
| Εύρος μοντέλων | Οποιοδήποτε μοντέλο του vLLM | sklearn → LLMs | Μεγάλα generative μοντέλα |
| Standard API | OpenAI (vLLM) | OpenAI + v1 predict | OpenAI (μέσω vLLM) |
| Status στο CNCF | vLLM (ανεξάρτητο) | Incubating | Sandbox |
Με δυο λόγια: το KServe και το llm-d δεν είναι ανταγωνιστές. Λύνουν διαφορετικά layers, και η σύγκλιση έχει ήδη γίνει. Το KServe έφερε το LLMInferenceService στο v0.16, και το documentation λέει ρητά ότι είναι «built on the foundation of llm-d». Το KServe δημιουργεί το InferencePool και τρέχει τον Endpoint Picker του llm-d για λογαριασμό σου, οπότε το cache-aware routing αυτού του post το παίρνεις μέσα από ένα CRD του KServe.
Έτσι το stack δεν είναι δίλημμα, είναι ένας καθαρός διαχωρισμός σε τρία layers:
| Layer | Ποιος το έχει | Ευθύνη |
|---|---|---|
| Engine | vLLM | GPU compute, KV-cache, εκτέλεση του μοντέλου |
| Orchestration | llm-d | InferencePool, EPP scorers, prefill/decode disaggregation |
| Platform | KServe | Lifecycle του μοντέλου, governance, OpenAI-compatible API, multi-framework abstraction |
Άρα το mental model δεν είναι «KServe ή llm-d». Είναι: κλασικό InferenceService για το fleet με τα sklearn/ONNX/XGBoost, LLMInferenceService για τα λίγα μεγάλα generative μοντέλα που καθορίζουν τον λογαριασμό των GPU, και τα δύο σε ένα control plane. Το να τρέξεις το llm-d standalone, όπως κάνει αυτό το demo, παραμένει ο σωστός τρόπος για να καταλάβεις το routing layer, και η σωστή επιλογή αν δεν είσαι σε KServe.
Το demo χωρίς GPU: llm-d σε Kind με το vLLM simulator#
Το πρόβλημα με ένα hands-on post για το llm-d είναι το εξής: όλο το νόημα του project είναι να μοιράζει το inference σε GPU, και GPU είναι ακριβώς αυτό που δεν έχει ένα laptop. Το να νοικιάσεις ένα node 8xH100 για να γράψεις ένα demo σε blog δεν είναι ούτε φθηνό ούτε κάτι που μπορεί να αναπαράγει ο αναγνώστης.
Τη λύση τη δίνει το llm-d-inference-sim: ένα ελαφρύ, OpenAI-compatible simulator που μιμείται τη συμπεριφορά του vLLM (token streaming, μοτίβα latency, prefix cache hits, metrics του KV-cache, ακόμα και prefill/decode disaggregation) χωρίς GPU και χωρίς πραγματικό μοντέλο. Είναι το ίδιο εργαλείο με το οποίο οι maintainers του llm-d δοκιμάζουν σε κλίμακα τον scheduler και τη λογική του routing στο CI. Μπορούμε δηλαδή να τρέξουμε ολόκληρο το orchestration layer του llm-d (gateway, endpoint picker, scorers, InferencePool, τοπολογία του disaggregation) σε ένα Kind cluster και να παρατηρήσουμε τις αποφάσεις του routing, ενώ οι «model servers» είναι πανάλαφρα sim pods.
Τι αποδεικνύει το demo: το control plane, το inference-aware routing και την τοπολογία του disaggregation. Τι δεν αποδεικνύει: πραγματικά tokens/sec σε πραγματικό hardware. Γι’ αυτό χρειάζεσαι GPU, και το production path το καλύπτω παρακάτω.
Αρχιτεκτονική του demo#
flowchart TB
subgraph Kind["Kind cluster (laptop, no GPU)"]
GAIE["GAIE CRDs v1.5.0<br/>(applied server-side)"]
subgraph Flux["Flux HelmRelease"]
EPP["llm-d router v0.9.0<br/>Endpoint Picker + Envoy<br/>+ InferencePool"]
end
SIM["llm-d-inference-sim x4<br/>(fake vLLM, OpenAI-compatible)"]
GAIE --> EPP
EPP -->|"scorer chain picks endpoint"| SIM
EPP -.->|"InferencePool selects app=qwen-sim"| SIM
end
Dev["curl / OpenAI SDK"] -->|"/v1/completions"| EPP
style Kind fill:#0d1117,stroke:#30363d,color:#c9d1d9
style Flux fill:#161b22,stroke:#30363d,color:#c9d1d9
Προαπαιτούμενα#
# Tooling (Apple Silicon or Linux)
kind version # v0.23+
kubectl version # v1.30+
flux --version # v2.3+
helm version # v3.12+
Βήμα 1: Bootstrap του cluster (Kind + Cilium + Flux)#
Το demo ακολουθεί το ίδιο μοτίβο με το υπόλοιπο monorepo srekubecraft-demo: πρώτα imperative bootstrap, μετά GitOps. Όλα τα οδηγεί ένα Taskfile, οπότε ολόκληρο το run είναι μία εντολή:
git clone https://github.com/nicknikolakakis/srekubecraft-demo
cd srekubecraft-demo/llm-d
task setup # kind + cilium + flux -> GAIE CRDs -> router -> sim -> ready
Από κάτω, το bootstrap φτιάχνει ένα Kind cluster με τρία nodes (control-plane, έναν worker system για τον router και έναν worker inference για τα sim pods), εγκαθιστά το Cilium ως CNI και εγκαθιστά το Flux.
Βήμα 2: Τα CRDs του Gateway API Inference Extension#
Το InferencePool του router ανήκει στο API group inference.networking.k8s.io, που έρχεται από το Gateway API Inference Extension. Το demo το καρφώνει στο v1.5.0, για να ταιριάζει με το guides/env.sh του llm-d v0.8.x, και το κάνει apply server-side επειδή τα CRD manifests είναι μεγάλα:
# kubernetes/gaie/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/kubernetes-sigs/gateway-api-inference-extension/releases/download/v1.5.0/v1-manifests.yaml
Βήμα 3: Οι προσομοιωμένοι model servers#
Αντί για ένα container vllm/vllm-openai που απαιτεί nvidia.com/gpu: 1, το Deployment των model servers τρέχει το simulator. Πρόσεξε ότι δεν υπάρχει κανένα resource request για GPU, και ότι το label app: qwen-sim είναι αυτό με το οποίο επιλέγει pods το InferencePool του router:
# kubernetes/modelserver/sim-deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
name: qwen-sim
namespace: llm-d
labels:
app: qwen-sim
spec:
replicas: 4
selector:
matchLabels:
app: qwen-sim
template:
metadata:
labels:
app: qwen-sim
spec:
nodeSelector:
workload: inference # land on the simulated GPU pool
containers:
- name: vllm-sim
image: "ghcr.io/llm-d/llm-d-inference-sim:v0.10.0"
args:
- "--model=Qwen/Qwen3-0.6B"
- "--port=8000"
- "--mode=random" # random | echo
- "--max-num-seqs=16"
ports:
- name: http
containerPort: 8000
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
Τέσσερις «model servers» χωράνε άνετα σε ένα laptop, γιατί ο καθένας είναι ένα Go binary που παριστάνει ένα μοντέλο 0.6B, όχι το ίδιο το μοντέλο.
Βήμα 4: Ο router - Endpoint Picker, proxy και InferencePool#
Το InferencePool δεν το γράφεις με το χέρι. Το router chart του llm-d (oci://ghcr.io/llm-d/charts/llm-d-router-standalone, v0.9.0) στήνει τον Endpoint Picker (EPP), έναν ενσωματωμένο Envoy proxy και το InferencePool, όλα από ένα σετ values. Το standalone mode δεν χρειάζεται εξωτερικό Gateway ή LoadBalancer, και γι’ αυτό ακριβώς τρέχει σε Kind. Το HelmRelease (που το διαχειρίζεται το Flux) δείχνει το pool στα sim pods και συνδέει την αλυσίδα των scorers:
# kubernetes/flux/router-release.yaml (values excerpt)
values:
router:
modelServers:
matchLabels:
app: qwen-sim # selects the sim pods into the InferencePool
targetPorts:
- number: 8000
protocol: http
epp:
pluginsConfigFile: "optimized-baseline-plugins.yaml"
pluginsCustomConfig:
optimized-baseline-plugins.yaml: |
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: queue-scorer
- type: kv-cache-utilization-scorer
- type: prefix-cache-scorer
- type: no-hit-lru-scorer
schedulingProfiles:
- name: default
plugins:
- pluginRef: queue-scorer
weight: 2
- pluginRef: kv-cache-utilization-scorer
weight: 2
- pluginRef: prefix-cache-scorer
weight: 3 # prefix reuse gets the highest weight
- pluginRef: no-hit-lru-scorer
weight: 2
Η μία παγίδα: τα default resource requests του chart για τα epp και proxy είναι cpu: 4 / memory: 8Gi το καθένα. Μια χαρά για μηχάνημα με GPU, unschedulable σε laptop. Το demo τα κατεβάζει και τα δύο σε 200m/512Mi. Αυτό το override είναι η διαφορά ανάμεσα σε router pods που τρέχουν και σε router pods που μένουν Pending για πάντα.
Βήμα 5: Στείλε traffic και δες το routing#
Το task test:route κάνει port-forward το Service του router και στέλνει requests μέσα από τον Envoy proxy. Πρόσεξε ότι το Service παίρνει το όνομά του από τον Endpoint Picker (llm-d-router-epp), όχι από το release: llm-d-router λέγονται το HelmRelease και το InferencePool.
Το response έχει ένα header x-inference-pod με το όνομα του pod που διάλεξε ο EPP, και αυτό είναι που κάνει το routing παρατηρήσιμο:
kubectl port-forward -n llm-d svc/llm-d-router-epp 8080:80 &
PROMPT="You are an SRE assistant with deep Kubernetes knowledge. Follow this runbook precisely: first restart the pod, then check the readiness probes, then verify the rollout status."
curl -sS -D - -o /dev/null http://localhost:8080/v1/completions \
-H 'Content-Type: application/json' \
-d "{\"model\":\"Qwen/Qwen3-0.6B\",\"prompt\":\"$PROMPT\",\"max_tokens\":8}" | grep -i x-inference-pod
Στείλε το ίδιο prompt οκτώ φορές, μετά οκτώ διαφορετικά prompts, και μέτρα ποιο pod εξυπηρέτησε το καθένα. Αυτό είναι το πραγματικό output από το Kind cluster:
8x identical prefix -> 8 x-inference-pod: qwen-sim-6fbd95f76d-5njsd
8x distinct prompts -> 2 x-inference-pod: qwen-sim-6fbd95f76d-xnmpc
2 x-inference-pod: qwen-sim-6fbd95f76d-jqxkw
2 x-inference-pod: qwen-sim-6fbd95f76d-65m85
2 x-inference-pod: qwen-sim-6fbd95f76d-5njsd
Αυτή είναι όλη η θέση του post σε οκτώ γραμμές. Το traffic με κοινό prefix μαζεύεται σε ένα μόνο pod, γιατί ο prefix-cache scorer ξέρει ότι εκείνο το pod έχει ήδη το prefix του prompt. Το cold traffic, από prompts άσχετα μεταξύ τους, μοιράζεται απόλυτα ομοιόμορφα και στα τέσσερα, γιατί ο no-hit LRU scorer αναλαμβάνει όταν κανένα pod δεν έχει cache hit. Ένα round-robin Service θα είχε σκορπίσει το πρώτο σετ και, σε οκτώ requests, δεν θα εγγυόταν την ομοιόμορφη μοιρασιά στο δεύτερο.
Μετά κοίτα τα metrics του EPP για να δεις το scoring στην πράξη (task test:metrics). Το port των metrics θέλει authentication: χωρίς bearer token επιστρέφει 401, οπότε βγάλε πρώτα ένα:
TOKEN=$(kubectl create token default -n llm-d --duration=10m)
kubectl port-forward -n llm-d svc/llm-d-router-epp 9090:9090 &
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:9090/metrics \
| grep -Ei 'inference_|prefix|score|queue|kv'
Εδώ χρειάζεται λίγο RBAC που το router chart δεν φέρνει: το system:auth-delegator στο ServiceAccount του EPP, για να μπορεί να κάνει TokenReview, και ένα role που διαβάζει το /metrics, δεμένο στο ServiceAccount του οποίου το token χρησιμοποιείς. Και τα δύο βρίσκονται στο kubernetes/metrics/rbac.yaml του demo και γίνονται apply με το task metrics:rbac. Χωρίς το πρώτο, ο EPP απαντάει 500 Authentication failed ακόμα και με έγκυρο token.
Η αλυσίδα των scorers φαίνεται ονομαστικά στο output:
inference_extension_info{build_ref="v0.9.0",...} 1
inference_extension_plugin_duration_seconds_count{extension_point="DataProducer",plugin_name="approx-prefix-cache-producer",...} 20
inference_extension_plugin_duration_seconds_count{extension_point="PreRequest",plugin_name="no-hit-lru-scorer",...} 20
inference_extension_plugin_duration_seconds_count{extension_point="Picker",plugin_name="max-score-picker",...} 20
Σημείωση: κάθε εντολή και κάθε output παραπάνω βγήκε από πραγματικό run σε Kind (sim
v0.10.0, GAIEv1.5.0, router chartv0.9.0), σε laptop χωρίς GPU. Μια παγίδα που αξίζει να κρατήσεις: το sim θέλει το--force-dummy-tokenizerόταν του δίνεις πραγματικό όνομα μοντέλου από το Hugging Face, όπως τοQwen/Qwen3-0.6B. Χωρίς αυτό, το sim ακολουθεί το path του HF tokenization, προσπαθεί να φτάσει ένα render service στοlocalhost:8082, και κάθε model-server pod πέφτει σεCrashLoopBackOff.
llm-d: Υπέρ και κατά#
Υπέρ#
| Πλεονέκτημα | Περιγραφή |
|---|---|
| Inference-aware routing | Ο EPP κάνει routing με βάση το cache locality, την πίεση στο KV-cache και το queue depth, δηλαδή τα metrics που καθορίζουν πραγματικά το latency ενός LLM. Το round-robin δεν βλέπει κανένα από αυτά. |
| Βασισμένο σε standards | Χτισμένο πάνω στο Gateway API Inference Extension, ένα project των Kubernetes SIGs, όχι σε proprietary control plane. Το InferencePool είναι πραγματικό, portable CRD. |
| Prefill/decode disaggregation | Κάνεις scale τις δύο φάσεις ανεξάρτητα και ταιριάζεις τύπο GPU στην καθεμία: μεγάλο κέρδος αποδοτικότητας στην πράξη, για workloads όπου prompts και outputs δεν έχουν το ίδιο βάρος. |
| KV-cache σε tiers | Το offload του LMCache σε GPU/CPU/disk κάνει τα μεγάλα multi-turn sessions οικονομικά εφικτά και μειώνει τους επανυπολογισμούς. |
| Engine-agnostic | vLLM σήμερα, με υποστήριξη για SGLang και TensorRT-LLM. Δεν είσαι κλειδωμένος σε ένα engine. |
| Δοκιμάζεται χωρίς GPU | Με το llm-d-inference-sim αναπτύσσεις και περνάς από CI ολόκληρο το orchestration layer σε commodity hardware. Σπάνιο και πραγματικά χρήσιμο. |
| Σοβαρή υποστήριξη | Δωρεά των IBM/Red Hat/Google, με NVIDIA, AMD, Hugging Face και Mistral από πίσω. Δεν είναι πείραμα ενός vendor. |
Κατά#
| Περιορισμός | Περιγραφή |
|---|---|
| Ωριμότητα Sandbox | Είναι CNCF Sandbox project. Τα APIs αλλάζουν ακόμα από release σε release (το kustomize-first migration ήρθε στο v0.7). Κάνε pin τις εκδόσεις και περίμενε churn. |
| Υψηλό κατώφλι πολυπλοκότητας | Gateway API + GAIE + InferencePool + EPP + LWS + LMCache: πολλά κινούμενα μέρη. Για ένα μικρό μοντέλο είναι overkill. |
| Η αξία θέλει κλίμακα | Κάτω από workloads με πολλά replicas, πολλά nodes και υψηλό QPS, ο μηχανισμός του routing και του disaggregation σου δίνει ελάχιστα. |
| Το GPU plumbing είναι δικό σου πρόβλημα | Το llm-d θεωρεί δεδομένους τους schedulable accelerators. Τον GPU Operator, τα node pools και έναν scheduler (Kueue/Volcano) τα φέρνεις εσύ. |
| Το documentation μένει πίσω από τον κώδικα | Κάποια paths στο site του documentation δίνουν 404 ή είναι πίσω από το repo. Όπως και στο KServe, το αρχείο guides/env.sh στο source είναι το πιο αξιόπιστο version pin. |
| Νεαρό ecosystem | Λιγότερα δοκιμασμένα production references από το KServe ή το σκέτο vLLM. Θα είσαι early adopter. |
Πότε να χρησιμοποιήσεις το llm-d#
Χρησιμοποίησέ το όταν:#
- Σερβίρεις ένα ή λίγα μεγάλα μοντέλα με πραγματικό traffic - τον λογαριασμό των GPU τον καθορίζουν λίγα μοντέλα και πρέπει να στύψεις το utilization
- Τα prompts σου μοιράζονται μεγάλα prefixes - RAG, μεγάλα system prompts, few-shot templates - όπου το routing με βάση το prefix cache κόβει πολύ latency
- Τα προφίλ του prefill και του decode διαφέρουν - μεγάλα prompts με μικρά outputs ή το αντίστροφο, όπου το disaggregation σε αφήνει να δώσεις το σωστό μέγεθος σε κάθε φάση
- Τρέχεις multi-node replicas μοντέλων - το orchestration με LeaderWorkerSet είναι first-class
- Κάνεις standardize πάνω στο Gateway API - το llm-d πατάει στο ίδιο GAIE που υιοθετούν το KServe και άλλοι
Δες εναλλακτικές όταν:#
- Σερβίρεις ένα μικρό μοντέλο με χαμηλό QPS - τρέξε το vLLM απευθείας πίσω από ένα Service. Ο μηχανισμός του llm-d εδώ είναι νεκρό βάρος.
- Χρειάζεσαι πλατφόρμα serving για πολλά frameworks - sklearn, XGBoost, ONNX και LLMs μέσα από ένα abstraction - χρησιμοποίησε το KServe. Όχι αντί για το llm-d: το
LLMInferenceServiceτου KServe τρέχει llm-d από κάτω, οπότε παίρνεις και τα δύο από ένα control plane. - Θέλεις έναν έτοιμο managed operator - το KAITO κάνει auto-provision τα GPU node pools και κάνει deploy open-weight μοντέλα με λιγότερη συναρμολόγηση.
- Το bottleneck σου είναι το GPU scheduling, όχι το routing - ξεκίνα με Kueue ή Volcano για fair-share και gang scheduling.
Troubleshooting#
| Πρόβλημα | Συμπτώματα | Λύση |
|---|---|---|
| Το InferencePool δεν κάνει reconcile | Το Gateway δεν έχει endpoints, τα requests παίρνουν 503 | Βεβαιώσου ότι τα GAIE CRDs (inference.networking.k8s.io) είναι εγκατεστημένα και ότι το InferencePool.spec.selector ταιριάζει ακριβώς με τα labels των model-server pods. |
| Όλα τα requests πάνε σε ένα pod | Άνισο φορτίο ακόμα και με διαφορετικά prompts | Ο no-hit LRU scorer πρέπει να είναι ενεργός μαζί με τον prefix scorer. Μόνο με prefix scoring, τα cold requests μπορεί να στοιβαχτούν. Έλεγξε το scorer config του EPP. |
| Sim pods σε CrashLoopBackOff | failed to create vLLM simulator ... dial tcp [::1]:8082: connection refused | Ένα πραγματικό όνομα μοντέλου HF στέλνει το sim στο path του HF tokenizer, που χρειάζεται το render service. Πρόσθεσε το --force-dummy-tokenizer ή χρησιμοποίησε όνομα μοντέλου που δεν είναι HF. |
| Τα metrics του EPP επιστρέφουν 401 / 500 | /metrics άδειο ή Authentication failed | Το port θέλει authentication. Δώσε bearer token και κάνε bind το system:auth-delegator στο ServiceAccount του EPP, για να μπορεί να κάνει TokenReview. Το router chart δεν φέρνει κανένα από τα δύο. |
| Δεν βρίσκεται το Service του router | Το port-forward αποτυγχάνει, connection refused | Το Service είναι το llm-d-router-epp. Το llm-d-router είναι το όνομα του HelmRelease και του InferencePool, όχι του Service. |
| Το Kind cluster δεν δημιουργείται | Bind for 0.0.0.0:80 failed: port is already allocated | Κάποιο άλλο cluster ή ένας ingress controller κρατάει το host port 80. Βγάλε τα extraPortMappings από το Kind config: το demo φτάνει παντού με port-forward. |
| Version skew | CRD no matches for kind | Ταίριαξε τις εκδόσεις του GAIE, του router chart και του EPP image με το guides/env.sh του llm-d release που στοχεύεις (GAIE v1.5.0, router v0.9.0 για το v0.8.x). |
| Gateway pending σε Kind | Το Address του Gateway δεν συμπληρώνεται ποτέ | Το Kind δεν έχει cloud LB. Χρησιμοποίησε το standalone path με το clusterIP του EPP ή port-forward, όπως κάνει το demo. |
Τι αλλάζει στο production#
Το demo βάζει ένα simulator στη θέση των GPU. Ένα πραγματικό deployment διαφέρει ακριβώς εκεί που μετράει περισσότερο.
Πραγματικές GPU και το engine#
Στο production τα model-server pods τρέχουν vllm/vllm-openai με limits nvidia.com/gpu, --enable-prefix-caching, --tensor-parallel-size για multi-GPU και έναν connector --kv-transfer-config για το disaggregation. Ένα μοντέλο 32B σε μία H100 δίνει δεκάδες tokens/sec ανά stream. Το disaggregation και το cache-aware routing είναι αυτά που κρατούν το p99 σταθερό όσο προσθέτεις replicas. Το simulator αναπαράγει τη συμπεριφορά του routing, όχι όμως αυτά τα νούμερα throughput.
Sizing του prefill/decode#
Το disaggregation αποδίδει μόνο αν όντως χωρίσεις τα pods και τους δώσεις μέγεθος με βάση το traffic σου. Ένα RAG workload με prompts 4k tokens και απαντήσεις 200 tokens είναι prefill-heavy: δώσε περισσότερο capacity στο prefill. Ένα chat workload με μικρά prompts και μεγάλες απαντήσεις είναι decode-heavy: κάνε το αντίθετο. Μέτρα την αναλογία prompt προς output πριν κλειδώσεις τη μοιρασιά.
Tiers του KV-cache#
Η ιεραρχία offload του LMCache χρειάζεται πραγματικό storage από πίσω στο production: γρήγορο τοπικό NVMe για το tier CPU→disk, ή ένα shared cache (Redis, ένα object store) αν θέλεις cache reuse ανάμεσα σε pods. Το sizing αυτών των tiers είναι η διαφορά ανάμεσα στο να συνεχίσεις μια συνομιλία αμέσως και στο να την υπολογίσεις ξανά.
Autoscaling#
Κάνε scale τα pools των model servers με βάση τα inference metrics (KV-cache utilization και queue depth από τον EPP), όχι τη CPU. Συνδύασέ το με έναν node autoscaler (Karpenter ή cluster-autoscaler) που κάνει scale το ίδιο το GPU node pool, και με Kueue για fair-share ανάμεσα στις ομάδες.
Κόστος#
Εδώ η ιστορία κλείνει τον κύκλο της. Distributed inference σημαίνει πολλές ακριβές GPU που τρέχουν πολλές ώρες, και το cache-aware routing μαζί με το disaggregation υπάρχουν σε μεγάλο βαθμό για να μειώσουν αυτόν τον λογαριασμό. Για να δεις τον λογαριασμό (ανά μοντέλο, ανά ομάδα, ανά GPU-hour) χρειάζεσαι cost attribution. Αυτό είναι χωράφι του OpenCost, και είναι το φυσικό επόμενο post της σειράς: να βάλουμε ένα ποσό σε δολάρια πάνω στην πλατφόρμα GPU inference που χτίζουμε, πρώτα με το KServe και τώρα με το llm-d.
Το repository του demo#
Ολόκληρο το demo βρίσκεται στο github.com/nicknikolakakis/srekubecraft-demo/tree/main/llm-d. Περιλαμβάνει:
- Ένα
Taskfile.ymlμεtask up/task downγια να σηκώσεις όλο το setup σε Kind - Manifests
GitRepository+Kustomizationτου Flux για παράδοση με GitOps - Τη βάση με GAIE + Gateway API, το model-server Deployment με το
llm-d-inference-sim, τοInferencePoolκαι τοHelmReleaseτου EPP - Τα Mermaid διαγράμματα αυτού του post
- Ένα README με walkthrough και τα routing metrics όπως καταγράφηκαν σε πραγματικό run σε Kind
Γρήγορη εκκίνηση:
git clone https://github.com/nicknikolakakis/srekubecraft-demo
cd srekubecraft-demo/llm-d
task up # creates the kind cluster, bootstraps flux, reconciles everything
Συμπέρασμα#
Το llm-d απαντάει σε μια ερώτηση που το KServe άφησε σκόπιμα ανοιχτή: όταν ένα μεγάλο μοντέλο πρέπει να εξυπηρετήσει πραγματικό traffic σε ένα fleet από GPU, πώς κάνεις routing, disaggregation και caching του inference ώστε το tail latency να μένει σταθερό και οι GPU να μην κάθονται; Ένα απλό Kubernetes Service δεν μπορεί, γιατί τα σήματα που μετράνε (prefix cache locality, πίεση στο KV-cache, queue depth, ο διαχωρισμός prefill/decode) είναι αόρατα στο επίπεδο του HTTP. Το llm-d τα φέρνει στην επιφάνεια μέσω του Gateway API Inference Extension και τα κάνει αποφάσεις routing.
Είναι CNCF Sandbox project, οπότε περίμενε churn στα APIs και φέρε το δικό σου GPU plumbing. Η αρχιτεκτονική όμως είναι σωστή, η υποστήριξη είναι σοβαρή, και το ότι μπορείς να τρέξεις ολόκληρο το orchestration layer σε ένα laptop με το vLLM simulator (χωρίς GPU, πλήρως αναπαραγώγιμο μέσω Flux) δείχνει ότι οι maintainers νοιάζονται για την καθημερινή λειτουργία, όχι μόνο για τα benchmarks.
Αν τρέχεις ένα μικρό μοντέλο, χρησιμοποίησε το vLLM απευθείας. Αν λίγα μεγάλα μοντέλα καθορίζουν τον λογαριασμό των GPU και το p99 σου ανεβαίνει σιγά σιγά υπό φορτίο, το llm-d είναι το εργαλείο του 2026 που φτιάχτηκε ακριβώς γι’ αυτό το πρόβλημα. Και αν είσαι ήδη σε KServe, δεν είναι δεύτερη πλατφόρμα που πρέπει να υιοθετήσεις: το LLMInferenceService τρέχει llm-d από κάτω, οπότε η συμπεριφορά του routing που είδες εδώ φτάνει σε σένα μέσα από ένα CRD που ήδη ξέρεις.
Επόμενο στη σειρά: βάζουμε κόστος σε όλα αυτά με το OpenCost.
Αν σου φάνηκε χρήσιμο, ίσως σε ενδιαφέρουν και τα σχετικά posts μου:
- KServe - Production ML serving σε Kubernetes, από sklearn μέχρι LLMs
- Φτιάχνοντας έναν Kubernetes agent με το ADK της Google για Go
- Dapr σε Kubernetes - Το Distributed Application Runtime

