Skip to content

Commit e86caf6

Browse files
refactor(model): build Hy V3 and MiniMax-M2 from builder-backed model configs (#6392)
Signed-off-by: Kamran Jafari <kjafarisadeg@nvidia.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9332d71 commit e86caf6

5 files changed

Lines changed: 313 additions & 72 deletions

File tree

‎src/megatron/bridge/models/hy_v3/hy_v3_bridge.py‎

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

1515
from functools import partial
16+
from typing import Any
1617

1718
import torch
1819
from megatron.core.models.gpt.gpt_layer_specs import get_gpt_decoder_block_spec
1920
from megatron.core.models.gpt.gpt_model import GPTModel
21+
from transformers import PretrainedConfig
2022

2123
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
2224
from megatron.bridge.models.conversion.model_bridge import MegatronModelBridge
@@ -25,8 +27,8 @@
2527
GatedMLPMapping,
2628
QKVMapping,
2729
)
30+
from megatron.bridge.models.gpt.model_config import BridgeGPTModelConfig
2831
from megatron.bridge.models.gpt_provider import GPTModelProvider
29-
from megatron.bridge.models.hf_pretrained.causal_lm import PreTrainedCausalLM
3032

3133

3234
try:
@@ -57,49 +59,62 @@ class HYV3Bridge(MegatronModelBridge):
5759
Example:
5860
>>> from megatron.bridge import AutoBridge
5961
>>> bridge = AutoBridge.from_hf_pretrained("tencent/Hy3-preview-Base")
60-
>>> provider = bridge.to_megatron_provider()
62+
>>> model_config = bridge.get_model_config()
6163
"""
6264

63-
def provider_bridge(self, hf_pretrained: PreTrainedCausalLM) -> GPTModelProvider:
64-
"""Convert HuggingFace Hy V3 config to GPTModelProvider."""
65-
provider = super().provider_bridge(hf_pretrained)
66-
hf_config = hf_pretrained.config
65+
MODEL_CONFIG_CLASS = BridgeGPTModelConfig
66+
USE_MODEL_CONFIG_FOR_CONVERSION = True
6767

68-
provider.transformer_layer_spec = partial(get_gpt_decoder_block_spec, use_transformer_engine=HAVE_TE)
68+
def hf_config_to_model_config_kwargs(self, hf_config: PretrainedConfig) -> dict[str, Any]:
69+
"""Convert a Hugging Face Hy V3 config to builder config kwargs.
6970
70-
provider.normalization = "RMSNorm"
71-
provider.gated_linear_unit = True
72-
provider.add_bias_linear = False
73-
provider.add_qkv_bias = False # Hy V3 does NOT have QKV bias
74-
provider.hidden_dropout = 0.0
75-
provider.qk_layernorm = True # Hy V3 uses QK layernorm
76-
provider.attention_softmax_in_fp32 = False
77-
provider.autocast_dtype = torch.bfloat16
78-
provider.fp16 = False
79-
provider.bf16 = True
80-
provider.params_dtype = torch.bfloat16
81-
provider.moe_grouped_gemm = True
82-
provider.moe_token_dispatcher_type = "alltoall"
83-
provider.moe_router_load_balancing_type = "none"
84-
provider.moe_router_pre_softmax = False
85-
provider.moe_router_score_function = "sigmoid"
86-
provider.moe_router_enable_expert_bias = True
87-
provider.moe_router_bias_update_rate = 0
88-
provider.moe_router_dtype = "fp32"
89-
provider.moe_permute_fusion = True
90-
provider.moe_shared_expert_overlap = False
91-
provider.moe_aux_loss_coeff = 0.0
92-
provider.moe_router_topk_scaling_factor = float(hf_config.router_scaling_factor)
93-
94-
# Shared experts hidden dim = moe_intermediate_size * num_shared_experts.
95-
provider.moe_shared_expert_intermediate_size = hf_config.moe_intermediate_size * hf_config.num_shared_experts
96-
97-
# Dense-first layer pattern.
98-
provider.moe_layer_freq = [0] * hf_config.first_k_dense_replace + [1] * (
99-
hf_config.num_hidden_layers - hf_config.first_k_dense_replace
71+
The builder's default MoE layer spec already uses the GPT decoder block
72+
spec, which honors the dense-first ``moe_layer_freq``.
73+
"""
74+
config_kwargs = super().hf_config_to_model_config_kwargs(hf_config)
75+
config_kwargs.update(
76+
normalization="RMSNorm",
77+
gated_linear_unit=True,
78+
add_bias_linear=False,
79+
add_qkv_bias=False, # Hy V3 does NOT have QKV bias
80+
hidden_dropout=0.0,
81+
qk_layernorm=True, # Hy V3 uses QK layernorm
82+
attention_softmax_in_fp32=False,
83+
autocast_dtype=torch.bfloat16,
84+
fp16=False,
85+
bf16=True,
86+
params_dtype=torch.bfloat16,
87+
masked_softmax_fusion=True,
88+
rope_scaling=False,
89+
rope_scaling_factor=1.0,
90+
moe_grouped_gemm=True,
91+
moe_token_dispatcher_type="alltoall",
92+
moe_router_load_balancing_type="none",
93+
moe_router_pre_softmax=False,
94+
moe_router_score_function="sigmoid",
95+
moe_router_enable_expert_bias=True,
96+
moe_router_bias_update_rate=0,
97+
moe_router_dtype="fp32",
98+
moe_permute_fusion=True,
99+
moe_shared_expert_overlap=False,
100+
moe_aux_loss_coeff=0.0,
101+
moe_router_topk_scaling_factor=float(hf_config.router_scaling_factor),
102+
# Shared experts hidden dim = moe_intermediate_size * num_shared_experts.
103+
moe_shared_expert_intermediate_size=hf_config.moe_intermediate_size * hf_config.num_shared_experts,
104+
# Dense-first layer pattern.
105+
moe_layer_freq=[0] * hf_config.first_k_dense_replace
106+
+ [1] * (hf_config.num_hidden_layers - hf_config.first_k_dense_replace),
100107
)
108+
config_kwargs.setdefault("position_embedding_type", "rope")
109+
return config_kwargs
101110

102-
return provider
111+
def hf_config_to_provider_kwargs(self, hf_config: PretrainedConfig) -> dict[str, Any]:
112+
"""Adapt the canonical builder mapping to the deprecated provider path."""
113+
provider_kwargs = self.hf_config_to_model_config_kwargs(hf_config)
114+
# The provider's default layer spec builds homogeneous layers; the decoder
115+
# block spec is needed for the dense-first MoE layer pattern.
116+
provider_kwargs["transformer_layer_spec"] = partial(get_gpt_decoder_block_spec, use_transformer_engine=HAVE_TE)
117+
return provider_kwargs
103118

104119
@classmethod
105120
def megatron_to_hf_config(cls, provider: GPTModelProvider) -> dict:

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

Lines changed: 43 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
@@ -28,6 +29,7 @@
2829
MegatronParamMapping,
2930
QKVMapping,
3031
)
32+
from megatron.bridge.models.gpt.model_config import BridgeGPTModelConfig
3133
from megatron.bridge.models.minimax_m2.minimax_m2_provider import minimax_m2_layer_spec
3234

3335

@@ -128,46 +130,53 @@ class MiniMaxM2Bridge(MegatronModelBridge):
128130
Example:
129131
>>> from megatron.bridge import AutoBridge
130132
>>> bridge = AutoBridge.from_hf_pretrained("MiniMaxAI/MiniMax-M2")
131-
>>> provider = bridge.to_megatron_provider()
133+
>>> model_config = bridge.get_model_config()
132134
"""
133135

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

148169
# MiniMax-M2 uses rotary_dim instead of partial_rotary_factor
149170
rotary_dim = getattr(hf_config, "rotary_dim", None)
150171
head_dim = getattr(hf_config, "head_dim", None)
151172
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
173+
config_kwargs["rotary_percent"] = rotary_dim / head_dim
174+
175+
return config_kwargs
176+
177+
def hf_config_to_provider_kwargs(self, hf_config: PretrainedConfig) -> dict[str, Any]:
178+
"""Adapt the canonical builder mapping to the deprecated provider path."""
179+
return self.hf_config_to_model_config_kwargs(hf_config)
171180

172181
def maybe_modify_loaded_hf_weight(
173182
self, hf_param: str | dict[str, str], hf_state_dict: Mapping[str, torch.Tensor]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from dataclasses import fields
16+
from unittest.mock import Mock, patch
17+
18+
import pytest
19+
import torch
20+
from megatron.core.models.gpt.gpt_layer_specs import get_gpt_decoder_block_spec
21+
from transformers import HYV3Config
22+
23+
from megatron.bridge.models.conversion.model_bridge import MegatronModelBridge
24+
from megatron.bridge.models.gpt.model_config import BridgeGPTModelConfig
25+
from megatron.bridge.models.hf_pretrained.causal_lm import PreTrainedCausalLM
26+
from megatron.bridge.models.hy_v3.hy_v3_bridge import HYV3Bridge
27+
28+
29+
def _hy_v3_config(**overrides) -> HYV3Config:
30+
kwargs = dict(
31+
hidden_size=64,
32+
intermediate_size=128,
33+
moe_intermediate_size=32,
34+
num_hidden_layers=4,
35+
num_attention_heads=8,
36+
num_key_value_heads=4,
37+
head_dim=8,
38+
num_experts=8,
39+
num_experts_per_tok=2,
40+
num_shared_experts=1,
41+
first_k_dense_replace=1,
42+
router_scaling_factor=2.5,
43+
max_position_embeddings=4096,
44+
vocab_size=128,
45+
torch_dtype="bfloat16",
46+
)
47+
kwargs.update(overrides)
48+
config = HYV3Config(**kwargs)
49+
config.architectures = ["HYV3ForCausalLM"]
50+
return config
51+
52+
53+
class TestHYV3Bridge:
54+
def test_bridge_registration(self):
55+
assert issubclass(HYV3Bridge, MegatronModelBridge)
56+
57+
def test_provider_bridge_is_inherited_compatibility_only(self):
58+
assert "provider_bridge" not in HYV3Bridge.__dict__
59+
60+
def test_conversion_uses_builder_config(self):
61+
assert HYV3Bridge.MODEL_CONFIG_CLASS is BridgeGPTModelConfig
62+
assert HYV3Bridge.USE_MODEL_CONFIG_FOR_CONVERSION is True
63+
64+
def test_hf_config_to_model_config_uses_direct_mapping(self):
65+
config = _hy_v3_config()
66+
bridge = HYV3Bridge()
67+
with (
68+
patch.object(bridge, "provider_bridge", side_effect=AssertionError("provider path used")),
69+
patch.object(bridge, "hf_config_to_provider_kwargs", side_effect=AssertionError("provider kwargs used")),
70+
):
71+
result = bridge.hf_config_to_model_config(config)
72+
73+
assert isinstance(result, BridgeGPTModelConfig)
74+
# The builder's default MoE spec is the decoder block spec, so none is configured here.
75+
assert result.transformer_layer_spec is None
76+
assert result.moe_layer_freq == [0, 1, 1, 1]
77+
assert result.moe_shared_expert_intermediate_size == config.moe_intermediate_size * config.num_shared_experts
78+
assert result.moe_router_topk_scaling_factor == 2.5
79+
assert result.moe_router_score_function == "sigmoid"
80+
assert result.moe_router_enable_expert_bias is True
81+
assert result.qk_layernorm is True
82+
assert result.params_dtype == torch.bfloat16
83+
assert result.bf16 is True
84+
assert result.position_embedding_type == "rope"
85+
86+
@pytest.mark.parametrize("torch_dtype", ["bfloat16", "float16"])
87+
def test_model_config_matches_provider_runtime_config(self, torch_dtype):
88+
config = _hy_v3_config(torch_dtype=torch_dtype)
89+
mock_pretrained = Mock(spec=PreTrainedCausalLM)
90+
mock_pretrained.config = config
91+
bridge = HYV3Bridge()
92+
93+
model_config = bridge.hf_config_to_model_config(config)
94+
with pytest.warns(FutureWarning, match=r"deprecated.*get_model_config.*get_model"):
95+
provider = bridge.provider_bridge(mock_pretrained)
96+
97+
provider_fields = {field.name for field in fields(provider)}
98+
model_config_fields = {field.name for field in fields(model_config)}
99+
model_config_fields.update(field.name for field in fields(model_config.transformer))
100+
for field_name in sorted((provider_fields & model_config_fields) - {"transformer_layer_spec"}):
101+
assert getattr(model_config, field_name) == getattr(provider, field_name), field_name
102+
# Hy V3 always builds in bf16, whatever the source checkpoint dtype.
103+
assert model_config.params_dtype == provider.params_dtype == torch.bfloat16
104+
# The provider keeps its explicit decoder block spec for the dense-first layer pattern.
105+
assert provider.transformer_layer_spec.func is get_gpt_decoder_block_spec
106+
107+
def test_megatron_to_hf_config_from_model_config(self):
108+
model_config = HYV3Bridge().hf_config_to_model_config(_hy_v3_config())
109+
110+
hf_config = HYV3Bridge.megatron_to_hf_config(model_config)
111+
112+
assert hf_config["first_k_dense_replace"] == 1
113+
assert hf_config["num_shared_experts"] == 1
114+
assert hf_config["router_scaling_factor"] == 2.5
115+
assert hf_config["moe_intermediate_size"] == 32

0 commit comments

Comments
 (0)