Skip to content

[Bugfix] Drop stray messages on the DP startup handshake socket instead of failing startup - #60758

Open
localityv-hash wants to merge 1 commit into
vllm-project:mainfrom
localityv-hash:fix-dp-startup-stray-messages
Open

localityv-hash wants to merge 1 commit into
vllm-project:mainfrom
localityv-hash:fix-dp-startup-stray-messages

Conversation

@localityv-hash

@localityv-hash localityv-hash commented Oct 9, 2026 •

Copy link
Copy Markdown

Overview

In multi-node DP, any message on --data-parallel-rpc-port that no engine sent
(a port scanner, a metrics scraper) fails startup. This PR drops such messages
with one warning and keeps waiting for the engines. Fixes #38677 (closed as
stale without a fix).

Claims

  • A message that does not parse as an engine's HELLO or READY no longer fails
    wait_for_engine_startup: wrong frame count, an identity no engine has, or a
    body that is not an engine's handshake msgpack map (including bodies that make
    untyped decoding raise UnicodeDecodeError, TypeError or RecursionError).
  • A well-formed engine message is held to exactly the checks it met before. An
    unexpected rank, a local/remote or --headless mismatch, a message out of
    turn, or an engine, coordinator or watched front-end process exiting still
    fails startup.
  • Nothing changes after startup: the handshake socket is only read until every
    engine is READY, so model output is unaffected.

Validation

New tests in tests/v1/engine/test_startup_watch_processes.py run the real
wait_for_engine_startup on a ROUTER bound to loopback TCP, with
EngineCoreProc.startup_handshake on the engine side:

  • test_wait_for_engine_startup_drops_messages_no_engine_sent: before and
    between the engines' HELLO and READY, DEALER, REQ and ROUTER peers send junk
    (msgpack int, str and map, invalid UTF-8, an unhashable map key, an HTTP
    request line, an empty frame, arrays and maps nested 20,000 deep, a two-frame
    message, an unowned engine-sized identity), a raw TCP client sends the scrape
    from [Bug]: data_parallel_rpc_port is not robust to invalid traffic and can crash multi-node startup #38677, and one engine sends junk under its own identity before its
    HELLO. Startup must complete.
  • test_wait_for_engine_startup_still_fails_on_an_unexpected_rank: after the
    same stray traffic, a well-formed HELLO from rank 5 still fails startup.
.venv/bin/python -m pytest tests/v1/engine/test_startup_watch_processes.py -v \
  -W error::pytest.PytestUnhandledThreadExceptionWarning

Results on this branch (7619693, on main
201e537), in an editable install
(VLLM_USE_PRECOMPILED=1 uv pip install -e .), CPU only, Python 3.12,
torch 2.13.0+cu130, msgspec 0.22.0:

with this change:     21 passed            (5 runs in a row)
without this change:  2 failed, 19 passed  (the two new tests)

Without the change both new tests fail on the first stray message the front-end
reads, e.g. RuntimeError: Message from engine with unexpected data parallel rank: 453525693440 (a DEALER's auto-generated identity read as a rank).

We also replayed 18 kinds of stray traffic, one kind per run, at the real
wait_for_engine_startup on a loopback TCP ROUTER (the payloads above plus raw
TCP). On main, 17 of the 18 end startup; with this change, none do. Applied to
an older main commit, #38731 still fails on 2 of them and #47389 on 5.

In a multi-node DP deployment with hybrid load balancing, with a process beside
each leader sending stray traffic at its own handshake port every 10 s for as
long as the port was open, each handshake logged the warning once and completed
(about 400 stray messages each over five and a half minutes), and the deployment
then passed our functional and long-context checks.

CI: the existing V1 Engine Buildkite step (pytest -v -s v1/engine) runs
this file.

Details

Root cause. In multi-node DP the rank 0 front-end binds a ROUTER socket to
--data-parallel-rpc-port on --data-parallel-address, a routable address,
and wait_for_engine_startup reads it until every engine is READY. Anything
that connects can send to it, and main raises on whatever is not an engine's
handshake:

  • an identity no engine has raises
    RuntimeError: Message from engine with unexpected data parallel rank: <n>;
  • a message with other than two frames (a REQ socket's, for one) raises
    ValueError: too many values to unpack (expected 2);
  • a body that is not an engine's handshake raises from
    msgspec.msgpack.decode, or from msg["status"]
    (TypeError: 'int' object is not subscriptable) when its identity happens to
    match an engine's.

#38677 reports the first case from a Prometheus scraper: with no ZMTP greeting,
libzmq takes the sender for an unversioned (ZMTP/1.0) peer and the request's
first bytes for its identity, which becomes the huge "rank" in the error. We see
the same class of traffic as the security scanner in #44486: a periodic probe
that connects to every ZMQ port in our pods. On our deployment the
socket is open for about five and a half minutes per start, and one stray
message in that window costs a full restart.

Fix. _decode_engine_handshake accepts only an engine's shape: two frames,
the second a msgpack map with a str status and bool local and
headless. Anything else is dropped with logger.warning_once and the loop
keeps waiting; the poll path that detects exited processes is untouched.
Untyped decoding of arbitrary bytes can raise msgspec.DecodeError,
UnicodeDecodeError, TypeError (an unhashable map key, msgspec < 0.22;
0.22 raises DecodeError) and, for input nested deeper than about 10,000
arrays or maps, RecursionError, which is a RuntimeError and easy to miss.
All of them mean "not an engine".

Tradeoff. A well-formed HELLO from a rank this front-end does not own still
raises instead of being dropped. wait_for_engine_startup has no timeout for
remote engines, so dropping a real engine's message would turn a configuration
error into an indefinite wait.

Why this is not a duplicate.

AI assistance

AI assistance (Claude) was used to investigate this failure, write the change
and its tests, and run the replays above. I have reviewed the change end to end
and can answer for it.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (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.

…ad of failing startup

In multi-node data parallel deployments the rank 0 front-end binds a ROUTER
socket to --data-parallel-rpc-port on a routable address, and
wait_for_engine_startup reads it until every engine is READY. A message from
anything that is not an engine, such as a port scanner or the metrics scraper
in vllm-project#38677, raised and failed startup.

Drop any message that does not parse as an engine's HELLO or READY (two
frames, the second a msgpack map with a str "status" and bool "local" and
"headless"), log one warning and keep waiting. A well-formed engine message
keeps every existing check, so an unexpected rank, a local/remote or
--headless mismatch, an out-of-turn message or an exiting process still fails
startup.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: localityv-hash <245116777+localityv-hash@users.noreply.github.com>
@localityv-hash
localityv-hash requested a review from njhill as a code owner October 9, 2026 02:40

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added the bug Something isn't working label Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: 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.

🚀

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: data_parallel_rpc_port is not robust to invalid traffic and can crash multi-node startup

1 participant