feat(sidecar): direct-gRPC dispatch from router to stock engine containers (v2) - #13292
Draft
tanmayv25 wants to merge 15 commits into
Draft
feat(sidecar): direct-gRPC dispatch from router to stock engine containers (v2)#13292tanmayv25 wants to merge 15 commits into
tanmayv25 wants to merge 15 commits into
Conversation
Introduce a StreamingDispatch transport seam beneath PushRouter so the frontend can dispatch the final hop straight to a TensorRT-LLM container's gRPC server, removing the per-worker sidecar forwarding hop while keeping instance selection, fault detection, overload, and migration. - runtime: TransportType::Grpc, StreamingDispatch trait (AddressedPushRouter is the default request-plane impl), PushRouter::from_client_with_dispatch, and direct-gRPC endpoint register/unregister discovery helpers. - backend-common: --direct registrar mode (register_direct_orchestrator) with a hysteresis + timeout health/liveness loop; LLMEngine::health_check; shared register_model prologue; --advertise-grpc-endpoint for multi-node (advertise a routable address distinct from the sidecar's local endpoint). - llm: DirectDispatchProvider registry + ModelWatcher selection. - trtllm sidecar: GrpcDispatch (tonic::Status -> top-level ErrorType fault map, one shared dispatch per model so channel pools don't leak across routing rebuilds), TrtllmDirectDispatchProvider, and the dynamo-trtllm-frontend composition-root binary. The concrete gRPC dispatch lives in the trtllm crate and registers at a composition root, keeping dynamo-llm engine-agnostic (dependency inversion). KV-aware routing stays dormant on the direct path: a stock trtllm --grpc container does not expose KV events (the SubscribeKvEvents RPC is absent from the vendored proto), so direct workers route round-robin/least-loaded. Validation: cargo build (all sidecars + affected crates) + unit tests green (runtime component serde, trtllm fault map + advertised-endpoint, backend- common lifecycle). Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
GrpcDispatch + tonic::Status->top-level ErrorType fault map + VllmDirectDispatchProvider, dynamo-vllm-frontend composition root, and --direct wiring reusing the engine-agnostic register_direct_orchestrator. vLLM gRPC has no health RPC; health_check uses a fresh-channel liveness probe. KV routing off (stock vLLM gRPC emits no KV events). Validated e2e locally (chat + streaming + CannotConnect fault/migration + health-loop recovery). Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
…vice gRPC) GrpcDispatch + fault map + SglangDirectDispatchProvider, dynamo-sglang-frontend composition root, --direct wiring. Handles SGLang v0.5.16 CUMULATIVE output_ids via token_offset delta emission (naive delta reuse reproduced a repetition/token-overcount bug). health_check via SglangService HealthCheck; abort-on-cancel via Abort RPC. KV routing off. Validated e2e locally (chat + clean streaming deltas + max_tokens honored + fault/migration + recovery). Note: request-plane engine.rs has the same latent cumulative bug for v0.5.16 (out of scope). Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
Part 1 (one frontend, many providers): a single `dynamo-direct-frontend` (lib/sidecar/frontend) registers all three direct-dispatch providers (trtllm / vLLM / SGLang, keyed by the model card's `direct_backend`) then runs the standard `Input::Http` frontend. Removes the three per-engine `dynamo-<engine>-frontend` binaries + their `[[bin]]` entries. Part 2 (thin discoverability shim): new `dynamo-direct-register` (lib/sidecar/register) on `dynamo-runtime` + `dynamo-llm` only (NOT `backend-common`, NOT `LLMEngine`/`Worker`) defines the committed API — the `DirectBackend` trait (connect / health_check / cleanup) + `DirectRegistration`. This is the contract for the registrar that replaces `register_direct_orchestrator`. Deferred (reported): `run_direct` (the lifecycle driver) + per-engine migration to it + the `backend-common --direct` revert. `build_local_model` is ~100 lines of intricate `WorkerConfig`/`EngineConfig`-coupled logic (runtime-config assembly, HF path resolution, disagg endpoint) that can't be safely reimplemented in one pass without risking the validated build, so engines stay on the v1 `--direct` registrar until `run_direct` lands. Builds green: dynamo-direct-frontend, dynamo-direct-register, and the trtllm/ vllm/sglang sidecars compile; backend-common unchanged. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
Implement the standalone run_direct lifecycle driver in dynamo-direct-register (DRT + signals, model-card build, register_direct_endpoint_instance, hysteresis health loop, graceful teardown) on dynamo-runtime + dynamo-llm only. Add DirectConfig and extend DirectRegistration with model_name / parser fields. Migrate the TensorRT-LLM sidecar onto it: add TrtllmDirectBackend (connect / health / cleanup), branch main.rs on --direct via launch_from_env, and drop the engine's is_direct runtime_data surfacing and health_check override (that logic now lives in the DirectBackend). backend-common still owns the legacy --direct path for vLLM/SGLang until they migrate. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
Add VllmDirectBackend (connect / fresh-channel liveness health / cleanup) and branch vLLM's main.rs on --direct via launch_from_env. Drop the engine's is_direct runtime_data surfacing and health_check override; the liveness probe now lives in the DirectBackend. Extract the shared clap parse helper so the direct and request-plane paths reuse it. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
Add SglangDirectBackend, which connects + bootstraps SGLang's native gRPC discovery (model / context / parsers), rejects non-aggregated engines, and health-gates via the HealthCheck RPC. Split the engine's from_args into parse + from_parsed so the direct path skips the request-plane bootstrap, and branch main.rs on --direct via launch_from_env. Drop the engine's is_direct runtime_data surfacing and health_check override. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
All three sidecars now drive the direct-gRPC path through the standalone dynamo-direct-register shim, so backend-common's engine-agnostic --direct registrar is dead. Remove CommonArgs.is_direct + advertise_grpc_endpoint, WorkerConfig.is_direct, register_direct_orchestrator, the run_inner is_direct branch, the LLMEngine::health_check default trait method (+ its EngineKind forwarder), and DIRECT_GRPC_ENDPOINT_KEY (now owned by the register crate). Move --direct / --advertise-grpc-endpoint onto the TensorRT-LLM and vLLM sidecars' own Args (SGLang already had its own), and drop the now-unused is_direct field from the Python bindings' WorkerConfig construction. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
Teach the direct-gRPC discoverability shim about disaggregation. DirectConfig gains a disaggregation_mode and DirectRegistration gains bootstrap_host / bootstrap_port / data_parallel_size, so the shim registers each worker with the ModelType / WorkerType / topology-needs implied by its role (mirroring backend-common's Worker) and publishes a prefill worker's bootstrap endpoint on the model card's disaggregated_endpoint. The SGLang adapter resolves its role from native gRPC discovery at arg-parse time (component prefill vs backend), re-verifies it on connect, and resolves the bootstrap host/port + DP size for prefill workers by reusing the request-plane helpers. The vLLM adapter derives the role from --disaggregation-mode and leaves the bootstrap endpoint unset (it hands off KV via prefill_result, not a bootstrap port). TensorRT-LLM stays aggregated-only. Scope: vLLM and SGLang only; TensorRT-LLM direct disagg remains unsupported. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
Give each GrpcDispatch a DisaggregationMode (and, for SGLang prefill, the discovery-resolved bootstrap host/port) instead of hardcoding Aggregated, so build_generate_request / build_kv_parameters emit the right KV-transfer handoff per role. The frontend still stamps bootstrap_info / prefill_result upfront, which the request translation prefers; the dispatch mode is the fallback and the switch that turns the handoff on. Each DirectDispatchProvider now caches one dispatch per (model name, worker_type) rather than per model name, because a prefill card and a decode card share a name but need different modes and bootstrap endpoints. The role is read off the card's worker_type. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
When the prefill model card advertises a direct_backend, build its transport dispatch from the composition-root-registered provider and swap the prefill PushRouter onto from_client_with_dispatch, keeping all of PushRouter's selection / fault-detection / migration behavior and changing only the final-hop transport. This mirrors the decode hop in the discovery watcher. The prefill card is fetched once and reused for both the EAGLE mode and the direct dispatch, across the KV and simple router paths. A request-plane prefill card (no direct_backend) keeps the existing from_client_with_monitor path unchanged. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
…patch The frontend-side SGLang direct GrpcDispatch drove the prefill hop like the decode hop: it streamed intermediate tokens and never set disaggregated_params on the terminal output. On the bootstrap disaggregation path the frontend already stamps request.bootstrap_info upfront, so the decode handoff worked, but its background prefill task inspects the prefill output's disaggregated_params and logged NoDisaggregatedParams on every request. Mirror the request-plane engine path: for the prefill role suppress intermediate tokens and emit a single terminal carrying the bootstrap host/port/room handoff. The background prefill task now completes cleanly and the direct prefill dispatch matches the sidecar engine behavior. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
…ruct
The direct-disagg decode hop forwards the prefill NixlConnector handoff
(kv_transfer_params) across the vLLM gRPC contract as a
google.protobuf.Struct, whose numbers are all IEEE-754 doubles. vLLM's
gRPC frontend hands those to Python as floats, so an integer remote_port
(e.g. 7100) reaches the decode engine as 7100.0. NixlConnector builds its
handshake socket path with make_zmq_path -> f"tcp://{host}:{port}", so
the path becomes tcp://host:7100.0, which urllib3 rejects with
LocationParseError and the KV load fails (decode returns 0 tokens).
Stringify remote_port on the decode handoff so it survives the round-trip
as an exact, dot-free token. Other numeric fields (block_ids, tp_size)
cross as floats too but are only used in numeric contexts, which tolerate
floats; only the port feeds a strict URL parse. Verified end-to-end on a
single GPU with two vllm-rs NixlConnector engines: the decode engine now
completes 'Finished recving KV transfer' and generates coherent output.
Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
…ibers The discovery-removal watcher deduped by EndpointId and let a single dispatch own the endpoint's on_instance_removed / on_instance_added callbacks. That was safe while AddressedPushRouter was the only StreamingDispatch (its hooks act on shared per-DRT state), but once a heterogeneous dispatch (e.g. direct gRPC) shares an endpoint, whichever router registered second received no lifecycle events and leaked its per-instance state. Keep one list_and_watch per endpoint but fan each event out to a set of per-endpoint Weak subscribers. Each router holds the sole strong ref to its InstanceWatcherSubscription (RAII), so dropping the router prunes it on the watcher's next event. Regression test: two dispatches on one endpoint each receive add/remove. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
…doff Replace the ad-hoc stringify_remote_port map-poking with a typed KvTransferParams at the vLLM engine boundary. remote_port is pinned to a string via a custom deserializer that accepts a number or string, and #[serde(flatten)] extra round-trips every other field losslessly. This kills the float-port bug CLASS at the type level: the kv_transfer_params handoff crosses the gRPC contract as a google.protobuf.Struct whose numbers decode to IEEE-754 doubles, so any string-context field must be pinned to its wire type in one typed place. The framework's PrefillResult.disaggregated_params stays deliberately opaque passthrough. Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
|
🔄 Datadog auto-retried 13 jobs - 13 passed on retry 🎯 Code Coverage (details) 🔗 Commit SHA: db1a68f | Docs | Datadog PR Page | Give us feedback! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets the Dynamo frontend/router dispatch the final request hop directly over gRPC to stock inference-engine containers (vLLM, SGLang, TensorRT-LLM), removing the per-worker request-plane forwarding hop while preserving router richness (selection, fault detection, migration). The sidecar shrinks to a thin registrar (discovery + health); the engine runs as its stock upstream container instead of a custom Dynamo worker image.
Built on the
StreamingDispatchtransport seam (#12447):PushRouter's final hop is a trait object, so a per-engineGrpcDispatchswaps only the transport below the seam and inherits report-down / overload / migration by mappingtonic::Status→ top-levelErrorType.v2 architecture
TransportType::Grpconly. All engine-specific code lives inlib/sidecar/*composition crates, deliberately out of theai-dynamowheel (notonic/engine deps in the wheel).run_directthin registrar shim (dynamo-direct-register) — standalone on runtime+llm, not anLLMEngine;backend-commonis clean of the old--directpath.dynamo-direct-frontendregisters per-engine providers keyed by the model card'sruntime_data["direct_backend"].Before → after
flowchart LR subgraph BEFORE["Today — engine embedded in the Dynamo Python worker"] FE1["Frontend / Router"] -->|"request plane (NATS/TCP)"| W1["Dynamo Python worker<br/>engine in-process · custom image"] end subgraph AFTER["This PR — direct gRPC to the stock container"] FE2["dynamo-direct-frontend<br/>Router + StreamingDispatch seam"] -->|"direct gRPC — final hop"| ENG["Stock engine container<br/>vLLM · SGLang · TRT-LLM"] REG["run_direct registrar<br/>discovery + health"] -.->|"health probe"| ENG REG -.->|"MDC + TransportType::Grpc"| DISC[("discovery")] DISC -.->|"discovers Grpc endpoint"| FE2 endDisaggregated (two-phase) direct route
flowchart LR FE["Frontend<br/>PrefillRouter"] -->|"1 — prefill hop (direct gRPC)"| P["Prefill engine<br/>component: prefill"] P -->|"handoff: bootstrap / kv_transfer_params"| FE FE -->|"2 — decode hop (direct gRPC)"| D["Decode engine<br/>component: backend"] P -.->|"KV over NIXL (cuda_ipc / RDMA)"| D D -->|"stream tokens"| FEWhat's included
kv_transfer_paramshandoff (removes the protobuf-Structfloat-port bug class).Known gaps (called out, not in this PR)
GetModelInforeturns 0 for max seq len; no disaggregation params in the response path)./metrics(trtllm--grpcdefaults metrics off).Validation
cargo buildgreen acrossdynamo-runtime,dynamo-backend-common,dynamo-llm, alllib/sidecar/*crates, anddynamo-direct-frontend.push_router(incl. the fan-out + fake-dispatch lifecycle tests), vLLM sidecar (incl. the typed-handoff round-trip + float-port tests), sglang/trtllm sidecar suites.Follow-ups
Split into reviewable PRs · re-add KV-aware routing over gRPC · trtllm disagg (pending upstream SMG/proto additions) · container packaging.