- Overview
- Core Goals
- Filters, Scorers, and Scrapers
- Configuration
- Metric Scraping
- Disaggregated Encode/Prefill/Decode (E/P/D)
- InferencePool & InferenceModel Design
- References
llm-d is an extensible architecture designed to schedule inference requests efficiently across model-serving pods. A central component of this architecture is the Inference Gateway, which builds on the Kubernetes-native Gateway API Inference Extension (GIE) to enable scalable, flexible, and pluggable request scheduling.
The design enables:
- Support for multiple base models within a shared cluster (see serving multiple inference pools)
- Efficient routing based on KV cache locality, session affinity, load, and model metadata
- Disaggregated Prefill/Decode (P/D) execution
- We have introduced experimental Encode/Prefill/Decode (E/P/D and all its permutations) execution. For a detailed explanation, see Disaggregated Inference Serving
- Pluggable filters, scorers, and scrapers for extensible scheduling
- Schedule inference requests to optimal pods based on:
- Base model compatibility
- KV cache reuse
- Load balancing
- Support multi-model deployments on heterogeneous hardware
- Enable runtime extensibility with pluggable logic (filters, scorers, scrapers)
- Community-aligned implementation using GIE and Envoy + External Processing (EPP)
- Pluggability: No core changes are needed to add new scorers or filters
- Isolation: Each component operates independently
-
Filtering
- Pods in an
InferencePoolgo through a sequential chain of filters - Pods may be excluded based on criteria like model compatibility, resource usage, or custom logic
- Pods in an
-
Scoring
- Filtered pods are scored using a weighted set of scorers
- Scorers currently run sequentially (future: parallel execution)
- Scorers access a shared datastore populated by scrapers
-
Pod Selection
- The highest-scored pod is selected
- If multiple pods share the same score, one is selected at random
The llm-d Endpoint Picker relies on a YAML-based configuration—provided either as a file or an in-line parameter—to determine which lifecycle hooks (plugins) are active.
Specifically, this configuration establishes the following components:
-
Plugins: The specific plugins to instantiate, along with their parameters. Because each instantiated plugin is assigned a unique name, you can configure the same plugin type multiple times if necessary. -
SchedulingProfiles: A collection of profiles that dictate the exact set of plugins invoked when scheduling a given request.
The configuration text has the following form:
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
plugins:
- ....
- ....
schedulingProfiles:
- ....
- ....The first two lines of the configuration are constant and must appear as is.
The plugins section in the configuration defines the set of plugins that will be instantiated and their parameters. Each entry in this section has the following form:
- name: aName
type: a-type
parameters:
param1: val1
param2: val2The fields in a plugin entry are:
- name (optional): provides a name by which the plugin instance can be referenced. If this field is omitted, the plugin's type will be used as its name.
- type: specifies the type of the plugin to be instantiated.
- parameters (optional): defines the set of parameters used to configure the plugin in question. The actual set of parameters varies from plugin to plugin.
The schedulingProfiles section defines the set of scheduling profiles that can be used in scheduling
requests to pods. The number of scheduling profiles one defines, depends on the use case. For simple
serving of requests, one is enough. For disaggregated prefill, two profiles are required. Each entry
in this section has the following form:
- name: aName
plugins:
- pluginRef: plugin1
- pluginRef: plugin2
weight: 50The fields in a schedulingProfile entry are:
- name: specifies the scheduling profile's name.
- plugins: specifies the set of plugins to be used when this scheduling profile is chosen for a request.
- pluginRef: reference to the name of the plugin instance to be used
- weight: weight to be used if the referenced plugin is a scorer.
A complete configuration might look like this:
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: precise-prefix-cache-producer
parameters:
indexerConfig:
tokenProcessorConfig:
blockSize: 5
kvBlockIndexConfig:
maxPrefixBlocksToMatch: 256
- type: prefix-cache-scorer
parameters:
prefixMatchInfoProducerName: precise-prefix-cache-producer
- type: decode-filter
- type: max-score-picker
- type: single-profile-handler
schedulingProfiles:
- name: default
plugins:
- pluginRef: decode-filter
- pluginRef: max-score-picker
- pluginRef: prefix-cache-scorer
weight: 50If the configuration is in a file, the EPP command line argument --config-file should be used
to specify the full path of the file in question. If the configuration is passed as in-line
text the EPP command line argument --config-text should be used.
The EPP injects these plugins when they are absent, so a configuration does not need to list them. Some example configs list them anyway for clarity; that has the same effect as omitting them. To override a default, configure it explicitly.
Scheduling:
- When
schedulingProfilesis omitted, a singledefaultprofile is created and populated with the listed plugins that are filters, scorers, or pickers. - When there is exactly one profile (including the auto-created
default) and no handler is configured,single-profile-handleris added. max-score-pickeris added when no picker is configured, and appended to any profile that lacks one.- A scorer's
weightdefaults to1.0when omitted.
RequestHandler:
- When no parsers are configured,
openai-parser,anthropic-parser, andvllmhttp-parserare used.
FlowControl:
fcfs-ordering-policy,global-strict-fairness-policy, andstatic-usage-limit-policyare configured when absent.utilization-detectoris configured as the saturation detector when none is set.
DataLayer:
metrics-data-sourceandcore-metrics-extractorare injected and wired together. Skipped whendataLayer.injectDefaultsisfalse, or when the config's owndataLayer.sourcesalready lists ametrics-data-sourceentry.
DataProducer:
- When a plugin needs data from a producer but none is configured, the default producer for that data
is created automatically. Defaults:
token-producer,approx-prefix-cache-producer,mm-embeddings-cache-producer,inflight-load-producer,predicted-latency-producer,session-id-producer.
To learn more about the available plugins, check the plugins README.md file.
- Scrapers collect metrics (e.g., memory usage, active adapters)
- Data is injected into the shared datastore for scorers
- Scoring can rely on numerical metrics or metadata (model ID, adapter tags)
When enabled, the router:
- Selects one pod for Prefill (prompt processing)
- Selects another pod for Decode (token generation)
Note
Encode disaggregation is an experimental feature. When enabled, the router identifies all pods capable of encoding, and the vLLM sidecar distributes multimedia requests to randomly selected pods from that subset. More sophisticated selection strategies are planned for future versions.
The vLLM sidecar handles orchestration between Encode, Prefill and Decode stages. It allows:
- Queuing
- Local memory management
- Experimental protocol compatibility
Note
The detailed E/P/D design is available in this document: Disaggregated Inference Serving in llm-d
Chunked decode is an experimental feature of the pd-sidecar that splits the decode stage into a sequence of shorter decode calls, each capped at a configurable token budget. It applies at the decode stage regardless of whether P/D disaggregation is in use. After each chunk the generated text is appended to the conversation context so the next chunk continues seamlessly from where the previous one left off.
- Improve average Time-To-First-Token among all requests
- Prevent head-of-line blocking by long requests in run-to-completion
- Get more predictable execution time
- The sidecar receives a
/v1/chat/completionsrequest at the decode stage. - Each chunk is dispatched as a separate request to the local decoder with
max_tokenscapped atdecode-chunk-size. - From the second chunk onward,
continue_final_message=trueandadd_generation_prompt=falseare set so the model continues the existing assistant turn rather than starting a new one. The generated text from the previous chunk is also appended to the request context. - Generation stops when the model returns a terminal
finish_reason(anything other thanlength), or when the original token budget is exhausted. - For non-streaming requests, all chunk outputs are concatenated and returned as a single
response. The
usagefield reports the originalprompt_tokens(from the first chunk) and the totalcompletion_tokensacross all chunks. - For streaming requests, each chunk's tokens are re-emitted as SSE delta events in real time,
and a
[DONE]sentinel closes the stream once all chunks are complete.
Enable chunked decode via the pd-sidecar flag:
| Flag | Default | Description |
|---|---|---|
--decode-chunk-size |
0 (disabled) |
Token budget per chunk. Set to a positive integer to enable chunked decode. For best performance use a multiple of the KV cache block size. |
Note
If the request's max_tokens / max_completion_tokens is less than or equal to --decode-chunk-size,
the sidecar falls back to a single regular decode call without chunking.
- Single
InferencePooland singleEPPdue to Envoy limitations - Model-based filtering can be handled within EPP
- Currently only one base model per
InferencePoolis supported. Multiple models are supported via multipleInferencePools.
Note
The InferenceModel CRD is in the process of being significantly changed in IGW.
Once finalized, these changes would be reflected in llm-d as well.