Repository navigation
[PD] Coalesced transfers, UCX error-handling mode, larger default budget - #150
Conversation
…ng mode, larger default budget Four fixes to prefill-decode disaggregation, measured on one GH200 node with the prefill engine on GPU 0 and the decode engine on GPU 1: - The coordinator built every model with OuroConfig(**info["model"]), so a real Huginn checkpoint failed at startup (unexpected keyword argument 'n_embd'); the repository's Huginn PD test skips without a checkpoint. It now picks the config class from the model_type the worker reports. - KV segments that are contiguous on both sides are coalesced into one descriptor, and each progress pass fills the in-flight budget instead of sending one chunk per request. The overlap counter now also credits transfers issued during a prefill computation that drained before the next chunk was computed (coalesced descriptors drain fast); the wavefront overlap GPU test counted 0 before. - VLLM_RLT_NIXL_UCX_ERROR_HANDLING=none|peer creates the UCX backend explicitly with ucx_error_handling_mode (nixl_agent_config cannot pass backend init parameters). NIXL's default (peer) makes UCX skip shared memory and cuda_ipc, which have no peer failure handler, and emulate intra-node RMA with active messages over TCP; with none, UCX's log shows cuda_ipc and shared memory and no TCP lane. Peer failures are then reported only through the control channel, so the default is unchanged. - Chunks are submitted, and their completion observed, only between engine steps, so a request whose KV exceeds the in-flight budget waits extra steps after its prefill. The default budget is now 256 MiB chunks and 2 GiB in flight, enough for a 2048-token Ouro-1.4B prompt (1.6 GB of KV). In-flight bytes are not a buffer: NIXL writes straight from the registered prefill pool into the decode pool. pd_serve's CLI defaults read the PDConfig defaults. Ouro-1.4B 1P1D, 2048-token prompts, 64 output tokens, 32 requests, packed prefill, error handling none, two runs per arm in alternating order: 64 MiB / 256 MiB: 170-279 tok/s, TTFT p50 3.4-3.9 s, commit tail 0.96-1.05 s 256 MiB / 1 GiB: 254-290 tok/s, TTFT p50 3.2-3.5 s, commit tail 0.68-0.90 s 256 MiB / 2 GiB: 463-466 tok/s, TTFT p50 2.0 s, commit tail 0.017 s Switching the link alone (peer -> none, old budget, one run each) changed a 64 MiB chunk from 20.0 to 16.9 ms: the budget rounds, not TCP, set the tail. The engine's stats() also reports prefix-cache hits and queries. GPU PD tests: 41 passed with the first two changes; the error-handling mode and the budget were exercised by the benchmark runs above. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with InferMatrixLab#140 (model-neutral PD coordinator metadata): - vllm_rlt/pd/engine.py: take main's prepare_model path, which resolves the model configuration at the entry point and passes it to the workers. This supersedes this branch's reconstruction of the config class from the ready message, so _CONFIG_CLASSES and its test are removed. main's explicit rejection of Huginn in PD is kept. - tests/test_pd.py: keep main's Huginn-rejection and Nanbeige startup tests next to this branch's default-budget test. The coalesced transfers, UCX error-handling mode and 256 MiB / 2 GiB default budget are unchanged. CPU: 838 passed, 369 skipped; ruff check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
bjf-frz
left a comment
There was a problem hiding this comment.
Make the UCX transport configuration explicit by default.
| yield local, remote, size | ||
|
|
||
|
|
||
| def nixl_backend_params(backend): |
There was a problem hiding this comment.
Please set ucx_error_handling_mode="none" directly when creating the UCX backend, and remove the environment variable and nixl_backend_params() helper. Users should not need a hidden switch to enable the faster transport path. Verify that the existing P/D control channel handles peer termination without hanging or leaking resources.
There was a problem hiding this comment.
Done in 38a0d6c. NixlConnector now creates the UCX backend with ucx_error_handling_mode="none" directly, and nixl_backend_params() plus VLLM_RLT_NIXL_UCX_ERROR_HANDLING are gone; other backends keep the previous construction. Two CPU tests pin both paths, including that an obsolete value of the variable no longer changes the mode. On GH200, get_backend_params("UCX") reports ucx_error_handling_mode: none, and the UCX context keeps sysv/posix shared memory next to cuda_ipc.
On peer termination: the control channel already owns worker liveness, and it ends the deployment well before any transfer timeout. StageClient.poll() sees the dead worker (exited process or closed pipe) and raises, PDEngine.step() records the failure, and _terminate() stops and joins both roles and closes both channels. test_control_channel_detects_a_worker_exit_and_reaps_every_peer covers that path without a GPU.
GH200 run at 38a0d6c (NIXL 1.4.1, UCX 1.22, torch 2.14.1+cu130): the test nodes expose one GPU, so the two- and four-GPU cases skip, and I ran the same PD engine with both workers on GPU 0. Outputs matched a local engine (86,208 bytes of KV in 24 NIXL transfers). Killing either worker with transfers outstanding returned control from step() in about 0.5 s (the transfer timeout is 90 s), with closed=True, both channels closed, both processes reaped, and no child, /dev/shm or GPU process left behind. CPU suite on GH200: 867 passed, 342 skipped; ruff passes.
One caveat, also reflected in the description: on this build a 256 MiB single VRAM write ran at 580–870 GB/s in the default, peer and none constructions, so the mode's effect here is not a large-write TCP fallback, and the peer measurement row is now labelled as NIXL's default mode rather than TCP.
bjf-frz
left a comment
There was a problem hiding this comment.
Please keep the stats interface unchanged in this PR.
| used_blocks=cache.num_used_blocks, | ||
| num_blocks=cache.num_blocks, | ||
| prefix_hit_tokens=cache.prefix_hits, | ||
| prefix_queries=cache.prefix_queries, |
There was a problem hiding this comment.
Please remove the added num_blocks, prefix_hit_tokens, and prefix_queries fields from stats() and revert the corresponding test changes. These statistics are unrelated to the transfer optimizations; let's keep this PR focused.
There was a problem hiding this comment.
Done in 699b6ad. stats() is back to requests and used_blocks, and the two assertions in tests/test_stage_engine.py compare that payload verbatim again, so the PR no longer touches statistics. The prefix-cache counters themselves remain in KVCacheManager, where main already defines them.
GH200 at 699b6ad: ruff passes; CPU suite 867 passed, 342 skipped; the three affected test files pass on their own (118 passed, 9 skipped). The PR description no longer mentions the statistics.
|
others lgtm |
Review follow-up: the error-handling mode is not a user-facing option. When the configured backend is UCX, create it with ucx_error_handling_mode="none" and drop nixl_backend_params() and VLLM_RLT_NIXL_UCX_ERROR_HANDLING. NIXL's default (peer) mode requires a peer failure handler, which only the network transports provide, so UCX leaves the shared-memory lanes out of intra-node RMA. Peer loss is instead reported by the P/D control channel, which owns worker liveness: StageClient.poll() notices an exited worker and raises, and PDEngine._terminate() stops and joins both roles and closes both channels. New tests: the UCX backend is created with the mode for UCX and left alone for other backends, and the control channel detects an exited worker and reaps every peer. On GH200, killing either worker with transfers outstanding returns control to the coordinator in ~0.5 s (the transfer timeout is 90 s) and leaves no child, /dev/shm or GPU process behind. Tests on GH200 (NIXL 1.4.1, UCX 1.22, torch 2.14.1+cu130): CPU suite 867 passed, 342 skipped; ruff passes. The PD GPU suite needs one prefill and one decode device and the GH200 nodes here expose one GPU, so the PD path was run with both engines on GPU 0: outputs matched a local engine (86,208 bytes over 24 transfers). Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Review follow-up: num_blocks, prefix_hit_tokens and prefix_queries are unrelated to the transfer changes. Restore the previous stats() payload, and the two test assertions in tests/test_stage_engine.py that compare it verbatim. The prefix-cache counters themselves stay in KVCacheManager; they are not part of this PR. Tests on GH200: ruff passes; CPU suite 867 passed, 342 skipped. The affected test files pass on their own (118 passed, 9 skipped). Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
|
lgtm |
Summary
Four fixes to prefill–decode disaggregation, measured on one GH200 node with the prefill engine on GPU 0 and the decode engine on GPU 1:
model_typethe worker reports, because the coordinator built every model withOuroConfig.mainnow resolves the model configuration at the entry point, passes it to the workers and rejects Huginn in PD before spawning them; the merge ofmainkeeps that and drops this change.ucx_error_handling_mode="none"; theVLLM_RLT_NIXL_UCX_ERROR_HANDLINGvariable andnixl_backend_params()helper are gone. NIXL's default (peer) mode requires a peer failure handler, which only the network transports provide, so UCX leaves the shared-memory lanes out of intra-node RMA. Withnone,get_backend_params("UCX")reports the mode and UCX keepssysv/posixshared memory next tocuda_ipc. A lost worker is reported by the control channel, which owns worker liveness, so the transfer path needs no failure handler and the choice is not a user-facing option.pd_serve's CLI defaults now read thePDConfigdefaults.Measurements
Ouro-1.4B 1P1D, 2048-token prompts, 64 output tokens, 32 requests, packed prefill, error handling
none, two runs per arm in alternating order:peer(default), 256 MiB / 1 GiBSwitching the link alone (
peer→none, old budget, one run each) changed a 64 MiB chunk from 20.0 to 16.9 ms and left the commit tail near 1 s: the budget rounds, not the transport, set the tail. Earlier, behind per-row prefill, chunks four times larger took four times longer under the default mode. Only 2048-token prompts were measured; much longer prompts still need several rounds with the 2 GiB default. Thepeerrow uses NIXL's default error-handling mode, which drops the shared-memory lanes at startup; a 256 MiB single VRAM write measured 580–870 GB/s in the default,peerandnoneconstructions on the GH200 build tested here, so the mode does not turn large writes into TCP on that build.Platforms. GH200: torch 2.14.0+cu130, Triton 3.8.0, NIXL 1.4.1. RTX 5090: torch 2.13.0+cu130, Triton 3.7.1. Models: Ouro-1.4B (24 layers, 4 loops, fixed depth) and Huginn-0125 (2 prelude + 4 recurrent + 2 coda layers, 32 loops). Burst workloads issue C requests at once with random-token prompts; session workloads replay multi-turn agent traces with the prefix cache on.
Tests
ucx_error_handling_mode="none"while other backends are untouched, the control channel detects an exited worker and reaps every peer, and the default budget holds a 2048-token Ouro prompt.create_pd's two- and four-GPU cases skip. The same PD engine was run with both worker processes on GPU 0: outputs matched a local engine, KV travelled over NIXL (86,208 bytes in 24 transfers), and killing either worker with transfers outstanding raised through the control channel in about 0.5 s (the transfer timeout is 90 s), closed both channels, reaped both workers and left no child,/dev/shmor GPU process behind.pytest --run-gpu tests/test_pd.py tests/test_transfer_connector.py -k "not flash"): 56 passed;main(66920da) with the same command: 52 passed.Notes
The GPU numbers come from a development branch that contains all changes of this series; most cells are single runs, and on the shared GH200 host two runs of
mainat 32 requests differed by up to 16%, so smaller differences are ties. This branch alone was checked with the CPU suite, ruff and the GPU PD tests above.Series. Draft PRs from one study of serving looped models. They are independent except #152, which is stacked on #147; several touch the same files, so later ones may need a rebase after earlier ones merge.