Skip to content

Commit b244f24

Browse files
committed
feat(model): add MiMo-V2.6-Flash text checkpoint conversion
Signed-off-by: Neil Kale <263453039+kalectory@users.noreply.github.com>
1 parent 1e2362d commit b244f24

6 files changed

Lines changed: 668 additions & 0 deletions

File tree

‎examples/models/mimo_v2_flash/README.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,35 @@ This directory contains example scripts for [MiMo-V2-Flash](https://huggingface.
44

55
The HF checkpoint depends on custom modeling code, so all commands below pass `--trust-remote-code`.
66

7+
## MiMo-V2.6-Flash-RL
8+
9+
[`XiaomiMiMo/MiMo-V2.6-Flash-RL`](https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Flash-RL)
10+
(309B total / 15B active) uses `MiMoV2Bridge` for its **text backbone**, reusing
11+
the MiMo-V2-Flash provider and attention layers:
12+
13+
```python
14+
from megatron.bridge import AutoBridge
15+
16+
bridge = AutoBridge.from_hf_pretrained(
17+
"XiaomiMiMo/MiMo-V2.6-Flash-RL", trust_remote_code=True
18+
)
19+
provider = bridge.to_megatron_provider()
20+
# Configure TP/PP/EP/ETP before constructing the model, as below.
21+
```
22+
23+
The published checkpoint mixes FP8 attention weights with packed MXFP4 expert
24+
weights. Import dequantizes both to BF16 and reads `metadata.tp_size` from the
25+
safetensors index to reconstruct fused QKV rows. Keep that index with the shards.
26+
Export restores the source quantization and QKV layout, preserves the source
27+
configuration, and copies untouched vision, audio, speech-embedding, and MTP
28+
tensors. Requantization is lossy; quantized bytes need not round-trip exactly.
29+
Export dtype overrides are unsupported for quantized source checkpoints.
30+
31+
Use `to_megatron_provider()`; the generic `get_model_config()` builder is not
32+
supported. The same CP and TP restrictions below apply. Vision/audio execution
33+
and MTP training are outside this text-backbone implementation. Adapter export
34+
does not establish support for serving LoRA adapters in vLLM or SGLang.
35+
736
## Hardware Requirements
837

938
MiMo-V2-Flash requires **at least 2 nodes (16 GPUs)** for inference and conversion. Although the source checkpoint is FP8, conversion dequantizes its weights to BF16 before loading them into Megatron. The 47 MoE layers contain about 302.8B expert parameters, or about 605.6 GB in BF16, so the parallel layout must shard the target expert weights with enough headroom for the rest of the model and communication workspaces.

‎src/megatron/bridge/models/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
from megatron.bridge.models.mamba.mamba_provider import MambaModelProvider
125125
from megatron.bridge.models.mimo.mimo_bridge import MimoBridge
126126
from megatron.bridge.models.mimo_v2_flash import (
127+
MiMoV2Bridge,
127128
MiMoV2FlashBridge,
128129
MiMoV2FlashModelProvider,
129130
)
@@ -287,6 +288,7 @@
287288
"MambaModelProvider",
288289
"MimoBridge",
289290
# MiMo-V2-Flash
291+
"MiMoV2Bridge",
290292
"MiMoV2FlashBridge",
291293
"MiMoV2FlashModelProvider",
292294
# Nemotron Models

‎src/megatron/bridge/models/mimo_v2_flash/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from megatron.bridge.models.mimo_v2_flash.mimo_v2_bridge import MiMoV2Bridge # noqa: F401
1516
from megatron.bridge.models.mimo_v2_flash.mimo_v2_flash_bridge import MiMoV2FlashBridge # noqa: F401
1617
from megatron.bridge.models.mimo_v2_flash.mimo_v2_flash_provider import MiMoV2FlashModelProvider # noqa: F401
1718

1819

1920
__all__ = [
21+
"MiMoV2Bridge",
2022
"MiMoV2FlashBridge",
2123
"MiMoV2FlashModelProvider",
2224
]
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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+
"""MiMo-V2.6 text checkpoint conversion using the existing MiMo-V2-Flash layers."""
16+
17+
import json
18+
from collections.abc import Iterable, Mapping
19+
from copy import deepcopy
20+
from functools import lru_cache
21+
from pathlib import Path
22+
23+
import torch
24+
from megatron.core.models.gpt.gpt_model import GPTModel
25+
from megatron.core.transformer.transformer_config import TransformerConfig
26+
from torch import nn
27+
28+
from megatron.bridge.models.conversion import quantization_utils
29+
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
30+
from megatron.bridge.models.conversion.model_bridge import (
31+
HFSourcedWeightTuple,
32+
HFWeightTuple,
33+
MegatronModel,
34+
MegatronModelBridge,
35+
WeightConversionTask,
36+
)
37+
from megatron.bridge.models.conversion.param_mapping import AutoMapping
38+
from megatron.bridge.models.conversion.peft_bridge import AdapterWeight
39+
from megatron.bridge.models.conversion.utils import unwrap_model
40+
from megatron.bridge.models.hf_pretrained.causal_lm import PreTrainedCausalLM
41+
from megatron.bridge.models.hf_pretrained.state import SafeTensorsStateSource
42+
from megatron.bridge.models.mimo_v2_flash.mimo_v2_flash_bridge import MiMoV2FlashBridge, MiMoV2FlashQKVMapping
43+
from megatron.bridge.models.mimo_v2_flash.mimo_v2_flash_provider import MiMoV2FlashModelProvider
44+
45+
46+
@lru_cache(maxsize=8)
47+
def _checkpoint_tp_size(index_path: Path) -> int:
48+
if not index_path.exists():
49+
return 1
50+
tp_size = json.loads(index_path.read_text()).get("metadata", {}).get("tp_size", 1)
51+
if not isinstance(tp_size, int) or isinstance(tp_size, bool) or tp_size < 1:
52+
raise ValueError(f"Invalid MiMo checkpoint tp_size: {tp_size!r}")
53+
return tp_size
54+
55+
56+
def _reorder_qkv(
57+
weight: torch.Tensor, sizes: tuple[int, int, int], source_chunks: int, target_chunks: int
58+
) -> torch.Tensor:
59+
"""Repartition [Q0,K0,V0,...] without changing rows within a projection."""
60+
if weight.shape[0] != sum(sizes) or any(s % c for s in sizes for c in (source_chunks, target_chunks)):
61+
raise ValueError("MiMo fused QKV dimensions must be divisible by checkpoint and attention group counts")
62+
trailing = weight.shape[1:]
63+
parts = weight.reshape(source_chunks, -1, *trailing).split([s // source_chunks for s in sizes], dim=1)
64+
return torch.cat(
65+
[part.reshape(target_chunks, size // target_chunks, *trailing) for part, size in zip(parts, sizes)],
66+
dim=1,
67+
).reshape(weight.shape)
68+
69+
70+
class MiMoV2FusedQKVMapping(AutoMapping):
71+
"""Convert checkpoint TP chunks to Megatron's per-KV-head QKV groups."""
72+
73+
def __init__(self, megatron_param: str, hf_param: str, checkpoint_tp_size: int = 1) -> None:
74+
super().__init__(megatron_param, hf_param)
75+
self.checkpoint_tp_size = checkpoint_tp_size
76+
77+
@staticmethod
78+
def _sizes(config: TransformerConfig) -> tuple[int, int, int]:
79+
return (
80+
config.num_attention_heads * config.kv_channels,
81+
config.num_query_groups * config.kv_channels,
82+
config.num_query_groups * config.v_head_dim,
83+
)
84+
85+
def hf_to_megatron(self, hf_weights: torch.Tensor, megatron_module: nn.Module) -> torch.Tensor:
86+
"""Reorder before the ordinary column-parallel scatter."""
87+
if self.tp_rank == 0:
88+
config = self._get_config(megatron_module)
89+
hf_weights = _reorder_qkv(
90+
hf_weights, self._sizes(config), self.checkpoint_tp_size, config.num_query_groups
91+
)
92+
return super().hf_to_megatron(hf_weights, megatron_module)
93+
94+
def megatron_to_hf(
95+
self, megatron_weights: torch.Tensor | None, megatron_module: nn.Module | None
96+
) -> dict[str, torch.Tensor]:
97+
"""Gather and restore the source checkpoint's fused row order."""
98+
config = self._get_config(megatron_module) if megatron_module is not None else None
99+
shape = (self._sizes(config), config.num_query_groups) if config is not None else None
100+
shape = self.broadcast_obj_from_pp_rank(shape, "mimo_qkv_shape")
101+
result = super().megatron_to_hf(megatron_weights, megatron_module)
102+
return {
103+
name: _reorder_qkv(weight, shape[0], shape[1], self.checkpoint_tp_size) for name, weight in result.items()
104+
}
105+
106+
def resolve(self, captures: tuple[str, ...]) -> "MiMoV2FusedQKVMapping":
107+
"""Preserve checkpoint partitioning when resolving layer wildcards."""
108+
megatron_param, hf_param = self._resolve_names(captures)
109+
return type(self)(megatron_param, hf_param, self.checkpoint_tp_size)
110+
111+
112+
@MegatronModelBridge.register_bridge(
113+
source="MiMoV2ForCausalLM",
114+
target=GPTModel,
115+
provider=MiMoV2FlashModelProvider,
116+
model_type="mimo_v2",
117+
)
118+
class MiMoV2Bridge(MiMoV2FlashBridge):
119+
"""Text-backbone conversion for MiMo-V2.6-Flash-RL (309B total / 15B active).
120+
121+
Attention and MoE execution reuse MiMo-V2-Flash. Source-format export
122+
preserves the quantized representation and untouched multimodal/MTP weights.
123+
"""
124+
125+
# The generic GPT builder cannot represent this family's custom attention.
126+
MODEL_CONFIG_CLASS = None
127+
128+
_HF_PASSTHROUGH_PREFIXES = ("visual.", "audio_encoder.", "speech_embeddings.", "model.mtp.")
129+
130+
def provider_bridge(self, hf_pretrained: PreTrainedCausalLM) -> MiMoV2FlashModelProvider:
131+
"""Reuse the text provider and retain the full source configuration."""
132+
provider = super().provider_bridge(hf_pretrained)
133+
if hf_pretrained.config.attention_projection_layout != "fused_qkv":
134+
raise ValueError("MiMoV2Bridge requires the fused_qkv checkpoint layout")
135+
provider.mimo_v2_hf_config = deepcopy(hf_pretrained.config.to_dict())
136+
provider.mtp_num_layers = 0
137+
# The reference router casts logits to FP32, regardless of metadata dtype.
138+
provider.moe_router_dtype = "fp32"
139+
provider.moe_router_bias_update_rate = 0.0
140+
return provider
141+
142+
@classmethod
143+
def megatron_to_hf_config(cls, provider: MiMoV2FlashModelProvider) -> dict[str, object]:
144+
"""Preserve V2 architecture, quantization, and untrained encoder metadata."""
145+
if provider.mimo_v2_hf_config is None:
146+
raise ValueError("MiMo-V2 export requires a provider created from the source HF configuration")
147+
return deepcopy(provider.mimo_v2_hf_config)
148+
149+
def _source_tp_size(self) -> int:
150+
state = getattr(getattr(self, "hf_pretrained", None), "state", None)
151+
source = getattr(state, "source", None)
152+
if isinstance(source, SafeTensorsStateSource):
153+
return _checkpoint_tp_size(source.path / "model.safetensors.index.json")
154+
return 1 # In-memory HF models use canonical [Q,K,V].
155+
156+
def mapping_registry(self) -> MegatronMappingRegistry:
157+
"""Reuse all text mappings except the newly fused QKV representation."""
158+
mappings = []
159+
for mapping in super().mapping_registry().mappings:
160+
if mapping.megatron_param.startswith("mtp."):
161+
continue
162+
if isinstance(mapping, MiMoV2FlashQKVMapping):
163+
mapping = MiMoV2FusedQKVMapping(
164+
mapping.megatron_param,
165+
mapping.hf_param["q"].replace(".q_proj.", ".qkv_proj."),
166+
self._source_tp_size(),
167+
)
168+
mappings.append(mapping)
169+
return MegatronMappingRegistry(*mappings)
170+
171+
@staticmethod
172+
def get_hf_import_param_names(
173+
hf_param: str | dict[str, str], available_hf_param_names: set[str] | None = None
174+
) -> tuple[str, ...]:
175+
"""Include MXFP4 scale sidecars in incremental import dependencies."""
176+
names = list(MiMoV2FlashBridge.get_hf_import_param_names(hf_param, available_hf_param_names))
177+
mapped = (hf_param,) if isinstance(hf_param, str) else hf_param.values()
178+
if available_hf_param_names is not None:
179+
names.extend(f"{name}_scale" for name in mapped if f"{name}_scale" in available_hf_param_names)
180+
return tuple(dict.fromkeys(names))
181+
182+
def _fp8_chunks(
183+
self, name: str, weight: torch.Tensor, scale: torch.Tensor
184+
) -> Iterable[tuple[torch.Tensor, torch.Tensor]]:
185+
chunks = self._source_tp_size() if name.endswith(".qkv_proj.weight") else 1
186+
if weight.shape[0] % chunks:
187+
raise ValueError(f"{name}: FP8 rows are not divisible by checkpoint tp_size={chunks}")
188+
block = quantization_utils.FP8_BLOCK_SIZE
189+
expected = (
190+
chunks * ((weight.shape[0] // chunks + block - 1) // block),
191+
(weight.shape[1] + block - 1) // block,
192+
)
193+
if tuple(scale.shape) != expected:
194+
raise ValueError(f"{name}: expected FP8 scales {expected}, got {tuple(scale.shape)}")
195+
return zip(weight.chunk(chunks, dim=0), scale.chunk(chunks, dim=0))
196+
197+
def maybe_modify_loaded_hf_weight(
198+
self, hf_param: str | dict[str, str], hf_state_dict: Mapping[str, torch.Tensor]
199+
) -> torch.Tensor | dict[str, torch.Tensor]:
200+
"""Decode MXFP4 experts and separately block-scaled FP8 QKV chunks."""
201+
if isinstance(hf_param, dict):
202+
return {role: self.maybe_modify_loaded_hf_weight(name, hf_state_dict) for role, name in hf_param.items()}
203+
weight = hf_state_dict[hf_param]
204+
if weight.dtype == torch.uint8:
205+
return quantization_utils.dequantize_mxfp4_e2m1_packed(weight, hf_state_dict[f"{hf_param}_scale"])
206+
if weight.dtype == torch.float8_e4m3fn:
207+
scale = hf_state_dict[f"{hf_param}_scale_inv"]
208+
return torch.cat(
209+
[
210+
quantization_utils.dequantize_fp8_blockwise(w, s)
211+
for w, s in self._fp8_chunks(hf_param, weight, scale)
212+
]
213+
)
214+
return weight
215+
216+
def maybe_modify_converted_hf_weight(
217+
self,
218+
task: WeightConversionTask,
219+
converted_weights_dict: dict[str, torch.Tensor],
220+
hf_state_dict: Mapping[str, torch.Tensor],
221+
) -> dict[str, torch.Tensor]:
222+
"""Restore source quantization after conversion and any LoRA merge."""
223+
result = {}
224+
for name, weight in converted_weights_dict.items():
225+
mxfp4_scale, fp8_scale = f"{name}_scale", f"{name}_scale_inv"
226+
if task.weight_dtype is not None and (mxfp4_scale in hf_state_dict or fp8_scale in hf_state_dict):
227+
raise ValueError(
228+
"MiMo source-format export preserves quantization; weight_dtype overrides are unsupported"
229+
)
230+
if mxfp4_scale in hf_state_dict:
231+
packed, scale = quantization_utils.quantize_mxfp4_e2m1_like_scale(
232+
weight, hf_state_dict[mxfp4_scale], name=name
233+
)
234+
result[name], result[mxfp4_scale] = packed.view(torch.uint8), scale
235+
elif fp8_scale in hf_state_dict:
236+
chunks = [
237+
quantization_utils.quantize_fp8_e4m3fn_like_scale(w, s, name=name)
238+
for w, s in self._fp8_chunks(name, weight, hf_state_dict[fp8_scale])
239+
]
240+
result[name] = torch.cat([w for w, _ in chunks])
241+
result[fp8_scale] = torch.cat([s for _, s in chunks])
242+
else:
243+
result[name] = weight
244+
return result
245+
246+
def _qkv_adapter_rows(
247+
self, megatron_model: list[MegatronModel], name: str, weight: torch.Tensor, target_chunks: int
248+
) -> torch.Tensor:
249+
layer = int(name.split(".")[2])
250+
config = unwrap_model(megatron_model)[0].config
251+
groups = (
252+
config.swa_num_query_groups
253+
if config.hybrid_attention_pattern[layer]
254+
else config.full_attn_num_query_groups
255+
)
256+
sizes = (
257+
config.num_attention_heads * config.kv_channels,
258+
groups * config.kv_channels,
259+
groups * config.v_head_dim,
260+
)
261+
return _reorder_qkv(weight, sizes, groups, target_chunks)
262+
263+
def _get_fused_adapter_linear_out_slices(
264+
self,
265+
megatron_model: list[MegatronModel],
266+
base_hf_weight_names: list[str],
267+
linear_out_tensor: torch.Tensor,
268+
is_expert: bool = False,
269+
) -> dict[str, torch.Tensor] | None:
270+
"""Export adapter B in canonical HF Q/K/V order, independent of base storage."""
271+
if len(base_hf_weight_names) == 1 and base_hf_weight_names[0].endswith(".self_attn.qkv_proj.weight"):
272+
name = base_hf_weight_names[0]
273+
return {name: self._qkv_adapter_rows(megatron_model, name, linear_out_tensor, 1)}
274+
return super()._get_fused_adapter_linear_out_slices(
275+
megatron_model, base_hf_weight_names, linear_out_tensor, is_expert=is_expert
276+
)
277+
278+
def _merge_lora_adapter_weights(
279+
self,
280+
megatron_model: list[MegatronModel],
281+
converted_weights_dict: dict[str, torch.Tensor],
282+
adapter_weights: list[AdapterWeight],
283+
) -> dict[str, torch.Tensor]:
284+
"""Merge fused QKV adapters in the source checkpoint's row order."""
285+
names = list(converted_weights_dict)
286+
if len(names) != 1 or not names[0].endswith(".self_attn.qkv_proj.weight"):
287+
return super()._merge_lora_adapter_weights(megatron_model, converted_weights_dict, adapter_weights)
288+
(adapter,) = adapter_weights
289+
name = names[0]
290+
linear_out = self._qkv_adapter_rows(
291+
megatron_model, name, adapter.linear_out_weight.weight, self._source_tp_size()
292+
)
293+
return {
294+
name: self._merge_single_adapter_weight(
295+
converted_weights_dict[name], adapter.alpha, adapter.dim, adapter.linear_in_weight.weight, linear_out
296+
)
297+
}
298+
299+
def stream_weights_megatron_to_hf(
300+
self,
301+
megatron_model: MegatronModel | list[MegatronModel],
302+
hf_pretrained: PreTrainedCausalLM,
303+
cpu: bool = True,
304+
show_progress: bool = True,
305+
conversion_tasks: list[WeightConversionTask] | None = None,
306+
*,
307+
merge_adapter_weights: bool = True,
308+
weight_dtype: torch.dtype | None = None,
309+
with_megatron_names: bool = False,
310+
) -> Iterable[HFWeightTuple | HFSourcedWeightTuple]:
311+
"""Export text updates while preserving untrained encoder and MTP tensors."""
312+
if weight_dtype is not None and getattr(hf_pretrained.config, "quantization_config", None):
313+
raise ValueError(
314+
"MiMo source-format export preserves quantization; weight_dtype overrides are unsupported"
315+
)
316+
yield from super().stream_weights_megatron_to_hf(
317+
megatron_model,
318+
hf_pretrained,
319+
cpu=cpu,
320+
show_progress=show_progress,
321+
conversion_tasks=conversion_tasks,
322+
merge_adapter_weights=merge_adapter_weights,
323+
weight_dtype=weight_dtype,
324+
with_megatron_names=with_megatron_names,
325+
)
326+
state = hf_pretrained.state
327+
for name in state.source.get_all_keys():
328+
if name.startswith(self._HF_PASSTHROUGH_PREFIXES):
329+
yield from HFWeightTuple(name, state[name]).iter_finalized(
330+
cpu=cpu, megatron_param_names=() if with_megatron_names else None
331+
)

0 commit comments

Comments
 (0)