Skip to content

Commit ac6bb20

Browse files
committed
Merge branch 'main' into release/1.6
2 parents bbffa78 + 96d66fb commit ac6bb20

19 files changed

Lines changed: 1751 additions & 227 deletions

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
megatron-core>=0.16,<0.20
22
modelscope
3-
peft>=0.11,<0.20
3+
peft>=0.11,<0.21
44
safetensors
55
tqdm
6-
transformers>=4.33,<5.13.0
6+
transformers>=4.33,<5.15.0

src/mcore_bridge/bridge/gpt_bridge.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) ModelScope Contributors. All rights reserved.
22
import math
3+
import os
34
import re
45
import torch
56
import torch.distributed as dist
@@ -61,6 +62,7 @@ def __init__(self, config: ModelConfig):
6162
self._peft_format = False
6263
self._adapter_name = 'default'
6364
self._is_saving = False
65+
self._source_model_dir = None
6466
self.model_type = config.hf_model_type
6567
self.llm_model_type = config.llm_model_type
6668
self.is_multimodal = config.is_multimodal
@@ -1915,6 +1917,8 @@ def load_weights(
19151917
"""
19161918
self._peft_format = peft_format
19171919
self._adapter_name = adapter_name
1920+
if not peft_format:
1921+
self._source_model_dir = hf_model_dir
19181922
mg_models = unwrap_model(mg_models)
19191923
self._disable_tqdm = False
19201924
self._is_saving = False
@@ -1997,6 +2001,7 @@ def save_weights(
19972001
adapter_name: str = 'default',
19982002
converter: Optional[Callable] = None,
19992003
max_shard_size: str = '5GB',
2004+
save_missing_weights: Union[bool, str] = False,
20002005
) -> None:
20012006
"""Save Megatron model checkpoint in safetensors (HuggingFace) format.
20022007
@@ -2013,10 +2018,15 @@ def save_weights(
20132018
adapter_name: Name of the adapter for PEFT models. Defaults to 'default'.
20142019
converter: Used to perform key-value conversion on the newly exported state_dict.
20152020
max_shard_size: Maximum size of a single storage file, default is '5GB'.
2021+
save_missing_weights: Whether to copy tensors that exist in the source checkpoint but are
2022+
absent from the exported weights, such as submodules Megatron does not support. Pass a
2023+
path to specify the source checkpoint, otherwise the one recorded by `load_weights` is
2024+
used. Ignored when `peft_format` is True.
20162025
"""
20172026
gc_collect()
20182027
saver = StreamingSafetensorSaver(save_dir=output_dir, max_shard_size=max_shard_size, peft_format=peft_format)
20192028
mg_models = unwrap_model(mg_models)
2029+
saved_keys = set()
20202030
for k, v in self.export_weights(
20212031
mg_models,
20222032
target_device='cpu',
@@ -2028,9 +2038,37 @@ def save_weights(
20282038
disable_tqdm=False,
20292039
_is_saving=True):
20302040
saver.add_tensor(k, v)
2041+
saved_keys.add(k)
2042+
if save_missing_weights and not peft_format:
2043+
source_model_dir = save_missing_weights if isinstance(save_missing_weights, str) else None
2044+
self._save_missing_weights(saver, saved_keys, source_model_dir)
20312045
saver.finalize()
20322046
dist.barrier() # Ensure all weights are saved completely
20332047

2048+
def _save_missing_weights(self, saver, saved_keys, source_model_dir=None) -> None:
2049+
"""Copy tensors present in the source checkpoint but absent from the exported ones.
2050+
2051+
Megatron only materializes the modules it knows about, so weights of unsupported
2052+
submodules (for instance the DSpark stages under `mtp.*`) would silently vanish
2053+
from the exported checkpoint. Restoring them verbatim keeps the saved model
2054+
functionally complete.
2055+
"""
2056+
source_model_dir = source_model_dir or self._source_model_dir
2057+
if source_model_dir is None or not is_master():
2058+
return
2059+
if not os.path.isdir(source_model_dir):
2060+
logger.warning(f'Source model dir does not exist, skip restoring missing weights: {source_model_dir}')
2061+
return
2062+
with SafetensorLazyLoader(source_model_dir) as loader:
2063+
state_dict = loader.get_state_dict()
2064+
missing_keys = sorted(set(state_dict.keys()) - saved_keys)
2065+
if not missing_keys:
2066+
return
2067+
logger.info(f'Restoring {len(missing_keys)} weights from the source checkpoint '
2068+
f'that were not exported by Megatron, e.g. {missing_keys[:3]}.')
2069+
for key in missing_keys:
2070+
saver.add_tensor(key, state_dict[key].load())
2071+
20342072
@contextmanager
20352073
def _patch_hf_initialize_weight(self):
20362074

src/mcore_bridge/config/model_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ class ModelConfig(TransformerConfig):
196196
attention_output_gate: bool = False
197197
linear_decoupled_in_proj: bool = False
198198

199+
# nemotron_h (hybrid mamba2 + attention + moe)
200+
hybrid_layer_pattern: Optional[str] = None
201+
199202
# dsa
200203
experimental_attention_variant: Optional[Literal['gated_delta_net', 'dsa', 'dsv4_hybrid']] = None
201204
dsa_indexer_n_heads: Optional[int] = None

src/mcore_bridge/config/parser.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) ModelScope Contributors. All rights reserved.
22
import torch.nn.functional as F
33
from functools import partial
4+
from megatron.core.activations import squared_relu
45
from transformers import PretrainedConfig
56
from typing import Any, Dict
67

@@ -14,7 +15,7 @@
1415
'num_attention_heads': ['num_attention_heads'],
1516
'num_query_groups': ['num_key_value_heads'],
1617
'max_position_embeddings': ['max_position_embeddings'],
17-
'layernorm_epsilon': ['rms_norm_eps'],
18+
'layernorm_epsilon': ['rms_norm_eps', 'layer_norm_epsilon'],
1819
'rotary_base': ['rope_theta'],
1920
'padded_vocab_size': ['vocab_size'],
2021
'attention_dropout': ['attention_dropout'],
@@ -26,7 +27,7 @@
2627
'hf_model_type': ['model_type'],
2728
# moe
2829
'moe_ffn_hidden_size': ['moe_intermediate_size'],
29-
'moe_shared_expert_intermediate_size': ['shared_expert_intermediate_size'],
30+
'moe_shared_expert_intermediate_size': ['shared_expert_intermediate_size', 'moe_shared_expert_intermediate_size'],
3031
'moe_router_topk': ['num_experts_per_tok', 'moe_topk', 'moe_k', 'top_k_experts'],
3132
'moe_router_num_groups': ['n_group'],
3233
'moe_router_group_topk': ['topk_group'],
@@ -67,6 +68,14 @@
6768
'mhc_sinkhorn_iterations': ['hc_sinkhorn_iters'],
6869
'moe_n_hash_layers': ['mlp_layer_types'],
6970
'activation_func_clamp_value': ['swiglu_limit'],
71+
# nemotron_h / mamba2
72+
'mamba_num_heads': ['mamba_num_heads'],
73+
'mamba_head_dim': ['mamba_head_dim'],
74+
'mamba_state_dim': ['ssm_state_size', 'mamba_state_dim'],
75+
'mamba_num_groups': ['n_groups', 'mamba_num_groups'],
76+
'hybrid_layer_pattern': ['hybrid_override_pattern'],
77+
'fp32_residual_connection': ['residual_in_fp32'],
78+
'mtp_hybrid_override_pattern': ['mtp_hybrid_override_pattern'],
7079
# other
7180
'original_max_position_embeddings': ['original_max_position_embeddings'],
7281
'partial_rotary_factor': ['partial_rotary_factor'],
@@ -198,6 +207,15 @@ def hf_to_mcore_config(hf_config: PretrainedConfig) -> Dict[str, Any]:
198207
res['swiglu'] = False
199208
res['gated_linear_unit'] = True
200209
res['activation_func'] = partial(F.gelu, approximate='tanh')
210+
elif hf_model_type == 'muse_glimmer':
211+
# 39 sliding layers (window 2048) interleaved with 13 full-attention layers; the latter are
212+
# exactly the NoPE layers (`layer_rope_theta == 0`).
213+
res['window_size'] = f'{window_size - 1},0'
214+
window_attn_skip_freq = ','.join(['1' if lt == 'sliding_attention' else '0' for lt in layer_types])
215+
res['window_attn_skip_freq'] = f'[{window_attn_skip_freq}]'
216+
# The four per-layer norms are `CenteredRMSNorm` (`x * (1.0 + w)`). The final `norm` is a plain
217+
# RMSNorm, so `MuseGlimmerLoader.build_model` opts that single module back out.
218+
res['layernorm_zero_centered_gamma'] = True
201219
elif llm_model_type == 'gpt_oss':
202220
res['add_bias_linear'] = True
203221
res['bias_dropout_fusion'] = False
@@ -255,6 +273,19 @@ def hf_to_mcore_config(hf_config: PretrainedConfig) -> Dict[str, Any]:
255273
res['add_qkv_bias'] = False
256274
res['moe_router_score_function'] = 'sigmoid'
257275
res['moe_router_load_balancing_type'] = 'seq_aux_loss'
276+
elif llm_model_type == 'nemotron_h':
277+
res['is_hybrid_model'] = True
278+
res['position_embedding_type'] = 'none'
279+
# relu^2 ("relu2") activation: non-gated, so fc1 is a single up_proj (no gate_proj).
280+
res['swiglu'] = False
281+
res['gated_linear_unit'] = False
282+
res['activation_func'] = squared_relu
283+
res['add_bias_linear'] = False
284+
res['add_qkv_bias'] = False
285+
res['qk_layernorm'] = False
286+
res['moe_router_score_function'] = 'sigmoid'
287+
res['moe_router_enable_expert_bias'] = True
288+
res['moe_router_load_balancing_type'] = 'seq_aux_loss'
258289

259290
if 'partial_rotary_factor' not in res and 'partial_rotary_factor' in rope_scaling:
260291
res['partial_rotary_factor'] = rope_scaling['partial_rotary_factor']

src/mcore_bridge/model/constant.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class LLMModelType:
1212
bailing_hybrid = 'bailing_hybrid'
1313
deepseek_v4 = 'deepseek_v4'
1414
glm_moe_dsa = 'glm_moe_dsa'
15+
nemotron_h = 'nemotron_h'
1516

1617
qwen3_emb = 'qwen3_emb'
1718

@@ -41,6 +42,8 @@ class MLLMModelType:
4142

4243
minicpmv4_6 = 'minicpmv4_6'
4344

45+
muse_glimmer = 'muse_glimmer'
46+
4447

4548
class ModelType(LLMModelType, MLLMModelType):
4649
pass

src/mcore_bridge/model/gpt_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ def _preprocess(
203203
return decoder_input, mtp_decoder_input, rotary_pos_emb, rotary_pos_cos, rotary_pos_sin, sequence_len_offset
204204

205205
def _set_inv_freq(self):
206+
if getattr(self, 'rotary_pos_emb', None) is None:
207+
return
206208
new_inv_freq, self.config.attention_scaling = get_rope_inv_freq(self.config)
207209
self.rotary_pos_emb.inv_freq = new_inv_freq.to(self.rotary_pos_emb.inv_freq.device)
208210

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Copyright (c) ModelScope Contributors. All rights reserved.
2-
from . import (bailing_hybrid, bailing_moe, deepseek_v4, glm4, glm_moe_dsa, hunyuan, llm, minimax_m2, olmoe, qwen3_emb,
3-
qwen3_next)
2+
from . import (bailing_hybrid, bailing_moe, deepseek_v4, glm4, glm_moe_dsa, hunyuan, llm, minimax_m2, nemotron_h, olmoe,
3+
qwen3_emb, qwen3_next)

src/mcore_bridge/model/gpts/deepseek_v4.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,38 @@ def _patch_YarnRotaryEmbedding(config):
6060
delattr(config, attr)
6161

6262

63+
def _apply_mla_rope(t, freqs, *, config, cu_seqlens, cp_group, inverse=False):
64+
"""Apply DSv4's MLA RoPE to a tensor whose frequencies are already expanded per token.
65+
66+
`GPTModel` pre-indexes the rotary table by `position_ids`, so `freqs` is row-aligned with
67+
`t`: row i holds the frequency of token i. That holds for every layout DSv4 supports --
68+
unpacked, packed (thd), and packed CP: swift CP-splits `position_ids` with the same
69+
partition mode as the hidden states, so the pre-indexed frequencies come out rank-local
70+
while still carrying absolute positions. The multiply is therefore purely elementwise and
71+
needs no `cu_seqlens`-based segment alignment, under any `cp_partition_mode`.
72+
73+
Enforcing that invariant here matters: when it does not hold, the generic
74+
`apply_rotary_pos_emb` thd path re-derives positions from `cu_seqlens` assuming a *zigzag*
75+
CP split, which is wrong for DSv4's contiguous split and would corrupt positions silently.
76+
Asserting row alignment turns any future layout change into an immediate, explicit failure.
77+
"""
78+
assert freqs.shape[0] == t.shape[0], (
79+
f'DSv4 MLA RoPE expects per-token frequencies row-aligned with the input, got '
80+
f'freqs.shape[0]={freqs.shape[0]} vs tokens={t.shape[0]}. `GPTModel` must pre-index the '
81+
'rotary table by `position_ids` (requires `apply_rope_fusion=False`), and under CP the '
82+
'`position_ids` must be split with the same partition mode as the hidden states.')
83+
return apply_rotary_pos_emb(
84+
t,
85+
freqs,
86+
config=config,
87+
cu_seqlens=cu_seqlens,
88+
cp_group=cp_group,
89+
mla_rotary_interleaved=True,
90+
mla_output_remove_interleaving=True,
91+
inverse=inverse,
92+
)
93+
94+
6395
class DSv4HybridSelfAttention(McoreDSv4HybridSelfAttention):
6496

6597
def __init__(self, config, *args, **kwargs):
@@ -153,6 +185,15 @@ def qkv_up_proj_and_rope_apply(q_compressed,
153185
When sequence packing enabled, the input tensors adopt a packed shape of [t, ...];
154186
otherwise, they maintain the unpacked shape [s, b, ...]. In subsequent code comments,
155187
we uniformly use [num_tokens, ...] to denote [s, b, ...] or [t, ...] for two cases.
188+
189+
RoPE frequency layout: `GPTModel` pre-indexes the rotary table by `position_ids`
190+
(see gpt_model.py, the `not apply_rope_fusion` branch), so `rotary_pos_emb` here is
191+
already expanded per token -- row i belongs to token i -- rather than being a
192+
position->frequency lookup table. Every RoPE call below is therefore an elementwise
193+
multiply; see `_apply_mla_rope` for why that invariant is asserted. Under CP the
194+
frequencies arrive rank-local because `position_ids` is split alongside the hidden
195+
states, which is also why the boundary rows carry their own frequencies
196+
(`boundary_rotary_pos_emb`) instead of being re-derived from positions.
156197
"""
157198
# q_compressed: [num_tokens, q_lora_rank]
158199
# q: [num_tokens, n * (qk_head_dim + qk_pos_emb_head_dim)]
@@ -166,6 +207,8 @@ def qkv_up_proj_and_rope_apply(q_compressed,
166207
if boundary_kv_compressed is not None:
167208
boundary_rows = boundary_kv_compressed.shape[0]
168209
kv_projection_input = torch.cat([boundary_kv_compressed, kv_compressed], dim=0)
210+
# The boundary rows precede this rank's block, so their frequencies must precede
211+
# too -- keeping kv_rotary_pos_emb row-aligned with kv_projection_input.
169212
kv_rotary_pos_emb = torch.cat([boundary_rotary_pos_emb, rotary_pos_emb], dim=0)
170213
else:
171214
kv_projection_input = kv_compressed
@@ -182,29 +225,25 @@ def qkv_up_proj_and_rope_apply(q_compressed,
182225

183226
# RoPE and query (shared for wkv and latent)
184227
# q_pos_emb: [num_tokens, n, qk_pos_emb_head_dim]
185-
q_pos_emb = apply_rotary_pos_emb(
228+
q_pos_emb = _apply_mla_rope(
186229
q_pos_emb,
187230
rotary_pos_emb,
188231
config=self.config,
189232
cu_seqlens=cu_seqlens_q,
190233
cp_group=self.pg_collection.cp,
191-
mla_rotary_interleaved=True,
192-
mla_output_remove_interleaving=True,
193234
)
194235
# query: [num_tokens, n, (qk_head_dim + v_head_dim)]
195236
query = torch.cat([q_no_pe, q_pos_emb], dim=-1)
196237

197238
kv_no_pe, k_pos_emb = torch.split(kv, [kv.size(-1) - pos_dim, pos_dim], dim=-1)
198239

199240
# k_pos_emb:[num_tokens, 1, qk_pos_emb_head_dim]
200-
k_pos_emb = apply_rotary_pos_emb(
241+
k_pos_emb = _apply_mla_rope(
201242
k_pos_emb,
202243
kv_rotary_pos_emb,
203244
config=self.config,
204245
cu_seqlens=cu_seqlens_kv,
205246
cp_group=self.pg_collection.cp,
206-
mla_rotary_interleaved=True,
207-
mla_output_remove_interleaving=True,
208247
)
209248

210249
# Single head: key = value = [num_tokens, 1, v_head_dim]
@@ -384,15 +423,13 @@ def forward(
384423
rot_part_in = rot_part.squeeze(1)
385424
else:
386425
rot_part_in = rot_part
387-
rot_part_out = apply_rotary_pos_emb(
426+
rot_part_out = _apply_mla_rope(
388427
rot_part_in,
389428
rotary_pos_emb,
390-
self.config,
429+
config=self.config,
391430
cu_seqlens=cu_seqlens_kv,
392431
cp_group=self.pg_collection.cp,
393-
mla_rotary_interleaved=True,
394432
inverse=True,
395-
mla_output_remove_interleaving=True,
396433
)
397434
if packed_seq:
398435
rot_part = rot_part_out.unsqueeze(1)

0 commit comments

Comments
 (0)