|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +from types import SimpleNamespace |
15 | 16 | from unittest.mock import Mock, patch |
16 | 17 |
|
17 | 18 | import pytest |
18 | 19 | import torch |
19 | 20 | import torch.nn.functional as F |
20 | 21 |
|
21 | 22 | from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry |
| 23 | +from megatron.bridge.models.conversion.param_mapping import GatedMLPMapping, QKVMapping |
22 | 24 | from megatron.bridge.models.hf_pretrained.causal_lm import PreTrainedCausalLM |
23 | 25 | from megatron.bridge.models.qwen_omni import Qwen3OmniBridge, Qwen3OmniModelProvider |
24 | 26 |
|
@@ -201,6 +203,91 @@ def test_mapping_registry_resolves_parallel_parameter_names(self): |
201 | 203 | assert mapping is not None, megatron_param |
202 | 204 | assert mapping.hf_param == hf_param |
203 | 205 |
|
| 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 | + |
204 | 291 | def test_provider_bridge_warns_for_audio_output_stack(self, mock_hf_pretrained, caplog): |
205 | 292 | mock_hf_pretrained.config.enable_audio_output = True |
206 | 293 | caplog.set_level("WARNING") |
|
0 commit comments