Repository navigation
[Bugfix][KV Offload][P2P] Retain unconfirmed source WRITEs across session teardown - #64250
Merged
Merged
Conversation
…sion teardown Part of vllm-project#63602 (gap 3). ServerRole.close() cancelled inflight transfers in "immediate" mode and returned every store job as failed. That released NIXL handles still in PROC/PEND and let the engine unpin source blocks a WRITE may still be reading; remove_remote_peer() then freed the descriptors those handles were prepped against. The abort drain and store timeout already refuse this; teardown was the one path that didn't. - ServerRole.close() cancels in "wait" mode. Transfers still running are returned as SessionCloseResult.retained with the store jobs they read; those jobs stay pinned instead of being failed. - NixlTransport.remove_remote_peer() retires the peer's still-inflight transfers: they leave poll(), so a reconnect under the same peer_id never sees them, and the old registration's release is deferred until they stop. poll_retired() reports stopped ones (same terminal rule as wait-mode cancel, without faulting the reconnected peer). - The manager holds retained transfers and fails their store jobs once the transport reports them stopped, or at _RETAINED_HOLD_CAP_S (120 s), after which force_release_retired() releases them with an error log. Signed-off-by: Liran Schour <lirans@il.ibm.com>
5 of 6 tasks
This was referenced Oct 28, 2026
A reaped peer's registration stays until its retired transfers stop. A reconnect meanwhile loads the peer's NIXL agent again under the same name: NIXL keeps the old UCX endpoints, which may have failed, and the deferred release's remove_remote_agent then tears down the new session's agent. Reject outbound and inbound sessions for such a peer until its retained transfers stop, and lower the hold cap to 30s since it now also bounds how long the peer is unreachable. drain_jobs() also waits for retained transfers: reset_cache() asserts every job finished once it returns, and their store jobs are reported only when they stop. Signed-off-by: Liran Schour <lirans@il.ibm.com>
orozery
approved these changes
Oct 28, 2026
Contributor
|
/ci run |
orozery
enabled auto-merge (squash)
October 28, 2026 13:35
|
✅ @liranschour, CI is now available for this PR.
|
|
✅ Triggered Buildkite CI #97963 for commit ℹ️ This PR is 11 commits behind upstream |
6 tasks done
Open
5 of 6 tasks
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.
Overview
Part of #63602 (gap 3). When a P2P session is torn down, the source no longer releases a NIXL WRITE that may still be running. It keeps that transfer's handle, its source pins and the peer's old registration until the transport reports the transfer stopped, bounded at 120 s.
Claims
ServerRole.close()no longer releasesPROC/PENDtransfers or fails (unpins) the store jobs they read.remove_remote_peer()no longer frees the descriptor list and remote agent that a running transfer was prepped against.peer_idnever sees the old session's retained transfers._RETAINED_HOLD_CAP_S(120 s) the transfer is force-released with an error log, so a wedged handle can't permanently shrink the cache.Validation
The issue's CPU reproducer
test_gap3_unsafe_close.py:New tests, run with this PR's test files on
main's code (7 of 8 fail for the reported reason):(The one that passes,
test_remove_remote_peer_without_inflight_releases_at_once, guards the unchanged path.)With this PR:
Not validated on GPU. CI runs these tests under
misc.yaml(pytest -v -s v1/kv_offload), and they are CPU-only, using the fake transport and a mocked NIXL agent. An e2e run that actually wedges a WRITE (for examplerc_gdare-enabled, as in #63499) has not been done yet.Details
Root cause. On reap,
_reap_dead_sessions/_reap_idle_sessionscallsession.close()and thenServerRole.close(). That rantransport.cancel(all inflight)in the default"immediate"mode and returned every store job as failed. The manager then calledremove_remote_peer(). Releasing aPROChandle does not stop the RDMA WRITE (NixlTransport.canceldocstring; #59790 saw a WRITE land afterremove_remote_agent). So this unpinned CPU blocks the NIC may still read, and freed NIXL request and descriptor state while it was still in use._drain_abortand_timeout_pending_store_jobsalready refuse to do this, and teardown was the one path that didn't.Change.
ServerRole.close()cancels in"wait"mode. Transfers that are still running come back asSessionCloseResult.retained(tid → store job_ids), and their jobs are left out offailed_stores.NixlTransport.remove_remote_peer()retired.poll()skips retired entries, so a reconnected session under the same id can't receive a stale completion.poll_retired()releases transfers that have stopped. It uses the same terminal rule as wait-mode cancel: done, a non-PROC state, orNIXL_ERR_REMOTE_DISCONNECT. It does not charge the peer, since a reconnected peer under that id isn't at fault.DataTransportgets defaultpoll_retired()/force_release_retired(), so other transports need no change._retainedand calls_poll_retained()once per poll round. A store job fails (unpins) only once no retained transfer reads it.force_release_retired()and logs an error.Tradeoffs and limits.
Complexity: about 190 non-test lines (medium), all within
vllm/v1/kv_offload/tiering/p2p/. No core components are touched.Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
🤖 Generated with Claude Code