Skip to content

Commit be00179

Browse files
committed
fix(modelopt): exclude quantizer state from real-quant task discovery
Signed-off-by: Meng Xin <164952785+mxinO@users.noreply.github.com>
1 parent 625dad5 commit be00179

7 files changed

Lines changed: 207 additions & 28 deletions

File tree

‎src/megatron/bridge/models/conversion/auto_bridge.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,12 @@ def build_hf_modelopt_export_plan(
888888
if not isinstance(model, list):
889889
model = [model]
890890
if conversion_tasks is None:
891-
conversion_tasks = self._model_bridge.build_conversion_tasks(self.hf_pretrained, model)
891+
from modelopt.torch.quantization.nn import AnyQuantizer
892+
893+
# ModelOpt reads quantizer state from each owning weight, not separate HF mappings.
894+
conversion_tasks = self._model_bridge.build_conversion_tasks(
895+
self.hf_pretrained, model, exclude_module_types=AnyQuantizer
896+
)
892897
return build_modelopt_export_plan(
893898
conversion_tasks,
894899
model=model,

‎src/megatron/bridge/models/conversion/model_bridge.py‎

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@
107107
_BridgeImplClass = TypeVar("_BridgeImplClass", bound="MegatronModelBridge")
108108

109109

110+
def _named_conversion_parameters(
111+
model: torch.nn.Module,
112+
exclude_module_types: tuple[type[torch.nn.Module], ...] = (),
113+
) -> Iterable[tuple[str, torch.Tensor]]:
114+
"""Iterate parameters and persistent buffers outside excluded module subtrees."""
115+
parameters = itertools.chain(model.named_parameters(), persistent_buffers(model))
116+
if not exclude_module_types:
117+
yield from parameters
118+
return
119+
120+
excluded_names = {name for name, module in model.named_modules() if isinstance(module, exclude_module_types)}
121+
for name, tensor in parameters:
122+
parent_name = name.rpartition(".")[0]
123+
while parent_name and parent_name not in excluded_names:
124+
parent_name = parent_name.rpartition(".")[0]
125+
if parent_name not in excluded_names:
126+
yield name, tensor
127+
128+
110129
class ModelConfigNotSupportedError(NotImplementedError):
111130
"""Raised when a bridge has no builder-backed model config path."""
112131

@@ -1045,11 +1064,14 @@ def mapping_registry(self):
10451064
raise NotImplementedError("Subclass must implement mapping_registry method")
10461065

10471066
def _megatron_global_param_names_all_pp_ranks(
1048-
self, megatron_model: Union[MegatronModel, List[MegatronModel]]
1067+
self,
1068+
megatron_model: Union[MegatronModel, List[MegatronModel]],
1069+
*,
1070+
exclude_module_types: tuple[type[torch.nn.Module], ...] = (),
10491071
) -> List[str]:
10501072
"""Get all parameter names across all pipeline parallel ranks."""
1051-
# Cache the result after first call
1052-
if hasattr(self, "_cached_param_names"):
1073+
# Filtered discovery must neither reuse nor replace ordinary conversion's cache.
1074+
if not exclude_module_types and hasattr(self, "_cached_param_names"):
10531075
return self._cached_param_names
10541076

10551077
# Compute the result
@@ -1061,8 +1083,7 @@ def _megatron_global_param_names_all_pp_ranks(
10611083
models_list = megatron_model if isinstance(megatron_model, list) else [megatron_model]
10621084

10631085
for vp_stage, model in enumerate(models_list):
1064-
# persistent buffers are part of the model's state_dict, but not the named_parameters, so we must include them here separately
1065-
for local_param_name, _ in itertools.chain(model.named_parameters(), persistent_buffers(model)):
1086+
for local_param_name, _ in _named_conversion_parameters(model, exclude_module_types):
10661087
if "_extra_state" in local_param_name:
10671088
continue
10681089
local_param_name = self._unwrap_name(local_param_name)
@@ -1085,10 +1106,10 @@ def _megatron_global_param_names_all_pp_ranks(
10851106
# change this might cause a hang
10861107
gathered_global_param_names = sorted(flattened_names, key=extract_sort_key)
10871108

1088-
# Cache the result
1089-
self._cached_param_names = gathered_global_param_names
1109+
if not exclude_module_types:
1110+
self._cached_param_names = gathered_global_param_names
10901111

1091-
return self._cached_param_names
1112+
return gathered_global_param_names
10921113

10931114
def _with_progress_tracking(self, tasks, description: str, show_progress: bool = True):
10941115
"""Helper method to wrap an iterable with progress tracking.
@@ -2233,11 +2254,15 @@ def build_conversion_tasks(
22332254
hf_pretrained: HFPreTrained,
22342255
megatron_model: List[MegatronModel],
22352256
weight_dtype: Optional[torch.dtype] = None,
2257+
*,
2258+
exclude_module_types: tuple[type[torch.nn.Module], ...] = (),
22362259
) -> List[WeightConversionTask]:
22372260
"""Construct the conversion tasks between HF and megatron.
22382261
22392262
Args:
22402263
weight_dtype: Export dtype recorded on each task. Overrides must forward it.
2264+
exclude_module_types: Module types whose parameters and persistent buffers
2265+
are excluded from discovery. Remaining parameters still require mappings.
22412266
22422267
The algorithm walks over every parameter of every destination model,
22432268
asks the :class:`MegatronMappingRegistry` whether it has a mapping for that
@@ -2263,7 +2288,10 @@ def build_conversion_tasks(
22632288
model_config = unwrapped_model.config
22642289
embeddings_are_tied = self._share_embeddings_and_output_weights(model_config)
22652290
pp_rank = _get_pp_rank(megatron_model)
2266-
sorted_global_param_names_all_pp_ranks = self._megatron_global_param_names_all_pp_ranks(megatron_model)
2291+
sorted_global_param_names_all_pp_ranks = self._megatron_global_param_names_all_pp_ranks(
2292+
megatron_model,
2293+
**({"exclude_module_types": exclude_module_types} if exclude_module_types else {}),
2294+
)
22672295

22682296
# Filter out the output_layer weight if embeddings are tied to it -- it doesn't exist
22692297
# as a separate parameter in that case. Other `output_layer.*` parameters (e.g. a
@@ -2284,8 +2312,7 @@ def build_conversion_tasks(
22842312

22852313
pending_tasks: list[WeightConversionTask | None] = [None] * len(sorted_global_param_names_all_pp_ranks)
22862314
for vp_stage, model in enumerate(megatron_model):
2287-
# persistent buffers are part of the model's state_dict, but not the named_parameters, so we must include them here separately
2288-
for local_name, _ in itertools.chain(model.named_parameters(), persistent_buffers(model)):
2315+
for local_name, _ in _named_conversion_parameters(model, exclude_module_types):
22892316
if "_extra_state" in local_name or self._is_adapter_param_name(local_name):
22902317
continue
22912318

‎src/megatron/bridge/models/conversion/modelopt_utils.py‎

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class ModelOptExportPlan(NamedTuple):
4747
conversion_tasks: list[WeightConversionTask]
4848
quantization_config: dict[str, Any]
4949
quantized_params: frozenset[str]
50-
local_quantization_specs: dict[str, tuple[object | None, bool]]
50+
local_quantization_specs: dict[str, object | None]
5151

5252

5353
@dataclass(frozen=True)
@@ -57,7 +57,6 @@ class _SourceState:
5757
parallelism: str | None = None
5858
qkv_layout: tuple[int, int, int, int, bool] | None = None
5959
weight_dtype: torch.dtype | None = None
60-
input_quantizer_enabled: bool = False
6160

6261

6362
def _same_storage(left: object, right: object) -> bool:
@@ -163,22 +162,16 @@ def _capture_source_spec(task: WeightConversionTask) -> _SourceState | None:
163162
return _SourceState(None, tuple(task.param_weight.shape), weight_dtype=task.param_weight.dtype)
164163

165164
from modelopt.torch.export.quantized_weight_export import get_quantized_weight_export_spec
166-
from modelopt.torch.quantization.utils import quantizer_attr_names
167165

168166
spec = get_quantized_weight_export_spec(task.megatron_module, weight_name)
169-
input_quantizer = None
170167
if spec is not None:
171168
_validate_quantized_task(task)
172-
input_quantizer = getattr(task.megatron_module, quantizer_attr_names(weight_name).input_quantizer, None)
173-
if input_quantizer is None:
174-
input_quantizer = getattr(task.megatron_module, "input_quantizer", None)
175169
return _SourceState(
176170
spec,
177171
tuple(task.param_weight.shape),
178172
_mapping_parallelism(task.mapping, task.megatron_module) if spec is not None else None,
179173
_qkv_layout(task.mapping, task.megatron_module) if spec is not None else None,
180174
task.param_weight.dtype,
181-
input_quantizer is not None and input_quantizer.is_enabled,
182175
)
183176

184177

@@ -786,7 +779,7 @@ def build_modelopt_export_plan(
786779
export_tasks,
787780
quantization_config,
788781
quantized_params,
789-
{name: (source.state, source.input_quantizer_enabled) for name, source in local_specs.items()},
782+
{name: source.state for name, source in local_specs.items()},
790783
)
791784

792785

@@ -798,11 +791,7 @@ def prepare_modelopt_export_tasks(plan: ModelOptExportPlan) -> list[WeightConver
798791
if task.global_param_name not in plan.local_quantization_specs:
799792
continue
800793
source = _capture_source_spec(task)
801-
if (
802-
source is None
803-
or (source.state, source.input_quantizer_enabled)
804-
!= plan.local_quantization_specs[task.global_param_name]
805-
):
794+
if source is None or source.state != plan.local_quantization_specs[task.global_param_name]:
806795
raise ValueError(f"ModelOpt quantization configuration changed for {task.global_param_name}")
807796
except Exception as error:
808797
validation_error = f"{type(error).__name__}: {error}"

‎src/megatron/bridge/models/kimi/kimi_k3_bridge.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ def build_conversion_tasks(
345345
hf_pretrained,
346346
megatron_model,
347347
weight_dtype=None,
348+
*,
349+
exclude_module_types: tuple[type[torch.nn.Module], ...] = (),
348350
) -> list[WeightConversionTask]:
349351
"""Expose virtual BF16 expert weights for MXFP4 packed/scale pairs."""
350352
original_get_all_keys = hf_pretrained.state.source.get_all_keys
@@ -362,7 +364,12 @@ def _get_all_keys_with_virtual() -> list[str]:
362364

363365
hf_pretrained.state.source.get_all_keys = _get_all_keys_with_virtual
364366
try:
365-
return super().build_conversion_tasks(hf_pretrained, megatron_model, weight_dtype=weight_dtype)
367+
return super().build_conversion_tasks(
368+
hf_pretrained,
369+
megatron_model,
370+
weight_dtype=weight_dtype,
371+
**({"exclude_module_types": exclude_module_types} if exclude_module_types else {}),
372+
)
366373
finally:
367374
hf_pretrained.state.source.get_all_keys = original_get_all_keys
368375

‎src/megatron/bridge/models/kimi_vl/kimi_k25_vl_bridge.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ def build_conversion_tasks(
212212
hf_pretrained,
213213
megatron_model,
214214
weight_dtype=None,
215+
*,
216+
exclude_module_types: tuple[type[torch.nn.Module], ...] = (),
215217
) -> List:
216218
"""Override to synthesize virtual weight keys from INT4 quantized triplets.
217219
@@ -235,7 +237,12 @@ def _get_all_keys_with_virtual():
235237

236238
hf_pretrained.state.source.get_all_keys = _get_all_keys_with_virtual
237239
try:
238-
return super().build_conversion_tasks(hf_pretrained, megatron_model, weight_dtype=weight_dtype)
240+
return super().build_conversion_tasks(
241+
hf_pretrained,
242+
megatron_model,
243+
weight_dtype=weight_dtype,
244+
**({"exclude_module_types": exclude_module_types} if exclude_module_types else {}),
245+
)
239246
finally:
240247
hf_pretrained.state.source.get_all_keys = original_get_all_keys
241248

‎tests/unit_tests/models/test_model_bridge.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,3 +1650,56 @@ def __init__(self, megatron_param, hf_param):
16501650
assert len(tasks) == 1
16511651
assert tasks[0].global_param_name == "decoder.weight"
16521652
assert tasks[0].megatron_module is None
1653+
1654+
1655+
def test_conversion_discovery_excludes_complete_module_subtrees():
1656+
class Excluded(torch.nn.Module):
1657+
def __init__(self):
1658+
super().__init__()
1659+
self.weight = torch.nn.Parameter(torch.ones(1))
1660+
self.register_buffer("amax", torch.ones(1))
1661+
self.child = torch.nn.Linear(1, 1, bias=False)
1662+
1663+
model = torch.nn.Module()
1664+
model.quantizer = Excluded()
1665+
model.quantizer_neighbor = torch.nn.Linear(1, 1, bias=False)
1666+
ordinary = dict(model_bridge_module._named_conversion_parameters(model))
1667+
1668+
filtered = dict(model_bridge_module._named_conversion_parameters(model, (Excluded,)))
1669+
1670+
assert set(filtered) == {"quantizer_neighbor.weight"}
1671+
assert "quantizer.amax" in ordinary
1672+
assert "quantizer.child.weight" in ordinary
1673+
assert dict(model_bridge_module._named_conversion_parameters(model)).keys() == ordinary.keys()
1674+
assert list(model_bridge_module._named_conversion_parameters(model.quantizer, (Excluded,))) == []
1675+
1676+
1677+
@pytest.mark.parametrize("model_family", ["kimi_k3", "kimi_k25"])
1678+
def test_kimi_conversion_tasks_forward_module_exclusions(monkeypatch, model_family):
1679+
from megatron.bridge.models.kimi.kimi_k3_bridge import KimiK3Bridge
1680+
from megatron.bridge.models.kimi_vl.kimi_k25_vl_bridge import KimiK25VLBridge
1681+
1682+
bridge_cls = KimiK3Bridge if model_family == "kimi_k3" else KimiK25VLBridge
1683+
keys = ["expert.weight_packed", "expert.weight_scale", "expert.weight_shape"]
1684+
get_all_keys = lambda: keys
1685+
source = SimpleNamespace(get_all_keys=get_all_keys)
1686+
pretrained = SimpleNamespace(state=SimpleNamespace(source=source))
1687+
tasks = []
1688+
1689+
def build_tasks(_self, hf_pretrained, model, *, weight_dtype, exclude_module_types):
1690+
assert model == []
1691+
assert weight_dtype == torch.bfloat16
1692+
assert exclude_module_types == (torch.nn.ReLU,)
1693+
assert "expert.weight" in hf_pretrained.state.source.get_all_keys()
1694+
return tasks
1695+
1696+
monkeypatch.setattr(MegatronModelBridge, "build_conversion_tasks", build_tasks)
1697+
bridge = bridge_cls()
1698+
1699+
assert (
1700+
bridge.build_conversion_tasks(
1701+
pretrained, [], weight_dtype=torch.bfloat16, exclude_module_types=(torch.nn.ReLU,)
1702+
)
1703+
is tasks
1704+
)
1705+
assert source.get_all_keys is get_all_keys

‎tests/unit_tests/models/test_modelopt_utils.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818

1919
import pytest
2020
import torch
21+
from transformers import PretrainedConfig
2122

2223

2324
mtq = pytest.importorskip("modelopt.torch.quantization")
2425
weight_export = pytest.importorskip("modelopt.torch.export.quantized_weight_export")
2526

2627
from megatron.bridge.models.conversion import modelopt_utils
2728
from megatron.bridge.models.conversion.auto_bridge import AutoBridge
29+
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
2830
from megatron.bridge.models.conversion.model_bridge import HFWeightTuple, MegatronModelBridge, WeightConversionTask
2931
from megatron.bridge.models.conversion.param_mapping import (
3032
AutoMapping,
@@ -119,6 +121,95 @@ def test_build_plan_delegates_fp8_packing_and_config_to_modelopt():
119121
assert plan.quantization_config["config_groups"]["group_0"]["targets"] == ["Linear"]
120122

121123

124+
@pytest.mark.parametrize("cached_names", [False, True])
125+
@pytest.mark.parametrize("quantization", ["fp8", "static_nvfp4"])
126+
def test_public_modelopt_plan_excludes_quantizer_state_before_mapping_validation(
127+
monkeypatch, cached_names, quantization
128+
):
129+
monkeypatch.setenv("ENABLE_BRIDGE_QUANT_MAPPING", "1")
130+
131+
class ProjectionBridge(MegatronModelBridge):
132+
def provider_bridge(self, hf_pretrained):
133+
raise NotImplementedError
134+
135+
def mapping_registry(self):
136+
return MegatronMappingRegistry(ColumnParallelMapping("projection.weight", "model.projection.weight"))
137+
138+
implementation = ProjectionBridge()
139+
monkeypatch.setattr(implementation, "_megatron_global_adapters_info_all_pp_ranks", lambda _models: [])
140+
141+
class ProjectionAutoBridge(AutoBridge):
142+
@property
143+
def _model_bridge(self):
144+
return implementation
145+
146+
model = torch.nn.Module()
147+
model.config = SimpleNamespace(share_embeddings_and_output_weights=False)
148+
if quantization == "static_nvfp4":
149+
model.projection = mtq.quantize(
150+
torch.nn.Linear(16, 4, bias=False),
151+
{
152+
"quant_cfg": [
153+
{"quantizer_name": "*", "enable": False},
154+
{
155+
"quantizer_name": "*weight_quantizer",
156+
"enable": True,
157+
"cfg": {
158+
"num_bits": (2, 1),
159+
"block_sizes": {-1: 16, "type": "static", "scale_bits": (4, 3)},
160+
},
161+
},
162+
],
163+
"algorithm": "max",
164+
},
165+
lambda candidate: candidate(torch.ones(2, 16)),
166+
)
167+
assert isinstance(model.projection.weight_quantizer, mtq.nn.NVFP4StaticQuantizer)
168+
assert model.projection.weight_quantizer.global_amax is not None
169+
else:
170+
model.projection = _fp8_linear()
171+
original_state = dict(model.named_buffers())
172+
model_bridge = modelopt_utils.model_bridge_utils
173+
monkeypatch.setattr(model_bridge, "_get_pg_collection_from_model", lambda _model: None)
174+
monkeypatch.setattr(model_bridge, "_get_pp_group", lambda _model: SimpleNamespace(size=lambda: 1))
175+
monkeypatch.setattr(model_bridge, "_get_pp_rank", lambda _model: 0)
176+
monkeypatch.setattr(model_bridge, "unwrap_model", lambda candidate: candidate)
177+
monkeypatch.setattr(model_bridge, "_megatron_local_name_to_global", lambda _models, _config, name, _vp: name)
178+
monkeypatch.setattr(
179+
torch.distributed, "all_gather_object", lambda output, value, group: output.__setitem__(0, value)
180+
)
181+
monkeypatch.setattr(model_bridge.parallel_state, "get_tensor_model_parallel_group", lambda: None)
182+
monkeypatch.setattr(model_bridge.parallel_state, "get_expert_tensor_parallel_group", lambda: None)
183+
monkeypatch.setattr(model_bridge.parallel_state, "get_expert_model_parallel_group", lambda: None)
184+
bridge = ProjectionAutoBridge(PretrainedConfig())
185+
if cached_names:
186+
implementation._megatron_global_param_names_all_pp_ranks([model])
187+
188+
plan = bridge.build_hf_modelopt_export_plan(model)
189+
190+
assert [task.global_param_name for task in plan.conversion_tasks] == ["projection.weight"]
191+
assert hasattr(implementation, "_cached_param_names") == cached_names
192+
exported = dict(bridge.export_hf_weights_modelopt(model, cpu=True, show_progress=False, export_plan=plan))
193+
if quantization == "static_nvfp4":
194+
assert exported["model.projection.weight"].dtype == torch.uint8
195+
expected_scale = model.projection.weight_quantizer.global_amax / (6 * 448)
196+
torch.testing.assert_close(exported["model.projection.weight_scale_2"], expected_scale)
197+
assert "model.projection.weight_scale" in exported
198+
else:
199+
assert exported["model.projection.weight"].dtype == torch.float8_e4m3fn
200+
assert dict(model.named_buffers()).keys() == original_state.keys()
201+
assert all(dict(model.named_buffers())[name] is value for name, value in original_state.items())
202+
if quantization == "static_nvfp4":
203+
with pytest.raises(ValueError, match="_global_amax"):
204+
implementation.build_conversion_tasks(bridge.hf_pretrained, [model])
205+
else:
206+
ordinary_tasks = implementation.build_conversion_tasks(bridge.hf_pretrained, [model])
207+
assert any(isinstance(task.mapping, AmaxMapping) for task in ordinary_tasks)
208+
model.register_buffer("unmapped_buffer", torch.tensor(1.0))
209+
with pytest.raises(ValueError, match="unmapped_buffer"):
210+
bridge.build_hf_modelopt_export_plan(model)
211+
212+
122213
def test_numbered_grouped_weight_uses_exact_modelopt_quantizer():
123214
module = _GroupedWeights()
124215
hf_name = "model.layers.0.mlp.experts.0.down_proj.weight"

0 commit comments

Comments
 (0)