Skip to content

Commit d44b78b

Browse files
kamran-nvidiaclaude
andcommitted
refactor(model): build MiniMax-M2 from a builder-backed model config
Move MiniMax-M2 onto the ModelConfig/ModelBuilder path, following Muse Glimmer. MiniMaxM2Bridge maps Hugging Face configs in hf_config_to_model_config_kwargs(), including the full-dimension QK-norm layer spec, and the deprecated provider path reuses the same kwargs. USE_MODEL_CONFIG_FOR_CONVERSION = True. Signed-off-by: Kamran Jafari <kjafarisadeg@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent dbf0f9b commit d44b78b

2 files changed

Lines changed: 128 additions & 34 deletions

File tree

‎src/megatron/bridge/models/minimax_m2/minimax_m2_bridge.py‎

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
# limitations under the License.
1414

1515
from collections.abc import Mapping
16-
from typing import Dict, Optional
16+
from typing import Any, Dict, Optional
1717

1818
import torch
1919
import torch.nn as nn
2020
from megatron.core.models.gpt.gpt_model import GPTModel
21+
from transformers import PretrainedConfig
2122

2223
from megatron.bridge.models.conversion import quantization_utils
2324
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
@@ -128,46 +129,52 @@ class MiniMaxM2Bridge(MegatronModelBridge):
128129
Example:
129130
>>> from megatron.bridge import AutoBridge
130131
>>> bridge = AutoBridge.from_hf_pretrained("MiniMaxAI/MiniMax-M2")
131-
>>> provider = bridge.to_megatron_provider()
132+
>>> model_config = bridge.get_model_config()
132133
"""
133134

134-
def provider_bridge(self, hf_pretrained):
135-
"""Convert HuggingFace MiniMax-M2 config to GPTModelProvider."""
136-
provider = super().provider_bridge(hf_pretrained)
137-
138-
hf_config = hf_pretrained.config
139-
140-
provider.normalization = "RMSNorm"
141-
provider.gated_linear_unit = True
142-
provider.position_embedding_type = "rope"
143-
provider.add_bias_linear = False
144-
provider.add_qkv_bias = False
145-
provider.hidden_dropout = 0.0
146-
provider.autocast_dtype = torch.bfloat16
135+
USE_MODEL_CONFIG_FOR_CONVERSION = True
136+
137+
def hf_config_to_model_config_kwargs(self, hf_config: PretrainedConfig) -> dict[str, Any]:
138+
"""Convert a Hugging Face MiniMax-M2 config to builder config kwargs."""
139+
config_kwargs = super().hf_config_to_model_config_kwargs(hf_config)
140+
config_kwargs.update(
141+
normalization="RMSNorm",
142+
gated_linear_unit=True,
143+
position_embedding_type="rope",
144+
add_bias_linear=False,
145+
add_qkv_bias=False,
146+
hidden_dropout=0.0,
147+
autocast_dtype=torch.bfloat16,
148+
masked_softmax_fusion=True,
149+
rope_scaling=False,
150+
rope_scaling_factor=1.0,
151+
# Full-dimension QK norm via custom layer spec (see minimax_m2_provider.py).
152+
# qk_layernorm=True so mcore creates QK norms; the spec overrides the default
153+
# TENorm with FullDimQNorm/FullDimKNorm for full-dimension normalization.
154+
qk_layernorm=True,
155+
transformer_layer_spec=minimax_m2_layer_spec,
156+
# MoE settings — sigmoid routing with expert bias (same pattern as DeepSeek V3)
157+
moe_grouped_gemm=True,
158+
moe_router_pre_softmax=False,
159+
moe_router_load_balancing_type="aux_loss",
160+
moe_aux_loss_coeff=getattr(hf_config, "router_aux_loss_coef", 1e-3),
161+
moe_token_dispatcher_type="alltoall",
162+
moe_permute_fusion=True,
163+
moe_router_score_function="sigmoid",
164+
moe_router_enable_expert_bias=True,
165+
)
147166

148167
# MiniMax-M2 uses rotary_dim instead of partial_rotary_factor
149168
rotary_dim = getattr(hf_config, "rotary_dim", None)
150169
head_dim = getattr(hf_config, "head_dim", None)
151170
if rotary_dim is not None and head_dim is not None:
152-
provider.rotary_percent = rotary_dim / head_dim
153-
154-
# Full-dimension QK norm via custom layer spec (see minimax_m2_provider.py).
155-
# qk_layernorm=True so mcore creates QK norms; the spec overrides the default
156-
# TENorm with FullDimQNorm/FullDimKNorm for full-dimension normalization.
157-
provider.qk_layernorm = True
158-
provider.transformer_layer_spec = minimax_m2_layer_spec
159-
160-
# MoE settings — sigmoid routing with expert bias (same pattern as DeepSeek V3)
161-
provider.moe_grouped_gemm = True
162-
provider.moe_router_pre_softmax = False
163-
provider.moe_router_load_balancing_type = "aux_loss"
164-
provider.moe_aux_loss_coeff = getattr(hf_config, "router_aux_loss_coef", 1e-3)
165-
provider.moe_token_dispatcher_type = "alltoall"
166-
provider.moe_permute_fusion = True
167-
provider.moe_router_score_function = "sigmoid"
168-
provider.moe_router_enable_expert_bias = True
169-
170-
return provider
171+
config_kwargs["rotary_percent"] = rotary_dim / head_dim
172+
173+
return config_kwargs
174+
175+
def hf_config_to_provider_kwargs(self, hf_config: PretrainedConfig) -> dict[str, Any]:
176+
"""Adapt the canonical builder mapping to the deprecated provider path."""
177+
return self.hf_config_to_model_config_kwargs(hf_config)
171178

172179
def maybe_modify_loaded_hf_weight(
173180
self, hf_param: str | dict[str, str], hf_state_dict: Mapping[str, torch.Tensor]

‎tests/unit_tests/models/minimax_m2/test_minimax_m2_bridges.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
FullDimKNorm,
3434
FullDimQNorm,
3535
_FullDimRMSNorm,
36+
minimax_m2_layer_spec,
3637
)
3738

3839

@@ -503,3 +504,89 @@ def test_megatron_to_hf_deduplicates_replicated_kv_shards(self):
503504
result["model.layers.*.self_attn.k_norm.weight"],
504505
torch.arange(8, dtype=torch.bfloat16),
505506
)
507+
508+
509+
class TestMiniMaxM2BuilderConfig:
510+
"""Builder-backed ModelConfig path for MiniMax-M2."""
511+
512+
@staticmethod
513+
def _config(**attrs):
514+
from transformers import MiniMaxM2Config
515+
516+
config = MiniMaxM2Config(
517+
hidden_size=64,
518+
intermediate_size=32,
519+
num_hidden_layers=2,
520+
num_attention_heads=8,
521+
num_key_value_heads=4,
522+
head_dim=8,
523+
num_local_experts=8,
524+
num_experts_per_tok=2,
525+
max_position_embeddings=4096,
526+
vocab_size=128,
527+
torch_dtype="bfloat16",
528+
)
529+
config.architectures = ["MiniMaxM2ForCausalLM"]
530+
for key, value in attrs.items():
531+
setattr(config, key, value)
532+
return config
533+
534+
def test_provider_bridge_is_inherited_compatibility_only(self):
535+
assert "provider_bridge" not in MiniMaxM2Bridge.__dict__
536+
537+
def test_conversion_uses_builder_config(self):
538+
assert MiniMaxM2Bridge.USE_MODEL_CONFIG_FOR_CONVERSION is True
539+
540+
def test_hf_config_to_model_config_uses_direct_mapping(self):
541+
from megatron.bridge.models.gpt.model_config import BridgeGPTModelConfig
542+
543+
config = self._config(rotary_dim=4)
544+
bridge = MiniMaxM2Bridge()
545+
with (
546+
patch.object(bridge, "provider_bridge", side_effect=AssertionError("provider path used")),
547+
patch.object(bridge, "hf_config_to_provider_kwargs", side_effect=AssertionError("provider kwargs used")),
548+
):
549+
result = bridge.hf_config_to_model_config(config)
550+
551+
assert isinstance(result, BridgeGPTModelConfig)
552+
assert result.transformer_layer_spec is minimax_m2_layer_spec
553+
assert result.qk_layernorm is True
554+
assert result.rotary_percent == 0.5
555+
assert result.position_embedding_type == "rope"
556+
assert result.moe_router_score_function == "sigmoid"
557+
assert result.moe_router_enable_expert_bias is True
558+
assert result.moe_aux_loss_coeff == config.router_aux_loss_coef
559+
560+
@pytest.mark.parametrize("attrs", [{}, {"rotary_dim": 4}], ids=["full-rotary", "rotary-dim"])
561+
def test_model_config_matches_provider_runtime_config(self, attrs):
562+
from dataclasses import fields
563+
564+
config = self._config(**attrs)
565+
mock_pretrained = Mock(spec=PreTrainedCausalLM)
566+
mock_pretrained.config = config
567+
bridge = MiniMaxM2Bridge()
568+
569+
model_config = bridge.hf_config_to_model_config(config)
570+
with pytest.warns(FutureWarning, match=r"deprecated.*get_model_config.*get_model"):
571+
provider = bridge.provider_bridge(mock_pretrained)
572+
573+
provider_fields = {field.name for field in fields(provider)}
574+
model_config_fields = {field.name for field in fields(model_config)}
575+
model_config_fields.update(field.name for field in fields(model_config.transformer))
576+
for field_name in sorted((provider_fields & model_config_fields) - {"transformer_layer_spec"}):
577+
assert getattr(model_config, field_name) == getattr(provider, field_name), field_name
578+
# Both paths build the full-dimension QK norm through the same layer spec.
579+
assert model_config.transformer_layer_spec is minimax_m2_layer_spec
580+
assert provider.transformer_layer_spec is minimax_m2_layer_spec
581+
582+
def test_layer_spec_matches_between_provider_and_builder_config(self):
583+
config = self._config()
584+
mock_pretrained = Mock(spec=PreTrainedCausalLM)
585+
mock_pretrained.config = config
586+
bridge = MiniMaxM2Bridge()
587+
model_config = bridge.hf_config_to_model_config(config)
588+
with pytest.warns(FutureWarning):
589+
provider = bridge.provider_bridge(mock_pretrained)
590+
591+
# functools.partial (used for the MoE layer) compares by identity, so compare structure.
592+
assert repr(minimax_m2_layer_spec(model_config)) == repr(minimax_m2_layer_spec(provider))

0 commit comments

Comments
 (0)