Repository navigation
[Bugfix][KV Offload][P2P] Settle buffered terminal messages after disconnect - #64299
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Documentation preview: https://vllm--64299.org.readthedocs.build/en/64299/ |
6f661d2 to
fed423e
Compare
liranschour
left a comment
There was a problem hiding this comment.
Thanks for splitting this out. The fix itself looks right to me: ZmqTransport.poll() can buffer the peer's final TransferDone in _recv_router() and then mark_dead() the connection in _check_monitors() during the same sweep, and the old guard dropped that message, so the reap reported a finished load as failed. on_transfer_done / on_abort_ack only touch client state and never send, results are collected before _reap_dead_sessions(), and work-starting messages are still dropped. All good.
Two things before this goes in:
- Move the real-model test out of
test_sessions.py(inline comment below). - One trust edge case on the relaxed guard (inline comment below): please confirm the intended behaviour.
| llm.llm_engine.engine_core.shutdown() | ||
|
|
||
|
|
||
| def test_inference_uses_completion_buffered_after_disconnect(vllm_runner, monkeypatch): |
There was a problem hiding this comment.
Could this move out of test_sessions.py, or be dropped? It is the only real-model test in tests/v1/kv_offload/tiering/p2p/, and it lands in a unit-test module of ~3,800 lines. It spawns a second engine process, downloads SmolLM2 from HF, has 180 s timeouts, and patches zmq.Socket.send / recv_multipart for the whole class. It would then run on every v1-kv-offload job (H200 MIG 35 GB, two engines at 0.30 memory each).
The parametrized test_buffered_terminal_settles_load_after_disconnect already covers both death paths (peer Disconnect and monitor mark_dead) for both terminals. Also, this e2e test injects Disconnect before TransferDone on the wire, an order a real producer never sends. The realistic race is the monitor path, which only the unit test covers.
If you want to keep an e2e check, a dedicated e2e module, or the P2P e2e matrix (#61846 / fault slice #64511), would be a better home.
Small issue if it stays: assert producer.exitcode == 0 in the finally hides the original failure whenever the try body raises.
There was a problem hiding this comment.
Thanks, addressed in 7a104af. The test now lives in its own module, tests/v1/kv_offload/tiering/test_p2p_lifecycle_e2e.py, and it reproduces the realistic race: the producer is unpatched, and the consumer fakes EVENT_DISCONNECTED from the socket monitor only after it has received the real TransferDone. The exitcode assert is out of the finally too. LGTM.
| if self._conn is not None and not self._conn.alive: | ||
| return | ||
| msg_type = msg.get(TYPE_KEY) if isinstance(msg, dict) else None | ||
| # Buffered terminals can still settle loads after disconnect. |
There was a problem hiding this comment.
Is the trust edge case here intended? The guard now accepts terminals whatever the cause of death, including deaths we declared ourselves. Example: a peer sends a malformed message, _protocol_error marks the session dead, and a TransferDone(success=True) later in the same batch is now accepted as a successful load from a peer we just judged to be misbehaving. The same applies after our own lookup-timeout or abort-ack-timeout disconnects.
For the race this PR targets (peer Disconnect / socket monitor), accepting the buffered completion makes sense. If trusting a peer we already faulted isn't intended, one option is to exempt terminals only when the death came from the peer side (DisconnectMsg or the monitor), not from our own protocol-error or timeout verdicts. If it is intended, a short comment explaining why would help.
There was a problem hiding this comment.
Thanks, addressed in 7a104af. Clearing _accept_buffered_terminals for any death reason other than a peer disconnect means a buffered terminal now only settles a load after a peer DisconnectMsg or a socket-monitor death, and never after one of our own verdicts (protocol error, timeouts, data-plane fault, handshake/registration failures). test_local_protocol_fault_rejects_buffered_success covers it, both with and without a peer Disconnect first. LGTM; one small non-blocking suggestion is in the approval.
liranschour
left a comment
There was a problem hiding this comment.
LGTM, thanks for the quick turnaround. Both review points are addressed, and the test_sessions.py unit tests pass locally at 7a104af (152 passed).
One non-blocking suggestion: _accept_buffered_terminals depends on reason != "peer disconnected", which has to match the reason string passed at the DisconnectMsg branch exactly. If someone later rewords that reason, the fix silently turns off. A module-level constant for the reason, or a peer_side: bool argument to _mark_dead, would make that coupling explicit. Fine as a follow-up.
|
/ci run |
|
✅ Triggered Buildkite CI #99189 for commit ℹ️ This PR is 260 commits behind upstream |
|
✅ @jiuyueQ, CI is now available for this PR.
|
|
/ci run |
|
✅ Triggered Buildkite CI #99306 for commit ℹ️ This PR is 25 commits behind upstream |
|
This pull request has merge conflicts that must be resolved before it can be |
…connect Preserve validated peer-side terminal completions and bound concurrent test caches. Co-authored-by: OpenAI Codex Signed-off-by: jiuyueQ <631905507@qq.com>
343e405 to
416afd3
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #99475 for commit ℹ️ This PR is 33 commits behind upstream |
Overview
A poll can receive both a disconnect and the terminal confirmation for a completed transfer. Dropping every message once the connection is dead loses that confirmation and reports the completed load as a failure.
Extracts B from #61667 as requested in review. Refs #61532.
Claims
Validation
Rebased and tested on main
9cd2e98b595e, including merged #64300. Used cu130 wheel artifacts fromb2a9480dc7aa(identical C++/CUDA sources to this base), Python 3.12, PyTorch 2.14.0+cu130, RTX 4060 Ti and NIXL 1.4.1 over UCX TCP localhost.The real LLM test runs a producer and consumer in separate processes. The producer prefills a 192-token prompt and NIXL transfers its KV. External ZMQ socket fault injection reports a monitor disconnect after the successful TransferDone is received but before session dispatch. The producer sends its ordinary successful completion. The consumer must produce the same four greedy tokens as the producer with zero failed promotions.
Reproduction commands from a configured checkout/venv:
PYTHONHASHSEED=0 UCX_TLS=tcp,self .venv/bin/python -m pytest -q \ --confcutdir=tests/v1/kv_offload \ tests/v1/kv_offload/tiering/p2p \ tests/v1/kv_offload/tiering/test_tiering_offloading.py \ -k 'not test_inference' PYTHONHASHSEED=0 UCX_TLS=tcp,self VLLM_USE_FLASHINFER_SAMPLER=0 \ .venv/bin/python -m pytest -v -s \ tests/v1/kv_offload/tiering/test_p2p_lifecycle_e2e.py::test_inference_uses_completion_buffered_after_disconnectBase failure excerpt:
With the fix:
1 passed.Model: real
HuggingFaceTB/SmolLM2-135Mweights at93efa2f097d58c2a74874c7e644dbc9b0cee75a2, FP16, eager, greedy sampling, andVLLM_BATCH_INVARIANT=1. Model validation covers deterministic generation and fault handling; no task-accuracy or performance claim is made. The existingv1-kv-offloadBuildkite step selects these tests and its dependency list covers the changed P2P sources. All applicable Python and documentation hooks ran, including mypy, CI test tethering, and commit sign-off checks.Details
The dead-connection dispatch guard accepts buffered terminals only after peer Disconnect or socket-monitor death. Local protocol errors, repeated dispatch errors, handshake/lookup deadlines, send errors and abort-ack timeouts disable this exception. Terminal messages still pass their existing validation and match by request ID and round. Work-starting messages retain the previous guard. A malformed terminal also disables subsequent terminals in the same batch, even when a peer disconnect was observed first. AbortAck settles the deferred release established by #64256 without producing a second failed outcome. The data-plane teardown and slot-release contract are unchanged.
Duplicate-work check: this is the explicitly requested split from the original PR. Merged #64256 implements the core timed-out-load/deferred-release work using #63779's API; this PR adds the separate B fix on top of that implementation. The other split fixes can land independently. Relevant open-PR searches were refreshed before submission.
Rendered behavior documentation is updated in
docs/features/kv_offloading_usage.md. Fresh documentation checks passed through local, GitHub, and web retrieval.AI assistance: OpenAI Codex assisted implementation, tests, and PR preparation.
Pull Request Checklist
/pr-checklistskill.Rebase validation (2026-10-29)
Adapted to #64256's immediate-failure/deferred-release lifecycle. All three split PRs remain independent. The same real-weight regression was rerun on the latest base with only the new tests overlaid, and on this fix; results are shown above. The original #61667 branch remains unchanged.
Review follow-up (2026-10-30)
Moved the real-weight test into a dedicated P2P lifecycle e2e module, retained CI selection, changed the injected race to the realistic socket-monitor path, and moved the child-exit assertion out of cleanup so it cannot mask the original failure. Added protocol-fault rejection coverage, including a malformed terminal after peer disconnect.
CI follow-up (2026-11-02)
Official CI #99189 exposed overlapping memory profiling when the two real engines initialize on one GPU: the consumer counted the producer's newly allocated cache against its own budget and sized zero blocks. Both engines now use an explicit 64 MiB KV cache budget, retaining concurrent startup, the original timeouts, the real NIXL transfer, exact token comparison and zero-failed-promotions assertion. The same test still fails on main and passes with this PR; 445 P2P/tiering component tests and applicable repository hooks pass. Local verification uses the RTX 4060 Ti environment above; the H200 MIG verdict awaits the next upstream CI run.
Rebase compatibility (2026-11-03)
Preserved #64300's implementation and real-weight tests while combining the lifecycle module and retaining the explicit 64 MiB cache budget. Before this rebase, the memory-budget fix passed official Buildkite #99306. After rebasing: 467 component tests, both lifecycle inference tests and repository hooks passed locally. New-head upstream GPU CI is still pending.