Three projects sit above the inference engine, and two of them document themselves wrongly: vLLM production-stack's README lists three routing algorithms while the parser in the release it shipped on 24 July 2026 accepts six, and llm-d's version badge reads 0.8 while the project is on v0.9.0. For a team running models on its own hardware, the separator is not throughput but what one upgrade moves underneath you. llm-d v0.9.0 carried vLLM from v0.23.0 to v0.26.0 across four image variants; Dynamo v1.4.1 states that backend runtime versions are unchanged from v1.4.0 and moved only Go modules; production-stack's chart resolves four images to tag latest, sets imagePullPolicy Always on three of them, and declares no appVersion at all. llm-d and Dynamo both land on vLLM v0.26.0 and SGLang v0.5.16, so they disagree about how often you get dragged to a new engine, not about where it is. The configuration shapes differ more than the routing quality: llm-d composes an EndpointPickerConfig plugin pipeline, Dynamo takes a --router-mode enum with seven values, production-stack takes a required --routing-logic enum with six. Every manifest you write this quarter is against an alpha or in-migration API: llm-d.ai/v1alpha1 for llm-d, v1alpha1 and v1beta1 with different storage versions for Dynamo, v1alpha1 only for production-stack's four operator CRDs. Rank the three by engine pinning first, CRD stability second, registry surface third, routing sophistication fourth, and this week run helm template or kustomize build against whichever one you are trialling and grep the rendered output for image tags.
Most comparisons of llm-d vs the vLLM production stack, and of either one against NVIDIA Dynamo, are written from README files, and two of the three projects document themselves wrongly. production-stack's README lists three routing algorithms and marks prefix-aware work in progress, while the argument parser in the release it shipped on 24 July 2026 accepts six values. Its roadmap still lists autoscaling and disaggregated prefill as upcoming, and the same tag ships both. llm-d's version badge reads 0.8 while the project is on v0.9.0.
For a team running models on its own hardware, the separator is not throughput. It is what one upgrade moves underneath you. llm-d v0.9.0 carried vLLM from v0.23.0 to v0.26.0 across four image variants in a single release. Dynamo v1.4.1 states in its own notes that backend runtime versions are unchanged from v1.4.0. production-stack's chart leaves the engine, router, sidecar and cache-server images at tag latest and declares no appVersion, so there is no engine version in the artifact to re-qualify against. Everything below is read from those three tags.
Three layers above the engine, and what this comparison is not
Both projects that define this layer define it the same way. Dynamo's README calls itself an orchestration layer above inference engines and says it does not replace SGLang, TensorRT-LLM or vLLM; llm-d's says model servers run models on accelerators and llm-d orchestrates above them. production-stack is the same layer on a narrower base: one engine.
This is replica selection inside one model pool by cache overlap, not routing between models or providers. Which provider or price tier a request should go to is the AI gateway decision and routing cheap models first, a different layer with a different buyer. This post also starts after the engine choice, which is SGLang versus vLLM and vLLM versus Ollama versus TensorRT-LLM.
Prefill and decode disaggregation no longer separates them. All three ship it: llm-d has a pd-disaggregation guide, Dynamo marks disaggregated serving supported on all three of its backends, and production-stack has two disaggregated-prefill routers. Whether to split the pools belongs to prefill and decode disaggregation at serving scale.
Three and not five, deliberately. KServe v0.20.0, published 6 August 2026, upgraded its llm-d components and its Gateway API Inference Extension dependency, so its LLM path consumes llm-d rather than competing with it. AIBrix is a real fourth option, Apache-2.0, under the vLLM organisation, released v0.7.0 on 18 June 2026, with a white paper at arXiv:2504.03648. We read its feature list, not its code, so it gets no table row.
| Project | Latest release | Stable releases in 2026 | Engines shipped in the release | Runs without Kubernetes |
|---|---|---|---|---|
| llm-d | v0.9.0, 17 Aug 2026 | 7 | vLLM v0.26.0, SGLang v0.5.16, TRT-LLM 1.3.0rc23 | Yes, via the no-Kubernetes guide: endpoint picker plus Envoy, plain YAML |
| NVIDIA Dynamo | v1.4.1, 22 Aug 2026 | 15 | vLLM v0.26.0, SGLang v0.5.16, TensorRT-LLM v1.3.0rc22 | Yes, --discovery-backend file, no etcd and no NATS |
| vLLM production-stack | vllm-stack-0.1.12, 24 Jul 2026 | 4 | vLLM only | No, it is a Helm chart |
Release cadence and blast radius: what one upgrade actually moves
servingEngineSpec:
imagePullPolicy: "IfNotPresent" # chart default is Always
modelSpec: # this key is a list, one entry per model
- name: "qwen"
repository: "lmcache/vllm-openai"
tag: "<the tag or digest you qualified>" # chart default is latest
sidecar:
image: "lmcache/lmstack-sidecar:<qualified tag>"
imagePullPolicy: "IfNotPresent" # chart default is Always
routerSpec:
repository: "lmcache/lmstack-router"
tag: "<qualified tag>" # chart default is latest
imagePullPolicy: "IfNotPresent" # chart default is Always
cacheserverSpec:
image:
repository: "lmcache/vllm-openai"
tag: "<qualified tag>" # chart default is latestapiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
images:
- name: REPLACE_MODEL_SERVER_IMAGE
newName: docker.io/vllm/vllm-openai
newTag: v0.26.0The shipping rhythm
The gap before each project's current release says more than the yearly counts: 7 days for Dynamo, 52 for llm-d, 78 for production-stack. Do not read the three version strings as a maturity ranking. vllm-stack-0.1.12 is the version field of helm/Chart.yaml. llm-d v0.9.0 is a release-train tag whose own components ship ahead of it, since guides/env.sh sets ROUTER_RELEASE_VERSION, ROUTER_CHART_VERSION and ROUTER_EPP_VERSION to v0.10.0. Only Dynamo's is a product semver.
What moved underneath the engine
llm-d v0.9.0 is a large move: three minor versions of vLLM, v0.23.0 to v0.26.0, across four image variants, plus SGLang and a TensorRT-LLM image that had no previous tag. Dynamo v1.4.1 is the opposite, and says so under Key Dependencies, where backend runtime versions are unchanged from v1.4.0 and the only dependency changes it lists are Go modules. Both land on vLLM v0.26.0 and SGLang v0.5.16 anyway, so they disagree about how often you get dragged there, not about where there is. In a validated environment, a release that re-qualifies the engine is a change-control event, not a patch.
The image tag is the change-control decision
At vllm-stack-0.1.12, production-stack's helm/values.yaml leaves four images at tag latest: the serving engine and the cache server, both lmcache/vllm-openai, plus the router and the sidecar. Three of the four also carry imagePullPolicy Always; only the cache server defaults to IfNotPresent. The documented install is helm repo add vllm https://vllm-project.github.io/production-stack then helm install vllm vllm/vllm-stack -f tutorials/assets/values-01-minimal-example.yaml. In a regulated environment, a pod restart six weeks later can pull a different inference engine, with no change ticket and nothing to compare against. Override four image tags and three pull policies on day one: llm-d makes the pin an artifact you can review, as a kustomize Component: A sibling nightly/ directory holds the same Component with a nightly tag, so the choice is made by which path a guide includes and shows up in a diff. Dynamo removes the question with version-tagged runtime containers on nvcr.io: sglang-runtime:1.4.1, tensorrtllm-runtime:1.4.1 and vllm-runtime:1.4.1.
| Project | How the engine is pinned | What the most recent release changed in the engine |
|---|---|---|
| llm-d | kustomize image Component with an explicit newTag | vLLM v0.23.0 to v0.26.0 on four image variants, TPU image from v0.25.0, SGLang v0.5.13.post1 to v0.5.16, TRT-LLM image added |
| Dynamo | version-tagged NGC runtime containers at 1.4.1 | nothing: the notes state backend runtime versions are unchanged from v1.4.0 |
| production-stack | Helm values, default tag latest with imagePullPolicy Always, so the engine is whatever lmcache/vllm-openai:latest resolves to at pull time | not stated: Chart.yaml declares no appVersion |
Routing: a plugin graph, a mode flag, and a required mode flag
The real difference is not which algorithms exist but the shape each project makes you express the decision in: a plugin graph, an optional enum and a required enum.
production-stack takes --routing-logic, required, with no default and six values at the shipped tag. Both of its README files understate this, one listing three algorithms and the other only roundrobin and session. The parser is the authority, and the spellings are literal: prefixaware is one word, the two disaggregated values use underscores, and the KV-aware value is kvaware. A hyphen in any of them fails at startup.
vllm-router --port 8001 \
--service-discovery k8s \
--k8s-namespace vllm \
--k8s-label-selector environment=production \
--routing-logic prefixaware \
--prefix-min-match-length 512 \
--engine-stats-interval 10 \
--log-format jsonThat --prefix-min-match-length 512 is not decoration: the flag defaults to 0, which disables the threshold, and below it the router falls back to QPS-based routing. The prefix-aware router's docstring is honest about its model of the world: it assumes no eviction of the prefix cache. kvaware avoids that assumption by asking the engines and pays a dependency for it, building an LMCache controller manager over ZMQ sockets, with --kv-aware-threshold defaulting to 2000.
Dynamo puts the same decision behind --router-mode, or DYN_ROUTER_MODE, with seven values, of which kv is the cache-aware one. The KV path is then tuned numerically rather than structurally: --router-kv-overlap-score-credit defaults to 1.0 and at 0 the router ignores prefix caches and skips creating a local indexer, and host and disk cache hits weigh 0.75 against 0.25. Selecting the mode is one flag on the frontend, plus KV event publishing on each worker:
export PYTHONHASHSEED=0
python3 -m dynamo.frontend --router-mode kv &
DYN_SYSTEM_PORT=8081 CUDA_VISIBLE_DEVICES=0 python3 -m dynamo.vllm \
--model Qwen/Qwen3-0.6B \
--block-size 64 \
--kv-events-config '{"publisher":"zmq","topic":"kv-events","endpoint":"tcp://*:20080","enable_kv_cache_events":true}' &
DYN_SYSTEM_PORT=8082 CUDA_VISIBLE_DEVICES=1 python3 -m dynamo.vllm \
--model Qwen/Qwen3-0.6B \
--block-size 64 \
--kv-events-config '{"publisher":"zmq","topic":"kv-events","endpoint":"tcp://*:20081","enable_kv_cache_events":true}' &llm-d has no mode enum at all. Routing is a document: an EndpointPickerConfig at llm-d.ai/v1alpha1, with a plugins list and schedulingProfiles that reference plugins by pluginRef. The optimized-baseline guide composes four.
apiVersion: llm-d.ai/v1alpha1 kind: EndpointPickerConfig plugins: - type: approx-prefix-cache-producer - type: inflight-load-producer - type: prefix-cache-affinity-filter - type: token-load-scorer schedulingProfiles: - name: default plugins: - pluginRef: prefix-cache-affinity-filter - pluginRef: token-load-scorer
One tag, two vocabularies. The no-Kubernetes guide in the same release writes an EndpointPickerConfig with entirely different plugin names for the same job: file-discovery, then queue-scorer, kv-cache-utilization-scorer, prefix-cache-scorer and no-hit-lru-scorer at weights 2, 2, 3 and 2. Its own comment claims that set mirrors the optimized baseline, which it does not. Treat them as two configurations to choose between, not as old and new.
The tunables carry hardware assumptions, and llm-d says so: peakPrefillThroughput 15928 on prefix-cache-affinity-filter is calibrated for Qwen3-32B on an H100 80 GB at tensor parallel 2, and the guide tells you to measure your own.
None of this is the engine's own prefix cache, a separate mechanism covered in why prefix cache hit rates collapse in multi-turn agents. This layer decides which replica a request reaches, so that the cache on that replica is warm for it. A perfect engine cache behind round-robin selection is a cache you pay for and do not use.
| Project | How you select a strategy | What is available | What else it needs |
|---|---|---|---|
| llm-d | an EndpointPickerConfig document at llm-d.ai/v1alpha1: plugins plus schedulingProfiles | composable producers, filters, scorers and pickers: four plugins in the optimized baseline, six in the precise prefix-cache guide | a Gateway implementation with GAIE CRDs, or plain Envoy on the no-Kubernetes path |
| Dynamo | --router-mode flag or DYN_ROUTER_MODE | round-robin (default), random, power-of-two, kv, direct, least-loaded, device-aware-weighted | nothing required: etcd and NATS are optional |
| production-stack | --routing-logic flag, required, no default | roundrobin, session, kvaware, prefixaware, disaggregated_prefill, disaggregated_prefill_orchestrated | an LMCache controller for kvaware |
Backends: a published matrix, three sets of images, and one engine by design
Dynamo is the only one of the three that publishes a per-backend feature matrix, worth reading before you assume parity. It lists six features across SGLang, TensorRT-LLM and vLLM. Five of them, disaggregated serving, KV-aware routing, the SLA-based planner, multimodal and tool calling, are supported on all three. The sixth, KVBM, is marked in progress on SGLang and supported on the other two. Run two engines behind one layer, as the SGLang versus vLLM comparison argues is increasingly common, and that matrix tells you which capability disappears when a workload moves.
llm-d ships images for all three engines plus a new Intel XPU SGLang image, so it is not the vLLM-centric option. Its well-lit paths do lean vLLM: v0.9.0 carries 19 guide directories, 9 of them with committed benchmark-results, and workload-autoscaling alone holds six variants.
production-stack is vLLM-only by design and coupled to LMCache: its default images come from the lmcache organisation, and kvaware will not start without an LMCache controller. Our own Ollama versus vLLM comparison called this stack Helm charts, dashboards and model-aware routing out of the box, which is worth correcting: the routing is a strategy flag you must set, and the charts are unpinned.
Read the published numbers carefully. llm-d's README lists six performance results and Dynamo's lists five, most crediting an outside party on named hardware, but two of llm-d's six and one of Dynamo's five link only to the project's own blog. Treat the set as existence proofs of a mechanism, not as numbers you will hit. production-stack publishes no headline figure, while shipping a benchmark harness and two benchmarking tutorials.
The Gateway API convergence, and what actually ports
llm-d and Dynamo have converged on the same Kubernetes standard: an endpoint picker behind a Gateway API Inference Extension gateway, with the pool exposed as an InferencePool on a standard HTTPRoute. This is the object both sit behind:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: qwen
spec:
parentRefs:
- name: inference-gateway
rules:
- matches:
- headers:
- name: X-Gateway-Model-Name
type: Exact
value: Qwen/Qwen3-0.6B
path:
type: PathPrefix
value: /
backendRefs:
- group: inference.networking.k8s.io
kind: InferencePool
name: qwen-pool
port: 8000
timeouts:
request: 300sllm-d v0.9.0 pins Gateway API CRDs at v1.5.1 and the inference extension at v1.5.0 in guides/env.sh. Upstream, the inference extension released v1.6.0 on 17 August 2026, the same day llm-d shipped v0.9.0, so llm-d is one minor behind by hours rather than by neglect. Dynamo documents two topologies: its native path, client to frontend to router to workers, and the gateway path, client to gateway to endpoint picker to a frontend sidecar running in --router-mode direct.
What ports is the wire path: the Gateway, the HTTPRoute, the InferencePool and the header-based model match. What does not port is the tuning. llm-d writes an EndpointPickerConfig with its own plugin type names; Dynamo writes an epp component with its own, a disagg-profile-handler and a label-filter keyed on nvidia.com/dynamo-component-type. Migrating means rewriting every scheduling decision you made, under a gateway that stays byte-identical. production-stack sits outside this convergence, with its own router process and its own operator CRD group.
On-prem: air gap, alpha CRDs, and what you re-qualify
A managed endpoint hides this whole layer, which is why the cloud buyer never chooses. A team with 8 to 32 GPUs in its own racks has to. Start with the API version, where all three answer differently.
Every manifest a platform team writes this quarter targets an alpha API or a group mid-migration. That belongs in the risk register at design time, not in the postmortem of the first upgrade. Dynamo's request kind is at least already served at v1beta1:
apiVersion: nvidia.com/v1beta1
kind: DynamoGraphDeploymentRequest
metadata:
name: my-model
spec:
model: Qwen/Qwen3-0.6B
backend: vllm
sla:
ttft: 200.0 # ms
itl: 20.0 # ms
autoApply: trueThose two SLA numbers drive Dynamo's planner, whose own defaults are an on-prem shape check: ttft_ms 500.0, itl_ms 50.0, max_gpu_budget 8, min_endpoint 1. A default GPU budget of 8 is the size of cluster this argument is about. Do not accept 500 ms and 50 ms without checking them against your workload; TTFT and tokens-per-second targets by workload is the basis for setting them.
Then the cluster question. Dynamo runs on a single node with --discovery-backend file and needs neither etcd nor NATS. llm-d has a no-Kubernetes path too, deploying the endpoint picker, Envoy and vLLM workers as plain YAML, with the picker reading /etc/epp/endpoints.yaml, where every entry must be a literal IPv4 address because file-discovery does not resolve hostnames. production-stack has no such path: it is a Helm chart.
The awkward part for llm-d is that the route an air-gapped team would take is pinned furthest behind the release. That same guide exports VLLM_IMAGE as vllm/vllm-openai:v0.19.1 while the rest of the v0.9.0 release ships v0.26.0, and it uses the second plugin vocabulary. Nothing explains the gap, so verify that floor yourself.
None of the three documents an air-gapped install procedure, so the mirror list is something you assemble. llm-d pulls charts and its endpoint picker image from ghcr.io, engine images from docker.io and the TensorRT-LLM image from nvcr.io. Dynamo pulls its platform chart from an NGC Helm host and its runtimes from nvcr.io. production-stack pulls its chart from a GitHub Pages repository, its images from the lmcache organisation, and two Prometheus subcharts from an OCI registry. Node-level egress is covered in air-gapped vLLM deployment; this layer multiplies the registries a change window has to escort.
| Project | API group | Kinds you author | Served versions |
|---|---|---|---|
| llm-d | llm-d.ai for routing config, inference.networking.k8s.io for the pool | EndpointPickerConfig, InferencePool | EndpointPickerConfig at v1alpha1, with GAIE CRDs v1.5.0 and Gateway API CRDs v1.5.1 |
| Dynamo | nvidia.com | DynamoGraphDeployment, DynamoGraphDeploymentRequest, and seven more CRDs | v1alpha1 and v1beta1 both served: DynamoGraphDeployment stores at v1alpha1, DynamoGraphDeploymentRequest at v1beta1 |
| production-stack | production-stack.vllm.ai | VLLMRuntime, VLLMRouter, CacheServer, LoRAAdapter | v1alpha1 only |
Which one to run, by cluster size, engine commitment and change-control appetite
Run Dynamo if your accelerators are NVIDIA, you want one artifact to qualify, and you value that a patch release does not move the engine underneath you. It is also the cheapest of the three to pilot, since it needs no cluster.
Run llm-d if you need AMD, Intel XPU, TPU or CPU serving, or if you want committed reproducible recipes per accelerator rather than a matrix in a README. Budget for an engine re-qualification per minor release, and for a routing configuration that is alpha-versioned and composed rather than selected.
Run the vLLM production stack if you are vLLM-only, already run LMCache, and want the smallest thing that works. It is the shortest install of the three, and the only one whose defaults will hurt you in a validated environment, so pin the four image tags and the three pull policies first.
For a regulated buyer, score them in this order: engine pinning first, CRD API stability second, registry surface third, routing sophistication fourth. Throughput claims do not enter the ranking, because no published figure at this layer was measured on your hardware. The rest of the framework for this cluster sits on our LLM models pillar.
This week, render whichever one you are trialling and read the images out of it. For the Helm chart that is helm template vllm vllm/vllm-stack -f your-values.yaml | grep -E 'image:'; for llm-d it is kustomize build on the overlay your guide points at. Diff that list against the engine version your validation evidence describes. If any line ends in :latest, you have found the thing that will move underneath you, before the auditor did.
FAQ
Quick answers to the questions this post tends to raise.



