From 346a634cd2b5133cc7084a93b30c7cb5df910c46 Mon Sep 17 00:00:00 2001 From: Ezra Silvera Date: Tue, 26 May 2026 18:26:41 +0300 Subject: [PATCH 1/5] running-llm-d-without-kubernetes Signed-off-by: Ezra Silvera --- ...-05-26_running-llm-d-without-kubernetes.md | 543 ++++++++++++++++++ blog/authors.yml | 7 + .../llm-d-file-discovery-arch.png | Bin 0 -> 33578 bytes 3 files changed, 550 insertions(+) create mode 100644 blog/2026-05-26_running-llm-d-without-kubernetes.md create mode 100644 static/img/blogs/running-llm-d-without-kubernetes/llm-d-file-discovery-arch.png diff --git a/blog/2026-05-26_running-llm-d-without-kubernetes.md b/blog/2026-05-26_running-llm-d-without-kubernetes.md new file mode 100644 index 0000000..1a5bc2b --- /dev/null +++ b/blog/2026-05-26_running-llm-d-without-kubernetes.md @@ -0,0 +1,543 @@ +--- +title: "No Kubernetes? No Problem: llm-d Now Runs Anywhere" +description: "llm-d's new file-discovery plugin lets you run the full routing stack on bare metal, Slurm, Ray, or your laptop, with no Kubernetes required." +slug: running-llm-d-without-kubernetes +date: 2026-05-26T09:00 + +authors: + - ezrasilvera + +tags: [blog, scheduling, inference, llm-d] +--- + +# No Kubernetes? No Problem: llm-d Now Runs Anywhere + +llm-d was designed as a Kubernetes-native inference stack, and its guides assume you have a cluster handy. However, a large class of inference workloads runs on infrastructure that isn't managed by Kubernetes, and until recently llm-d was not a fit for them. + +With the **llm-d router**'s new **file-discovery plugin**, that changes. llm-d can now run as a plain process or container in any environment, with no dependency on Kubernetes or any other cluster framework. A YAML file lists your endpoints; the router reads it and reconciles changes live. That's the whole interface. + +That opens the door to deployments like: + +- **HPC clusters** running Slurm, where GPU nodes are allocated per-job and there is no cluster API +- **Ray-based training loops** (VERL, OpenRLHF) where rollout workers are Ray actors, not pods +- **Bare-metal inference farms** provisioned statically +- **Local development** on a workstation with one or two GPUs + +This post introduces the new endpoint-discovery plugin mechanism in the llm-d router. It then shows how to use llm-d without a Kubernetes cluster by enabling the file-discovery plugin, which reads endpoints from a YAML file on disk. We illustrate this with two concrete examples that generate the endpoints file from a Ray cluster and a Slurm job. + + + +## How llm-d normally discovers endpoints + +The Endpoint Picker (EPP), the routing engine inside the llm-d router, normally watches a Kubernetes `InferencePool` object and the pods it selects. As pods come and go, the EPP's internal datastore is updated automatically via the controller-runtime manager. + +That machinery requires a live Kubernetes API server, an `InferencePool` CRD, and appropriate RBAC. On an HPC cluster or a Ray job, none of that exists. + +## The llm-d Discovery plugin + +To support alternative endpoint-discovery mechanisms, we recently introduced a general `EndpointDiscovery` plugin interface in the EPP framework. Anything that can enumerate endpoints and stream upsert/delete events can be plugged in: a file on disk, Consul, etcd, a custom registry, a cloud provider's service-discovery API, etc. + +In the future, the existing Kubernetes watch-based discovery is also expected to move behind this interface, so all discovery paths share the same plugin model. + +The interface is small ([`pkg/epp/framework/interface/datalayer/discovery.go`](https://github.com/llm-d/llm-d-router/blob/main/pkg/epp/framework/interface/datalayer/discovery.go)): + +```go +type EndpointDiscovery interface { + fwkplugin.Plugin + // Start begins discovery; blocks until ctx is cancelled or a fatal error occurs. + Start(ctx context.Context, notifier DiscoveryNotifier) error + // Ready is used to gate request-serving until the datastore is populated. + Ready() <-chan struct{} +} + +type DiscoveryNotifier interface { + // Upsert adds or updates an endpoint in the datastore. + Upsert(endpoint *EndpointMetadata) + // Delete removes an endpoint from the datastore. + Delete(id types.NamespacedName) +} +``` + +A plugin tells the EPP about endpoints by calling `Upsert` and `Delete` on the notifier. `Start` runs the plugin's main loop, typically an initial enumeration of the source followed by a watch that emits further `Upsert`/`Delete` calls as endpoints come and go. `Ready()` returns a channel that closes once the initial enumeration has populated the EPP datastore, so request-serving can be gated on a non-empty endpoint pool. + +## The file-discovery plugin + +The file-discovery plugin uses a plain YAML or JSON file on disk as its source of inference endpoints. The plugin reads the file at startup and optionally watches it (via `fsnotify`) for subsequent changes, emitting `Upsert`/`Delete` events as entries are added, modified, or removed. + +When this plugin is used, **the EPP has no dependency on any Kubernetes service or object**: no API server, no watchers, no controller manager, no `InferencePool` CRD, no RBAC, no `kubeconfig`. **It can run on a host without a Kubernetes cluster anywhere in sight.** + +The core EPP features are unchanged. KV-cache-utilization scoring, prefix-cache affinity, and Prometheus metrics all work identically. + +Some features that are currently configured through Kubernetes CRDs, FlowControl (driven by `InferenceObjective`) and model-name rewriting (driven by `InferenceModelRewrite`), are not available when using the file-discovery plugin. A subset of these may move behind plugin interfaces in the future. + +
+ llm-d file-discovery architecture +

Figure 1: FileDiscovery plugin in llm-d

+
+ +## Setting it up + +### 1. The endpoints file + +Save as `/etc/epp/endpoints.yaml`: + +```yaml +endpoints: + - name: vllm-0 + address: "10.0.0.1" + port: "8000" + labels: + model: llama-3-8b + + - name: vllm-1 + address: "10.0.0.2" + port: "8000" + labels: + model: llama-3-8b +``` + +An endpoint is defined using the following fields: + +| Field | Required | Notes | +|---|---|---| +| `name` | yes | Identifier of the endpoint; must be unique within the file. Used as the endpoint key in the EPP datastore and in metrics labels. | +| `address` | yes | The IP address of the inference worker (the host running vLLM). Must be a valid IPv4 address. The EPP uses `address:port` both for routing requests and for scraping the worker's `/metrics` endpoint. | +| `port` | yes | TCP port on `address` where vLLM is listening. Integer 1-65535, written as a string. | +| `namespace` | no | Logical grouping name for the endpoint, retained from the Kubernetes-native data model where endpoints live in a namespace. Outside Kubernetes there is no real namespace concept; this is just a string tag used in the endpoint's identity (`namespace/name`) and in metrics labels. Defaults to `"default"` and most non-Kubernetes deployments can leave it unset. | +| `labels` | no | Arbitrary key/value pairs surfaced to scheduler plugins. Used for things like `llm-d.ai/role: prefill` in P/D setups, or `model: llama-3-8b` for model-aware filters. | + +> **Note:** `address` must be a literal IPv4 address. Hostnames are not resolved by the plugin. In environments where you only have hostnames (common in Slurm and Ray), resolve them upstream of writing the file. The Slurm and Ray examples later in this post show how. + +### 2. EPP config + +Save as `/etc/epp/config.yaml`: + +```yaml +apiVersion: llm-d.ai/v1alpha1 +kind: EndpointPickerConfig + +plugins: + - name: file-discovery + type: file-discovery + parameters: + path: /etc/epp/endpoints.yaml + watchFile: true # optional, default is false. When true, reconcile the datastore whenever the file changes + + - name: max-score-picker + type: max-score-picker + + - name: single-profile-handler + type: single-profile-handler + + - name: metrics-source + type: metrics-data-source + + - name: metrics-extractor + type: core-metrics-extractor + +schedulingProfiles: + - name: default + plugins: + - pluginRef: max-score-picker + +dataLayer: + injectDefaults: false + discovery: + pluginRef: file-discovery # this line loads the file-discovery plugin and effectively switches off the Kubernetes path + sources: + - pluginRef: metrics-source + extractors: + - pluginRef: metrics-extractor +``` + +This is a minimal config. For a more complete plugin set (saturation detector, prefix-cache affinity, flow control, etc.), see the [`optimized-baseline` router values](https://github.com/llm-d/llm-d/blob/main/guides/optimized-baseline/router/optimized-baseline.values.yaml) or the [router recipes](https://github.com/llm-d/llm-d/tree/main/guides/recipes/router); the file-discovery section drops in alongside the rest. + +Controlling whether the EPP takes the file-discovery path comes down to the `dataLayer.discovery.pluginRef` field. When present, the EPP takes the file-discovery path. When it is absent, the EPP behaves as before and requires Kubernetes. + +When `watchFile` is `false`, the file is read once at startup and never re-read. When set to `true`, it enables live reload: the EPP upserts new endpoints and deletes removed ones whenever the file changes, without a restart. This is the key property that makes dynamic environments, where workers appear and disappear, work correctly. + +### 3. Start the EPP + +```bash +epp \ + --pool-name my-pool \ + --config-file /etc/epp/config.yaml \ + --grpc-port 9002 \ + --grpc-health-port 9003 \ + --metrics-port 9090 +``` + +The binary is built from the `cmd/epp` target of the [`llm-d-router`](https://github.com/llm-d/llm-d-router) repo (build from the latest `main` until the release lands), or pulled from the `ghcr.io/llm-d/llm-d-router-endpoint-picker` image, which will include file-discovery starting with the upcoming **llm-d 0.8** release. + +`--pool-name` is optional in file-discovery mode and defaults to `epp` if unset. The value is arbitrary; it does not reference any Kubernetes object and is used only as the pool identifier in metrics and logs. The startup command above passes it explicitly so the metrics labels reflect a meaningful name. + +Similarly, `--pool-namespace` defaults to `default` outside Kubernetes (where there is no Downward API); pass it explicitly if you want metrics and logs labeled for your environment. The EPP emits a startup warning if it falls back to the default. + +On startup the EPP logs `EPP starting (file discovery mode)` along with the discovery plugin name and the resolved pool name and namespace. If `watchFile: true`, the file-discovery plugin also logs `watching endpoints file for changes`, and re-emits `endpoints file changed, reloading` on each subsequent reload. + +### 4. Envoy config + +The EPP selects an endpoint but does not proxy traffic itself. You still need Envoy (or a compatible proxy) to accept client requests and forward them to the EPP-selected backend. + +The EPP communicates its selection by setting the `x-gateway-destination-endpoint` header on the `ext_proc` response. Envoy's `ORIGINAL_DST` cluster type reads that header and forwards the request to the address it contains. The Envoy config is fully static; no Kubernetes service discovery involved. + +This is the same shape as llm-d's **standalone deployment mode**, where the EPP runs alongside an Envoy proxy without a Gateway API controller. The reference is the standalone Helm chart values file: + +> [`config/charts/llm-d-router-standalone/values.yaml`](https://github.com/llm-d/llm-d-router/blob/main/config/charts/llm-d-router-standalone/values.yaml). See `router.proxy.presets.envoy.configMap.data` for the upstream-blessed Envoy config. The version below is a trimmed equivalent suitable for running directly on a host. + +Save as `/etc/envoy/envoy.yaml`: + +```yaml +admin: + address: + socket_address: { address: 127.0.0.1, port_value: 19000 } + +static_resources: + listeners: + - name: inference + address: + socket_address: { address: 0.0.0.0, port_value: 8080 } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: inference + route_config: + virtual_hosts: + - name: inference + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: original_destination_cluster + timeout: 86400s + idle_timeout: 86400s + http_filters: + - name: envoy.filters.http.ext_proc + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor + grpc_service: + envoy_grpc: + cluster_name: epp + authority: localhost:9002 + timeout: 10s + processing_mode: + request_header_mode: SEND + response_header_mode: SEND + request_body_mode: FULL_DUPLEX_STREAMED + response_body_mode: FULL_DUPLEX_STREAMED + request_trailer_mode: SEND + response_trailer_mode: SEND + message_timeout: 1000s + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: epp + type: STATIC + connect_timeout: 86400s + lb_policy: LEAST_REQUEST + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: epp + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: { address: 127.0.0.1, port_value: 9002 } + + - name: original_destination_cluster + type: ORIGINAL_DST + connect_timeout: 1000s + lb_policy: CLUSTER_PROVIDED + circuit_breakers: + thresholds: + - max_connections: 40000 + max_pending_requests: 40000 + max_requests: 40000 + original_dst_lb_config: + use_http_header: true + http_header_name: x-gateway-destination-endpoint +``` + +For production deployments outside Kubernetes, where there is no kubelet to restart a crashed EPP, it is worth adding a gRPC `health_checks` block to the `epp` cluster so Envoy stops routing to a dead EPP. The standalone chart linked above shows the canonical configuration (10s interval, unhealthy threshold of 3, gRPC health-check service `envoy.service.ext_proc.v3.ExternalProcessor`). + +The config above assumes Envoy and the EPP are colocated on the same host (the gRPC link uses `127.0.0.1:9002`, plaintext). When Envoy and the EPP run on separate nodes, for example Envoy on a Slurm head node and the EPP on a service node, the gRPC link traverses the network and should be secured with TLS. Configure an `UpstreamTlsContext` on the `epp` cluster's `transport_socket` (the `llm-d-router-standalone` chart's `transport_socket` block is the reference) and serve gRPC over TLS on the EPP side. + +### 5. Start Envoy + +```bash +envoy -c /etc/envoy/envoy.yaml +``` + +Requests to `http://localhost:8080/v1/completions` are now routed by the EPP to one of the vLLM instances. + +## P/D disaggregated setup + +llm-d also supports **prefill/decode disaggregation** (P/D), where the compute-bound prefill stage and the memory-bandwidth-bound decode stage run on separate workers and the KV cache is transferred between them. The deployment is two pools: prefill workers running vLLM directly, and decode workers running vLLM behind a `pd-sidecar` that orchestrates remote prefill and the KV transfer. + +The full deployment recipe (sidecar flags, vLLM `kv-transfer-config`, NIXL/RDMA setup, EPP plugin wiring with `disagg-profile-handler` and `prefix-based-pd-decider`, and the scheduling profiles) is documented upstream and is identical for non-Kubernetes deployments. Use those as the reference: + +- [`llm-d/guides/pd-disaggregation`](https://github.com/llm-d/llm-d/tree/main/guides/pd-disaggregation): end-to-end deployment guide. +- [`llm-d-router/docs/disaggregation.md`](https://github.com/llm-d/llm-d-router/blob/main/docs/disaggregation.md): request-lifecycle and component reference. +- [`llm-d/guides/pd-disaggregation/router/pd-disaggregation.values.yaml`](https://github.com/llm-d/llm-d/blob/main/guides/pd-disaggregation/router/pd-disaggregation.values.yaml): canonical P/D EPP config (full plugin set with prefill and decode profiles). + +**The only thing this post adds is how to swap Kubernetes-driven discovery for the YAML file.** Two changes: + +1. Add the file-discovery plugin and `dataLayer.discovery.pluginRef` to the upstream P/D EPP config (same as in the single-pool setup earlier in this post). +2. Mark each endpoint's role in the YAML with the `llm-d.ai/role` label: `prefill` for prefill workers, `decode` for decode workers. For decode endpoints, the `port` is the pd-sidecar's port, not vLLM's. The router's prefill/decode filters select candidates by this label. + +```yaml +endpoints: + - name: prefill-0 + address: "10.0.0.10" + port: "8000" # vLLM directly + labels: + llm-d.ai/role: prefill + + - name: decode-0 + address: "10.0.0.20" + port: "8000" # the pd-sidecar's port + labels: + llm-d.ai/role: decode +``` + +The full set of role label values (including combined roles like `prefill-decode` and `encode-prefill-decode`) is listed in [`bylabel/roles.go`](https://github.com/llm-d/llm-d-router/blob/main/pkg/epp/framework/plugins/scheduling/filter/bylabel/roles.go). + +## Integrating with non-Kubernetes orchestrators + +Integrating llm-d in file-discovery mode with any non-Kubernetes environment comes down to two things: + +1. **Run the EPP and Envoy** on a node that can reach your vLLM workers, using the configs and commands from the [Setting it up](#setting-it-up) section above. +2. **Generate the endpoints file** in the format shown above, using whatever source knows your worker set: Ray's Python API, Slurm's `$SLURM_JOB_NODELIST`, an inventory tool, a static list, and so on. If the endpoint set needs to change at runtime as workers come and go, set `watchFile: true` and the EPP will reconcile continuously. + +The two examples below show one way to generate the endpoint list, for Ray and Slurm. + +> **Note:** For deployments with very dynamic endpoint inventories, where workers come and go faster than is comfortable to track via a regenerated file, a dedicated `SlurmDiscovery` or `RayDiscovery` plugin can be implemented against the same `EndpointDiscovery` interface. Such a plugin would talk to the orchestrator's API directly (Ray's Python API or Slurm's controller) and emit `Upsert`/`Delete` events as workers change, without any file in the loop. The file-discovery plugin shown here is the simplest path and works well for most cases; the orchestrator-native plugins are an optimization for the most dynamic scenarios. + +### Ray + +In a Ray deployment, vLLM workers run as remote processes on Ray cluster nodes. The Ray Python API exposes the current cluster membership, including node IP addresses, so generating the endpoints file is straightforward. + +```python +#!/usr/bin/env python3 +""" +generate_epp_endpoints.py + +Usage: python generate_epp_endpoints.py [vllm_port] [output_path] + +Run this after Ray workers are started and before launching the EPP. +""" +import ray +import yaml +import socket +import sys + +VLLM_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 +OUTPUT = sys.argv[2] if len(sys.argv) > 2 else "/etc/epp/endpoints.yaml" + +ray.init(address="auto") + +endpoints = [] +for i, node in enumerate(ray.nodes()): + if not node["Alive"]: + continue + # Skip nodes with no GPU resources - they are not running vLLM + if node.get("Resources", {}).get("GPU", 0) == 0: + continue + + # NodeManagerAddress is the raylet's bind address - typically already an IP, + # but resolve defensively in case a Ray deployment exposes it as a hostname. + address = node["NodeManagerAddress"] + ip = socket.gethostbyname(address) + + endpoints.append({ + "name": f"vllm-{i}", + "address": ip, + "port": str(VLLM_PORT), + "labels": { + "ray-node-id": node["NodeID"][:12], + }, + }) + +with open(OUTPUT, "w") as f: + yaml.dump({"endpoints": endpoints}, f, default_flow_style=False) + +print(f"Wrote {len(endpoints)} endpoints to {OUTPUT}") +``` + +This fits naturally into a startup sequence: + +```bash +# 1. Start Ray workers and vLLM on GPU nodes (your existing orchestration) +python launch_rollout_workers.py + +# 2. Generate the endpoints file +python generate_epp_endpoints.py 8000 /etc/epp/endpoints.yaml + +# 3. Start EPP and Envoy +epp \ + --pool-name ray-pool \ + --config-file /etc/epp/config.yaml \ + --grpc-port 9002 --grpc-health-port 9003 --metrics-port 9090 & + +envoy -c /etc/envoy/envoy.yaml & +``` + +Because `watchFile: true` is set in the EPP config, the endpoints file can be regenerated whenever the worker pool changes, for example between RL training rounds when rollout workers are restarted with a new model checkpoint. The EPP reconciles the change without a restart: + +```python +# Regenerate after workers are replaced for the next training round +generate_endpoints(new_worker_ips, "/etc/epp/endpoints.yaml.tmp") +os.rename("/etc/epp/endpoints.yaml.tmp", "/etc/epp/endpoints.yaml") +# The atomic rename triggers fsnotify; the EPP updates its pool automatically +``` + +### Slurm + +In a Slurm environment, a batch job requests a fixed set of nodes via `#SBATCH --nodes`. The standard approach is to designate the first node as the "head" (running EPP and Envoy) and use the remaining nodes for vLLM. + +Slurm provides the allocated node list in `$SLURM_JOB_NODELIST` as a compact range expression like `node[01-05]`. The `scontrol show hostnames` command expands that into individual hostnames, and a short Python snippet resolves them to IPs for the endpoints file. + +```bash +#!/bin/bash +#SBATCH --job-name=llm-d-serve +#SBATCH --nodes=5 +#SBATCH --gpus-per-node=8 +#SBATCH --time=04:00:00 + +MODEL=meta-llama/Meta-Llama-3-8B +MODEL_PORT=8000 +WORK_DIR=/scratch/$USER/$SLURM_JOB_ID + +mkdir -p $WORK_DIR/epp + +# --- Resolve node list ------------------------------------------------- +ALL_NODES=($(scontrol show hostnames $SLURM_JOB_NODELIST)) +HEAD_NODE=${ALL_NODES[0]} +WORKER_NODES=("${ALL_NODES[@]:1}") + +# --- Generate endpoints.yaml ------------------------------------------- +python3 - < /dev/null 2>&1; do + if (( waited >= MAX_WAIT_SECS )); then + echo "ERROR: $node not ready after ${MAX_WAIT_SECS}s, aborting" >&2 + exit 1 + fi + sleep 5 + waited=$(( waited + 5 )) + done + echo " $node ready" +done + +# --- Start EPP + Envoy on the head node -------------------------------- +srun --ntasks=1 --nodes=1 --nodelist="$HEAD_NODE" \ + epp \ + --pool-name slurm-$SLURM_JOB_ID \ + --config-file $WORK_DIR/epp/config.yaml \ + --grpc-port 9002 \ + --grpc-health-port 9003 \ + --metrics-port 9090 & + +srun --ntasks=1 --nodes=1 --nodelist="$HEAD_NODE" \ + envoy -c $WORK_DIR/envoy.yaml & + +wait +``` + +For jobs where the serving pool may change during the allocation (a node fails and is replaced, or model weights are swapped), the endpoints file can be atomically replaced and the EPP will reconcile without downtime: + +```bash +python3 regenerate_endpoints.py > $WORK_DIR/epp/endpoints.yaml.tmp +mv $WORK_DIR/epp/endpoints.yaml.tmp $WORK_DIR/epp/endpoints.yaml +``` + +## Troubleshooting + +A few failure modes that trip up first-time deployments: + +- **`address` is a hostname, not an IP.** The EPP rejects entries where `address` doesn't parse as an IP. Slurm and Ray surface hostnames, so resolve them with `socket.gethostbyname` (or equivalent) before writing the file. +- **EPP can't reach vLLM's metrics port.** The EPP scrapes `/metrics` on each endpoint at `address:port`. If a host firewall or a network policy blocks that port from the EPP node, scoring plugins silently degrade to default values: routing still works, but KV-cache scoring becomes meaningless. Check the EPP's pool-health metrics on `--metrics-port` to confirm endpoints are reporting. +- **Envoy returns 503 with `no_healthy_upstream`.** Almost always means the EPP gRPC connection is down. Check that the EPP is running on `localhost:9002`, that `--grpc-port` matches Envoy's `authority`, and (if you added the `health_checks` block) that the EPP's gRPC health service is enabled. +- **`watchFile: true` doesn't pick up an edit.** The watcher reacts to fsnotify events on rename/replace, which is what `mv tmp final` produces. Editors that truncate-then-write (some `vim` configurations, certain IDEs) may emit a different event sequence and either double-fire or miss. Always update the file via atomic rename, as both examples in this post do. +- **vLLM hasn't finished loading weights when the EPP starts.** If the EPP scrapes a vLLM that isn't yet serving, the endpoint shows up as unhealthy and gets excluded until the next reconcile. The Slurm script avoids this by polling `/health` on each worker before starting the EPP; do the same in any orchestration that doesn't already gate on readiness. + +## Parity with the Kubernetes-native llm-d deployment + +The file-discovery plugin gives you most of the llm-d routing stack outside of Kubernetes: + +- **KV-cache-utilization scoring**: routes requests away from instances with high cache pressure +- **Prefix-cache affinity**: sends requests with shared prompt prefixes to the instance most likely to have them cached +- **Saturation-based admission**: the saturation detector still gates request admission, so a saturated pool sheds load rather than overloading backends. The full FlowControl layer (per-flow queueing and fairness, driven by `InferenceObjective`) is not active in file-discovery mode; see the caveat earlier in this post. +- **Prometheus metrics**: EPP exports scheduling and pool health metrics on `--metrics-port` + +What is no longer handled by llm-d outside Kubernetes is endpoint lifecycle: there is no automatic deregistration when a vLLM process dies. This responsibility shifts to the surrounding framework or orchestrator (Ray, Slurm, a custom controller, etc.) which needs to detect failed workers and rewrite the endpoints file accordingly. For production deployments, this typically means adding a health-monitoring agent that drops unavailable workers from the file. + +## What's next + +The file-discovery plugin is the simplest non-Kubernetes integration point. It works well when the worker pool is relatively static and changes infrequently; regenerating the file at those transitions is enough. For environments where the worker set churns more frequently, a static file with periodic regeneration still works but requires external orchestration to keep it in sync. + +**Additional / future plugins.** The `EndpointDiscovery` interface is intentionally minimal so more plugins can be added as the need arises. A few directions we expect to see: + +- **Orchestrator-native plugins**: a `RayDiscovery` or `SlurmDiscovery` plugin that talks to Ray's Python API or Slurm's controller directly, emitting `Upsert`/`Delete` events as workers change without any file in the loop. Useful for highly dynamic worker pools. +- **Service-registry plugins**: Consul, etcd, or a cloud provider's service-discovery API as the source of endpoints. +- **Migrating Kubernetes discovery to a plugin**: the existing watch-based Kubernetes path is currently wired into the EPP directly. Moving it behind the same `EndpointDiscovery` interface would unify all discovery paths under a single model and remove a special case from the EPP. + +**RL integration.** We are currently working on integrating the no-Kubernetes llm-d with RL frameworks that run on Ray and Slurm (VERL, OpenRLHF). Our next blog post will cover that integration and initial results. This will include a custom `EndpointDiscovery` plugin that registers and deregisters endpoints in real time as Ray actors come up and are torn down between training rounds. We will also show how llm-d's prefix-cache routing translates into a concrete throughput benefit for the repeated-prompt patterns typical of RLHF rollouts. diff --git a/blog/authors.yml b/blog/authors.yml index fe14792..ab7a328 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -198,3 +198,10 @@ abdullahgharaibeh: url: https://github.com/ahg-g image_url: https://avatars.githubusercontent.com/u/40361897?v=4 +ezrasilvera: + name: Ezra Silvera + title: Senior Technical Staff Member, IBM + image_url: https://avatars.githubusercontent.com/ezrasilvera?v=4 + email: ezra@il.ibm.com + socials: + github: https://github.com/ezrasilvera diff --git a/static/img/blogs/running-llm-d-without-kubernetes/llm-d-file-discovery-arch.png b/static/img/blogs/running-llm-d-without-kubernetes/llm-d-file-discovery-arch.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c23c06703272b428855eb3821c8c8e59aaa19b GIT binary patch literal 33578 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 literal 0 HcmV?d00001 From e39bae0ff729e0079813b28abf8bccebea959d62 Mon Sep 17 00:00:00 2001 From: Pete Cheslock Date: Tue, 26 May 2026 15:02:39 -0400 Subject: [PATCH 2/5] Update blog author image Signed-off-by: Pete Cheslock --- blog/authors.yml | 3 ++- static/img/blogs/ezrasilvera.webp | Bin 0 -> 22368 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 static/img/blogs/ezrasilvera.webp diff --git a/blog/authors.yml b/blog/authors.yml index ab7a328..050bcbb 100644 --- a/blog/authors.yml +++ b/blog/authors.yml @@ -201,7 +201,8 @@ abdullahgharaibeh: ezrasilvera: name: Ezra Silvera title: Senior Technical Staff Member, IBM - image_url: https://avatars.githubusercontent.com/ezrasilvera?v=4 + image_url: /img/blogs/ezrasilvera.webp email: ezra@il.ibm.com socials: github: https://github.com/ezrasilvera + linkedin: ezra-silvera-9746682 diff --git a/static/img/blogs/ezrasilvera.webp b/static/img/blogs/ezrasilvera.webp new file mode 100644 index 0000000000000000000000000000000000000000..935bd6bb26670288a0cbc2c53edd2f285ab23abf GIT binary patch literal 22368 zcmV(zK<2+vNk&FkR{#K4MM6+kP&gn=R{#L;$O4@KDj)+O13r;Pol2#nsi~#X$>A^( z31>84Q`}>y$_#E^JpL2N=bn8XqtBXyXJUK8(wTo(@qXa{{pn-nf8_uC|BKj_{lDsc zIqc-g-zxm!`v|`0aR2AkK1;QIwmFS@CjCm=s;?TUL^kSb^mP|Ux>KhCS`+#`EYJ3M zuEhdncS~Qq^%UZxi&9>*byei~bs-j?Fm3Mv2?_>Bgdrcl(36_cGR~R~xtRX>B2tSc zK_3xc$XS5?|CAuM+<_p{hOwyEcj2eH#RcB3nXt{TI$(93xr{_}!b`nXc}idrnFN=@FDo;4v-awol8X2U&qy zv%t35;{gtlRQ$kgk=L$pf<`t9Y)fUXH|FYF^5E9=D+t!k;Mirxn~N$cFWnA{ zzn-Ct1R^DtqAbg_^2c{a`@2$5N8BUlG`AziZg^$X|KUL(E3M@Ee9{mtktXR zMpnDX{;Uh@mO(Og84QL$pP3hYU; zp{OCj@5B0Pi(U#+NO!+7mV-XpPbF^Z(bB#b-R8+)r_U-TGNphlbX)8kn%Q8OZUUOU zw%VEN$)src$RNrW)S(x;PeJk2lk}Nw`xC1~mG^?D#_k}zrfyXM;Ib5tXUSP5BKfyz zfQUxE#`T>J@!W%S1GWDG#O5^uvzI)|T%Tj$4h)SjsjyBbzDNF>AL+4O!06aD$=QEY z@KIL=l~T^DRt@I{yOD-|+pYHz6xWmN3>oTt>j@Kh1-RA8N?xh*Qh?=-q37;FXaSkO z(LnH7ZF+wa>t6ul#H_1Dt4I_!G+h(X0nkw+Z$X~d8{(`WbZVl((myY{%+t`eWYT4` z>NQ6YoCV4bD)7qoX_iL%D!4|j3_enCX_={TM5{?>3J&>@j81gh_~f{FRQP`0EC}yXsTL2%CUg;B|aY;7$hJntzBnPyXjOjo%9W z$uQRFtFLx(Ka~^@$FP@P2g)w;2uojTd53>05 zfsJ~GH&`k$nYDQVy>B_Ba;Uaz&!+kdhlgPoxK+(iv(2kRKM@*sa#52LK5{Xs5t0{* zUCt13yK+*$;ynI)jzYBIo=Cm4%-|$gTSDdq6mAc9Gn~9@H@pEfhxWJsj0TaXgAD#D z<2JkEyn%Jl^NT4<*J*QRGD#(HvLcE!AsX8Qn5v;)2=IlrtzydFk?PhdGYiO~U}KD> zbEzieF=vT3A_Ct0XBRNSy_b<_-7>9Z-ENB2h>^0&~)9D{`&hf zxrRJ_JEf@2Nx*CoikrJgcx!29H4&qulzcIXlx!g%?~t7a8>mnol{L(f2`0d6kJJ`R zb*O%c3@t4cnyqYmTzLf$7xC6L(>e-EvJ4w)OzA&`0ltjj7*R0Kx@8;*P@e?!~FvJO%=VkGYRG-@F;(|B>4_) z{2eU&E&!?3wK#_&Z^O>9z?(IO-2A6M`#S t;OK_ zqCo}$M>7t2qCwy z*t9)Wf_^&HVi+Uc8g~#UtS>ECmA^_SBv{v#|ChTSW*%TostG9ttSyo^v|aogI~g|V zW?IpsK51|IxwH9WR{+y8y>q5!)MTabI9vCz@0WdCj3pW(mK9WTc6! zB1I5BuN2g4czD}`@fFHs#jE1ueDzEj1CWN;OU%Z!R>J2Y$)pVK;p@w ze`sFri6rhA2Q|Eh_B55w(ksm_z(0;Qpe1&y2_S&O)@vOsmWx&CdKJjF_yx4Gp0QS; zCopRgpN`*R{4Q8WT~YEh-T($j7VjC@ZQP$K$}0cz`R|d+q2z9OBx5k7wLOFb{^-ZD z-l}{Og`}?#tk3Vvld_wI$*?*j#iaRze37Ir^HP+n%<(mhm)mUtv5sUiP3cT56%%;} zcF{XmO+I!nhg316E~L~QrPX&Ebl{RWj_=x7<;fReBb5fi82e0sNeTI!H@KXu0b6=c z*M8rgv3$1sE<=LugZ;$G+=&Cjzwyf%NCe%Fl3Bu&Kmewj1&~b}aPzn{I%H0XmTzFq z%T0OeiSMv;*8`K-lg@aN8r2>T_qTM9{oz8elhg8li75awdrs-R& z4zZwwdkEx@b;=Gc|5}Fe2Z5n&(c~B$MB=Gu3%Ni*Oai5f>W|CjD74X2JXcKv*3S>m z6M*?3vO2CA4Xd~~i+6;)-C*kCj-TG6`-BCG@_w;aRA}cfMl(Dvey@6K`t%IfTqR zDcP8Ty<>7pEoz>3j;ew-;tLCVrB=$!v{Al!``H4@%)zO*)y!}ajg$j zyic}OMfvKs≺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 literal 0 HcmV?d00001 From 329f21c6bef64ab97fddcb7e654bcd5325720154 Mon Sep 17 00:00:00 2001 From: Pete Cheslock Date: Tue, 26 May 2026 15:05:49 -0400 Subject: [PATCH 3/5] Fix transformation issue Signed-off-by: Pete Cheslock --- .../scripts/test-fixtures/transformation-test.expected.md | 8 ++++++++ preview/scripts/test-fixtures/transformation-test.md | 8 ++++++++ preview/scripts/transformations.sh | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/preview/scripts/test-fixtures/transformation-test.expected.md b/preview/scripts/test-fixtures/transformation-test.expected.md index f3c9eb0..8a25550 100644 --- a/preview/scripts/test-fixtures/transformation-test.expected.md +++ b/preview/scripts/test-fixtures/transformation-test.expected.md @@ -122,6 +122,14 @@ Images with relative paths should be transformed: This should escape arrows: \<-> +## 6b. Autolink Test + +Bare HTTPS autolink: https://github.com/llm-d/llm-d/issues/680 + +Bare HTTP autolink: http://example.com/path/to/page + +A link already in markdown format should be unchanged: [llm-d](https://github.com/llm-d) + ## 7. HTML Image Tag Test Images with unquoted attributes should be quoted for MDX: diff --git a/preview/scripts/test-fixtures/transformation-test.md b/preview/scripts/test-fixtures/transformation-test.md index 83b033e..a0d9cca 100644 --- a/preview/scripts/test-fixtures/transformation-test.md +++ b/preview/scripts/test-fixtures/transformation-test.md @@ -96,6 +96,14 @@ Images with relative paths should be transformed: This should escape arrows: <-> +## 6b. Autolink Test + +Bare HTTPS autolink: + +Bare HTTP autolink: + +A link already in markdown format should be unchanged: [llm-d](https://github.com/llm-d) + ## 7. HTML Image Tag Test Images with unquoted attributes should be quoted for MDX: diff --git a/preview/scripts/transformations.sh b/preview/scripts/transformations.sh index fc47371..d40696d 100755 --- a/preview/scripts/transformations.sh +++ b/preview/scripts/transformations.sh @@ -41,6 +41,10 @@ apply_transformations() { # MDX escaping - escape special characters sed_inplace 's|<->|\\<->|g' "$file" + # Convert autolinks ( / ) to plain URLs + # MDX parses angle brackets as JSX and fails on the / in URLs + sed_inplace -E 's|<(https?://[^>]+)>|\1|g' "$file" + # Escape HTML comments for MDX (MDX doesn't support syntax) # Replace HTML comments with MDX comments: becomes {/* text */} # Handle both single-line and multi-line comments From b6e34a117c8bc19f9b41cf4adefc76906fe7e497 Mon Sep 17 00:00:00 2001 From: Ezra Silvera Date: Wed, 27 May 2026 11:49:34 +0300 Subject: [PATCH 4/5] blog(running-llm-d-without-kubernetes): FlowControl works in file-discovery mode Update the parity section: FlowControl now works in file-discovery mode with priority bands, fairness, ordering, and usage-limit policies configured statically in EndpointPickerConfig.flowControl. Per-request priority falls back to the configured default when no InferenceObjective CRD is available; the x-flow-fairness-id header drives fairness within a band. Model-name rewriting (InferenceModelRewrite) is now the only CRD-driven feature that remains unavailable outside Kubernetes. Signed-off-by: Ezra Silvera --- blog/2026-05-26_running-llm-d-without-kubernetes.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blog/2026-05-26_running-llm-d-without-kubernetes.md b/blog/2026-05-26_running-llm-d-without-kubernetes.md index 1a5bc2b..7b8db96 100644 --- a/blog/2026-05-26_running-llm-d-without-kubernetes.md +++ b/blog/2026-05-26_running-llm-d-without-kubernetes.md @@ -68,7 +68,7 @@ When this plugin is used, **the EPP has no dependency on any Kubernetes service The core EPP features are unchanged. KV-cache-utilization scoring, prefix-cache affinity, and Prometheus metrics all work identically. -Some features that are currently configured through Kubernetes CRDs, FlowControl (driven by `InferenceObjective`) and model-name rewriting (driven by `InferenceModelRewrite`), are not available when using the file-discovery plugin. A subset of these may move behind plugin interfaces in the future. +FlowControl (per-flow queueing, fairness, and admission) also works in file-discovery mode. The priority bands, fairness policies, ordering policies, and usage-limit policy are configured statically in `EndpointPickerConfig.flowControl` (the same block the Kubernetes deployment uses). Without `InferenceObjective` CRDs to consult, per-request priority falls back to a default value; static bands still apply, and a per-request `x-flow-fairness-id` header still drives fairness within a band. Model-name rewriting (driven by `InferenceModelRewrite`) is the one CRD-driven feature that is not yet available outside Kubernetes; a subset of these may move behind plugin interfaces in the future.
llm-d file-discovery architecture @@ -525,7 +525,8 @@ The file-discovery plugin gives you most of the llm-d routing stack outside of K - **KV-cache-utilization scoring**: routes requests away from instances with high cache pressure - **Prefix-cache affinity**: sends requests with shared prompt prefixes to the instance most likely to have them cached -- **Saturation-based admission**: the saturation detector still gates request admission, so a saturated pool sheds load rather than overloading backends. The full FlowControl layer (per-flow queueing and fairness, driven by `InferenceObjective`) is not active in file-discovery mode; see the caveat earlier in this post. +- **Saturation-based admission**: the saturation detector still gates request admission, so a saturated pool sheds load rather than overloading backends. +- **FlowControl (per-flow queueing and fairness)**: works with priority bands, fairness, and ordering policies configured statically in `EndpointPickerConfig.flowControl`. Without `InferenceObjective` CRDs, per-request priority falls back to the configured default; the `x-flow-fairness-id` request header drives fairness within a band. - **Prometheus metrics**: EPP exports scheduling and pool health metrics on `--metrics-port` What is no longer handled by llm-d outside Kubernetes is endpoint lifecycle: there is no automatic deregistration when a vLLM process dies. This responsibility shifts to the surrounding framework or orchestrator (Ray, Slurm, a custom controller, etc.) which needs to detect failed workers and rewrite the endpoints file accordingly. For production deployments, this typically means adding a health-monitoring agent that drops unavailable workers from the file. From 3a7aa5632359cf520e65810b9fa556bed1ab4a52 Mon Sep 17 00:00:00 2001 From: Ezra Silvera Date: Tue, 2 Jun 2026 12:55:38 +0300 Subject: [PATCH 5/5] updated blog Signed-off-by: Ezra Silvera --- ...-05-26_running-llm-d-without-kubernetes.md | 284 +++++++----------- .../llm-d-file-discovery-arch.png | Bin 33578 -> 0 bytes .../no-kubernetes-deployment.svg | 1 + .../ray-endpoint-generator.svg | 127 ++++++++ 4 files changed, 240 insertions(+), 172 deletions(-) delete mode 100644 static/img/blogs/running-llm-d-without-kubernetes/llm-d-file-discovery-arch.png create mode 100644 static/img/blogs/running-llm-d-without-kubernetes/no-kubernetes-deployment.svg create mode 100644 static/img/blogs/running-llm-d-without-kubernetes/ray-endpoint-generator.svg diff --git a/blog/2026-05-26_running-llm-d-without-kubernetes.md b/blog/2026-05-26_running-llm-d-without-kubernetes.md index 7b8db96..2dbe368 100644 --- a/blog/2026-05-26_running-llm-d-without-kubernetes.md +++ b/blog/2026-05-26_running-llm-d-without-kubernetes.md @@ -29,13 +29,13 @@ This post introduces the new endpoint-discovery plugin mechanism in the llm-d ro ## How llm-d normally discovers endpoints -The Endpoint Picker (EPP), the routing engine inside the llm-d router, normally watches a Kubernetes `InferencePool` object and the pods it selects. As pods come and go, the EPP's internal datastore is updated automatically via the controller-runtime manager. +The Endpoint Picker (EPP), the routing engine inside the llm-d router, normally watches a Kubernetes `InferencePool` object and the pods it selects. As pods come and go, the llm-d EPP's internal datastore is updated automatically via the controller-runtime manager. That machinery requires a live Kubernetes API server, an `InferencePool` CRD, and appropriate RBAC. On an HPC cluster or a Ray job, none of that exists. ## The llm-d Discovery plugin -To support alternative endpoint-discovery mechanisms, we recently introduced a general `EndpointDiscovery` plugin interface in the EPP framework. Anything that can enumerate endpoints and stream upsert/delete events can be plugged in: a file on disk, Consul, etcd, a custom registry, a cloud provider's service-discovery API, etc. +To support alternative endpoint-discovery mechanisms, we recently introduced a general `EndpointDiscovery` plugin interface in the llm-d EPP framework. Anything that can enumerate endpoints and stream upsert/delete events can be plugged in: a file on disk, Consul, etcd, a custom registry, a cloud provider's service-discovery API, etc. In the future, the existing Kubernetes watch-based discovery is also expected to move behind this interface, so all discovery paths share the same plugin model. @@ -58,28 +58,48 @@ type DiscoveryNotifier interface { } ``` -A plugin tells the EPP about endpoints by calling `Upsert` and `Delete` on the notifier. `Start` runs the plugin's main loop, typically an initial enumeration of the source followed by a watch that emits further `Upsert`/`Delete` calls as endpoints come and go. `Ready()` returns a channel that closes once the initial enumeration has populated the EPP datastore, so request-serving can be gated on a non-empty endpoint pool. +A plugin tells the llm-d EPP about endpoints by calling `Upsert` and `Delete` on the notifier. `Start` runs the plugin's main loop, typically an initial enumeration of the source followed by a watch that emits further `Upsert`/`Delete` calls as endpoints come and go. `Ready()` returns a channel that closes once the initial enumeration has populated the llm-d EPP datastore, so request-serving can be gated on a non-empty endpoint pool. ## The file-discovery plugin The file-discovery plugin uses a plain YAML or JSON file on disk as its source of inference endpoints. The plugin reads the file at startup and optionally watches it (via `fsnotify`) for subsequent changes, emitting `Upsert`/`Delete` events as entries are added, modified, or removed. -When this plugin is used, **the EPP has no dependency on any Kubernetes service or object**: no API server, no watchers, no controller manager, no `InferencePool` CRD, no RBAC, no `kubeconfig`. **It can run on a host without a Kubernetes cluster anywhere in sight.** +When this plugin is used, **the llm-d EPP has no dependency on any Kubernetes service or object**: no API server, no watchers, no controller manager, no `InferencePool` CRD, no RBAC, no `kubeconfig`. **It can run on a host without a Kubernetes cluster anywhere in sight.** -The core EPP features are unchanged. KV-cache-utilization scoring, prefix-cache affinity, and Prometheus metrics all work identically. +The core llm-d EPP features are unchanged. KV-cache-utilization scoring, prefix-cache affinity, and Prometheus metrics all work identically. FlowControl (per-flow queueing, fairness, and admission) also works in file-discovery mode. The priority bands, fairness policies, ordering policies, and usage-limit policy are configured statically in `EndpointPickerConfig.flowControl` (the same block the Kubernetes deployment uses). Without `InferenceObjective` CRDs to consult, per-request priority falls back to a default value; static bands still apply, and a per-request `x-flow-fairness-id` header still drives fairness within a band. Model-name rewriting (driven by `InferenceModelRewrite`) is the one CRD-driven feature that is not yet available outside Kubernetes; a subset of these may move behind plugin interfaces in the future.
- llm-d file-discovery architecture + llm-d file-discovery architecture

Figure 1: FileDiscovery plugin in llm-d

+## Try It: A Well-Lit Path + +**Prereqs** + +- A host with the GPUs your model requires, Docker (or Podman), and a Hugging Face token. +- The canonical step-by-step deployment is the [No-Kubernetes Deployment well-lit path](https://github.com/llm-d/llm-d/blob/main/docs/well-lit-paths/no-kubernetes-deployment.md), with manifests in [`guides/no-kubernetes-deployment`](https://github.com/llm-d/llm-d/tree/main/guides/no-kubernetes-deployment). It includes ready-to-use llm-d EPP, endpoints, and Envoy configs (with the full optimized-baseline plugin set), Docker commands for vLLM/llm-d EPP/Envoy, verification, and a Prometheus scrape example. + +The walkthrough in [Setting it up](#setting-it-up) below is a smaller, learning-oriented version of the same path: a minimal llm-d EPP config and a trimmed Envoy config that make the moving parts visible. Use it to understand the design; use the upstream guide to deploy. + ## Setting it up +The well-lit-path guide [`guides/no-kubernetes-deployment`](https://github.com/llm-d/llm-d/tree/main/guides/no-kubernetes-deployment) is the canonical, deploy-ready reference: ready-to-use llm-d EPP, endpoints, and Envoy configs (with the optimized-baseline plugin set), Docker commands for vLLM/llm-d EPP/Envoy, verification, and a Prometheus scrape example. + +This section is a learning-oriented tour of the same path - the goal is to make the moving parts and the file-discovery-specific surface visible in isolation, so the upstream configs read as concrete instances of an understood pattern rather than as opaque YAML. Four pieces: + +1. **Endpoints file** - a YAML list of vLLM workers (`address`, `port`, optional `labels`) that the file-discovery plugin reads, and optionally watches for live updates. +2. **llm-d EPP config** - an `EndpointPickerConfig` with one `dataLayer.discovery.pluginRef: file-discovery` line that flips the llm-d EPP off the Kubernetes path. This is the central change; everything else (scoring, picker, metrics) is the same as in any llm-d EPP config. +3. **llm-d EPP process** - runs the llm-d EPP binary or container with the config above. On startup it logs `EPP starting (file discovery mode)`, confirming the switch took effect. +4. **Envoy proxy** - accepts client traffic, calls the llm-d EPP over `ext_proc`, and forwards each request to the address the llm-d EPP picks via the `x-gateway-destination-endpoint` response header. + +A final **Send a request** step at the end shows what a successful end-to-end response looks like. + ### 1. The endpoints file -Save as `/etc/epp/endpoints.yaml`: +The plugin reads a YAML file listing inference endpoints, e.g.: ```yaml endpoints: @@ -88,209 +108,117 @@ endpoints: port: "8000" labels: model: llama-3-8b - - - name: vllm-1 - address: "10.0.0.2" - port: "8000" - labels: - model: llama-3-8b ``` -An endpoint is defined using the following fields: +Schema: | Field | Required | Notes | |---|---|---| -| `name` | yes | Identifier of the endpoint; must be unique within the file. Used as the endpoint key in the EPP datastore and in metrics labels. | -| `address` | yes | The IP address of the inference worker (the host running vLLM). Must be a valid IPv4 address. The EPP uses `address:port` both for routing requests and for scraping the worker's `/metrics` endpoint. | -| `port` | yes | TCP port on `address` where vLLM is listening. Integer 1-65535, written as a string. | -| `namespace` | no | Logical grouping name for the endpoint, retained from the Kubernetes-native data model where endpoints live in a namespace. Outside Kubernetes there is no real namespace concept; this is just a string tag used in the endpoint's identity (`namespace/name`) and in metrics labels. Defaults to `"default"` and most non-Kubernetes deployments can leave it unset. | -| `labels` | no | Arbitrary key/value pairs surfaced to scheduler plugins. Used for things like `llm-d.ai/role: prefill` in P/D setups, or `model: llama-3-8b` for model-aware filters. | +| `name` | yes | Unique identifier; used as the endpoint key in the llm-d EPP datastore and in metrics labels. | +| `address` | yes | IPv4 address of the inference worker. The llm-d EPP uses `address:port` for routing and for scraping the worker's `/metrics`. | +| `port` | yes | TCP port where vLLM is listening, written as a string. | +| `namespace` | no | Logical grouping tag retained from the Kubernetes data model. Defaults to `"default"`; most non-Kubernetes deployments leave it unset. | +| `labels` | no | Arbitrary key/value pairs surfaced to scheduler plugins, e.g. `llm-d.ai/role: prefill` for P/D, or `model: llama-3-8b` for model-aware filters. | -> **Note:** `address` must be a literal IPv4 address. Hostnames are not resolved by the plugin. In environments where you only have hostnames (common in Slurm and Ray), resolve them upstream of writing the file. The Slurm and Ray examples later in this post show how. +> **Note:** `address` must be a literal IPv4 address. Hostnames are not resolved by the plugin. The Slurm and Ray examples later in this post resolve hostnames upstream of writing the file. -### 2. EPP config +### 2. llm-d EPP config -Save as `/etc/epp/config.yaml`: +What turns the llm-d EPP into a no-Kubernetes llm-d EPP is a single block in the `EndpointPickerConfig`: `dataLayer.discovery` pointed at the file-discovery plugin. ```yaml -apiVersion: llm-d.ai/v1alpha1 -kind: EndpointPickerConfig - plugins: - name: file-discovery type: file-discovery parameters: path: /etc/epp/endpoints.yaml - watchFile: true # optional, default is false. When true, reconcile the datastore whenever the file changes - - - name: max-score-picker - type: max-score-picker - - - name: single-profile-handler - type: single-profile-handler - - - name: metrics-source - type: metrics-data-source - - - name: metrics-extractor - type: core-metrics-extractor - -schedulingProfiles: - - name: default - plugins: - - pluginRef: max-score-picker + watchFile: true # reconcile the datastore whenever the file changes dataLayer: - injectDefaults: false discovery: - pluginRef: file-discovery # this line loads the file-discovery plugin and effectively switches off the Kubernetes path - sources: - - pluginRef: metrics-source - extractors: - - pluginRef: metrics-extractor + pluginRef: file-discovery # this line switches off the Kubernetes path ``` -This is a minimal config. For a more complete plugin set (saturation detector, prefix-cache affinity, flow control, etc.), see the [`optimized-baseline` router values](https://github.com/llm-d/llm-d/blob/main/guides/optimized-baseline/router/optimized-baseline.values.yaml) or the [router recipes](https://github.com/llm-d/llm-d/tree/main/guides/recipes/router); the file-discovery section drops in alongside the rest. +Wire scoring, picker, and metrics plugins around this block as you would in any llm-d EPP config. The upstream [`router/epp/config.yaml`](https://github.com/llm-d/llm-d/blob/main/guides/no-kubernetes-deployment/router/epp/config.yaml) ships the optimized-baseline plugin mix (`prefix-cache-scorer`, `queue-scorer`, `kv-cache-utilization-scorer`, `no-hit-lru-scorer`) already wired to file-discovery and is the recommended starting point; the Kubernetes-side [`optimized-baseline` router values](https://github.com/llm-d/llm-d/blob/main/guides/optimized-baseline/router/optimized-baseline.values.yaml) are the corresponding reference for cluster deployments. -Controlling whether the EPP takes the file-discovery path comes down to the `dataLayer.discovery.pluginRef` field. When present, the EPP takes the file-discovery path. When it is absent, the EPP behaves as before and requires Kubernetes. +`watchFile: true` enables live reload: the llm-d EPP upserts new endpoints and deletes removed ones whenever the file changes, without a restart. This is the key property that makes dynamic environments, where workers appear and disappear, work correctly. -When `watchFile` is `false`, the file is read once at startup and never re-read. When set to `true`, it enables live reload: the EPP upserts new endpoints and deletes removed ones whenever the file changes, without a restart. This is the key property that makes dynamic environments, where workers appear and disappear, work correctly. - -### 3. Start the EPP +### 3. Start the llm-d EPP ```bash epp \ - --pool-name my-pool \ --config-file /etc/epp/config.yaml \ + --pool-name my-pool \ --grpc-port 9002 \ --grpc-health-port 9003 \ --metrics-port 9090 ``` -The binary is built from the `cmd/epp` target of the [`llm-d-router`](https://github.com/llm-d/llm-d-router) repo (build from the latest `main` until the release lands), or pulled from the `ghcr.io/llm-d/llm-d-router-endpoint-picker` image, which will include file-discovery starting with the upcoming **llm-d 0.8** release. +`--pool-name` and `--pool-namespace` are arbitrary labels for metrics and logs in file-discovery mode; they don't reference any Kubernetes object. On startup the llm-d EPP logs `EPP starting (file discovery mode)`, and `endpoints file changed, reloading` on each subsequent reload when `watchFile: true`. The upstream guide covers the container and build-from-source options. -`--pool-name` is optional in file-discovery mode and defaults to `epp` if unset. The value is arbitrary; it does not reference any Kubernetes object and is used only as the pool identifier in metrics and logs. The startup command above passes it explicitly so the metrics labels reflect a meaningful name. +### 4. Envoy config -Similarly, `--pool-namespace` defaults to `default` outside Kubernetes (where there is no Downward API); pass it explicitly if you want metrics and logs labeled for your environment. The EPP emits a startup warning if it falls back to the default. +The llm-d EPP picks an endpoint but doesn't proxy traffic. Envoy (or any compatible proxy) accepts the client request, calls the llm-d EPP over `ext_proc`, reads the `x-gateway-destination-endpoint` header that the llm-d EPP sets on the response, and forwards the request to that address using its `ORIGINAL_DST` cluster type. The Envoy config is fully static; no Kubernetes service discovery involved. -On startup the EPP logs `EPP starting (file discovery mode)` along with the discovery plugin name and the resolved pool name and namespace. If `watchFile: true`, the file-discovery plugin also logs `watching endpoints file for changes`, and re-emits `endpoints file changed, reloading` on each subsequent reload. +This is the same shape as llm-d's **standalone deployment mode**. The upstream [`router/envoy/envoy.yaml`](https://github.com/llm-d/llm-d/blob/main/guides/no-kubernetes-deployment/router/envoy/envoy.yaml) is a host-friendly Envoy config wired exactly this way and works alongside the llm-d EPP config above. The standalone Helm chart [`llm-d-router-standalone/values.yaml`](https://github.com/llm-d/llm-d-router/blob/main/config/charts/llm-d-router-standalone/values.yaml) is the Kubernetes-side reference; it shows the `health_checks` and `transport_socket` (TLS) blocks worth adding when Envoy and the llm-d EPP run on separate hosts (e.g. Envoy on a Slurm head node and the llm-d EPP on a service node). -### 4. Envoy config - -The EPP selects an endpoint but does not proxy traffic itself. You still need Envoy (or a compatible proxy) to accept client requests and forward them to the EPP-selected backend. +### 5. Start Envoy -The EPP communicates its selection by setting the `x-gateway-destination-endpoint` header on the `ext_proc` response. Envoy's `ORIGINAL_DST` cluster type reads that header and forwards the request to the address it contains. The Envoy config is fully static; no Kubernetes service discovery involved. +```bash +envoy -c /etc/envoy/envoy.yaml +``` -This is the same shape as llm-d's **standalone deployment mode**, where the EPP runs alongside an Envoy proxy without a Gateway API controller. The reference is the standalone Helm chart values file: +Requests to `http://localhost:8080/v1/completions` are now routed by the llm-d EPP to one of the vLLM instances. -> [`config/charts/llm-d-router-standalone/values.yaml`](https://github.com/llm-d/llm-d-router/blob/main/config/charts/llm-d-router-standalone/values.yaml). See `router.proxy.presets.envoy.configMap.data` for the upstream-blessed Envoy config. The version below is a trimmed equivalent suitable for running directly on a host. +### 6. Send a request -Save as `/etc/envoy/envoy.yaml`: +End-to-end completion through Envoy -> llm-d EPP -> vLLM: -```yaml -admin: - address: - socket_address: { address: 127.0.0.1, port_value: 19000 } - -static_resources: - listeners: - - name: inference - address: - socket_address: { address: 0.0.0.0, port_value: 8080 } - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: inference - route_config: - virtual_hosts: - - name: inference - domains: ["*"] - routes: - - match: { prefix: "/" } - route: - cluster: original_destination_cluster - timeout: 86400s - idle_timeout: 86400s - http_filters: - - name: envoy.filters.http.ext_proc - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor - grpc_service: - envoy_grpc: - cluster_name: epp - authority: localhost:9002 - timeout: 10s - processing_mode: - request_header_mode: SEND - response_header_mode: SEND - request_body_mode: FULL_DUPLEX_STREAMED - response_body_mode: FULL_DUPLEX_STREAMED - request_trailer_mode: SEND - response_trailer_mode: SEND - message_timeout: 1000s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - clusters: - - name: epp - type: STATIC - connect_timeout: 86400s - lb_policy: LEAST_REQUEST - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: {} - load_assignment: - cluster_name: epp - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: { address: 127.0.0.1, port_value: 9002 } - - - name: original_destination_cluster - type: ORIGINAL_DST - connect_timeout: 1000s - lb_policy: CLUSTER_PROVIDED - circuit_breakers: - thresholds: - - max_connections: 40000 - max_pending_requests: 40000 - max_requests: 40000 - original_dst_lb_config: - use_http_header: true - http_header_name: x-gateway-destination-endpoint +```bash +curl -s http://localhost:8080/v1/completions \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "llama-3-8b", + "prompt": "Hello, world!", + "max_tokens": 32 + }' ``` -For production deployments outside Kubernetes, where there is no kubelet to restart a crashed EPP, it is worth adding a gRPC `health_checks` block to the `epp` cluster so Envoy stops routing to a dead EPP. The standalone chart linked above shows the canonical configuration (10s interval, unhealthy threshold of 3, gRPC health-check service `envoy.service.ext_proc.v3.ExternalProcessor`). +A successful response is a standard OpenAI-compatible completion: + +```json +{ + "id": "cmpl-...", + "object": "text_completion", + "model": "llama-3-8b", + "choices": [ + { "index": 0, "text": " ...", "finish_reason": "length" } + ], + "usage": { "prompt_tokens": 5, "completion_tokens": 32, "total_tokens": 37 } +} +``` -The config above assumes Envoy and the EPP are colocated on the same host (the gRPC link uses `127.0.0.1:9002`, plaintext). When Envoy and the EPP run on separate nodes, for example Envoy on a Slurm head node and the EPP on a service node, the gRPC link traverses the network and should be secured with TLS. Configure an `UpstreamTlsContext` on the `epp` cluster's `transport_socket` (the `llm-d-router-standalone` chart's `transport_socket` block is the reference) and serve gRPC over TLS on the EPP side. - -### 5. Start Envoy +You can also confirm the llm-d EPP datastore is populated and being scored via the metrics endpoint: ```bash -envoy -c /etc/envoy/envoy.yaml +curl -s http://localhost:9090/metrics | grep inference_pool ``` -Requests to `http://localhost:8080/v1/completions` are now routed by the EPP to one of the vLLM instances. +A `503` with `no_healthy_upstream` typically means the llm-d EPP gRPC connection from Envoy is down; see [Troubleshooting](#troubleshooting) for the common failure modes. ## P/D disaggregated setup llm-d also supports **prefill/decode disaggregation** (P/D), where the compute-bound prefill stage and the memory-bandwidth-bound decode stage run on separate workers and the KV cache is transferred between them. The deployment is two pools: prefill workers running vLLM directly, and decode workers running vLLM behind a `pd-sidecar` that orchestrates remote prefill and the KV transfer. -The full deployment recipe (sidecar flags, vLLM `kv-transfer-config`, NIXL/RDMA setup, EPP plugin wiring with `disagg-profile-handler` and `prefix-based-pd-decider`, and the scheduling profiles) is documented upstream and is identical for non-Kubernetes deployments. Use those as the reference: +The full deployment recipe (sidecar flags, vLLM `kv-transfer-config`, NIXL/RDMA setup, llm-d EPP plugin wiring with `disagg-profile-handler` and `prefix-based-pd-decider`, and the scheduling profiles) is documented upstream and is identical for non-Kubernetes deployments. Use those as the reference: - [`llm-d/guides/pd-disaggregation`](https://github.com/llm-d/llm-d/tree/main/guides/pd-disaggregation): end-to-end deployment guide. - [`llm-d-router/docs/disaggregation.md`](https://github.com/llm-d/llm-d-router/blob/main/docs/disaggregation.md): request-lifecycle and component reference. -- [`llm-d/guides/pd-disaggregation/router/pd-disaggregation.values.yaml`](https://github.com/llm-d/llm-d/blob/main/guides/pd-disaggregation/router/pd-disaggregation.values.yaml): canonical P/D EPP config (full plugin set with prefill and decode profiles). +- [`llm-d/guides/pd-disaggregation/router/pd-disaggregation.values.yaml`](https://github.com/llm-d/llm-d/blob/main/guides/pd-disaggregation/router/pd-disaggregation.values.yaml): canonical P/D llm-d EPP config (full plugin set with prefill and decode profiles). **The only thing this post adds is how to swap Kubernetes-driven discovery for the YAML file.** Two changes: -1. Add the file-discovery plugin and `dataLayer.discovery.pluginRef` to the upstream P/D EPP config (same as in the single-pool setup earlier in this post). +1. Add the file-discovery plugin and `dataLayer.discovery.pluginRef` to the upstream P/D llm-d EPP config (same as in the single-pool setup earlier in this post). 2. Mark each endpoint's role in the YAML with the `llm-d.ai/role` label: `prefill` for prefill workers, `decode` for decode workers. For decode endpoints, the `port` is the pd-sidecar's port, not vLLM's. The router's prefill/decode filters select candidates by this label. ```yaml @@ -314,17 +242,29 @@ The full set of role label values (including combined roles like `prefill-decode Integrating llm-d in file-discovery mode with any non-Kubernetes environment comes down to two things: -1. **Run the EPP and Envoy** on a node that can reach your vLLM workers, using the configs and commands from the [Setting it up](#setting-it-up) section above. -2. **Generate the endpoints file** in the format shown above, using whatever source knows your worker set: Ray's Python API, Slurm's `$SLURM_JOB_NODELIST`, an inventory tool, a static list, and so on. If the endpoint set needs to change at runtime as workers come and go, set `watchFile: true` and the EPP will reconcile continuously. +1. **Run llm-d** (llm-d EPP, Envoy, and the llm-d sidecar where applicable) on a node that can reach your vLLM workers, using the configs and commands from the [Setting it up](#setting-it-up) section above. +2. **Produce the endpoints file** in the format shown above, using whatever source knows your worker set. -The two examples below show one way to generate the endpoint list, for Ray and Slurm. +The first step is the same everywhere; the second is where most of the integration work lives. The right approach depends on how dynamic the worker pool is. There are a few common patterns, all of which use the same llm-d EPP config: -> **Note:** For deployments with very dynamic endpoint inventories, where workers come and go faster than is comfortable to track via a regenerated file, a dedicated `SlurmDiscovery` or `RayDiscovery` plugin can be implemented against the same `EndpointDiscovery` interface. Such a plugin would talk to the orchestrator's API directly (Ray's Python API or Slurm's controller) and emit `Upsert`/`Delete` events as workers change, without any file in the loop. The file-discovery plugin shown here is the simplest path and works well for most cases; the orchestrator-native plugins are an optimization for the most dynamic scenarios. +1. **Static file** - hand-edited or templated once at deployment time. Right when the worker set is known up-front and stable: bare-metal racks, lab machines, a fixed pool of long-lived services. No live reload needed; `watchFile` can stay at its default `false`. +2. **Generated once at startup** - a script that asks the orchestrator for the current worker set and writes the file before the llm-d EPP starts. Simplest dynamic path; works well when the worker set is fixed for the duration of a job (an HPC allocation, a single training run). +3. **Regenerated on change** - a small monitor process or job hook that rewrites the file via atomic rename whenever the worker set changes: a node failed, a training round completed and rollouts were respawned, an autoscaler added or removed capacity. With `watchFile: true` the llm-d EPP reconciles automatically without a restart. +4. **Orchestrator-native discovery plugin** (future work) - for the most dynamic case, where workers come and go faster than is comfortable to track via a regenerated file. A dedicated `SlurmDiscovery`, `RayDiscovery`, or similar plugin against the same `EndpointDiscovery` interface would talk to the orchestrator's API directly and emit `Upsert`/`Delete` events without any file in the loop. + +The source of truth varies by environment - Ray's Python API, Slurm's `$SLURM_JOB_NODELIST`, a CMDB or inventory tool, a cloud provider's service-discovery API, or just a static configuration - but the output format is always the same YAML schema as the [endpoints file](#1-the-endpoints-file). + +The two examples below show patterns 2 and 3 end to end, for Ray and Slurm. ### Ray In a Ray deployment, vLLM workers run as remote processes on Ray cluster nodes. The Ray Python API exposes the current cluster membership, including node IP addresses, so generating the endpoints file is straightforward. +
+ Endpoint generator script connected to the Ray head node, writing endpoints.yaml +

Figure 2: Endpoint generator queries the Ray head node and writes endpoints.yaml.

+
+ ```python #!/usr/bin/env python3 """ @@ -332,7 +272,7 @@ generate_epp_endpoints.py Usage: python generate_epp_endpoints.py [vllm_port] [output_path] -Run this after Ray workers are started and before launching the EPP. +Run this after Ray workers are started and before launching the llm-d EPP. """ import ray import yaml @@ -381,7 +321,7 @@ python launch_rollout_workers.py # 2. Generate the endpoints file python generate_epp_endpoints.py 8000 /etc/epp/endpoints.yaml -# 3. Start EPP and Envoy +# 3. Start llm-d EPP and Envoy epp \ --pool-name ray-pool \ --config-file /etc/epp/config.yaml \ @@ -390,18 +330,18 @@ epp \ envoy -c /etc/envoy/envoy.yaml & ``` -Because `watchFile: true` is set in the EPP config, the endpoints file can be regenerated whenever the worker pool changes, for example between RL training rounds when rollout workers are restarted with a new model checkpoint. The EPP reconciles the change without a restart: +Because `watchFile: true` is set in the llm-d EPP config, the endpoints file can be regenerated whenever the worker pool changes, for example between RL training rounds when rollout workers are restarted with a new model checkpoint. The llm-d EPP reconciles the change without a restart: ```python # Regenerate after workers are replaced for the next training round generate_endpoints(new_worker_ips, "/etc/epp/endpoints.yaml.tmp") os.rename("/etc/epp/endpoints.yaml.tmp", "/etc/epp/endpoints.yaml") -# The atomic rename triggers fsnotify; the EPP updates its pool automatically +# The atomic rename triggers fsnotify; the llm-d EPP updates its pool automatically ``` ### Slurm -In a Slurm environment, a batch job requests a fixed set of nodes via `#SBATCH --nodes`. The standard approach is to designate the first node as the "head" (running EPP and Envoy) and use the remaining nodes for vLLM. +In a Slurm environment, a batch job requests a fixed set of nodes via `#SBATCH --nodes`. The standard approach is to designate the first node as the "head" (running llm-d EPP and Envoy) and use the remaining nodes for vLLM. Slurm provides the allocated node list in `$SLURM_JOB_NODELIST` as a compact range expression like `node[01-05]`. The `scontrol show hostnames` command expands that into individual hostnames, and a short Python snippet resolves them to IPs for the endpoints file. @@ -432,7 +372,7 @@ port = $MODEL_PORT endpoints = [] for i, host in enumerate(worker_nodes): - # EPP requires IPs; Slurm gives hostnames + # llm-d EPP requires IPs; Slurm gives hostnames ip = socket.gethostbyname(host) endpoints.append({ "name": f"vllm-{i}", @@ -447,7 +387,7 @@ with open("$WORK_DIR/epp/endpoints.yaml", "w") as f: print(f"Wrote {len(endpoints)} endpoints") EOF -# --- Copy EPP and Envoy configs to work dir ---------------------------- +# --- Copy llm-d EPP and Envoy configs to work dir ---------------------------- cp /path/to/epp-config.yaml $WORK_DIR/epp/config.yaml cp /path/to/envoy.yaml $WORK_DIR/envoy.yaml @@ -469,7 +409,7 @@ for i in "${!WORKER_NODES[@]}"; do --tensor-parallel-size $GPUS_PER_NODE & done -# Wait for vLLM to finish loading weights before EPP starts polling. +# Wait for vLLM to finish loading weights before llm-d EPP starts polling. # Cap the wait so a stuck worker (OOM, weight download failure, etc.) fails # the job instead of holding the SBATCH allocation idle until --time expires. MAX_WAIT_SECS=1800 # 30 minutes @@ -487,7 +427,7 @@ for node in "${WORKER_NODES[@]}"; do echo " $node ready" done -# --- Start EPP + Envoy on the head node -------------------------------- +# --- Start llm-d EPP + Envoy on the head node -------------------------------- srun --ntasks=1 --nodes=1 --nodelist="$HEAD_NODE" \ epp \ --pool-name slurm-$SLURM_JOB_ID \ @@ -502,7 +442,7 @@ srun --ntasks=1 --nodes=1 --nodelist="$HEAD_NODE" \ wait ``` -For jobs where the serving pool may change during the allocation (a node fails and is replaced, or model weights are swapped), the endpoints file can be atomically replaced and the EPP will reconcile without downtime: +For jobs where the serving pool may change during the allocation (a node fails and is replaced, or model weights are swapped), the endpoints file can be atomically replaced and the llm-d EPP will reconcile without downtime: ```bash python3 regenerate_endpoints.py > $WORK_DIR/epp/endpoints.yaml.tmp @@ -513,11 +453,11 @@ mv $WORK_DIR/epp/endpoints.yaml.tmp $WORK_DIR/epp/endpoints.yaml A few failure modes that trip up first-time deployments: -- **`address` is a hostname, not an IP.** The EPP rejects entries where `address` doesn't parse as an IP. Slurm and Ray surface hostnames, so resolve them with `socket.gethostbyname` (or equivalent) before writing the file. -- **EPP can't reach vLLM's metrics port.** The EPP scrapes `/metrics` on each endpoint at `address:port`. If a host firewall or a network policy blocks that port from the EPP node, scoring plugins silently degrade to default values: routing still works, but KV-cache scoring becomes meaningless. Check the EPP's pool-health metrics on `--metrics-port` to confirm endpoints are reporting. -- **Envoy returns 503 with `no_healthy_upstream`.** Almost always means the EPP gRPC connection is down. Check that the EPP is running on `localhost:9002`, that `--grpc-port` matches Envoy's `authority`, and (if you added the `health_checks` block) that the EPP's gRPC health service is enabled. +- **`address` is a hostname, not an IP.** The llm-d EPP rejects entries where `address` doesn't parse as an IP. Slurm and Ray surface hostnames, so resolve them with `socket.gethostbyname` (or equivalent) before writing the file. +- **llm-d EPP can't reach vLLM's metrics port.** The llm-d EPP scrapes `/metrics` on each endpoint at `address:port`. If a host firewall or a network policy blocks that port from the llm-d EPP node, scoring plugins silently degrade to default values: routing still works, but KV-cache scoring becomes meaningless. Check the llm-d EPP's pool-health metrics on `--metrics-port` to confirm endpoints are reporting. +- **Envoy returns 503 with `no_healthy_upstream`.** Almost always means the llm-d EPP gRPC connection is down. Check that the llm-d EPP is running on `localhost:9002`, that `--grpc-port` matches Envoy's `authority`, and (if you added the `health_checks` block) that the llm-d EPP's gRPC health service is enabled. - **`watchFile: true` doesn't pick up an edit.** The watcher reacts to fsnotify events on rename/replace, which is what `mv tmp final` produces. Editors that truncate-then-write (some `vim` configurations, certain IDEs) may emit a different event sequence and either double-fire or miss. Always update the file via atomic rename, as both examples in this post do. -- **vLLM hasn't finished loading weights when the EPP starts.** If the EPP scrapes a vLLM that isn't yet serving, the endpoint shows up as unhealthy and gets excluded until the next reconcile. The Slurm script avoids this by polling `/health` on each worker before starting the EPP; do the same in any orchestration that doesn't already gate on readiness. +- **vLLM hasn't finished loading weights when the llm-d EPP starts.** If the llm-d EPP scrapes a vLLM that isn't yet serving, the endpoint shows up as unhealthy and gets excluded until the next reconcile. The Slurm script avoids this by polling `/health` on each worker before starting the llm-d EPP; do the same in any orchestration that doesn't already gate on readiness. ## Parity with the Kubernetes-native llm-d deployment @@ -527,7 +467,7 @@ The file-discovery plugin gives you most of the llm-d routing stack outside of K - **Prefix-cache affinity**: sends requests with shared prompt prefixes to the instance most likely to have them cached - **Saturation-based admission**: the saturation detector still gates request admission, so a saturated pool sheds load rather than overloading backends. - **FlowControl (per-flow queueing and fairness)**: works with priority bands, fairness, and ordering policies configured statically in `EndpointPickerConfig.flowControl`. Without `InferenceObjective` CRDs, per-request priority falls back to the configured default; the `x-flow-fairness-id` request header drives fairness within a band. -- **Prometheus metrics**: EPP exports scheduling and pool health metrics on `--metrics-port` +- **Prometheus metrics**: llm-d EPP exports scheduling and pool health metrics on `--metrics-port` What is no longer handled by llm-d outside Kubernetes is endpoint lifecycle: there is no automatic deregistration when a vLLM process dies. This responsibility shifts to the surrounding framework or orchestrator (Ray, Slurm, a custom controller, etc.) which needs to detect failed workers and rewrite the endpoints file accordingly. For production deployments, this typically means adding a health-monitoring agent that drops unavailable workers from the file. @@ -539,6 +479,6 @@ The file-discovery plugin is the simplest non-Kubernetes integration point. It w - **Orchestrator-native plugins**: a `RayDiscovery` or `SlurmDiscovery` plugin that talks to Ray's Python API or Slurm's controller directly, emitting `Upsert`/`Delete` events as workers change without any file in the loop. Useful for highly dynamic worker pools. - **Service-registry plugins**: Consul, etcd, or a cloud provider's service-discovery API as the source of endpoints. -- **Migrating Kubernetes discovery to a plugin**: the existing watch-based Kubernetes path is currently wired into the EPP directly. Moving it behind the same `EndpointDiscovery` interface would unify all discovery paths under a single model and remove a special case from the EPP. +- **Migrating Kubernetes discovery to a plugin**: the existing watch-based Kubernetes path is currently wired into the llm-d EPP directly. Moving it behind the same `EndpointDiscovery` interface would unify all discovery paths under a single model and remove a special case from the llm-d EPP. **RL integration.** We are currently working on integrating the no-Kubernetes llm-d with RL frameworks that run on Ray and Slurm (VERL, OpenRLHF). Our next blog post will cover that integration and initial results. This will include a custom `EndpointDiscovery` plugin that registers and deregisters endpoints in real time as Ray actors come up and are torn down between training rounds. We will also show how llm-d's prefix-cache routing translates into a concrete throughput benefit for the repeated-prompt patterns typical of RLHF rollouts. diff --git a/static/img/blogs/running-llm-d-without-kubernetes/llm-d-file-discovery-arch.png b/static/img/blogs/running-llm-d-without-kubernetes/llm-d-file-discovery-arch.png deleted file mode 100644 index d5c23c06703272b428855eb3821c8c8e59aaa19b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33578 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 diff --git a/static/img/blogs/running-llm-d-without-kubernetes/no-kubernetes-deployment.svg b/static/img/blogs/running-llm-d-without-kubernetes/no-kubernetes-deployment.svg new file mode 100644 index 0000000..7d0bb9c --- /dev/null +++ b/static/img/blogs/running-llm-d-without-kubernetes/no-kubernetes-deployment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/img/blogs/running-llm-d-without-kubernetes/ray-endpoint-generator.svg b/static/img/blogs/running-llm-d-without-kubernetes/ray-endpoint-generator.svg new file mode 100644 index 0000000..5a06270 --- /dev/null +++ b/static/img/blogs/running-llm-d-without-kubernetes/ray-endpoint-generator.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + Ray Cluster + + + + Head Node + + + + Envoy + + + + llm-d + EPP + + + + Worker Node + + vLLM + 10.0.0.10:8000 + + + + Worker Node + + vLLM + 10.0.0.11:8000 + + + + + + + + + ray.nodes() + + + + Endpoint Generator + generate_epp_endpoints.py + resolves IPs, writes YAML + + + + + writes + + + + endpoints.yaml + + + + + file-discovery + +