Vanilla vLLM GAIE On-ramp

Add Dynamo’s advanced KV-aware routing to GAIE deployments that use upstream vLLM images.

View as Markdown

This on-ramp uses the experimental standalone mode of the Dynamo Endpoint Picker Plugin (EPP). This is a special operator-free integration for existing vanilla vLLM fleets, not the standard operator-managed Gateway API path. Standalone mode is not available in a published Dynamo release yet. Use an image built from a Dynamo revision that includes standalone EPP support.

Use this on-ramp to add Dynamo’s advanced KV-aware routing to Gateway API Inference Extension (GAIE) deployments that run stock vllm serve workers. Stock vLLM means the upstream vLLM container images. You do not need to replace them with Dynamo vLLM images, install the Dynamo operator, or migrate the rest of the deployment to Dynamo. The standalone Dynamo Endpoint Picker Plugin (EPP) adds the routing layer while your Gateway, HTTPRoute, and InferencePool remain standard GAIE resources and your workers keep running the upstream vLLM images.

The EPP runs the same Dynamo worker selection service used by a full Dynamo deployment. In standalone mode, it runs in-process without a DynamoGraphDeployment (DGD) or the Dynamo distributed runtime and event plane.

This is still the Gateway API routing topology described in KV-Aware Routing on Kubernetes. Standalone mode changes who creates the resources and how the EPP discovers worker state; it does not add a third request-entry topology.

Architecture

The InferencePool is the source of truth for the worker labels and HTTP port. The EPP watches that pool and Ready pods in its namespace, subscribes directly to each worker’s KV event socket, and calls vLLM’s /v1/chat/completions/render endpoint to tokenize each routing request. The Gateway forwards the original request to the selected pod. The render Service is not a separate renderer workload; it is a stable Kubernetes Service address for the HTTP endpoint served by the same vLLM pods.

ConcernStandalone on-rampOperator-managed Gateway API routing
WorkersStock vLLM podsDynamo workers and direct-mode Frontend sidecars
Resource lifecycleUser-managed Deployments, Services, RBAC, InferencePool, and HTTPRouteDGD and Dynamo operator
Worker discoveryKubernetes pod watch driven by InferencePool.spec.selectorDynamo runtime discovery
KV stateDirect per-pod vLLM ZMQ subscriptionsDynamo event plane
TokenizationvLLM /v1/chat/completions/render ServiceDynamo model-card preprocessor
Supported serving shapeAggregated vLLM with data-parallel size 1 per podAggregated and disaggregated Dynamo deployments

For the operator-managed architecture and generated-resource boundary, see Gateway API Routing Architecture.

Before You Begin

You need:

  • A Kubernetes cluster with GPU nodes.
  • kubectl and Helm configured for the cluster.
  • A Dynamo source checkout.
  • A container registry that every cluster node can pull from.
  • Two schedulable GPUs for the example’s two vLLM replicas. You can scale the worker Deployment to one replica for a smoke test.

The example serves the public Qwen/Qwen3-0.6B model, which does not require a Hugging Face token.

Build the EPP Image

Standalone mode is not available in a published Dynamo image yet. From the repository root, build the Rust EPP from the current source revision and push it to your container registry:

$export EPP_IMAGE=registry.example.com/your-project/dynamo-rust-epp:standalone
$
$make -C deploy/inference-gateway/ext-proc \
> IMAGE_TAG="$EPP_IMAGE" \
> image-push

Choose a Deployment Path

Choose the path that matches your starting point:

  • Deploy from Scratch installs the Gateway API layer and applies the complete example, including vLLM workers, the EPP, an InferencePool, and an HTTPRoute.
  • Use the Dynamo EPP in an Existing GAIE Deployment keeps your current Gateway, HTTPRoute, InferencePool, and vLLM workers. Deploy a replacement EPP as shown below, or adapt the existing EPP Deployment in place with the same configuration.

Both paths use the same example names so the verification commands apply to either path:

ResourceExample Name
Workload namespacegaie-vllm-onramp ($NAMESPACE)
Gateway namespaceagentgateway-system ($AGW_NAMESPACE)
Gatewayinference-gateway
vLLM Deploymentvllm-qwen
vLLM HTTP/render Servicevllm-qwen-render
EPP Deployment and Servicedynamo-epp
InferencePoolvllm-qwen-pool
HTTPRoutevllm-qwen-route

For an existing GAIE deployment, these are example names. Replace them in the commands and YAML snippets with the names of your existing resources.

Set the shared namespace variables:

$export NAMESPACE=gaie-vllm-onramp
$export AGW_NAMESPACE=agentgateway-system

Install the Gateway API Layer

Install Gateway API, GAIE, agentgateway, and the Gateway:

$./deploy/inference-gateway/scripts/install_gaie_crd_agentgateway.sh

The script pins the supported Gateway API, GAIE, and agentgateway versions. It creates the inference-gateway Gateway in $AGW_NAMESPACE; the http listener allows HTTPRoute resources from workload namespaces. The on-ramp resources remain in $NAMESPACE.

Verify that the Gateway is programmed:

$kubectl wait gateway/inference-gateway -n "$AGW_NAMESPACE" \
> --for=condition=Programmed --timeout=180s

Configure Model Access

The public Qwen/Qwen3-0.6B model in the example does not require authentication. To substitute a gated or private Hugging Face model, create a Secret in the workload namespace before deploying:

$read -rsp "Hugging Face token: " HF_TOKEN
$echo
$kubectl create secret generic hf-token-secret -n "$NAMESPACE" \
> --from-literal=HF_TOKEN="$HF_TOKEN"
$unset HF_TOKEN

Add the Secret reference to the vLLM container in agg.yaml:

1env:
2 - name: HF_TOKEN
3 valueFrom:
4 secretKeyRef:
5 name: hf-token-secret
6 key: HF_TOKEN

Deploy the On-ramp

The installation script above creates the Gateway API and GAIE layer. The example manifest then creates the workload layer in $NAMESPACE:

  • Two stock vLLM workers with prefix caching, KV events, and readiness probes.
  • A vllm-qwen-render Service that selects those same workers and exposes their HTTP port for /v1/chat/completions/render.
  • Namespaced read-only RBAC for pods, InferencePool, and EndpointSlice resources.
  • Two standalone EPP replicas and their gRPC and replica-synchronization Service.
  • An InferencePool and an HTTPRoute that attaches to the cross-namespace Gateway.

Apply the manifest from the repository root using the image you pushed:

$sed "s#dynamo/dynamo-rust-epp:dev#$EPP_IMAGE#" \
> deploy/inference-gateway/ext-proc/examples/onramp/agg.yaml |
$ kubectl apply -n "$NAMESPACE" -f -

The checked-in manifest is also available as agg.yaml.

Verify the Deployment

These commands apply to both paths because they use the same example names. Wait for the vLLM workers and EPP:

$kubectl wait deployment/vllm-qwen -n "$NAMESPACE" \
> --for=condition=Available --timeout=600s
$
$kubectl wait deployment/dynamo-epp -n "$NAMESPACE" \
> --for=condition=Available --timeout=180s

Confirm that the route attached to the intended Gateway:

$kubectl get httproute/vllm-qwen-route -n "$NAMESPACE" \
> -o jsonpath='{range .status.parents[*]}{.parentRef.name}{"\\t"}{range .conditions[*]}{.type}={.status}{" "}{end}{"\\n"}{end}'

The output should identify inference-gateway and report Accepted=True and ResolvedRefs=True.

Find the Service generated for the Gateway and start a port-forward:

$export GATEWAY_SERVICE=$(kubectl get service -n "$AGW_NAMESPACE" \
> -l gateway.networking.k8s.io/gateway-name=inference-gateway \
> -o jsonpath='{.items[0].metadata.name}')
$
$kubectl port-forward -n "$AGW_NAMESPACE" "service/$GATEWAY_SERVICE" 8000:80

In another terminal, send an OpenAI-compatible request:

$curl --max-time 120 -sS http://localhost:8000/v1/chat/completions \
> -H 'content-type: application/json' \
> -d '{
> "model": "Qwen/Qwen3-0.6B",
> "messages": [
> {"role": "user", "content": "Write one sentence about prefix caching."}
> ],
> "max_tokens": 64
> }'

Inspect the EPP logs near the request:

$kubectl logs -n "$NAMESPACE" deployment/dynamo-epp --tail=200

The logs should show worker discovery and endpoint selection. If the model responds but the EPP logs show no selection, the route is bypassing the InferencePool.

EPP Replication

Each EPP replica has an in-process selector and KV index. When DYN_EPP_PEER_SERVICE is set, replicas watch their own Service’s EndpointSlice resources and discover its named TCP replica-agg port. They synchronize admission, prefill-complete, and free events so active-load accounting converges across replicas.

Replica synchronization does not copy the full KV index to a new EPP. A new replica warms its index from live worker events and optional replay. For consistency details, see Standalone Selection Service.

For a single EPP replica, set spec.replicas: 1 and remove DYN_EPP_PEER_SERVICE, POD_IP, and the replica-agg Service port.

Standalone EPP Configuration Reference

VariableRequiredMeaning
DYN_EPP_MODE=standaloneYesRuns the embedded selection service without the Dynamo runtime.
DYN_EPP_INFERENCE_POOL_NAMEYesName of the InferencePool that drives worker discovery.
POD_NAMESPACEYesNamespace containing the EPP, pool, and workers; inject with the downward API.
DYN_MODEL_NAMEYesServed model ID. Requests must use the same model value.
DYN_EPP_TOKENIZER_SERVICE_URLYesBase URL of the vLLM render Service.
DYN_EPP_TOKENIZER_PROTOCOL=vllm-renderYesTokenizer wire protocol; this is the only supported value.
DYN_KV_CACHE_BLOCK_SIZEYesMust equal vLLM --block-size.
DYN_EPP_KV_EVENT_PORTNoPer-pod vLLM KV event port; defaults to 5557.
DYN_EPP_KV_EVENT_REPLAY_PORTNoPer-pod replay port for KV event gap recovery.
DYN_EPP_MAX_NUM_BATCHED_TOKENSPolicy-dependentMust equal vLLM --max-num-batched-tokens; required when router queue thresholds are enabled.
DYN_EPP_TOTAL_KV_BLOCKSNoOptional per-worker total KV block hint.
DYN_EPP_SELECTION_INDEXER_THREADSNoKV indexer thread count; defaults to 4.
DYN_EPP_TOKENIZATION_TIMEOUT_MSNoRender request timeout; defaults to 5000.
DYN_EPP_TOKENIZER_MAX_RESPONSE_BYTESNoMaximum render response size; defaults to 16777216.
DYN_EPP_MAX_INFLIGHT_REQUESTSNoConcurrent EPP request guardrail; defaults to 1024, and excess requests receive 503.
DYN_EPP_PEER_SERVICENoEPP Service used to discover sibling replicas.
POD_IPWith peer serviceEPP pod IP used to exclude the local replica from its peer set.

Scope and Limitations

The standalone path currently targets aggregated vLLM serving with data-parallel size 1 per pod. It does not provide the DGD/operator lifecycle, Dynamo runtime discovery, disaggregated prefill/decode orchestration, request migration, topology-aware routing, or full operator-managed observability.

Use Route Requests with Gateway API when you want the supported operator-managed lifecycle, Dynamo workers, or disaggregated serving. Use Gateway API Routing Reference for the DGD and operator-generated resource contract; that contract does not configure standalone mode.

Troubleshoot

SymptomCheck
EPP exits with standalone is not yet supportedThe image predates the standalone implementation. Build or select the required dynamo-rust-epp revision.
EPP stays not readyConfirm the pool exists, its selector is valid, at least one matching pod is Ready, and EPP RBAC can watch all three resource types.
EPP reports no schedulable workersSet DYN_EPP_MAX_NUM_BATCHED_TOKENS to the vLLM --max-num-batched-tokens value when queueing is enabled.
Route has no accepted parentConfirm parentRefs[].namespace names the Gateway namespace, sectionName names its listener, and the listener allows routes from the workload namespace.
Tokenization failsCheck the render Service endpoints and call /v1/chat/completions/render from the EPP namespace.
Prefix locality does not improveConfirm the vLLM event publisher and EPP event port match, and that the EPP block size equals vLLM --block-size.

Follow the full request path when debugging:

$kubectl describe gateway/inference-gateway -n "$AGW_NAMESPACE"
$kubectl describe httproute/vllm-qwen-route -n "$NAMESPACE"
$kubectl get inferencepool/vllm-qwen-pool -n "$NAMESPACE" -o yaml
$kubectl get pods -n "$NAMESPACE" -l app=vllm-qwen -o wide
$kubectl logs -n "$NAMESPACE" deployment/dynamo-epp --tail=200

Clean Up the From-Scratch Example

If you used the Deploy from Scratch path, delete the example resources:

$kubectl delete -n "$NAMESPACE" \
> -f deploy/inference-gateway/ext-proc/examples/onramp/agg.yaml

Delete the namespace only if it contains no resources you want to keep:

$kubectl delete namespace "$NAMESPACE"