Skip to content

Commit fe7a92f

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/pr5479-hybrid-model
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
2 parents 4719ba0 + 4de0797 commit fe7a92f

15 files changed

Lines changed: 797 additions & 113 deletions

File tree

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

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,17 +1897,17 @@ def build_conversion_tasks(
18971897
hf_pretrained: HFPreTrained,
18981898
megatron_model: List[MegatronModel],
18991899
weight_dtype: Optional[torch.dtype] = None,
1900-
) -> List[None | WeightConversionTask]:
1900+
) -> List[WeightConversionTask]:
19011901
"""Construct the conversion tasks between HF and megatron.
19021902
19031903
Args:
19041904
weight_dtype: Export dtype recorded on each task. Overrides must forward it.
19051905
19061906
The algorithm walks over every parameter of every destination model,
19071907
asks the :class:`MegatronMappingRegistry` whether it has a mapping for that
1908-
parameter, and – if the corresponding HF weights actually exist – yields
1909-
an :class:`_HFLoadTask` describing exactly how that parameter will be
1910-
populated.
1908+
parameter and returns a concrete task describing exactly how that
1909+
parameter will be populated. Missing mappings or source weights are
1910+
conversion errors, not empty task slots.
19111911
"""
19121912

19131913
has_hf_state = hasattr(hf_pretrained, "state") and hasattr(hf_pretrained.state, "source")
@@ -1934,9 +1934,15 @@ def build_conversion_tasks(
19341934
name for name in sorted_global_param_names_all_pp_ranks if "output_layer" not in name
19351935
]
19361936

1937+
mappings_by_global_name = self._validate_conversion_mappings(
1938+
mapping_registry,
1939+
sorted_global_param_names_all_pp_ranks,
1940+
hf_keys,
1941+
)
1942+
19371943
global_names_index_dict = {name: idx for idx, name in enumerate(sorted_global_param_names_all_pp_ranks)}
19381944

1939-
tasks = [None] * len(sorted_global_param_names_all_pp_ranks)
1945+
pending_tasks: list[WeightConversionTask | None] = [None] * len(sorted_global_param_names_all_pp_ranks)
19401946
for vp_stage, model in enumerate(megatron_model):
19411947
# persistent buffers are part of the model's state_dict, but not the named_parameters, so we must include them here separately
19421948
for local_name, _ in itertools.chain(model.named_parameters(), persistent_buffers(model)):
@@ -1950,34 +1956,15 @@ def build_conversion_tasks(
19501956
print_rank_0(f"WARNING: {global_name} not in global_names_index_dict")
19511957
continue
19521958
global_name_idx = global_names_index_dict[global_name]
1953-
mapping = mapping_registry.megatron_to_hf_lookup(self._get_lora_unwrapped_name(global_name))
1954-
1955-
if not mapping:
1956-
logger.warning(f"WARNING: No mapping found for megatron_param: {global_name}")
1957-
continue
1958-
# Ensure hf weights exist (skip for config-only export where hf_keys is None)
1959-
if hf_keys is not None and not mapping.allow_hf_name_mismatch:
1960-
if isinstance(mapping.hf_param, str):
1961-
if mapping.hf_param not in hf_keys:
1962-
logger.warning(f"WARNING: Can't find {mapping.hf_param} in hf_keys")
1963-
continue
1964-
else:
1965-
missing_params = [
1966-
hf_param for hf_param in mapping.hf_param.values() if hf_param not in hf_keys
1967-
]
1968-
if missing_params:
1969-
logger.warning(
1970-
f"WARNING: Can't find the following HF parameters in hf_keys: {missing_params}"
1971-
)
1972-
continue
1959+
mapping = mappings_by_global_name[global_name]
19731960

19741961
local_module, local_weights = get_module_and_param_from_name(megatron_model, local_name, vp_stage)
19751962
if local_module is not None and not hasattr(local_module, "config"):
19761963
# If module is not a MegatronModule (e.g. torch.nn.Conv1d or a module list) we need
19771964
# to get the config from the model
19781965
setattr(local_module, "config", model_config)
19791966

1980-
tasks[global_name_idx] = WeightConversionTask(
1967+
pending_tasks[global_name_idx] = WeightConversionTask(
19811968
pp_rank=pp_rank,
19821969
vp_stage=vp_stage,
19831970
param_name=local_name,
@@ -1990,15 +1977,12 @@ def build_conversion_tasks(
19901977

19911978
# Fill the remaining ones for pp communications
19921979
for idx, global_name in enumerate(sorted_global_param_names_all_pp_ranks):
1993-
if tasks[idx] is None:
1994-
mapping = mapping_registry.megatron_to_hf_lookup(self._get_lora_unwrapped_name(global_name))
1995-
# Skip tasks with no mapping found
1996-
if mapping is None:
1997-
continue
1980+
if pending_tasks[idx] is None:
1981+
mapping = mappings_by_global_name[global_name]
19981982
# This is an exception here we pass in global name
19991983
# we are not using global_name to extract module and weights
20001984
# only use it for param mapping auto dispatch checks
2001-
tasks[idx] = WeightConversionTask(
1985+
pending_tasks[idx] = WeightConversionTask(
20021986
pp_rank=pp_rank,
20031987
vp_stage=None,
20041988
param_name=global_name,
@@ -2009,6 +1993,68 @@ def build_conversion_tasks(
20091993
weight_dtype=weight_dtype,
20101994
)
20111995

1996+
return self._require_concrete_tasks(pending_tasks)
1997+
1998+
def _validate_conversion_mappings(
1999+
self,
2000+
mapping_registry: MegatronMappingRegistry,
2001+
global_param_names: Iterable[str],
2002+
hf_keys: Iterable[str] | None = None,
2003+
) -> dict[str, MegatronParamMapping]:
2004+
"""Resolve and validate mappings for the full cross-PP parameter list."""
2005+
mappings_by_global_name: dict[str, MegatronParamMapping] = {}
2006+
missing_mappings: list[str] = []
2007+
missing_hf_weights: list[tuple[str, str]] = []
2008+
hf_key_set = set(hf_keys) if hf_keys is not None else None
2009+
2010+
for global_name in global_param_names:
2011+
mapping = mapping_registry.megatron_to_hf_lookup(self._get_lora_unwrapped_name(global_name))
2012+
if mapping is None:
2013+
missing_mappings.append(global_name)
2014+
continue
2015+
2016+
mappings_by_global_name[global_name] = mapping
2017+
if hf_key_set is None or mapping.allow_hf_name_mismatch:
2018+
continue
2019+
2020+
expected_hf_names = (
2021+
[mapping.hf_param] if isinstance(mapping.hf_param, str) else list(mapping.hf_param.values())
2022+
)
2023+
missing_hf_weights.extend(
2024+
(global_name, hf_name) for hf_name in expected_hf_names if hf_name not in hf_key_set
2025+
)
2026+
2027+
if missing_mappings:
2028+
missing_names = "\n ".join(missing_mappings)
2029+
raise ValueError(
2030+
"No mapping found for the following Megatron parameter(s):\n"
2031+
f" {missing_names}\n"
2032+
"Every global Megatron parameter must have a concrete mapping so import and export remain strict."
2033+
)
2034+
2035+
if missing_hf_weights:
2036+
missing_names = "\n ".join(f"{global_name} -> {hf_name}" for global_name, hf_name in missing_hf_weights)
2037+
raise ValueError(
2038+
"Hugging Face checkpoint is missing mapped parameter(s):\n"
2039+
f" {missing_names}\n"
2040+
"If the HF config determines whether the weight exists, register the mapping "
2041+
"conditionally on that config instead. If it does not, and the name is synthesized "
2042+
"or the weight is absent on only some layers, set allow_hf_name_mismatch on the "
2043+
"mapping."
2044+
)
2045+
2046+
return mappings_by_global_name
2047+
2048+
@staticmethod
2049+
def _require_concrete_tasks(
2050+
pending_tasks: Iterable[WeightConversionTask | None],
2051+
) -> list[WeightConversionTask]:
2052+
"""Return tasks after enforcing the internal no-empty-slot invariant."""
2053+
tasks: list[WeightConversionTask] = []
2054+
for task in pending_tasks:
2055+
if task is None:
2056+
raise RuntimeError("Internal error: conversion task construction left an empty slot")
2057+
tasks.append(task)
20122058
return tasks
20132059

20142060
def _detect_fp8_params(
@@ -2103,7 +2149,7 @@ def build_export_fp8_tasks(
21032149
*,
21042150
scale_inv_suffix: str = "_scale_inv",
21052151
fp8_scale_inv_attr: str = "_rowwise_scale_inv",
2106-
) -> List[None | WeightConversionTask]:
2152+
) -> List[WeightConversionTask]:
21072153
"""
21082154
Build Megatron→(export) conversion tasks, inserting extra *scale_inv* tasks for blockwise FP8 params.
21092155
"""
@@ -2128,6 +2174,11 @@ def build_export_fp8_tasks(
21282174
name for name in sorted_global_param_names_all_pp_ranks if "output_layer" not in name
21292175
]
21302176

2177+
mappings_by_global_name = self._validate_conversion_mappings(
2178+
mapping_registry,
2179+
sorted_global_param_names_all_pp_ranks,
2180+
)
2181+
21312182
# 1) Determine which global params are blockwise FP8 and gather flags across PP ranks
21322183
global_fp8_flags = self._detect_fp8_params(
21332184
megatron_model,
@@ -2160,10 +2211,7 @@ def build_export_fp8_tasks(
21602211
if global_name not in global_names_index_dict:
21612212
continue
21622213

2163-
mapping = mapping_registry.megatron_to_hf_lookup(self._get_lora_unwrapped_name(global_name))
2164-
if not mapping:
2165-
logger.warning(f"WARNING: No mapping found for megatron_param: {global_name}")
2166-
continue
2214+
mapping = mappings_by_global_name[global_name]
21672215
local_module, local_weights = get_module_and_param_from_name(megatron_model, local_name, vp_stage)
21682216
if local_module is not None and not hasattr(local_module, "config"):
21692217
setattr(local_module, "config", model_config)
@@ -2221,22 +2269,16 @@ def build_export_fp8_tasks(
22212269
# For scale_inv entries, reuse the base param's mapping type.
22222270
if global_name.endswith(scale_inv_suffix):
22232271
base_global_name = global_name[: -len(scale_inv_suffix)]
2224-
base_mapping = mapping_registry.megatron_to_hf_lookup(self._get_lora_unwrapped_name(base_global_name))
2225-
if base_mapping is not None:
2226-
# clone mapping instance to avoid sharing state across tasks.
2227-
base_mapping_for_scale = mapping_registry.resolve_mapping(base_mapping, ())
2228-
mapping = _HFNameSuffixMapping(
2229-
base_mapping_for_scale,
2230-
scale_inv_suffix,
2231-
self._fp8_scale_block_size(global_fp8_flags.get(base_global_name)),
2232-
)
2233-
else:
2234-
mapping = None
2272+
base_mapping = mappings_by_global_name[base_global_name]
2273+
# clone mapping instance to avoid sharing state across tasks.
2274+
base_mapping_for_scale = mapping_registry.resolve_mapping(base_mapping, ())
2275+
mapping = _HFNameSuffixMapping(
2276+
base_mapping_for_scale,
2277+
scale_inv_suffix,
2278+
self._fp8_scale_block_size(global_fp8_flags.get(base_global_name)),
2279+
)
22352280
else:
2236-
mapping = mapping_registry.megatron_to_hf_lookup(self._get_lora_unwrapped_name(global_name))
2237-
if mapping is None:
2238-
logger.warning(f"No mapping found for global_name: {global_name}")
2239-
continue
2281+
mapping = mappings_by_global_name[global_name]
22402282

22412283
tasks[idx] = WeightConversionTask(
22422284
pp_rank=pp_rank,
@@ -2248,7 +2290,7 @@ def build_export_fp8_tasks(
22482290
mapping=mapping,
22492291
)
22502292

2251-
return tasks
2293+
return self._require_concrete_tasks(tasks)
22522294

22532295
@staticmethod
22542296
def _fp8_scale_block_size(fp8_flag: bool | int | None) -> int | None:

src/megatron/bridge/models/conversion/param_mapping.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ def __init__(self, megatron_param: str, hf_param: Union[str, Dict[str, str]]):
125125
self._tp_group = None
126126
self._etp_group = None
127127

128-
# if a param mapping class takes in modified HF weight name from maybe_modify_loaded_hf_weight,
129-
# allow_hf_name_mismatch should be set to True to bypass a check in `build_conversion_tasks`
128+
# Set allow_hf_name_mismatch to True when the declared HF name will not be found verbatim
129+
# in the checkpoint's key set. That covers two cases: a name that is rewritten or
130+
# synthesized (see maybe_modify_loaded_hf_weight), and a weight that is legitimately
131+
# absent for some layers or configurations. Both bypass the hf_keys check in
132+
# `build_conversion_tasks`, which raises otherwise.
130133
self.allow_hf_name_mismatch = False
131134

132135
def set_process_groups_from_pg_collection(self, pg_collection: Any) -> None:
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
"""MLA attention spec helpers for the DeepSeek family."""
16+
17+
from dataclasses import replace
18+
from typing import Optional
19+
20+
from megatron.core.models.gpt.gpt_layer_specs import get_gpt_decoder_block_spec
21+
from megatron.core.transformer.identity_op import IdentityOp
22+
from megatron.core.transformer.mla_qk_norm_config import get_backend
23+
from megatron.core.transformer.multi_latent_attention import MLASelfAttention
24+
from megatron.core.transformer.spec_utils import ModuleSpec
25+
from megatron.core.transformer.transformer_config import TransformerConfig
26+
27+
28+
class MLASelfAttentionWithoutQueryNorm(MLASelfAttention):
29+
"""MLA self-attention that does not add a query norm when there is no query LoRA.
30+
31+
MCore derives Q and KV normalization from a single ``qk_layernorm`` flag. DeepSeek
32+
needs it enabled for ``kv_a_layernorm``, which every checkpoint ships. When
33+
``q_lora_rank`` is None, that same flag also makes MCore fuse a query normalization
34+
into ``linear_q_proj`` (``QKNormConfigResolver._resolve_mla_qk_layernorm``), but the
35+
HF architecture defines no query-side norm in that case: ``DeepseekV3Attention``
36+
builds a bare ``q_proj``.
37+
38+
The result is a trainable parameter with no HF counterpart, which cannot be loaded
39+
and is silently dropped on export. This subclass keeps the KV norm and drops the
40+
query norm so the converted model matches the source architecture.
41+
42+
Transformer Engine is required for the no-query-LoRA case. MCore builds
43+
``linear_q_proj`` from the backend's fused norm+linear implementation, which only
44+
Transformer Engine provides, so the local backend is rejected with an explicit
45+
message rather than an internal one.
46+
"""
47+
48+
def _resolve_qk_norm_config(self, submodules):
49+
"""Replace the fused query projection with a plain one when there is no query LoRA.
50+
51+
The standalone-``q_layernorm`` case is neutralised *before* delegating: an MLA spec
52+
may set ``q_layernorm`` to a real norm whenever ``qk_layernorm`` is on, and the
53+
parent resolver rejects that outright when there is no query LoRA to consume it
54+
(``_raise_unused_q_norm``). Dropping the query norm is exactly what this class
55+
exists to do, so the rejection would fire on a configuration this class already
56+
knows how to satisfy.
57+
"""
58+
if self.config.q_lora_rank is not None:
59+
return super()._resolve_qk_norm_config(submodules)
60+
61+
backend = get_backend(self.config.transformer_impl)
62+
if backend.column_parallel_layer_norm_linear() is None:
63+
raise ValueError(
64+
"DeepSeek without a query LoRA (`q_lora_rank=None`) requires "
65+
f"`transformer_impl='transformer_engine'`; `{self.config.transformer_impl}` "
66+
"provides no fused norm+linear projection. MCore's MLA resolver builds "
67+
"`linear_q_proj` from that fused implementation whenever `qk_layernorm` is "
68+
"on, and DeepSeek needs `qk_layernorm` on for `kv_a_layernorm`, so this "
69+
"backend cannot express the architecture."
70+
)
71+
72+
if submodules.q_layernorm not in (None, IdentityOp):
73+
submodules = replace(submodules, q_layernorm=IdentityOp)
74+
75+
layer_classes = super()._resolve_qk_norm_config(submodules)
76+
layer_classes["linear_q_proj"] = backend.column_parallel_linear()
77+
return layer_classes
78+
79+
80+
def get_deepseek_decoder_block_spec(
81+
config: TransformerConfig,
82+
use_transformer_engine: bool,
83+
normalization: Optional[str] = None,
84+
qk_l2_norm: Optional[bool] = False,
85+
vp_stage: Optional[int] = None,
86+
pp_rank: Optional[int] = None,
87+
) -> ModuleSpec:
88+
"""Build the decoder block spec, omitting the query norm when ``q_lora_rank`` is None.
89+
90+
The signature mirrors ``get_gpt_decoder_block_spec`` exactly, including ``vp_stage``
91+
and ``pp_rank``. ``GPTModelProvider.provide()`` inspects the callable's parameters and
92+
only forwards ``vp_stage`` when it is declared, so dropping it here would leave
93+
interleaved pipeline parallelism calling MCore's layer-offset helper without a virtual
94+
stage, which asserts.
95+
96+
Args:
97+
config: The model provider / transformer config.
98+
use_transformer_engine: Whether to build Transformer Engine submodules.
99+
normalization: Optional normalization override, forwarded unchanged.
100+
qk_l2_norm: Optional QK L2 norm flag, forwarded unchanged.
101+
vp_stage: Virtual pipeline stage, forwarded unchanged.
102+
pp_rank: Pipeline rank, forwarded unchanged.
103+
104+
Returns:
105+
The decoder block spec, with MLA self-attention replaced by
106+
:class:`MLASelfAttentionWithoutQueryNorm` when there is no query LoRA.
107+
"""
108+
spec = get_gpt_decoder_block_spec(
109+
config,
110+
use_transformer_engine=use_transformer_engine,
111+
normalization=normalization,
112+
qk_l2_norm=qk_l2_norm,
113+
vp_stage=vp_stage,
114+
pp_rank=pp_rank,
115+
)
116+
return replace_mla_self_attention(config, spec)
117+
118+
119+
def replace_mla_self_attention(config: TransformerConfig, spec: ModuleSpec) -> ModuleSpec:
120+
"""Swap MLA self-attention for the query-norm-free variant, in place, on every layer.
121+
122+
Shared with the MTP path: a standalone MTP pipeline stage owns no decoder layers, so
123+
the provider re-derives a layer spec straight from MCore and never passes through
124+
:func:`get_deepseek_decoder_block_spec`. Without this the MTP layer regains the query
125+
norm that the decoder layers just dropped.
126+
127+
Accepts either a block spec (``.layer_specs``) or a single layer spec.
128+
"""
129+
if getattr(config, "q_lora_rank", None) is not None:
130+
return spec
131+
132+
layer_specs = getattr(spec, "layer_specs", None)
133+
for layer_spec in layer_specs if layer_specs is not None else [spec]:
134+
self_attention = getattr(layer_spec.submodules, "self_attention", None)
135+
if self_attention is not None and self_attention.module is MLASelfAttention:
136+
self_attention.module = MLASelfAttentionWithoutQueryNorm
137+
return spec

0 commit comments

Comments
 (0)