feat: e2e dispatcher integration - #458
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for async dispatch via llm-d-async (Redis-backed queues) and introduces deployment + E2E coverage for the dispatcher/gating behaviors.
Changes:
- Introduce async inference client + resolver (
llm-d-asyncproducer/Redis) and wire it intoClientset+ processor config resolution. - Extend processor Helm config to support
dispatch_mode: asyncandasync_dispatchsettings. - Add dispatcher E2E tests and dev scripts/targets to deploy dispatchers and run the new E2E suite.
Reviewed changes
Copilot reviewed 24 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/clients/inference/async_inference_client.go |
New inference client that enqueues requests + polls results via llm-d-async. |
pkg/clients/inference/async_inference_client_resolver.go |
Resolver creating per-model async clients backed by a shared Redis client + producers. |
pkg/clients/inference/inference_client_resolver.go |
Adds Close() on resolver to release async resources (Redis/producers). |
internal/util/clientset/clientset.go |
Adds WithAsyncInference option and constructs async resolver when configured; closes inference resolver on shutdown. |
internal/processor/config/config.go / config_test.go |
Async mode validation changes + resolves async model→pool mapping into ResolvedGateways.Async. |
cmd/batch-processor/main.go |
Wires resolved async gateways into clientset and improves initialization logging by mode. |
charts/batch-gateway/templates/processor-configmap.yaml |
Emits dispatch_mode and async_dispatch config; supports inference_pool_name in model_gateways. |
test/e2e/dispatcher_test.go + test/e2e/dispatcher/*.yaml |
New E2E tests and Helm values used to validate dispatcher + gate behaviors. |
scripts/dev-deploy-dispatcher.sh / scripts/dev-clean.sh / Makefile |
Adds dev workflow to install dispatchers, patch Prometheus/vLLM sim, port-forward, and run dispatcher E2E tests. |
go.mod / go.sum + Dockerfiles |
Adds llm-d-async deps and a local replace, plus Docker build context copying. |
test/e2e/go.mod / test/e2e/go.sum |
Adds llm-d-async deps + replaces for E2E module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
override using PR at llm-d/llm-d-async#219 jaeger trace:
|
|
squashed+rebased |
|
rebased+pushed |
|
addressed comments, rebased, tested e2e locally |
| result_queue_name: "llm-d-async:results:sim-pool-prom" | ||
| request_path_url: "/v1/completions" | ||
| igw_base_url: "http://vllm-sim.default.svc.cluster.local:8000" | ||
| gate_type: "prometheus-query" |
There was a problem hiding this comment.
I am working on batch-gateway-operator to deploy llm-d-async
what gate_type and gate_params should be default values? or keep it as empty?
There was a problem hiding this comment.
I think prometheus-budget might be the safest, it should cascade automatically to different strategies in case the queries are missing data https://github.com/llm-d-incubation/llm-d-async#per-queue-dispatch-gates
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
Resolve go.mod/go.sum conflicts by accepting main's dependency versions and re-adding llm-d-async/api and llm-d-async/producer v0.7.2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lior Aronovich <lioraronpr@gmail.com>

Why is this PR needed?
The batch-gateway processor currently dispatches inference requests via direct HTTP calls to model gateways.
To integrate with the llm-d-async dispatcher we need an async dispatch path that enqueues requests
to Redis sorted sets and reads results from Redis lists, using the llm-d-async wire format.
Test this with:
As explained below the async interface calls from a more profound refactoring, but I suggest the team to take on this in a follow-up.
What does this PR do?
Originally I used the
apipackage directly, without realizing I should use theproducerpkg (neither were published).Now, if I understand correctly, the batch-gateway processor dispatches multiple requests per model concurrently. The
llm-d-asyncdispatcher also processes requests concurrently (batchSize × budget). So results arrive in the result queue in arbitrary order, not in submission order. A naive SubmitRequest + GetResult per goroutine would return someone else's result.The
AsyncInferenceClientuses a backgroundresultDispatchergoroutine to demux results from the shared result queue by request ID.A single
resultDispatcherper pool reads from the result queue (BRPOP) and routes each result to the correct caller via async.MapofrequestID -> chan. EachGenerate()call registers its channel before submitting, then selects on the channel or context cancellation. On context timeout, the waiter is unregistered to avoid leaks.Note that because of the current assumptions in the code I had to add a separate
p.processModelAsync(...)alongside the existingp.processModel(...)inexecuteJob(...). I refactored what I could in helper methods, but in general this should call for a more significant refactoring.The
Async*new set of interfaces is significantly different from the sync versions (they might be be aligned using channels).More details
Async inference client interface (
pkg/clients/inference/async_inference_client_interface.go):AsyncInferenceClientinterface withSubmit(ctx, req),GetResult(ctx), andClose(), symmetric with the syncInferenceClient.Generate()but non-blockingAsync inference client implementation (
pkg/clients/inference/async_inference_client_impl.go):asyncProducerClientimplementsAsyncInferenceClient, backed by anllm-d-async/producer.Producer.resultDispatchergoroutine per pool BRPOPs from the result queue and routesresults to the correct per-job client via a
sync.MapofrequestID -> channel.AsyncGatewayResolver.ClientFor, so concurrent batch jobs on the same model are isolated.Close()unregisters all pending waiters from the shared dispatcher.Async resolver (
pkg/clients/inference/async_inference_client_resolver.go):AsyncGatewayResolveris separate fromGatewayResolver(sync). Stores sharedasyncPoolinstances (producer + dispatcher per pool).
ClientForcreates a fresh per-job client each call.Async executor path (
internal/processor/worker/executor.go):processModelAsync— submit/collect pattern: sequential ZADD loop (fast), thenGetResultloop collecting results as they arrive. No semaphores, no AIMD — thellm-d-async dispatcher controls inference concurrency via gates.
processModel:readRequestLine,buildOutputLine,writeResult,drainAndFinalize,newErrorOutputLine,fairnessID.executeJobdispatches toprocessModelAsyncwhenp.asyncInference != nil.Processor setup (
internal/processor/worker/worker.go):initConcurrencyControlsextracted fromRun(). In async mode, only the job-levelworker semaphore is created — no global/endpoint semaphores or AIMD.
Clientset (
internal/util/clientset/clientset.go):AsyncInference *inference.AsyncGatewayResolverfield, separate fromInference.WithAsyncInference(cfg)setsAsyncInference;Close()closes whichever is set.Config & resolution (
internal/processor/config/config.go):ResolveModelGatewayspopulatesResolvedGateways.Asyncwith pool names frommodel_gateways[*].inference_pool_namewhendispatch_mode: async.Helm chart (
charts/batch-gateway/templates/processor-configmap.yaml):dispatch_mode,async_dispatch.result_poll_timeout,inference_pool_nameconditionally rendered.urlis set, fixing a falsy-zero regression.E2E tests & dev tooling:
test/e2e/dispatcher_test.go: 4 tests — batch round-trip, multi-request batch,Redis dispatch gate, Prometheus-query dispatch gate.
scripts/dev-deploy-dispatcher.sh: deploys three llm-d-async dispatcher instances(redis gate, endpoint-scrape gate, prometheus-query gate), configures Prometheus scrape
targets, enables fake metrics on vllm-sim, reconfigures processor for async mode.
make dev-deploy-dispatcherandmake test-e2e-dispatchertargets.How was this tested?
TestNewAsyncResolver: per-model routing, labels, error on invalid URLTestAsyncInferenceClient_Generate: enqueue/dequeue round-trip via producer, timeout, queue name derivationTestResolveModelGateways_Async: async config resolution, sync mode unaffectedTestDispatcher/BatchThroughDispatcher: single request end-to-end via async dispatchTestDispatcher/MultiRequestBatch: 3-request batch completionTestDispatcher/DispatchGate: Redis gate blocks/unblocks dispatchTestDispatcher/PrometheusGate: Prometheus-query gate blocks dispatch when vllm-simreports saturation (fake metrics), unblocks when saturation clears
make dev-deploy && make dev-deploy-dispatcherChecklist
git commit -s) per DCOmake ci)make test-e2e)