Skip to content

Commit be5afc3

Browse files
authored
Merge branch 'main' into fix/fc-saturation-staleness
2 parents 7b025ba + 2b8b61e commit be5afc3

88 files changed

Lines changed: 2261 additions & 1081 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pr-hold-gate.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@ on:
1010

1111
permissions:
1212
contents: read
13+
pull-requests: read
1314

1415
jobs:
1516
hold:
1617
runs-on: ubuntu-latest
1718
steps:
1819
- name: Block merge while held
19-
if: contains(github.event.pull_request.labels.*.name, 'hold') || contains(github.event.pull_request.labels.*.name, 'do-not-merge/hold')
20+
env:
21+
GH_TOKEN: ${{ github.token }}
22+
PR_URL: ${{ github.event.pull_request.html_url }}
2023
run: |
21-
echo "::error::A hold label is present (hold or do-not-merge/hold). Remove it (/hold cancel) to allow merge."
22-
exit 1
24+
blocking_labels=(hold do-not-merge/hold)
25+
26+
# fetch current labels
27+
labels=$(gh pr view "$PR_URL" --json labels -q '.labels[].name')
28+
for label in "${blocking_labels[@]}"; do
29+
if grep -qxF "$label" <<< "$labels"; then
30+
echo "::error::Blocking label '$label' is present. Remove it to allow merge (e.g., /hold cancel for hold)."
31+
exit 1
32+
fi
33+
done

.github/workflows/prow-github.yml

Lines changed: 0 additions & 35 deletions
This file was deleted.

.prowlabels.yaml

Lines changed: 0 additions & 11 deletions
This file was deleted.

README.coord.md

Lines changed: 3 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,21 @@
22

33
A Go service that orchestrates multi-phase LLM inference pipelines (Encode/Prefill/Decode) across specialized worker pools. It exposes OpenAI-compatible APIs and routes requests through an Inference Gateway to disaggregated vLLM workers.
44

5+
For the architecture, request lifecycle, EPP integration, and plugin API, see [docs/coordinator_architecture.md](docs/coordinator_architecture.md). For the exact per-stage wire formats, see [docs/communication.md](docs/communication.md).
6+
57
## Table of Contents
68

79
- [LLM-D Coordinator](#llm-d-coordinator)
810
- [Table of Contents](#table-of-contents)
9-
- [Architecture](#architecture)
1011
- [Quick Start](#quick-start)
1112
- [Configuration](#configuration)
12-
- [Server](#server)
13-
- [Gateway](#gateway)
14-
- [Pipeline](#pipeline)
15-
- [Built-in Step Parameters](#built-in-step-parameters)
16-
- [Gateway Routing](#gateway-routing)
17-
- [Plugin API](#plugin-api)
18-
- [Step Interface](#step-interface)
19-
- [Writing a Custom Step](#writing-a-custom-step)
20-
- [Registering the Step](#registering-the-step)
21-
- [Dependency Injection](#dependency-injection)
22-
- [RequestContext](#requestcontext)
2313
- [API Endpoints](#api-endpoints)
2414
- [Docker](#docker)
2515
- [Development](#development)
2616
- [Running Tests](#running-tests)
2717
- [Unit Tests](#unit-tests)
2818
- [End-to-End Tests](#end-to-end-tests)
2919

30-
## Architecture
31-
32-
```
33-
+--------+
34-
| Client |
35-
+--------+
36-
|
37-
v
38-
+-------------------+ +-------------+
39-
| Inference Gateway | <---> | Coordinator |
40-
+-------------------+ +-------------+
41-
|
42-
v
43-
+-----+
44-
| EPP |
45-
+-----+
46-
|
47-
v
48-
+--------------+
49-
| vLLM Workers |
50-
+--------------+
51-
```
52-
53-
The Coordinator processes each request through a configurable pipeline of steps:
54-
55-
1. **replace-media-urls** - Downloads image URLs and inlines them as base64
56-
2. **render** - Sends request to an external rendering/tokenization service
57-
3. **encode** - Parallel fan-out: one encode request per multimodal entry
58-
4. **prefill** - Combines encode results, sends to prefill worker
59-
5. **decode** - Forwards the final request to decode worker, streams response back
60-
6120
## Quick Start
6221

6322
The coordinator targets live in `Makefile.coord.mk`, which the root `Makefile`
@@ -79,188 +38,7 @@ make -f Makefile.coord.mk test
7938

8039
## Configuration
8140

82-
Configuration is a YAML file passed via the `--config` flag. See `config/coordinator/coordinator.yaml` for the default.
83-
84-
### Server
85-
86-
```yaml
87-
server:
88-
listen_addr: ":8080" # Address to listen on
89-
read_timeout: 30s # HTTP read timeout
90-
write_timeout: 120s # HTTP write timeout (long for streaming)
91-
```
92-
93-
### Gateway
94-
95-
Connection settings for the Inference Gateway that routes to vLLM worker pools:
96-
97-
```yaml
98-
gateway:
99-
address: "http://inference-gateway:80"
100-
max_idle_conns_per_host: 100 # Connection pool size
101-
idle_conn_timeout: 90s
102-
timeout: 60s # Per-request timeout
103-
```
104-
105-
The rendering service address is not a top-level setting; it is the `address` parameter of the `render` pipeline step (see below).
106-
107-
### Pipeline
108-
109-
The pipeline is an ordered list of steps. Each step has a `type` (registered name) and optional `params`:
110-
111-
```yaml
112-
pipeline:
113-
steps:
114-
- type: replace-media-urls
115-
params:
116-
download_timeout: 10s
117-
max_concurrent_downloads: 10
118-
- type: render
119-
params:
120-
address: "http://rendering-service:8080"
121-
- type: encode
122-
params:
123-
max_parallel: 8
124-
- type: prefill
125-
- type: decode
126-
```
127-
128-
To remove a step, delete it from the list. To reorder, move entries up or down. Steps execute sequentially in the order listed.
129-
130-
### Built-in Step Parameters
131-
132-
| Step | Parameter | Default | Description |
133-
|------|-----------|---------|-------------|
134-
| replace-media-urls | `download_timeout` | `10s` | Timeout for each image download |
135-
| replace-media-urls | `max_concurrent_downloads` | `10` | Max parallel downloads |
136-
| render | `address` | (required) | Base URL of the rendering service |
137-
| render | `timeout` | `30s` | Timeout for a single render call |
138-
| render | `max_total_tokens` | `0` (unlimited) | Reject requests whose tokenized prompt exceeds this |
139-
| render | `max_total_placeholder_tokens` | `0` (unlimited) | Reject requests whose summed image-placeholder length exceeds this |
140-
| encode | `max_parallel` | `8` | Max parallel encode requests |
141-
142-
### Gateway Routing
143-
144-
The coordinator sends every sub-request to the same gateway address. It does not use phase-specific URL prefixes; instead it stamps an `EPP-Phase` header (`encode`, `prefill`, or `decode`) so the Endpoint Picker can route to the correct worker pool. The request path is chosen by the request format:
145-
146-
| Phase | Header | Path |
147-
|-------|--------|------|
148-
| Encode | `EPP-Phase: encode` | `/v1/completions` for completions requests; otherwise `/inference/v1/generate`, or `/v1/chat/completions` when `use_openai_format` is set |
149-
| Prefill | `EPP-Phase: prefill` | same as encode |
150-
| Decode | `EPP-Phase: decode` | original client request path (`/v1/chat/completions` or `/v1/completions`) |
151-
152-
The decode step preserves the original client request path so the gateway can route it to the correct OpenAI-compatible endpoint on the decode worker.
153-
154-
## Plugin API
155-
156-
Custom pipeline steps can be added by implementing the `Step` interface and registering a factory function.
157-
158-
### Step Interface
159-
160-
```go
161-
package pipeline
162-
163-
type Step interface {
164-
Name() string
165-
Execute(ctx context.Context, reqCtx *RequestContext) error
166-
}
167-
```
168-
169-
### Writing a Custom Step
170-
171-
```go
172-
package mystep
173-
174-
import (
175-
"context"
176-
"github.com/llm-d/llm-d-router/pkg/coordinator/pipeline"
177-
)
178-
179-
func init() {
180-
pipeline.Register("my-step", NewMyStep)
181-
}
182-
183-
type MyStep struct {
184-
someParam string
185-
}
186-
187-
func NewMyStep(params map[string]any) (pipeline.Step, error) {
188-
s := &MyStep{someParam: "default"}
189-
if v, ok := params["some_param"].(string); ok {
190-
s.someParam = v
191-
}
192-
return s, nil
193-
}
194-
195-
func (s *MyStep) Name() string { return "my-step" }
196-
197-
func (s *MyStep) Execute(ctx context.Context, reqCtx *pipeline.RequestContext) error {
198-
// Access and modify the request context:
199-
// - reqCtx.Body (parsed JSON body, mutable)
200-
// - reqCtx.TokenIDs (token IDs from the render step)
201-
// - reqCtx.MultimodalEntries (multimodal content)
202-
// - reqCtx.ECTransferParams (encoder cache transfer params, per encode response)
203-
// - reqCtx.KVTransferParams (KV cache transfer params)
204-
// - reqCtx.Model (model name)
205-
// - reqCtx.Stream (whether client requested streaming)
206-
//
207-
// Return nil to continue, or an error to abort the pipeline.
208-
return nil
209-
}
210-
```
211-
212-
### Registering the Step
213-
214-
Import your step package in `cmd/coordinator/main.go`:
215-
216-
```go
217-
import _ "github.com/llm-d/llm-d-router/pkg/coordinator/steps/mystep"
218-
```
219-
220-
Then add it to the pipeline config:
221-
222-
```yaml
223-
pipeline:
224-
steps:
225-
- type: my-step
226-
params:
227-
some_param: "value"
228-
- type: decode
229-
```
230-
231-
### Dependency Injection
232-
233-
A step that needs the shared gateway HTTP client implements `gateway.ClientAware`. After building each step, the coordinator type-asserts it against this interface and calls `SetGatewayClient` when it matches:
234-
235-
```go
236-
// gateway.ClientAware receives the shared gateway HTTP client.
237-
type ClientAware interface {
238-
SetGatewayClient(*Client)
239-
}
240-
```
241-
242-
Step parameters from the YAML `params` map are the mechanism for everything else. For example, the render step reads its service address from `params.address` in its factory rather than through an injected interface. The render step does expose a `SetServiceAddress` method, but it is used only by tests to point the step at a local server and is not called in production.
243-
244-
### RequestContext
245-
246-
The `RequestContext` is the shared state passed between steps:
247-
248-
```go
249-
type RequestContext struct {
250-
RequestID string // Unique request ID
251-
OriginalPath string // Client request path (e.g., /v1/chat/completions)
252-
OriginalHeaders http.Header // Inbound request headers (forwarded upstream, minus hop-by-hop)
253-
OriginalBody []byte // Raw request body
254-
Body map[string]any // Parsed/mutable JSON body
255-
Model string // Model name
256-
Stream bool // SSE streaming requested
257-
TokenIDs []int // Token IDs from the render step
258-
MultimodalEntries []MultimodalEntry // Downloaded multimodal content
259-
ECTransferParams []map[string]any // Encode results, one entry per encode response (mm_hash -> descriptor)
260-
KVTransferParams map[string]any // Prefill KV-cache transfer hints, consumed by the KV connector at decode
261-
ResponseWriter http.ResponseWriter // Client response writer; decode steps stream the final response to it
262-
}
263-
```
41+
Configuration is a YAML file passed via the `--config` flag. See `config/coordinator/coordinator.yaml` for the annotated default, and [Configuring the pipeline](docs/coordinator_architecture.md#configuring-the-pipeline) for the full reference (top-level structure, environment overrides, connector selection, and the built-in steps).
26442

26543
## API Endpoints
26644

SECURITY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Security
2+
Please see the [llm-d Security Policy](https://github.com/llm-d/llm-d/blob/main/SECURITY.md) in the main repository for vulnerability reporting and disclosure information.

cmd/epp/runner/runner.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import (
111111
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/prefixcacheaffinity"
112112
sessionaffinityfilter "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/sessionaffinity"
113113
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/sloheadroomtier"
114+
utilizationfilter "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/utilization"
114115
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/picker/maxscore"
115116
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/picker/random"
116117
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/picker/weightedrandom"
@@ -558,6 +559,7 @@ func (r *Runner) registerInTreePlugins() {
558559
fwkplugin.Register(bylabel.PrefillRoleType, bylabel.PrefillRoleFactory)
559560
fwkplugin.Register(endpointattributefilter.EndpointAttributeFilterType, endpointattributefilter.EndpointAttributeFilterFactory)
560561
fwkplugin.Register(sessionaffinityfilter.SessionAffinityType, sessionaffinityfilter.Factory)
562+
fwkplugin.Register(utilizationfilter.UtilizationFilterType, utilizationfilter.Factory)
561563

562564
// dataparallel profile handler
563565
fwkplugin.Register(dataparallel.DataParallelProfileHandlerType, dataparallel.ProfileHandlerFactory)
@@ -911,7 +913,7 @@ func (r *Runner) initAdmissionControl(
911913
UsageLimitPolicy: eppConfig.FlowControlConfig.UsageLimitPolicy,
912914
},
913915
)
914-
return endpointCandidates, requestcontrol.NewFlowControlAdmissionController(fc, opts.PoolName), registry
916+
return endpointCandidates, requestcontrol.NewFlowControlAdmissionController(fc, opts.PoolName, endpointCandidates), registry
915917
}
916918

917919
// runWithFileDiscovery handles the execution path when a discovery plugin is configured.

deploy/config/probabilistic-admitter-epp-config.yaml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# EPP configuration for the probabilistic-admitter plugin.
22
#
3-
# The probabilistic-admitter handles all saturation-based admission decisions,
4-
# so the system-level utilization-detector is configured with effectively
5-
# infinite thresholds to prevent it from gating requests before the admitter
6-
# can evaluate them.
3+
# The probabilistic-admitter handles all saturation-based admission decisions
4+
# at the admission extension point. To let it own admission exclusively, the
5+
# system-level utilization-detector is configured with effectively infinite
6+
# thresholds (queueDepthThreshold: 999999999, kvCacheUtilThreshold: 1.0).
7+
# This prevents the flow control dispatch gate from queueing requests before
8+
# the admitter can evaluate them.
9+
#
10+
# Note: If these utilization-detector thresholds are raised, requests will
11+
# undergo flow control queueing before reaching the probabilistic-admitter.
712
apiVersion: inference.networking.x-k8s.io/v1alpha1
813
kind: EndpointPickerConfig
914
plugins:

0 commit comments

Comments
 (0)