Repository navigation
feat(orchestration): express prefill/decode disaggregation - #3763
Merged
Merged
Conversation
A disaggregated deployment splits serving in two: a prefill tier computes the prompt and hands its KV cache over, a decode tier generates from that cache, and a router in front sends each phase to the tier that owns it. SubmitConfig could not say any of this, so the one benchmark that needs it stayed on a bespoke launcher script. Modelled as two fields and one service type rather than a new composite: - `kv_role` on a vllm service marks it a producer or consumer tier and renders the matching `--kv-transfer-config`, alongside the NIXL side-channel exports the connector needs on each node. - `type: router` names the two tiers it fronts and renders `vllm-router --vllm-pd-disaggregation`. It is a model service, so `driver.policy_model` points at the router and clients never address a tier directly. A tier is then just an ordinary multi-node data-parallel service pinned to a node pool, which the executor already knows how to render. That is what keeps this small: 4 prefill nodes and 6 decode nodes are two pools, and the existing head/headless rank split builds each tier unchanged. Three things node placement alone did not cover, each found by rendering a 4P/6D config and reading it against the working launcher: - Both tiers coordinated their data-parallel ranks on the allocation's head node. Each tier now uses its own pool's head, and its own RPC port, so the two rank-0s do not collide. - The decode tier's health probe went to localhost, where nothing answers: the probe runs on the first node and decode's API rank is on node 4. A pinned service is now probed where it actually runs. - Ports that are bound per host -- the NIXL side channel and the DP RPC port -- are refused when the two tiers share them, rather than failing at startup. Non-P/D rendering is untouched: single-node, multi-node data-parallel and ray-spanning configs all render byte-identically to the parent commit. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
A tier pinned to a multi-node pool ran as one data-parallel engine across the pool, and the router addressed only its head. The launcher prefill/decode deployments are validated against runs one independent vllm serve on every node instead, with the router listing each node. server_per_node selects that shape: each node in the pool runs the plain single-node command, the router gets one --prefill or --decode per node, and the service is sized as a single-node deployment. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com> (cherry picked from commit 9d679b966d79a418f6b4530a36315ffe3cc295b1) Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
The router bound --host $(hostname), the node's interface address, while driver.policy_model and the health probe both call http://localhost:<port>. Nothing answered there, so the router never looked healthy and the driver could not have reached it. It is validated to sit on the driver's node, so bind all interfaces. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com> (cherry picked from commit ef8ce00a3f0e76964f3c83eda8ee1d1977957dda) Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
driver.policy_model always injected http://localhost:<port>/v1. That suits clients on the driver's node, but a sandboxed agent (SWE-bench Pro's opencode runs in a remote OpenSandbox) resolves localhost to the sandbox and never reaches the policy. Setting policy_base_url by hand is refused while policy_model is set, and unsetting policy_model also drops --model-type. driver.policy_host names the host instead; the default keeps localhost. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com> (cherry picked from commit 314f1f4d60a041e8064bfb11121953f9fe66dfac) Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
β¦ray head An unpinned router runs beside the driver, so it is probed on the driver's node. `_probe_host` reuses `_pool_head` for the host expansion. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
prokotg
requested changes
Sep 29, 2026
prokotg
left a comment
Contributor
There was a problem hiding this comment.
Looks good but I think a change in API might simplify things. Let's discuss
Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
Renders the 4 prefill + 6 decode layout, data-parallel and one server per node, and compares it byte for byte with a fixture, so the config shape can be reworked against a fixed output. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
β¦ed tiers A prefill/decode deployment is now a single `type: vllm_pd` service that nests its `prefill` and `decode` tiers and owns the router and KV settings (kv_connector, kv_load_failure_policy). Each tier's KV role is set automatically; kv_role, kv_connector and kv_load_failure_policy leave VllmServiceConfig. Tiers inherit container, model and served_model_name, and take the next ports after the router's and prefill's unless set. SubmitConfig expands the service into <name>-prefill, <name>-decode and <name> (the router) through deployed_services, so the existing rank builder, node placement and health checks run unchanged. The resolved config keeps the nested form, so it loads back. The pinned SWE-bench Pro job scripts render byte for byte the same. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
Main now suffixes service and driver logs with $SLURM_JOB_ID and adds an (empty here) resume prologue, so the two P/D golden scripts pick up those lines. Nothing else in them changes. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
prefill and decode are now plain vLLM service settings. server_per_node and nixl_side_channel_port are set once on the vllm_pd service (decode's NIXL port is prefill's + 1), and setting either on a tier is refused. The per-tier values are derived into private tier objects when the config loads, so the saved config never carries them. The golden P/D job scripts are unchanged. An empty KV config renders no --kv-transfer-config flag. Signed-off-by: Wojciech Prazuch <wprazuch@nvidia.com>
Contributor
|
π Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/36722816677 |
This branch was previously deployed
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.
Same change as #3657, rebased onto main on a new branch; #3657's branch stays untouched for now.
In one sentence
Express prefill/decode disaggregated serving, so a policy can be split across two tiers behind a router.
Why
A large policy served for an agentic benchmark spends most of its time in prefill. Splitting prefill and decode onto separate node tiers, with KV cache transferred between them and a router in front, is how that workload is served in practice today.
SubmitConfighad no way to say it.The change
One new service type,
vllm_pd. It nests its two tiers and owns the router and KV settings:prefill:anddecode:are plain vLLM service settings. Each inheritscontainer,modelandserved_model_name.vllm_pdservice.server_per_nodeapplies to both tiers.nixl_side_channel_portis prefill's; decode gets it + 1. Setting either on a tier is refused.Why
vllm_pd: the service is a vLLM prefill/decode deployment, not a general router. A different kind of router can have its own type later.No logic is duplicated. At validation, the service expands into three internal services:
policy-prefill,policy-decodeandpolicy(the router). The existing multi-node rank builder, node placement and health checks run on those unchanged. The resolved config written to the job directory keeps the nested form, so it loads back.Bugs found by generating a real job script
Before running anything, we generated the job script for SWE-bench Pro's real layout (4 prefill nodes, 6 decode nodes) and read it. That found three bugs, all still guarded:
Found on a real cluster
A SWE-bench Pro prefill/decode run on a real cluster exposed four more problems. All are fixed:
server_per_node: trueruns an independentvllm serveon every node, and the router lists each one.0.0.0.0. The driver and the health probe call it onlocalhost.driver.policy_host. An agent in a remote sandbox cannot reach the driver'slocalhost, so the injected policy URL can name a routable host.Testing
vllm_pd, and the new form renders both fixtures byte for byte. The collector's scrape config is also unchanged.server_per_node/nixl_side_channel_portset on a tier, and those fields set on a plain vLLM service.Verified end to end
SWE-bench Pro ran the full 4 prefill + 6 decode shape on a real cluster with this change. That was before the config was reshaped, and the job script is byte-identical. All 731 tasks x 3 = 2193 rollouts finished. Mean reward was 0.533, against a reference of 0.526 from the same checkpoint served another way.
A deployment still needs its fabric environment (
UCX_*,NCCL_*) supplied through each tier'senvorpre_command. This PR does not presume a set.