Skip to content

Commit 56ef843

Browse files
committed
fix(model): normalize Qwen3-Omni integer masks for TE
Signed-off-by: hbhflw2000 <417911774@qq.com>
1 parent 31121a4 commit 56ef843

3 files changed

Lines changed: 109 additions & 6 deletions

File tree

‎src/megatron/bridge/models/qwen_vl/modelling_qwen3_vl/attention.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ def _qwen_attention_mask_for_core_attention(
4343
or packed_seq_params is not None
4444
or not isinstance(attention_mask, Tensor)
4545
or attention_mask.ndim != 2
46-
or attention_mask.dtype != torch.bool
4746
):
4847
return attention_mask
49-
return (~attention_mask).unsqueeze(1).unsqueeze(1)
48+
49+
if attention_mask.dtype == torch.bool:
50+
valid_mask = attention_mask
51+
elif attention_mask.dtype in (torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64):
52+
valid_mask = attention_mask != 0
53+
else:
54+
return attention_mask
55+
56+
return (~valid_mask).unsqueeze(1).unsqueeze(1)
5057

5158

5259
class Qwen3VLSelfAttention(SelfAttention):

‎tests/unit_tests/models/qwen_omni/test_qwen3_omni_bridge.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from types import SimpleNamespace
1516
from unittest.mock import Mock, patch
1617

1718
import pytest
1819
import torch
1920
import torch.nn.functional as F
2021

2122
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
23+
from megatron.bridge.models.conversion.param_mapping import GatedMLPMapping, QKVMapping
2224
from megatron.bridge.models.hf_pretrained.causal_lm import PreTrainedCausalLM
2325
from megatron.bridge.models.qwen_omni import Qwen3OmniBridge, Qwen3OmniModelProvider
2426

@@ -201,6 +203,91 @@ def test_mapping_registry_resolves_parallel_parameter_names(self):
201203
assert mapping is not None, megatron_param
202204
assert mapping.hf_param == hf_param
203205

206+
def test_qkv_mapping_preserves_asymmetric_projection_slices(self):
207+
registry = Qwen3OmniBridge().mapping_registry()
208+
mapping = registry.megatron_to_hf_lookup(
209+
"thinker.language_model.decoder.layers.0.self_attention.linear_qkv.weight"
210+
)
211+
assert isinstance(mapping, QKVMapping)
212+
213+
config = SimpleNamespace(num_attention_heads=4, num_query_groups=2, kv_channels=2, hidden_size=4)
214+
module = SimpleNamespace(config=config)
215+
q = torch.arange(8 * 4, dtype=torch.float32).reshape(8, 4) + 10
216+
k = torch.arange(4 * 4, dtype=torch.float32).reshape(4, 4) + 100
217+
v = torch.arange(4 * 4, dtype=torch.float32).reshape(4, 4) + 200
218+
219+
with patch.object(mapping._tp_mapping, "hf_to_megatron", side_effect=lambda weight, _module: weight):
220+
fused = mapping.hf_to_megatron({"q": q, "k": k, "v": v}, module)
221+
222+
expected = torch.cat([q[:4], k[:2], v[:2], q[4:], k[2:], v[2:]], dim=0)
223+
torch.testing.assert_close(fused, expected)
224+
225+
with (
226+
patch.object(mapping, "broadcast_obj_from_pp_rank", side_effect=lambda value, _key: value),
227+
patch.object(mapping._tp_mapping, "megatron_to_hf", return_value={mapping.megatron_param: fused}),
228+
):
229+
restored = mapping.megatron_to_hf(fused, module)
230+
231+
torch.testing.assert_close(restored[mapping.hf_param["q"]], q)
232+
torch.testing.assert_close(restored[mapping.hf_param["k"]], k)
233+
torch.testing.assert_close(restored[mapping.hf_param["v"]], v)
234+
235+
def test_expert_mapping_preserves_asymmetric_gate_up_slices(self):
236+
registry = Qwen3OmniBridge().mapping_registry()
237+
mapping = registry.megatron_to_hf_lookup(
238+
"thinker.language_model.decoder.layers.0.mlp.experts.linear_fc1.weight7"
239+
)
240+
assert isinstance(mapping, GatedMLPMapping)
241+
242+
gate = torch.arange(12, dtype=torch.float32).reshape(4, 3) + 10
243+
up = torch.arange(12, dtype=torch.float32).reshape(4, 3) + 100
244+
fused = mapping.hf_to_megatron({"gate": gate, "up": up}, SimpleNamespace())
245+
torch.testing.assert_close(fused, torch.cat([gate, up], dim=0))
246+
247+
with patch.object(mapping, "broadcast_from_pp_rank", side_effect=lambda value, cache_key=None: value):
248+
restored = mapping.megatron_to_hf(fused, SimpleNamespace())
249+
250+
torch.testing.assert_close(restored[mapping.hf_param["gate"]], gate)
251+
torch.testing.assert_close(restored[mapping.hf_param["up"]], up)
252+
253+
def test_expert_mapping_exports_gate_up_across_two_ep_ranks(self, monkeypatch):
254+
registry = Qwen3OmniBridge().mapping_registry()
255+
mapping = registry.megatron_to_hf_lookup(
256+
"thinker.language_model.decoder.layers.0.mlp.experts.local_experts.1.linear_fc1.weight"
257+
)
258+
assert isinstance(mapping, GatedMLPMapping)
259+
260+
class _FakeGroup:
261+
def size(self):
262+
return 2
263+
264+
mapping.ep_group = _FakeGroup()
265+
monkeypatch.setattr(
266+
"megatron.bridge.models.conversion.param_mapping.get_pg_size",
267+
lambda group: 1 if group is None else group.size(),
268+
)
269+
monkeypatch.setattr(mapping, "broadcast_from_pp_rank", lambda value, cache_key=None: value)
270+
monkeypatch.setattr(mapping, "broadcast_obj_from_pp_rank", lambda value, cache_key=None: value)
271+
272+
def fake_all_gather(outputs, local_weight, group):
273+
assert group is mapping.ep_group
274+
outputs[0].copy_(local_weight)
275+
outputs[1].copy_(local_weight + 1000)
276+
277+
monkeypatch.setattr(torch.distributed, "all_gather", fake_all_gather)
278+
279+
gate = torch.arange(6, dtype=torch.float32).reshape(2, 3) + 10
280+
up = torch.arange(6, dtype=torch.float32).reshape(2, 3) + 100
281+
module = SimpleNamespace(config=SimpleNamespace(num_moe_experts=4))
282+
restored = mapping.megatron_to_hf(torch.cat([gate, up], dim=0), module)
283+
284+
gate_name = "thinker.model.layers.0.mlp.experts.{}.gate_proj.weight"
285+
up_name = "thinker.model.layers.0.mlp.experts.{}.up_proj.weight"
286+
torch.testing.assert_close(restored[gate_name.format(1)], gate)
287+
torch.testing.assert_close(restored[gate_name.format(3)], gate + 1000)
288+
torch.testing.assert_close(restored[up_name.format(1)], up)
289+
torch.testing.assert_close(restored[up_name.format(3)], up + 1000)
290+
204291
def test_provider_bridge_warns_for_audio_output_stack(self, mock_hf_pretrained, caplog):
205292
mock_hf_pretrained.config.enable_audio_output = True
206293
caplog.set_level("WARNING")

‎tests/unit_tests/models/qwen_vl/modelling_qwen3_vl/test_attention_mask.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,31 @@ class _FakeTEDotProductAttention:
2525
pass
2626

2727

28-
def test_converts_qwen_valid_mask_for_te(monkeypatch):
28+
@pytest.mark.parametrize("dtype", [torch.bool, torch.int32, torch.int64])
29+
def test_converts_qwen_valid_mask_for_te(monkeypatch, dtype):
2930
monkeypatch.setattr(attention_module, "TEDotProductAttention", _FakeTEDotProductAttention)
30-
valid_mask = torch.tensor([[True, True, False], [False, True, True]])
31+
valid_mask = torch.tensor([[1, 1, 0], [0, 1, 1]], dtype=dtype)
3132

3233
actual = _qwen_attention_mask_for_core_attention(_FakeTEDotProductAttention(), valid_mask, packed_seq_params=None)
3334

3435
expected = torch.tensor([[[[False, False, True]]], [[[True, False, False]]]])
36+
assert actual.dtype == torch.bool
3537
torch.testing.assert_close(actual, expected)
3638

39+
attention_scores = torch.tensor(
40+
[[[[1.0, 2.0, 3.0]]], [[[4.0, 5.0, 6.0]]]],
41+
)
42+
attention_probs = torch.softmax(attention_scores.masked_fill(actual, float("-inf")), dim=-1)
43+
assert torch.isfinite(attention_probs).all()
44+
assert torch.equal(attention_probs.masked_select(actual), torch.zeros(2))
45+
3746

3847
def test_converts_all_valid_qwen_mask_for_te_without_host_sync(monkeypatch):
3948
monkeypatch.setattr(attention_module, "TEDotProductAttention", _FakeTEDotProductAttention)
4049
monkeypatch.setattr(
4150
torch.Tensor, "item", lambda _tensor: pytest.fail("attention mask must not synchronize to host")
4251
)
43-
valid_mask = torch.ones((2, 4), dtype=torch.bool)
52+
valid_mask = torch.ones((2, 4), dtype=torch.long)
4453

4554
actual = _qwen_attention_mask_for_core_attention(_FakeTEDotProductAttention(), valid_mask, packed_seq_params=None)
4655
monkeypatch.undo()
@@ -51,7 +60,7 @@ def test_converts_all_valid_qwen_mask_for_te_without_host_sync(monkeypatch):
5160
@pytest.mark.parametrize(
5261
("mask", "packed_seq_params"),
5362
[
54-
(torch.ones((2, 4), dtype=torch.long), None),
63+
(torch.zeros((2, 4), dtype=torch.float32), None),
5564
(torch.ones((2, 1, 4, 4), dtype=torch.bool), None),
5665
(torch.ones((2, 4), dtype=torch.bool), object()),
5766
],

0 commit comments

Comments
 (0)