Repository navigation
Conversation
…ight sync
get_per_tensor_param() sends lm_head.weight as a separate tensor even
though it's tied to (byte-identical to) model.embed_tokens.weight. The
bucketed weight-transfer sender packs tensors into fixed-size buckets and
flushes on overflow; for small, large-vocabulary tied-embedding models
(e.g. Qwen2.5-0.5B-Instruct) the embedding is a large fraction of the
bucket size, so the duplicate lm_head.weight can land alone in a later
bucket. The receiver calls vLLM's model.load_weights() once per bucket,
so that isolated call fails vLLM's tied-weight completeness check:
ValueError: 'lm_head.weight' was skipped because it is tied to
'model.embed_tokens.weight' in Qwen2ForCausalLM, but
'model.embed_tokens.weight' was not found in the checkpoint, so the
tied weight is uninitialized.
vLLM's AutoWeightsLoader never actually loads lm_head.weight's bytes for
a tied model -- it only uses the name's presence to check the embedding
was loaded in the same call, then re-derives lm_head from it. Sending it
is pure redundant risk, so drop it instead.
Verified on real B60 hardware: colocated-RM + FSDP2 2-D HSDP
(world_size=4) + transfer_queue.enable=True previously deadlocked
deterministically (separate, pre-existing ack-on-error gap in the ZMQ
receiver, tracked upstream in verl-project#7366); with that combination and this fix,
2 real GRPO steps complete end-to-end.
WIP test-mule commit for cross-machine testing; message/attribution to
be finalized before opening the actual PR.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The shared exporter may leave independent output heads stale for non-vLLM backends.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Removes redundant tied lm_head.weight tensors from FSDP synchronization to prevent bucketed vLLM loading failures.
Changes:
- Filters tied weights in standard and merged-LoRA exports.
- Adds CPU tests for tied, untied, and default configurations.
| File | Summary |
|---|---|
verl/workers/engine/fsdp/transformer_impl.py |
Filters tied output weights during export. |
tests/workers/test_fsdp_drop_tied_lm_head_on_cpu.py |
Tests the filtering behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+127
to
+128
| if getattr(hf_config, "tie_word_embeddings", False): | ||
| params.pop("lm_head.weight", None) |
3 of 6 tasks
Owner
Author
|
invalid bug. fixed during verl update vllm to 0.29.0. bug happend due to forget to rebase or mege with main updates. |
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?
For a tied-embedding model,
FSDPEngine's per-tensor weight-sync stream sendslm_head.weightas a separate tensor even though it is byte-identical to(aliases)
model.embed_tokens.weight. The vLLM colocated rollout uses abucketed weight-transfer path: the sender packs tensors into fixed-size
buckets and flushes on overflow, and the receiver calls vLLM's
model.load_weights()once per bucket.If the duplicate
lm_head.weightlands in a different bucket thanmodel.embed_tokens.weight— likely for small, large-vocabulary tied modelswhere the embedding is a large fraction of the bucket size — that isolated
load_weights()call fails vLLM's tied-weight completeness check:vLLM's
AutoWeightsLoadernever actually loads a tiedlm_head.weight's bytes— it only uses the name's presence to verify the embedding was loaded in the
same
load_weights()call, then re-deriveslm_headfrom the embedding.Splitting the byte-identical duplicate into its own bucket is therefore pure
redundant risk. This PR drops
lm_head.weightfrom the sync stream whentie_word_embeddings=True, so the isolated bucket can never form.Checklist Before Starting
gh pr list --repo volcengine/verl --state open --search "tied lm_head"gh pr list --repo volcengine/verl --state open --search "bucket weight sync tied"gh pr list --repo volcengine/verl --state open --search "tie_word_embeddings"defensive liveness fix for the ZMQ ACK protocol; it turns a hang into a fast
error but does not address the redundant tied weight itself. This PR is
the root-cause fix; [vllm] fix: surface bucketed receiver failures verl-project/verl#7366 is complementary.
[{modules}] {type}: {description}→[fsdp, vllm] fix: ...Symptom / motivation
On the bucketed colocated weight-sync path with a tied-embedding model, the
first weight update after a rollout fails the tied-weight completeness check
above. Whether this surfaces as a clean error or a hang depends on the ZMQ
receiver's error handling (see verl-project#7366): without that fix the sender can block
waiting for an ACK that never comes, producing a deterministic deadlock rather
than a fast failure.
The bug is vLLM-version-dependent, which is why it can go unnoticed:
Qwen2ForCausalLM.load_weightsusesAutoWeightsLoader(self, skip_prefixes=["lm_head."] if tie_word_embeddings else None)— tiedlm_head.is unconditionally skipped and no completeness check runs.skip_prefixeswas dropped and a completeness checkAutoWeightsLoader._check_skipped_aliaseswas (re)introduced invllm/model_executor/models/utils.py, raising the exactValueErrorabove for an isolated tied-lm_headbucket.Test
1. Unit test (CPU, added in this PR) —
tests/workers/test_fsdp_drop_tied_lm_head_on_cpu.pycovers: tied model drops
lm_head.weight; untied model keeps it; tied modelwithout an
lm_head.weightkey is a no-op; and a config missingtie_word_embeddingsdefaults to untied (keeps the key).2. Live vLLM 0.29.0 reproduction (the exact failure mechanism).
Using the stock
vllm/vllm-openai:v0.29.0image and a real tiedQwen2ForCausalLM(Qwen2.5-0.5B-Instruct), I replayed the per-bucketload_weights()call the bucketed receiver makes:load_weights(...)input[("lm_head.weight", w)](embed in a prior bucket)ValueErrorabove — bug reproducedlm_head.weightdropped from stream; embed bucket[("model.embed_tokens.weight", w)]3. Bucket-split condition confirmed in the real pipeline.
On a 4×A100, FSDP2 + colocated vLLM GRPO run (Qwen2.5-0.5B-Instruct, gsm8k),
instrumenting the bucketed sender confirmed that
model.embed_tokens.weightlands in the first bucket while the tiedlm_head.weightlands alone in the last bucket — organically, i.e. exactlythe isolated-bucket condition Scenario A raises on. With this fix applied,
2 GRPO steps complete end-to-end.
API and Usage Example
No API, config, or CLI change. The fix is transparent — tied-embedding models
simply no longer transmit the redundant
lm_head.weightduring weight sync.Design & Code Changes
Single-file change in
verl/workers/engine/fsdp/transformer_impl.py:_drop_redundant_tied_lm_head(params, hf_config)popslm_head.weightfrom the param dict in place whenhf_config.tie_word_embeddingsis truthy (defaults to untied / no-op if theattribute is absent, and a no-op if the key isn't present).
convert_weight_keys(...):state_dict()path inget_per_tensor_param(), and_merged_lora_per_tensor_param().Checklist Before Submitting
pre-commit install && pre-commit run --all-files --show-diff-on-failure --color=always(
tests/workers/test_fsdp_drop_tied_lm_head_on_cpu.py). The isolated-bucketintegration failure requires a GPU + vLLM 0.29 and a colocated rollout, so
it is not added to CI; the unit test covers the drop logic and the live
repro is documented in the Test section above.
ci-requestmessage when ready for CI.recipesubmodule: n/a.