Skip to content

fix(refit): stream a parameter larger than the IPC staging buffer - #3356

Closed
terrykong wants to merge 1 commit into
mainfrom
terryk/refit-oversized-param
Closed

fix(refit): stream a parameter larger than the IPC staging buffer#3356
terrykong wants to merge 1 commit into
mainfrom
terryk/refit-oversized-param

Conversation

@terrykong

Copy link
Copy Markdown
Collaborator

The bug

stream_weights_via_ipc_zmq_impl sizes its two ping-pong staging buffers from free memory (NRL_REFIT_BUFFER_MEMORY_RATIO, default 0.3, halved again for ping-pong) with no floor at the largest parameter. A parameter that does not fit a single buffer cannot be packed at all, and the refit aborted on an assertion.

DeepSeek-V3 hits it. model.embed_tokens.weight is 129280 x 7168 bf16 = 1.73 GiB against a 1.65 GiB buffer — only 4.6% over:

AssertionError: Parameter model.embed_tokens.weight too large for buffer: 1853358080 > 1771261132

raised from MegatronPolicyWorker.stream_weights_via_ipc_zmq. It kills every colocated DSv3 run during the first refit, i.e. long after a 32-node allocation has been paid for. This is pre-existing on main and unrelated to any vLLM version.

The fix

Ship such a parameter on its own in a buffer sized to fit: flush whatever group is pending, send the oversized parameter alone, and consume its ACK before freeing the one-off buffer — unlike the ping-pong pair, it is not kept alive across the next send.

This mirrors the HTTP streaming path, which already gives an oversized parameter a bucket of its own (if bucket and bucket_size + param_size >= buffer_size_bytes) instead of raising. The two sibling implementations now agree.

Deliberately not fixed by raising the buffer size — neither by flooring the auto-computed value nor by setting refit_buffer_size_gb per recipe. The 70% of free memory that the buffer does not take is reserved for parameter all-gather across the expert-parallel dimension, so growing the buffer trades a clean assertion for an OOM, and a per-recipe constant has to be re-tuned per model and cluster.

Blast radius: none for working refits

The new branch is reachable only where the old code raised AssertionError — a guaranteed failure. Any refit that works today takes byte-for-byte the same path. test_parameters_that_fit_are_still_batched pins that.

Validation

tests/unit/models/policy/test_refit_oversized_param.py — 5 tests, ~1.5s, no GPU and no Ray: the packing and hand-off logic is plain Python around a byte buffer, so CPU tensors and a stub socket exercise it faithfully.

Against the unpatched code the suite splits exactly as intended, so it is not vacuous:

Test Unpatched Patched
test_oversized_parameter_is_streamed_instead_of_aborting AssertionError: Parameter model.embed_tokens.weight too large for buffer: 3072 > 2048
test_oversized_parameter_alone
test_consecutive_oversized_parameters
test_parameters_that_fit_are_still_batched
test_alignment_is_what_decides_oversized

The failure signature matches production exactly, at reduced scale.

Writing the harness surfaced one protocol detail worth recording: the receiver ACKs the end-of-stream COMPLETE marker as well as each data group, so a fake socket that only ACKs data groups deadlocks the streamer.

Not covered here

Not yet validated on a real refit. That needs a cluster allocation. DeepSeek-V3 is the natural end-to-end check, but it currently also needs the vLLM 0.25 node-spanning startup fixes from #3350 and #3355, so a green DSv3 run is not the success criterion for this PR on its own.

🤖 Generated with Claude Code

stream_weights_via_ipc_zmq_impl sizes its two ping-pong staging buffers
from *free memory* (NRL_REFIT_BUFFER_MEMORY_RATIO, default 0.3, halved
again for ping-pong) with no floor at the largest parameter. When one
parameter does not fit a single buffer it cannot be packed at all, and
the refit aborted on an assertion.

DeepSeek-V3 hits this: model.embed_tokens.weight is 129280 x 7168 bf16 =
1.73 GiB against a 1.65 GiB buffer, only 4.6% over, so

    AssertionError: Parameter model.embed_tokens.weight too large for
    buffer: 1853358080 > 1771261132

kills every colocated DSv3 run during the first refit, long after the
32-node allocation has been paid for.

Ship such a parameter on its own in a buffer sized to fit instead:
flush whatever group is pending, send the oversized parameter alone,
and wait for its ACK before freeing the one-off buffer (unlike the
ping-pong pair it is not kept alive across the next send). This mirrors
the HTTP streaming path, which already gives an oversized parameter a
bucket of its own rather than raising.

The new branch is only reachable where the old code raised, so refits
that work today are byte-for-byte unaffected; a regression test pins
that parameters which fit are still batched many per group.

Tested on CPU with a stub socket -- no GPU needed, since the packing and
hand-off logic is plain Python around a byte buffer. The three oversized
tests fail against the unpatched code with the same assertion seen in
CI, so they are not vacuous.

Signed-off-by: Terry Kong <terryk@nvidia.com>
@terrykong
terrykong requested review from a team as code owners July 27, 2026 02:36
@copy-pr-bot

copy-pr-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@terrykong terrykong added the CI:L1 Run doctests, unit tests, and functional tests label Jul 27, 2026
@terrykong

Copy link
Copy Markdown
Collaborator Author

/ok to test 831fa73

@terrykong

Copy link
Copy Markdown
Collaborator Author

Closing as superseded — folded into #3280 (commit dde85d331 on terryk/bump-vllm-0.25.1, now rebased onto main).

Rationale: validating DeepSeek-V3 requires the vLLM 0.25 node-spanning startup fixes (#3350, #3355) and this refit fix together. Keeping them on separate branches would have meant running the DSv3 perf suite twice, on two SKUs, for no added signal — so both live on one branch and get one validation cycle.

The change itself is unmodified from what was green here (90 pass / 0 fail): stream_weights_via_ipc_zmq_impl now ships a parameter larger than a single staging buffer on its own instead of asserting, plus tests/unit/models/policy/test_refit_oversized_param.py (5 tests, no GPU, 3 of which fail against the unpatched code with the production assertion at reduced scale).

Worth restating since it now rides a much larger PR: the new branch is reachable only where the old code raised AssertionError, so every refit that works today takes byte-for-byte the same path. The bug is pre-existing on main and unrelated to vLLM — if #3280 slips, this is the piece worth re-splitting out and landing on its own.

@terrykong terrykong closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:L1 Run doctests, unit tests, and functional tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant