Skip to content

Commit a049bdc

Browse files
committed
Sync upstream llm-d/llm-d-batch-gateway 3002a02
2 parents e20b0f2 + 3002a02 commit a049bdc

18 files changed

Lines changed: 75 additions & 72 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ The system is designed to facilitate efficient processing of batch workloads in
7474
- Priority queue, event channels, and status updates: `Redis` or `Valkey`.
7575
- File storage: `S3`, `filesystem`.
7676

77-
4. **Batch Dispatcher** *(optional, future component)* — implemented in [llm-d-incubation/llm-d-async](https://github.com/llm-d-incubation/llm-d-async)
77+
4. **Batch Dispatcher** *(optional)* — implemented in [llm-d/llm-d-async](https://github.com/llm-d/llm-d-async)
7878
- An optional component that provides metrics-driven flow control for batch request dispatching.
7979
- Monitors downstream inference system metrics (e.g. queue depth, latency, utilization) and dynamically adjusts dispatch flow to minimize interference with interactive requests while meeting batch job SLOs.
8080
- Not required for batch processing — the batch processor includes built-in AIMD-based adaptive concurrency control.
@@ -462,7 +462,7 @@ limitations under the License.
462462
## Related Projects
463463

464464
- [llm-d-router](https://github.com/llm-d/llm-d-router) - llm-d inference router.
465-
- [llm-d-async](https://github.com/llm-d-incubation/llm-d-async) - Metrics-driven batch dispatch flow control (optional).
465+
- [llm-d-async](https://github.com/llm-d/llm-d-async) - Metrics-driven batch dispatch flow control (optional).
466466

467467
## Support
468468

docs/design/batch-dispatcher-queue-design.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@
66

77
Related:
88
- [Batch Dispatcher](batch-dispatcher.md)
9-
- [Dispatch Budget](https://github.com/llm-d-incubation/llm-d-async/blob/main/docs/dispatch-budget.md) (llm-d-async)
9+
- [Dispatch Budget](https://github.com/llm-d/llm-d-async/blob/main/docs/dispatch-budget.md) (llm-d-async)
1010
- [Batch Processor Design](batch_processor_architecture.md)
1111
- [Batch Inference Architecture](batch_inference_architecture.md)
1212

1313
---
1414

1515
## Summary
1616

17-
This document describes the design of the request and result queues that connect the **batch-processor** to the **batch dispatcher** ([llm-d-async](https://github.com/llm-d-incubation/llm-d-async)). At this time we always assume that each target inference pool corresponds to a single connector. In other words, we assume that there will never be 2 queues targeting the same inference pool at once; we reserve this for future extensions, if needed.
17+
This document describes the design of the request and result queues that connect the **batch-processor** to the **batch dispatcher** ([llm-d-async](https://github.com/llm-d/llm-d-async)). At this time we always assume that each target inference pool corresponds to a single connector. In other words, we assume that there will never be 2 queues targeting the same inference pool at once; we reserve this for future extensions, if needed.
1818

1919
The batch-processor supports two mutually exclusive dispatch modes, selected via `dispatch_mode`:
2020

2121
- **`sync`** (default): The executor dispatches inference requests directly to the inference gateway via HTTP, using the existing AIMD + semaphore flow control.
22-
- **`async`**: The executor enqueues individual requests into **the dispatcher's request queue**; **the dispatcher pulls and forwards** them to the inference gateway based on the [dispatch budget](https://github.com/llm-d-incubation/llm-d-async/blob/main/docs/dispatch-budget.md). A **result consumer** in the batch-processor reads completed responses from **the dispatcher's result queue** and routes them back to the appropriate job's output writer.
22+
- **`async`**: The executor enqueues individual requests into **the dispatcher's request queue**; **the dispatcher pulls and forwards** them to the inference gateway based on the [dispatch budget](https://github.com/llm-d/llm-d-async/blob/main/docs/dispatch-budget.md). A **result consumer** in the batch-processor reads completed responses from **the dispatcher's result queue** and routes them back to the appropriate job's output writer.
2323

2424
This document describes the **async** dispatch mode and its queue design.
2525

@@ -83,7 +83,7 @@ The Redis URL is read from a mounted secret at runtime (not stored in the config
8383

8484
### Dispatcher Configuration
8585

86-
The dispatcher (llm-d-async) already supports the Redis sorted-set flow with dispatch budget gating. The request queue is configured via the [JSON queues config file](https://github.com/llm-d-incubation/llm-d-async/blob/main/README.md#redis-sorted-set-persisted) (`--redis.ss.queues-config-file`); the result queue is configured via `--redis.ss.result-queue-name`:
86+
The dispatcher (llm-d-async) already supports the Redis sorted-set flow with dispatch budget gating. The request queue is configured via the [JSON queues config file](https://github.com/llm-d/llm-d-async/blob/main/README.md#redis-sorted-set-persisted) (`--redis.ss.queues-config-file`); the result queue is configured via `--redis.ss.result-queue-name`:
8787

8888
```json
8989
[
@@ -107,7 +107,7 @@ The dispatcher (llm-d-async) already supports the Redis sorted-set flow with dis
107107
108108
The queue names must match those derived by the batch-processor's `RequestQueueName()` and `ResultQueueName()` functions.
109109
110-
The dispatcher pulls up to `max_SYS × budget` requests per poll cycle and forwards them to the inference gateway. See the [llm-d-async README](https://github.com/llm-d-incubation/llm-d-async/blob/main/README.md) and [Helm chart values](https://github.com/llm-d-incubation/llm-d-async/tree/main/charts/async-processor) for the full configuration.
110+
The dispatcher pulls up to `max_SYS × budget` requests per poll cycle and forwards them to the inference gateway. See the [llm-d-async README](https://github.com/llm-d/llm-d-async/blob/main/README.md) and [Helm chart values](https://github.com/llm-d/llm-d-async/tree/main/charts/async-processor) for the full configuration.
111111
112112
### Future Extension: Queue Registry
113113
@@ -130,7 +130,7 @@ Note: The request queue is currently implemented by a **Redis SortedSet** that h
130130
131131
Unlike direct dispatch to the inference gateway — where the EPP's flow control limits how many requests can be in-flight and excess requests are rejected with HTTP 429 — **the request queue is a passive buffer with no backpressure on writes**. The producer can enqueue requests as fast as it can read plan entries, without throttling or semaphore gating. This is safe because:
132132
133-
1. **Flow control is deferred to the dispatcher.** The [dispatch budget](https://github.com/llm-d-incubation/llm-d-async/blob/main/docs/dispatch-budget.md) gates how many requests leave the queue per poll cycle. The gate returns a `budget` value in [0, 1] representing remaining system capacity (generally `budget = D − B`, where `D` is the dispatch budget and `B` is the reserved baseline). The dispatcher pops up to `max_SYS × budget` requests per cycle, where `max_SYS` is a configurable measure of total system capacity. Enqueuing more requests than the dispatcher can immediately process simply means they wait in the queue until capacity opens up — they do not reach the inference gateway or compete with online traffic.
133+
1. **Flow control is deferred to the dispatcher.** The [dispatch budget](https://github.com/llm-d/llm-d-async/blob/main/docs/dispatch-budget.md) gates how many requests leave the queue per poll cycle. The gate returns a `budget` value in [0, 1] representing remaining system capacity (generally `budget = D − B`, where `D` is the dispatch budget and `B` is the reserved baseline). The dispatcher pops up to `max_SYS × budget` requests per cycle, where `max_SYS` is a configurable measure of total system capacity. Enqueuing more requests than the dispatcher can immediately process simply means they wait in the queue until capacity opens up — they do not reach the inference gateway or compete with online traffic.
134134
135135
2. **The queue is cheap storage.** Redis sorted sets are memory-efficient for this workload. Each request message is a few KB; even a full 50,000-request batch job at ~2 KB per message is ~100 MB — well within Redis capacity and far cheaper than holding in-flight HTTP connections.
136136
@@ -140,7 +140,7 @@ Unlike direct dispatch to the inference gateway — where the EPP's flow control
140140
141141
### Message Format
142142
143-
Request messages follow the wire format defined in the [llm-d-async README — Request Messages and Consumption](https://github.com/llm-d-incubation/llm-d-async/blob/main/README.md#request-messages-and-consumption). The `metadata` field carries batch-processor correlation data (`job_id`, `request_index`) that the dispatcher passes through opaquely and returns in the result. The `headers` field can carry HTTP headers that the dispatcher forwards to the inference gateway (e.g., fairness/SLO headers that the current executor attaches directly).
143+
Request messages follow the wire format defined in the [llm-d-async README — Request Messages and Consumption](https://github.com/llm-d/llm-d-async/blob/main/README.md#request-messages-and-consumption). The `metadata` field carries batch-processor correlation data (`job_id`, `request_index`) that the dispatcher passes through opaquely and returns in the result. The `headers` field can carry HTTP headers that the dispatcher forwards to the inference gateway (e.g., fairness/SLO headers that the current executor attaches directly).
144144
145145
The sorted-set score is the request's SLO deadline (Unix timestamp), so earliest-deadline requests are dispatched first across all jobs sharing the same pool — providing cross-job deadline-aware scheduling.
146146
@@ -175,7 +175,7 @@ As described above, the producer does not need to throttle enqueue operations. I
175175

176176
### Dispatcher (reads from request queue)
177177

178-
The dispatcher ([llm-d-async](https://github.com/llm-d-incubation/llm-d-async)) periodically polls the sorted set. On each cycle, it computes the budget on its "Dispatch Gate" to determine the current dispatch budget $D$. If $D > B$ (the reserved baseline), it pops up to `max_SYS × budget` requests (lowest score = earliest deadline first) and forwards them to the inference gateway via HTTP. See [Dispatch Budget](https://github.com/llm-d-incubation/llm-d-async/blob/main/docs/dispatch-budget.md) for the full gating logic.
178+
The dispatcher ([llm-d-async](https://github.com/llm-d/llm-d-async)) periodically polls the sorted set. On each cycle, it computes the budget on its "Dispatch Gate" to determine the current dispatch budget $D$. If $D > B$ (the reserved baseline), it pops up to `max_SYS × budget` requests (lowest score = earliest deadline first) and forwards them to the inference gateway via HTTP. See [Dispatch Budget](https://github.com/llm-d/llm-d-async/blob/main/docs/dispatch-budget.md) for the full gating logic.
179179

180180
The dispatcher requires no changes to support this integration, it already implements the Redis sorted-set flow with dispatch budget gating. Only the queue names need to match the naming convention.
181181

@@ -189,7 +189,7 @@ Note: the result queue is currently implemented by a Redis list.
189189

190190
### Message Format
191191

192-
Result messages follow the format defined in the [llm-d-async README — Results](https://github.com/llm-d-incubation/llm-d-async/blob/main/README.md#results). The `metadata` from the original request (containing `job_id` and `request_index`) is passed through by the dispatcher, allowing the consumer to route results back to the correct job.
192+
Result messages follow the format defined in the [llm-d-async README — Results](https://github.com/llm-d/llm-d-async/blob/main/README.md#results). The `metadata` from the original request (containing `job_id` and `request_index`) is passed through by the dispatcher, allowing the consumer to route results back to the correct job.
193193

194194
**Example:**
195195

docs/design/batch-dispatcher.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ Related:
55
- [\[PUBLIC\] EPP Flow Controller for Priority, Fairness, and Queuing](https://docs.google.com/document/d/1VZL7opFWuwgWquvgiOzLlXAJ633qZ9U-A0ZixGjBgaI/edit?tab=t.0#heading=h.hfyow92z2d0t)
66
- [\[PUBLIC\] Improved Flow Control Request Management](https://docs.google.com/document/d/1JxzJc8gNv2wKK5-a8ohb0btn78ymVKw9XMIb4-S-ncA/edit?tab=t.0#heading=h.rutawybt03nl)
77
- [https://gateway-api-inference-extension.sigs.k8s.io/api-types/inferencepool/](https://gateway-api-inference-extension.sigs.k8s.io/api-types/inferencepool/)
8-
- [Async Processor: Dispatch Gates](https://github.com/llm-d-incubation/llm-d-async/blob/main/README.md#dispatch-gates) (llm-d-async)
9-
- [Dispatch Budget](https://github.com/llm-d-incubation/llm-d-async/blob/main/docs/dispatch-budget.md) (llm-d-async)
8+
- [Async Processor: Dispatch Gates](https://github.com/llm-d/llm-d-async/blob/main/README.md#dispatch-gates) (llm-d-async)
9+
- [Dispatch Budget](https://github.com/llm-d/llm-d-async/blob/main/docs/dispatch-budget.md) (llm-d-async)
1010

1111
## Summary
1212

13-
This document details the design of a **Batch Dispatcher** (sometimes also referred to as the [**Async Processor**](https://github.com/llm-d-incubation/llm-d-async)) to extend the existing "online batch processing agent" architecture (see [\[Public Doc\] Serving Online Batch via Inference Gateway](https://docs.google.com/document/d/1notkq9s0qOmWmUNonZ8CfI-5jtGtHA4PGMI-xz8sGRE/edit?tab=t.0#heading=h.i76kzr3j3swj)). While the **llm-d Router** acts as the primary component for scheduling and flow control, the Batch Dispatcher serves as a system-load aware gatekeeper. It ensures that batch workloads (low-priority and sheddable) are pulled from message queues and forwarded to the llm-d Router only when the inference pool has sufficient capacity. This prevents low-priority traffic from flooding the system and competing with realtime requests.
13+
This document details the design of a **Batch Dispatcher** (sometimes also referred to as the [**Async Processor**](https://github.com/llm-d/llm-d-async)) to extend the existing "online batch processing agent" architecture (see [\[Public Doc\] Serving Online Batch via Inference Gateway](https://docs.google.com/document/d/1notkq9s0qOmWmUNonZ8CfI-5jtGtHA4PGMI-xz8sGRE/edit?tab=t.0#heading=h.i76kzr3j3swj)). While the **llm-d Router** acts as the primary component for scheduling and flow control, the Batch Dispatcher serves as a system-load aware gatekeeper. It ensures that batch workloads (low-priority and sheddable) are pulled from message queues and forwarded to the llm-d Router only when the inference pool has sufficient capacity. This prevents low-priority traffic from flooding the system and competing with realtime requests.
1414

1515
## Problem statement
1616

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ require (
1414
github.com/go-resty/resty/v2 v2.17.2
1515
github.com/google/uuid v1.6.0
1616
github.com/jackc/pgx/v5 v5.10.0
17-
github.com/llm-d-incubation/llm-d-async/api v0.7.3
18-
github.com/llm-d-incubation/llm-d-async/producer v0.7.3
17+
github.com/llm-d/llm-d-async/api v0.7.4
18+
github.com/llm-d/llm-d-async/producer v0.7.4
1919
github.com/pashagolub/pgxmock/v4 v4.9.0
2020
github.com/prometheus/client_golang v1.24.0
2121
github.com/quasilyte/go-ruleguard/dsl v0.3.23

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
8888
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
8989
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
9090
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
91-
github.com/llm-d-incubation/llm-d-async/api v0.7.3 h1:OI4zlNdKeXrWa9Q9AhFOSvJZvLC84Nnsv3maF4d8Tns=
92-
github.com/llm-d-incubation/llm-d-async/api v0.7.3/go.mod h1:m2zJUwD/AZypJv8RPws3uvCnfQoNW7iv+hH9wPk/Xw4=
93-
github.com/llm-d-incubation/llm-d-async/producer v0.7.3 h1:f06rUZ92g/2gQmcrquYYd5LNGeCYffb1vZdP+7luiA0=
94-
github.com/llm-d-incubation/llm-d-async/producer v0.7.3/go.mod h1:7FVFV0ggHyaSbD33DP1V68tV7bOVKqjAezd8UgD9Qik=
91+
github.com/llm-d/llm-d-async/api v0.7.4 h1:R3CBDMxlT+9GOyJkgNdQO7zqSpwVFgfz085vSlXn91Q=
92+
github.com/llm-d/llm-d-async/api v0.7.4/go.mod h1:hzjFDTFBJEyW9/1vrHAD2dtCVLTzqggU1GBW+7YhxlQ=
93+
github.com/llm-d/llm-d-async/producer v0.7.4 h1:dmMNtkorVykYX5UXRYY7qC7q8IesGtbHBY+RFwoaE60=
94+
github.com/llm-d/llm-d-async/producer v0.7.4/go.mod h1:nHxbBM/mVL7bTu2ZVZ+OMz/Up9m2PhpBmeLfoq899+0=
9595
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
9696
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
9797
github.com/pashagolub/pgxmock/v4 v4.9.0 h1:itlO8nrVRnzkdMBXLs8pWUyyB2PC3Gku0WGIj/gGl7I=

internal/processor/worker/execute_pipeline.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ func (p *Processor) executeJobAsync(ctx, sloCtx, userCancelCtx, requestAbortCtx
2828
return nil, fmt.Errorf("read model map: %w", err)
2929
}
3030

31-
if sloCtx.Err() == context.DeadlineExceeded {
32-
logger.V(logging.INFO).Info("SLO already expired at execution start",
33-
"total", modelMap.LineCount)
34-
return &openai.BatchRequestCounts{Total: modelMap.LineCount, Failed: modelMap.RejectedCount}, errExpired
35-
}
36-
3731
files, err := p.openDataFiles(params)
3832
if err != nil {
3933
return nil, err

internal/processor/worker/executor_test.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,9 @@ func TestExecuteJob_AbortCtxCancel_AbortsInflightRequests(t *testing.T) {
379379
}
380380

381381
// TestExecuteJob_SLOExpiredBeforeDispatch verifies that when the SLO deadline has already
382-
// passed before execution begins, executeJob returns errExpired immediately with the total
383-
// request count and no output/error files are written (early-exit fast path).
382+
// passed before execution begins, the pipeline still runs and drains all requests as
383+
// batch_expired errors. This ensures that handleExpired can upload a non-empty error file
384+
// and the DB reflects Failed == Total.
384385
func TestExecuteJob_SLOExpiredBeforeDispatch(t *testing.T) {
385386
cfg := config.NewConfig()
386387
cfg.WorkDir = t.TempDir()
@@ -393,7 +394,6 @@ func TestExecuteJob_SLOExpiredBeforeDispatch(t *testing.T) {
393394
env, jobInfo := setupExecutionJob(t, cfg, &mockInferenceClient{}, requests, map[string]string{"m1": "m1"})
394395

395396
ctx := testLoggerCtx(t)
396-
// SLO deadline already in the past: early check fires before any files are opened.
397397
sloCtx, cancel := context.WithDeadline(ctx, time.Now().Add(-1*time.Second))
398398
defer cancel()
399399

@@ -408,25 +408,33 @@ func TestExecuteJob_SLOExpiredBeforeDispatch(t *testing.T) {
408408
t.Fatal("expected non-nil counts")
409409
return
410410
}
411-
// Early exit: total is known from the model map, but no requests were dispatched or drained.
412411
if counts.Total != 3 {
413412
t.Fatalf("Total = %d, want 3", counts.Total)
414413
}
415414
if counts.Completed != 0 {
416415
t.Fatalf("Completed = %d, want 0", counts.Completed)
417416
}
418-
if counts.Failed != 0 {
419-
t.Fatalf("Failed = %d, want 0 (no drain on early exit)", counts.Failed)
417+
if counts.Failed != 3 {
418+
t.Fatalf("Failed = %d, want 3 (all requests drained as batch_expired)", counts.Failed)
420419
}
421420

422-
// No output or error files are written on early exit: files are only opened after the SLO check.
423-
outputPath, _ := env.p.jobOutputFilePath(jobInfo.JobID, jobInfo.TenantID)
424421
errorPath, _ := env.p.jobErrorFilePath(jobInfo.JobID, jobInfo.TenantID)
425-
if _, statErr := os.Stat(outputPath); !errors.Is(statErr, os.ErrNotExist) {
426-
t.Fatalf("output.jsonl should not exist on early SLO exit, got stat err: %v", statErr)
422+
errorData, readErr := os.ReadFile(errorPath)
423+
if readErr != nil {
424+
t.Fatalf("error.jsonl should exist: %v", readErr)
425+
}
426+
errorLines := bytes.Count(bytes.TrimSpace(errorData), []byte("\n")) + 1
427+
if errorLines != 3 {
428+
t.Fatalf("error.jsonl lines = %d, want 3", errorLines)
429+
}
430+
431+
outputPath, _ := env.p.jobOutputFilePath(jobInfo.JobID, jobInfo.TenantID)
432+
outputData, readErr := os.ReadFile(outputPath)
433+
if readErr != nil {
434+
t.Fatalf("output.jsonl should exist: %v", readErr)
427435
}
428-
if _, statErr := os.Stat(errorPath); !errors.Is(statErr, os.ErrNotExist) {
429-
t.Fatalf("error.jsonl should not exist on early SLO exit, got stat err: %v", statErr)
436+
if len(outputData) != 0 {
437+
t.Fatalf("output.jsonl should be empty, got %d bytes", len(outputData))
430438
}
431439
}
432440

0 commit comments

Comments
 (0)