-
Notifications
You must be signed in to change notification settings - Fork 1
Fix merge_qkv=False loading garbage attention weights #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
qywu
wants to merge
1
commit into
main
Choose a base branch
from
qywu/fix-merge-qkv-unfuse-load
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| """Regression for issue #87: merge_qkv=False must keep the checkpoint handler | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a pretty expensive test to run |
||
| in sync with the unfused module structure. | ||
|
|
||
| The builder's old per-attention unfuse never set ``model._unfused_for_tp``, so | ||
| the checkpoint handler kept merging q/k/v into ``qkv_proj`` keys that the | ||
| freshly created (empty) unfused modules could never receive — every attention | ||
| weight silently loaded as garbage, which full-weight weight-sync then shipped | ||
| to samplers verbatim. | ||
| """ | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from xorl.models.transformers.qwen3.configuration_qwen3 import Qwen3Config | ||
| from xorl.models.transformers.qwen3.modeling_qwen3 import Qwen3ForCausalLM | ||
|
|
||
|
|
||
| pytestmark = pytest.mark.cpu | ||
|
|
||
|
|
||
| def _tiny_config(): | ||
| return Qwen3Config( | ||
| hidden_size=64, | ||
| intermediate_size=128, | ||
| num_hidden_layers=2, | ||
| num_attention_heads=4, | ||
| num_key_value_heads=2, | ||
| head_dim=16, | ||
| vocab_size=128, | ||
| max_position_embeddings=64, | ||
| pad_token_id=0, | ||
| ) | ||
|
|
||
|
|
||
| def _make_model(): | ||
| torch.manual_seed(0) | ||
| return Qwen3ForCausalLM(_tiny_config()) | ||
|
|
||
|
|
||
| def test_fused_model_handler_merges_qkv(): | ||
| model = _make_model() | ||
| layer = model.model.layers[0] | ||
| assert hasattr(layer.self_attn, "qkv_proj"), "model should construct fused" | ||
| handler = model.get_checkpoint_handler(weights_path=None) | ||
| q = torch.randn(64, 64) | ||
| k = torch.randn(32, 64) | ||
| v = torch.randn(32, 64) | ||
| out = [] | ||
| for name, tensor in ( | ||
| ("model.layers.0.self_attn.q_proj.weight", q), | ||
| ("model.layers.0.self_attn.k_proj.weight", k), | ||
| ("model.layers.0.self_attn.v_proj.weight", v), | ||
| ): | ||
| out.extend(handler.on_load_weight(name, tensor)) | ||
| assert [name for name, _ in out] == ["model.layers.0.self_attn.qkv_proj.weight"] | ||
| assert torch.equal(out[0][1], torch.cat([q, k, v], dim=0)) | ||
|
|
||
|
|
||
| def test_model_level_unfuse_disables_handler_merges(): | ||
| """After model.unfuse_for_tp(), separate checkpoint keys must pass through | ||
| untouched: the unfused modules are the ONLY place those weights can land.""" | ||
| model = _make_model() | ||
| model.unfuse_for_tp() | ||
| layer = model.model.layers[0] | ||
| assert hasattr(layer.self_attn, "q_proj") and not hasattr(layer.self_attn, "qkv_proj") | ||
| assert hasattr(layer.mlp, "gate_proj") and not hasattr(layer.mlp, "gate_up_proj") | ||
| assert getattr(model, "_unfused_for_tp", False) is True | ||
|
|
||
| handler = model.get_checkpoint_handler(weights_path=None) | ||
| for name in ( | ||
| "model.layers.0.self_attn.q_proj.weight", | ||
| "model.layers.0.self_attn.k_proj.weight", | ||
| "model.layers.0.self_attn.v_proj.weight", | ||
| "model.layers.0.mlp.gate_proj.weight", | ||
| "model.layers.0.mlp.up_proj.weight", | ||
| ): | ||
| tensor = torch.randn(8, 8) | ||
| result = handler.on_load_weight(name, tensor) | ||
| assert result == [(name, tensor)], f"{name} must pass through unmerged" | ||
|
|
||
|
|
||
| def test_builder_unfuse_block_sets_handler_flag(): | ||
| """The exact code path build_training_model runs for merge_qkv=False: | ||
| the model-level unfuse must be preferred so the flag reaches the handler. | ||
|
|
||
| (The old buggy path — looping layer.self_attn.unfuse_for_tp() — left | ||
| _unfused_for_tp unset, and the handler kept merging.)""" | ||
| model = _make_model() | ||
| # Mirror the fixed builder block. | ||
| if hasattr(model, "unfuse_for_tp"): | ||
| model.unfuse_for_tp() | ||
| else: # pragma: no cover - qwen3 implements it | ||
| for layer in model.model.layers: | ||
| layer.self_attn.unfuse_for_tp() | ||
| model._unfused_for_tp = True | ||
|
|
||
| handler = model.get_checkpoint_handler(weights_path=None) | ||
| name = "model.layers.1.self_attn.q_proj.weight" | ||
| tensor = torch.randn(8, 8) | ||
| assert handler.on_load_weight(name, tensor) == [(name, tensor)] | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code should not have comments that explains why the previous version of the code was incorrect