Prhub

#1929 Feat/minimax m2.5 support

原始 PR 作者 xs1997zju 合并时间 2026-05-30 12:00 文件变更 9 提交数 5 评论 3 代码增减 +522 / -1

执行摘要

新增 MiniMax-M2.5 229B MoE 模型完整支持

PR 标题和描述明确指出需要为 MiniMax-M2.5(256 专家,top-8 路由)提供完整集成,满足用户在大规模 MoE 模型上的强化学习训练需求。作者在评论中提供了 AIME 评估结果,证明实现正确性。

该 PR 值得精读,特别是 MiniMaxM2SelfAttention 的 TP gather/scatter 实现和 MiniMaxM2Bridge 的配置映射,展示了如何适配自定义注意力结构到 Megatron 框架。建议合并后补充单元测试,并监控 TP 通信开销。

讨论亮点

主要讨论发生在评论线程中:合并者 zhuzilin 要求提供实现正确性的证据(W&B 截图、日志或验证输出)。作者 xs1997zju 回复了 AIME 评估结果截图和训练参数,证明模型在 DAPO-MATH-17k 数据集上的性能。随后 PR 被批准合并,未出现架构争议或重写。

实现拆解

实现步骤

  1. 自定义 SelfAttention 层slime_plugins/models/minimax_m2.py):继承 Megatron 的 SelfAttention,在 __init__ 中将默认的 per-head layernorm 替换为 IdentityOp,并构建全维度的 q_norm/k_norm(RMSNorm over all heads)。get_query_key_value_tensors 中通过 TP gather → norm → TP scatter 完成跨 GPU 的归一化。
  2. 权重映射桥接slime_plugins/mbridge/minimax_m2.py):继承 Qwen2MoEBridge,覆盖 _ATTENTION_MAPPING_MLP_MAPPING 以匹配 MiniMax 的 block_sparse_moe 前缀和 q_norm/k_norm 权重名。_build_config 设置 moe_router_score_function="sigmoid"rotary_percent=0.5 等独有配置。
  3. Megatron-to-HF 转换器slime/backends/megatron_utils/megatron_to_hf/minimax_m2.py):实现 convert_minimax_m2_to_hf 函数,将 Megatron 参数名转换为 HuggingFace 格式,处理专家权重拆分(w1/w2/w3)、QKV 融合拆分以及自定义 QK Norm 映射。
  4. 枢纽文件注册slime/backends/megatron_utils/megatron_to_hf/__init__.pyslime_plugins/mbridge/__init__.py):在 _convert_to_hf_core 中增加 minimaxm2/minimax_m2 模型名分支,并在 mbridge.__init__ 中导出 MiniMaxM2Bridge
  5. 训练与转换脚本scripts/ 目录):新增 4 个 Shell 脚本,包含模型架构参数(scripts/models/minimax-m2.sh)、RL 训练启动(scripts/run-minimax-m2.sh)、HF→Megatron 转换、Megatron→HF 转换。训练脚本使用 TP=2, PP=2, EP=4, 16 GPUs 并配置 GRPO 优势估计。

配套说明:无新增单元测试文件,仅依赖已有的转换测试框架。配置和脚本变动已包含最佳实践示例。

文件 模块 状态 重要度
slime_plugins/models/minimax_m2.py 模型规约 added 8.96
slime_plugins/mbridge/minimax_m2.py 桥接层 added 7.96
slime/backends/megatron_utils/megatron_to_hf/minimax_m2.py 后处理 added 7.84

关键符号

MiniMaxM2SelfAttention.__init__ MiniMaxM2SelfAttention.get_query_key_value_tensors get_minimax_m2_layer_spec MiniMaxM2Bridge._build_config convert_minimax_m2_to_hf

关键源码片段

slime_plugins/models/minimax_m2.py data-contract

核心模型规约文件,实现 MiniMax-M2.5 特有的全维度 QK Norm SelfAttention,是架构适配的关键。

from megatron.core import parallel_state
from megatron.core.models.gpt.gpt_layer_specs import get_gpt_decoder_block_spec
from megatron.core.tensor_parallel import (
    gather_from_tensor_model_parallel_region,
    scatter_to_tensor_model_parallel_region,
)
from megatron.core.transformer.attention import SelfAttention
from megatron.core.transformer.identity_op import IdentityOp
from megatron.core.transformer.spec_utils import build_module
​
​
class MiniMaxM2SelfAttention(SelfAttention):
    """Custom SelfAttention for MiniMax-M2.5 with full-dimension QK Norm.    MiniMax-M2.5 对 Q 和 K 分别做全维度 RMSNorm(所有 head 拼接),
    而不是 Megatron Core 默认的 per-head norm。
    使用 TP gather → norm → TP scatter 以保证跨 GPU 归一化正确性。
    """
​
    def __init__(self, config, submodules, *args, **kwargs):
        # 保存原始 layernorm 规格,暂时替换为 IdentityOp 阻止父类创建 per-head norms
        q_layernorm = submodules.q_layernorm
        k_layernorm = submodules.k_layernorm
        submodules.q_layernorm = IdentityOp
        submodules.k_layernorm = IdentityOp
​
        super().__init__(config, submodules, *args, **kwargs)
​
        # 恢复 submodules,避免影响其他地方复用
        submodules.q_layernorm = q_layernorm
        submodules.k_layernorm = k_layernorm
​
        # 创建全维度 norms: hidden_size = num_heads * head_dim
        self.q_norm = build_module(
            q_layernorm,
            hidden_size=self.hidden_size_per_attention_head * config.num_attention_heads,
            config=self.config,
            eps=self.config.layernorm_epsilon,
        )
        self.k_norm = build_module(
            k_layernorm,
            hidden_size=self.hidden_size_per_attention_head * config.num_query_groups,
            config=self.config,
            eps=self.config.layernorm_epsilon,
        )
​
    def get_query_key_value_tensors(self, hidden_states, key_value_states=None, *args, **kwargs):
        query, key, value = super().get_query_key_value_tensors(hidden_states, key_value_states, *args, **kwargs)
        # query: [sq, b, num_heads_local, head_dim]
        # key: [sq, b, num_kv_heads_local, head_dim]
​
        # 合并 head 维度 : [sq, b, num_heads_local * head_dim]
        query = query.reshape(*query.shape[:-2], -1)
        key = key.reshape(*key.shape[:-2], -1)
​
        # TP gather -> 全维度 norm -> TP scatter
        tp_size = parallel_state.get_tensor_model_parallel_world_size()
        if tp_size > 1:
            query = gather_from_tensor_model_parallel_region(query)
            key = gather_from_tensor_model_parallel_region(key)
​
        query = self.q_norm(query)
        key = self.k_norm(key)
​
        if tp_size > 1:
            query = scatter_to_tensor_model_parallel_region(query)
            key = scatter_to_tensor_model_parallel_region(key)
​
        # 恢复 head 维度 : [sq, b, num_heads_local, head_dim]
        query = query.view(*query.shape[:2], -1, self.hidden_size_per_attention_head)
        key = key.view(*key.shape[:2], -1, self.hidden_size_per_attention_head)
​
        return query, key, value
​
​
def get_minimax_m2_layer_spec(args, config, vp_stage=None):
    """Build layer spec for MiniMax-M2.5, 替换默认 SelfAttention 为自定义版本。"""
    kwargs = {"use_transformer_engine": args.transformer_impl == "transformer_engine"}
    if vp_stage is not None:
        kwargs["vp_stage"] = vp_stage
    spec = get_gpt_decoder_block_spec(config, **kwargs)
​
    for layer_spec in spec.layer_specs:
        layer_spec.submodules.self_attention.module = MiniMaxM2SelfAttention
​
    return spec
slime/backends/megatron_utils/megatron_to_hf/minimax_m2.py dependency-wiring

Megatron 到 HF 的检查点转换核心逻辑,处理专家权重拆分、QKV 融合拆分以及自定义 QK Norm 映射。

import re
import torchdef convert_minimax_m2_to_hf(args, name, param):
    """将 Megatron 参数名/张量转换为 HuggingFace 格式(MiniMax-M2.5)。    HF 使用 `block_sparse_moe` 前缀,专家权重名为 w1(gate)/w2(down)/w3(up)。
    自定义 SelfAttention 使用 `q_norm`/`k_norm` 而非 `q_layernorm`/`k_layernorm`。
    """
    # 顶层参数直接映射
    if name == "module.module.embedding.word_embeddings.weight":
        return [("model.embed_tokens.weight", param)]
    if name == "module.module.output_layer.weight":
        return [("lm_head.weight", param)]
    if name == "module.module.decoder.final_layernorm.weight":
        return [("model.norm.weight", param)]
​
    try:
        head_dim = args.kv_channels if args.kv_channels is not None else args.hidden_size // args.num_attention_heads
    except AttributeError:
        head_dim = args.hidden_size // args.num_attention_heads
    value_num_per_group = args.num_attention_heads // args.num_query_groups
​
    decoder_layers_pattern = r"module\.module\.decoder\.layers\.(\d+)\.(.+)"
    match = re.match(decoder_layers_pattern, name)
    if match:
        layer_idx, rest = match.groups()
​
        # MoE 专家层:linear_fc1 → w1 (gate) + w3 (up), linear_fc2 → w2 (down)
        expert_pattern = r"mlp.experts\.(.+)\.weight(\d+)"
        match = re.match(expert_pattern, rest)
        if match:
            rest, expert_idx = match.groups()
            if rest == "linear_fc1":
                gate_weight, up_weight = param.chunk(2, dim=0)
                return [
                    (f"model.layers.{layer_idx}.block_sparse_moe.experts.{expert_idx}.w1.weight", gate_weight),
                    (f"model.layers.{layer_idx}.block_sparse_moe.experts.{expert_idx}.w3.weight", up_weight),
                ]
            elif rest == "linear_fc2":
                return [
                    (f"model.layers.{layer_idx}.block_sparse_moe.experts.{expert_idx}.w2.weight", param),
                ]
            else:
                raise ValueError(f"Unknown expert parameter name: {name}")
​
        # Attention 输出投影
        if rest == "self_attention.linear_proj.weight":
            return [(f"model.layers.{layer_idx}.self_attn.o_proj.weight", param)]
​
        # 融合 QKV 拆分为 Q/K/V(GQA: 48 heads, 8 kv heads)
        elif rest == "self_attention.linear_qkv.weight":
            param = param.view(args.num_query_groups, -1, head_dim, args.hidden_size)
            q_param, k_param, v_param = torch.split(param, split_size_or_sections=[value_num_per_group, 1, 1], dim=1)
            q_param = q_param.reshape(-1, args.hidden_size)
            k_param = k_param.reshape(-1, args.hidden_size)
            v_param = v_param.reshape(-1, args.hidden_size)
            return [
                (f"model.layers.{layer_idx}.self_attn.q_proj.weight", q_param),
                (f"model.layers.{layer_idx}.self_attn.k_proj.weight", k_param),
                (f"model.layers.{layer_idx}.self_attn.v_proj.weight", v_param),
            ]
​
        # 输入 layernorm
        elif rest == "self_attention.linear_qkv.layer_norm_weight":
            return [(f"model.layers.{layer_idx}.input_layernorm.weight", param)]
​
        # QK Norm(自定义注意力使用 q_norm/k_norm,而非 q_layernorm/k_layernorm)
        elif rest == "self_attention.q_norm.weight":
            return [(f"model.layers.{layer_idx}.self_attn.q_norm.weight", param)]
        elif rest == "self_attention.k_norm.weight":
            return [(f"model.layers.{layer_idx}.self_attn.k_norm.weight", param)]
​
        # 后注意力 layernorm
        elif rest == "pre_mlp_layernorm.weight":
            return [(f"model.layers.{layer_idx}.post_attention_layernorm.weight", param)]
​
        # 路由器
        elif rest == "mlp.router.weight":
            return [(f"model.layers.{layer_idx}.block_sparse_moe.gate.weight", param)]
        elif rest == "mlp.router.expert_bias":
            return [(f"model.layers.{layer_idx}.block_sparse_moe.e_score_correction_bias", param)]
​
    raise ValueError(f"Unknown parameter name: {name}")

评论区精华

实现正确性验证 question

合并者 zhuzilin 要求提供实现正确的证据(W&B 截图、日志或验证输出)。

结论:作者 xs1997zju 提供了 AIME 评估结果截图和训练参数,证明模型在 DAPO-MATH-17k 数据集上的性能。 · 已解决

风险与影响

  1. 缺少测试覆盖:核心文件(slime_plugins/models/minimax_m2.pyslime_plugins/mbridge/minimax_m2.py 等)未对应新增单元测试,仅依赖作者提供的离线评估结果。后续修改可能引入回归。
  2. TP gather/scatter 性能开销MiniMaxM2SelfAttention 在全维度 QK Norm 时引入 TP gather 和 scatter 操作,增加通信量,对大规模 MoE 训练可能成为瓶颈。
  3. 桥接层兼容性MiniMaxM2Bridge 继承 Qwen2MoEBridge,如果父类内部接口变化,可能断裂。需关注父类演进。
  4. 转换器未覆盖所有参数convert_minimax_m2_to_hf 未处理 bias 或其他非线性层,若未来模型变体增加参数则需扩展。

对用户:新增 MiniMax-M2.5 模型支持,用户可直接使用提供的脚本进行 RL 训练和权重转换。对系统:引入新的注意力机制实现和桥接类,需维护;无破坏性变更。对团队:需维护该模型专有代码,但架构设计基于已有模式,学习成本低。

缺少单元测试 TP gather/scatter 通信开销 桥接层继承脆弱性 转换器参数未全覆盖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论