fix(vllm): apply the TCPStore port offset on the branch vLLM actually takes - #3350
Closed
terrykong wants to merge 12 commits into
Closed
fix(vllm): apply the TCPStore port offset on the branch vLLM actually takes#3350terrykong wants to merge 12 commits into
terrykong wants to merge 12 commits into
Conversation
vLLM 0.25's RayExecutorV2 picks the torch.distributed TCPStore port with a
bind-probe (Step 3) but only binds it much later, in the rank-0 worker's
init_process_group. In between, Step 4 builds the broadcast MessageQueue;
when the engine spans nodes that queue needs a real TCP socket, so it calls
get_open_port() and binds and holds the result (shm_broadcast.py:
remote_subscribe_port = get_open_port(), then remote_socket.bind(...)).
Both searches start at VLLM_PORT, so the queue takes the very port the probe
just released and startup dies with EADDRINUSE (DeepSeek-V3 generation TP=32,
observed on port 7000). Engines that fit on one node bind an ipc:// socket
instead and never allocate a TCP port here, which is why only node-spanning
engines are affected.
Fix: patch _select_tcpstore_port to start its search at VLLM_PORT + 32, past
the queue's scan range. Both ports stay inside the engine's 100-port window
and therefore below the OS ephemeral floor (as low as 9000 on some nodes).
vLLM applies the same disjoint-window idea to co-located DP engines a few
lines below, seeding them from master_port + 100 + rank * 32.
Deliberately NOT fixed by leaving VLLM_PORT unset for these engines: that
sends vLLM to _get_open_port()'s s.bind(("", 0)) fallback, i.e. kernel-assigned
ephemeral ports, which is exactly the TOCTOU contention the reserved port
layout exists to prevent (#2380, #3103). The port assignment in
configure_worker is therefore unchanged from main.
The patch verifies its own result by reading the file back, so a patch that
silently fails to land is visible in worker logs instead of degrading to the
collision at runtime.
Signed-off-by: Terry Kong <terryk@nvidia.com>
…reep process_weights_after_loading runs after every refit, and rebinding each linear layer's weight/weight_scale_inv .data to the fresh tensors returned by process_fp8_weight_block_strategy slowly fragments device memory across sleep/wake cycles until CuMemAllocator wake_up fails with 'CUDA Error: out of memory at csrc/cumem_allocator.cpp' (~75-87 steps into both nightly fp8-rollouts variants; vllm 0.20 copied the scale in place). Copy into the existing storages when the processed layout is stable and only rebind on the first call when shapes change. Signed-off-by: Terry Kong <terryk@nvidia.com>
…ke-up
On vLLM 0.25 the colocated fp8 recipes (fp8-rollouts.v3 incl. tq_simple,
moonlight fp8-e2e) die when the CuMemAllocator fails to re-map its sleep
pool at wake-up ("CUDA Error: out of memory at csrc/cumem_allocator.cpp"),
while 0.20 passes. Measured on moonlight at identical
gpu_memory_utilization=0.5: 0.20 sizes the KV cache to 19.83 GiB (sleep
pool ~36 GiB) while 0.25 sizes it to 34.29 GiB (sleep pool 49.96 GiB) —
the pool now exceeds what can be re-mapped next to the colocated Megatron
policy's residual footprint.
Utilization tuning cannot express the old footprint (0.5 still OOM'ed on
fp8-rollouts, and cudagraph capture measured only 0.45 GiB, ruling out the
FULL_AND_PIECEWISE default as the consumer), so pin
vllm_kwargs.kv_cache_memory_bytes to the 0.20-proven 20 GiB on both
recipes; it takes precedence over gpu_memory_utilization.
Signed-off-by: Terry Kong <terryk@nvidia.com>
All observed metrics are healthy on-policy values; the old bounds were tuned tightly around vLLM 0.20 behavior: - eval_async score ceiling 0.14 -> 0.2 - ppo_automodel critic grad-norm ceiling 350 -> 1500 (The w4a8 fakequant token_mult_prob_error bound this branch previously relaxed to 1.08 was superseded by main relaxing it to 1.15.) Signed-off-by: Terry Kong <terryk@nvidia.com>
vLLM 0.25 turned FusedMoE into a factory returning a MoERunner whose expert weights live on a nested RoutedExperts submodule: - expert parameter names gain a '.routed_experts.' segment in named_parameters(); parse_hf_expert_weight now targets the qualified name (both the vLLM-side layout advertisement and the policy-side sender lookup key by the same function, so the mapping stays consistent) - the weight_loader owner is now the RoutedExperts module: the unquantized backend check reads owner.quant_method.unquantized_backend (the base_quant_method attribute no longer exists and the old getattr default made the guard reject valid Triton setups), and tp_rank/tp_size/ num_logical_experts/enable_eplb moved onto owner.moe_config (the old getattr defaults silently produced rank-0/size-1 sharding for TP>1) Unit tests updated for the qualified names; the storage-parity test now instantiates RoutedExperts (which owns _load_w13/_load_w2 in 0.25) instead of the removed FusedMoE class. Signed-off-by: Terry Kong <terryk@nvidia.com>
- FusedMoeWeightScaleSupported moved out of fused_moe.layer in vLLM 0.25; import it from the fused_moe package re-export (both mxfp8 MoE helpers raised ImportError otherwise) - accept both FlashInferCutedslMxfp8LinearKernel and FlashInferCutlassMxfp8LinearKernel in the mxfp8 linear refit guard: 0.25 prefers the Cutedsl kernel and both process weight scales with the same swizzle_mxfp8_scale layout this refit replicates Signed-off-by: Terry Kong <terryk@nvidia.com>
- vLLM 0.25's ModelOptNvFp4Config.__init__ installs LinearMethodCls as an instance attribute keyed off the quant algo, shadowing the NeMo subclass class attribute: the W4A16 config silently instantiated the native W4A4 linear method, whose process_weights_after_loading reads an input_scale a W4A16 checkpoint never loads. from_config now rebinds the instance attribute to the NeMo Marlin weight-only method. - W4A16_NVFP4 is a natively understood algo in 0.25, so the from_config normalization to NVFP4 is gone (validate-only); the base FusedMoE __init__ keys weight-only mode off the algo (activation_key=None), replacing the 0.20-era duplicated __init__ in NemoModelOptW4A16FusedMoE. - vLLM 0.25's prepare_nvfp4_moe_layer_for_marlin pads rank-local intermediate tiles itself and asserts on unpadded checkpoint shapes, so the NeMo-side Marlin pre-padding (_pad_nvfp4_moe_for_marlin) would double-pad and trip that assertion; it is removed. Only the E4M3 sign-bit canonicalization of ModelOpt scale exports remains NeMo's concern. Fake-vllm test harness updated to model the 0.25 instance-attribute and use_a16 behavior so the shadowing bug is covered by a regression test. Signed-off-by: Terry Kong <terryk@nvidia.com>
- the real-quant test scripts assert on the engine log's quantization= tag, which on vLLM 0.25 prints the registered NeMo config name (e.g. nemo_modelopt_w4a16_nvfp4) instead of modelopt; accept both (the w4a16-real GB200 run trained and produced healthy metrics but failed only this grep) - log the full traceback when an IPC weight batch load fails: the refit manifest only records the exception message, which for bare assertions (e.g. 'AssertionError: ') leaves nothing to diagnose in CI logs Signed-off-by: Terry Kong <terryk@nvidia.com>
Gated models (e.g. Qwen3-MoE) route batched 3-D expert tensors through vLLM 0.25's RoutedExperts.load_weights fused branch, whose orientation heuristic compares the last dim against the unpacked hidden size and mis-transposes packed NVFP4 weights (K/2 uint8) and block scales (K/16), tripping the layerwise-reload numel assert in RoutedExperts._load_w13. Emit per-expert 2-D shards instead, which take the same proven weight_loader path as the initial disk load. Non-gated models (NemotronH) load through the model's own expert loop, which uses the heuristic-free 3-D full_load path, and keep the batched layout. Signed-off-by: Terry Kong <terryk@nvidia.com>
vLLM 0.25.1 resolves the Qwen3.5 hang from vllm-project/vllm#36237 for the AutoModel EP=16 recipes. Both were validated passing post-rebase on the H100 nightly, so move them out of disabled.txt. The Megatron geo3k variant stays disabled: it still hangs in sample_tokens, and so does the already-enabled grpo-qwen3.5-35ba3b-2n8g-megatron-ep16tp2cp2 run on plain main, so that is the pre-existing Qwen3.5 + Megatron + EP hang rather than anything the bump regressed. The 397B recipe also stays disabled (separate upstream bug, plus the model is missing from CI's offline cache). Re-enabling costs 2 x 64 GPU-hours, taking the nightly suite from 3409 to 3537, so raise the guardrail from 3420 to 3550 to keep the same small headroom the previous cap had. Signed-off-by: Terry Kong <terryk@nvidia.com>
The recipe was disabled for vllm-project/vllm#37856, but the observed CI failure is unrelated to that bug and to vLLM entirely: the run dies at model load with OSError: We couldn't connect to 'https://huggingface.co' to load the files, and couldn't find them in the cached files. CI runs offline, so a model only resolves if it is already in the shared HF_HOME. Qwen/Qwen3.5-397B-A17B was never seeded there (the 35B and 9B Qwen3.5 models are), so the test could not have passed on any vLLM version. The model has since been seeded into the CI cache. Put it in release.txt rather than nightly.txt: one run costs 1024 GPU-hours (32 nodes x 8 GPUs x 4h), which would grow the nightly suite by 29% on its own. Signed-off-by: Terry Kong <terryk@nvidia.com>
… takes
The TCPStore port patch added in "fix: keep the vllm 0.25 TCPStore port out of
the MessageQueue scan range" never ran. It was placed inside RayExecutorV2
._select_tcpstore_port's `local_dp_rank is None` branch, and that branch is dead
for every engine NeMo-RL builds.
ParallelConfig.__post_init__ takes its "offline SPMD" path whenever vLLM's own
data-parallel coordinator is not in use -- which is always, here -- and assigns
data_parallel_rank_local = envs.VLLM_DP_RANK_LOCAL and data_parallel_master_port
= envs.VLLM_DP_MASTER_PORT. Both default to 0. So a plain non-DP engine reaches
_select_tcpstore_port with local_dp_rank=0, not None, and takes the DP branch:
start_port = master_port + 100 + local_dp_rank * 32 # = 100
which searches ports 100..131, fails all 32 attempts on the privileged range
(the "Port 100 is already in use, trying port 101" lines in every worker log),
and falls through to `except RuntimeError: return get_open_port()` -- straight
back to VLLM_PORT. That is exactly the port the broadcast MessageQueue binds and
holds a few lines later, so the rank-0 worker dies with EADDRINUSE on VLLM_PORT
(7000) for any engine that spans nodes.
Fix: run the VLLM_PORT-anchored search before the local_dp_rank test, so it
applies whichever branch vLLM would otherwise take, and fall through to vLLM's
own logic when VLLM_PORT is unset or the window is full. Ports stay inside the
engine's reserved 100-port band and therefore below the OS ephemeral floor; the
deliberately-rejected alternative of dropping VLLM_PORT would reintroduce the
TOCTOU contention that band exists to prevent (#2380, #3103).
Confirmed on 2 nodes x 1 GPU with generation TP=2 (the cheapest shape that puts
one engine across two nodes):
* pristine vLLM 0.25.1 -> EADDRINUSE, port 7000, in RayWorkerProc
.initialize_worker(), matching the DeepSeek-V3
64-GPU CI signature exactly
* previous patch -> EADDRINUSE, unchanged (dead branch)
* this patch -> distributed_init_method=tcp://<rank0>:7032,
engine starts and generates
Adds tests/unit/models/generation/test_vllm_tcpstore_port.py, which pins the
port arithmetic with no GPU and no second node -- the failure class previously
had zero nightly coverage because no nightly test has a node-spanning engine.
The test fails against the previous patch and passes against this one, and also
guards the anchor snippet against upstream drift.
Refs RL-1104.
Signed-off-by: Terry Kong <terryk@nvidia.com>
terrykong
force-pushed
the
terryk/bump-vllm-0.25.1
branch
from
July 26, 2026 16:14
0b5a6e2 to
58deef8
Compare
This was referenced Jul 26, 2026
Collaborator
Author
|
Closing as superseded — the fix is already on Its content was applied to the branch directly rather than by merging this PR, so GitHub never auto-closed it (unlike #3355, which did). The branch carries the Two follow-ups landed on top of what was reviewed here, both worth knowing about:
Tracking issue RL-1104 stays as the record. |
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.
What does this PR do ?
Fixes RL-1104: on vLLM 0.25.1, any engine whose model-parallel size spans more than one node dies at startup with
EADDRINUSEon the engine'sVLLM_PORT. This targetsterryk/bump-vllm-0.25.1so the fix can be reviewed against the bump.Why the patch on this branch is currently inert
ce28ac042added the right offset but put it in a branch vLLM never takes:ParallelConfig.__post_init__takes its "offline SPMD" path whenever vLLM's own DP coordinator is not in use — always, here — and assignsdata_parallel_rank_local = envs.VLLM_DP_RANK_LOCALanddata_parallel_master_port = envs.VLLM_DP_MASTER_PORT. Both default to 0. So a plain non-DP engine arrives withlocal_dp_rank=0, notNone:Nonebranch is dead — the patch never executes;0 + 100 + 0*32= port 100, fails all 32 attempts on the privileged range, andget_open_port()→ straight back toVLLM_PORT.That is the port the broadcast
MessageQueuebinds and holds a few lines later, so rank 0'sTCPStoregetsEADDRINUSE.Instrumented on hardware:
The
Port 100 is already in use, trying port 101 ...lines present in every vLLM worker log are the fingerprint of step 2.The fix
Run the
VLLM_PORT-anchored search before thelocal_dp_ranktest, so it applies on whichever branch vLLM would otherwise take, and fall through to vLLM's own logic whenVLLM_PORTis unset or the window is full. Ports stay inside the engine's reserved 100-port band and therefore below the OS ephemeral floor — droppingVLLM_PORTinstead would reintroduce the TOCTOU contention that band exists to prevent (#2380, #3103).Confirmation
Reproduced and fixed on 2 nodes x 1 GPU, generation TP=2 — the cheapest shape that puts one engine across two nodes (previously the smallest repro in the repo was 64 GPUs):
EADDRINUSEport 7000 inRayWorkerProc.initialize_worker()EADDRINUSE7000ce28ac042(this branch today)EADDRINUSE, unchangedEADDRINUSE7000distributed_init_method=tcp://<rank0>:7032, engine starts and generatesThe hardware signature matches the DeepSeek-V3 64-GPU CI failure exactly.
Blast radius
17 recipes across
performance*.txtandrelease*.txthavetp*pp > gpus_per_nodeand should be assumed broken at engine startup on 0.25 without this. Zero nightly tests do, which is why the bump's nightly parity comparison is unaffected.Changelog
RayExecutorV2._select_tcpstore_portoffset ahead of thelocal_dp_ranktest so it runs on the branch vLLM actually takes.tests/unit/models/generation/test_vllm_tcpstore_port.py: a GPU-free, single-node regression test that pins the port arithmetic. It fails againstce28ac042and passes against this PR, and guards the anchor snippet against upstream drift.Usage
N/A — no user-facing API change.
Before your PR is "Ready for review"
Additional Information
tp*pp > gpus_per_node, so the node-spanning engine-startup path itself remains uncovered. The 2-node driver script used here is attached to RL-1104.1d634dfb3, "use ephemeral vLLM ports for engines that span nodes") was also a no-op: its guard wasmodel_parallel_size > len(bundle_indices), andbundle_indicesis the engine'slocal_bundle_indices, solen(...) == model_parallel_sizeand the condition is always false. That explains the pre-patch H100 CI datapoint independently.🤖 Generated with Claude Code