use crate::backends::gguf::types::MetaValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MappedTensor {
Dense { hf: String, gguf: String },
ExpertWeight {
hf: String,
layer: u32,
expert: u32,
role: ExpertRole,
gguf_stacked: String,
},
Router { hf: String, gguf: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpertRole {
Gate, Up, Down, }
impl ExpertRole {
pub const fn gguf_prefix(self) -> &'static str {
match self {
ExpertRole::Gate => "ffn_gate",
ExpertRole::Up => "ffn_up",
ExpertRole::Down => "ffn_down",
}
}
}
pub fn map_tensor_name(hf_name: &str) -> Option<MappedTensor> {
match hf_name {
"model.embed_tokens.weight" => {
return Some(MappedTensor::Dense {
hf: hf_name.to_string(),
gguf: "token_embd.weight".to_string(),
})
}
"model.norm.weight" => {
return Some(MappedTensor::Dense {
hf: hf_name.to_string(),
gguf: "output_norm.weight".to_string(),
})
}
"lm_head.weight" => {
return Some(MappedTensor::Dense {
hf: hf_name.to_string(),
gguf: "output.weight".to_string(),
})
}
_ => {}
}
let stripped = hf_name.strip_prefix("model.layers.")?;
let dot = stripped.find('.')?;
let (layer_str, rest_with_dot) = stripped.split_at(dot);
let layer: u32 = layer_str.parse().ok()?;
if layer.to_string() != layer_str {
return None; }
let rest = &rest_with_dot[1..];
if let Some(suffix) = match_dense_suffix(rest) {
return Some(MappedTensor::Dense {
hf: hf_name.to_string(),
gguf: format!("blk.{layer}.{suffix}"),
});
}
match rest {
"block_sparse_moe.gate.weight" => {
return Some(MappedTensor::Router {
hf: hf_name.to_string(),
gguf: format!("blk.{layer}.ffn_gate_inp.weight"),
});
}
"block_sparse_moe.e_score_correction_bias" | "block_sparse_moe.e_score_correction.bias" => {
return Some(MappedTensor::Router {
hf: hf_name.to_string(),
gguf: format!("blk.{layer}.exp_probs_b.bias"),
});
}
"block_sparse_moe.e_score_correction" | "block_sparse_moe.e_score_correction.weight" => {
return Some(MappedTensor::Router {
hf: hf_name.to_string(),
gguf: format!("blk.{layer}.exp_probs_b.weight"),
});
}
_ => {}
}
if let Some(rest2) = rest.strip_prefix("block_sparse_moe.experts.") {
let dot2 = rest2.find('.')?;
let (eid_str, after) = rest2.split_at(dot2);
let expert: u32 = eid_str.parse().ok()?;
if expert.to_string() != eid_str {
return None;
}
let role = match after {
".w1.weight" => ExpertRole::Gate,
".w2.weight" => ExpertRole::Down,
".w3.weight" => ExpertRole::Up,
_ => return None,
};
let gguf_stacked = format!("blk.{layer}.{}_exps.weight", role.gguf_prefix());
return Some(MappedTensor::ExpertWeight {
hf: hf_name.to_string(),
layer,
expert,
role,
gguf_stacked,
});
}
None
}
fn match_dense_suffix(rest: &str) -> Option<&'static str> {
Some(match rest {
"input_layernorm.weight" => "attn_norm.weight",
"post_attention_layernorm.weight" => "ffn_norm.weight",
"self_attn.q_proj.weight" => "attn_q.weight",
"self_attn.k_proj.weight" => "attn_k.weight",
"self_attn.v_proj.weight" => "attn_v.weight",
"self_attn.o_proj.weight" => "attn_output.weight",
"self_attn.q_norm.weight" => "attn_q_norm.weight",
"self_attn.k_norm.weight" => "attn_k_norm.weight",
_ => return None,
})
}
pub fn build_metadata(
config: &serde_json::Value,
file_type: u32,
model_card: Option<&crate::convert::model_card::ModelCard>,
sampling: Option<&crate::convert::model_card::SamplingConfig>,
model_dir_basename: Option<&str>,
size_label_override: Option<&str>,
) -> Vec<(String, MetaValue)> {
use crate::convert::model_card::{
emit_general_postlude, emit_general_prelude, get_model_id_components,
};
let raw_name = model_dir_basename
.map(|s| s.to_string())
.or_else(|| {
config
.get("_name_or_path")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.unwrap_or_else(|| "model".to_string());
let id_components = get_model_id_components(&raw_name);
let display_name = id_components
.name
.clone()
.unwrap_or_else(|| raw_name.clone());
let hidden_size = config["hidden_size"]
.as_u64()
.expect("config.json missing required key `hidden_size`") as u32;
let n_layers = config["num_hidden_layers"]
.as_u64()
.expect("config.json missing required key `num_hidden_layers`") as u32;
let n_head = config["num_attention_heads"]
.as_u64()
.expect("config.json missing required key `num_attention_heads`") as u32;
let ctx_len = config["max_position_embeddings"]
.as_u64()
.expect("config.json missing required key `max_position_embeddings`")
as u32;
let rms_eps = config["rms_norm_eps"]
.as_f64()
.expect("config.json missing required key `rms_norm_eps`") as f32;
let ffn_len = config["intermediate_size"]
.as_u64()
.expect("config.json missing required key `intermediate_size`") as u32;
let rotary_dim = config["rotary_dim"]
.as_u64()
.expect("config.json missing required key `rotary_dim`") as u32;
let n_head_kv = config
.get("num_key_value_heads")
.and_then(|v| v.as_u64())
.map(|x| x as u32)
.unwrap_or(n_head);
let rope_theta = config
.get("rope_theta")
.and_then(|v| v.as_f64())
.unwrap_or(10000.0) as f32;
let head_dim = config
.get("head_dim")
.and_then(|v| v.as_u64())
.map(|x| x as u32);
let n_experts = config
.get("num_local_experts")
.and_then(|v| v.as_u64())
.or_else(|| config.get("num_experts").and_then(|v| v.as_u64()))
.map(|x| x as u32);
let n_experts_used = config
.get("num_experts_per_tok")
.and_then(|v| v.as_u64())
.map(|x| x as u32);
let expert_gating_func: Option<u32> = config
.get("scoring_func")
.or_else(|| config.get("score_function"))
.or_else(|| config.get("score_func"))
.or_else(|| config.get("moe_router_activation"))
.or_else(|| config.get("moe_router_activation_func"))
.and_then(|v| v.as_str())
.and_then(|s| match s {
"sigmoid" => Some(2),
"softmax" => Some(1),
_ => None,
});
let mut kv: Vec<(String, MetaValue)> = emit_general_prelude(
"minimax-m2",
display_name,
&id_components,
size_label_override,
model_card,
sampling,
);
kv.push(("minimax-m2.block_count".into(), MetaValue::U32(n_layers)));
kv.push(("minimax-m2.context_length".into(), MetaValue::U32(ctx_len)));
kv.push((
"minimax-m2.embedding_length".into(),
MetaValue::U32(hidden_size),
));
kv.push((
"minimax-m2.feed_forward_length".into(),
MetaValue::U32(ffn_len),
));
kv.push((
"minimax-m2.attention.head_count".into(),
MetaValue::U32(n_head),
));
kv.push((
"minimax-m2.attention.head_count_kv".into(),
MetaValue::U32(n_head_kv),
));
kv.push((
"minimax-m2.rope.freq_base".into(),
MetaValue::F32(rope_theta),
));
kv.push((
"minimax-m2.attention.layer_norm_rms_epsilon".into(),
MetaValue::F32(rms_eps),
));
if let Some(n) = n_experts {
kv.push(("minimax-m2.expert_count".into(), MetaValue::U32(n)));
}
if let Some(n) = n_experts_used {
kv.push(("minimax-m2.expert_used_count".into(), MetaValue::U32(n)));
}
if let Some(g) = expert_gating_func {
kv.push(("minimax-m2.expert_gating_func".into(), MetaValue::U32(g)));
}
if let Some(hd) = head_dim {
kv.push(("minimax-m2.attention.key_length".into(), MetaValue::U32(hd)));
kv.push((
"minimax-m2.attention.value_length".into(),
MetaValue::U32(hd),
));
}
kv.push((
"minimax-m2.expert_feed_forward_length".into(),
MetaValue::U32(ffn_len),
));
kv.push((
"minimax-m2.rope.dimension_count".into(),
MetaValue::U32(rotary_dim),
));
kv.extend(emit_general_postlude(file_type));
kv
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn minimax_m2_dense_tensor_name_round_trip() {
let cases: &[(&str, &str)] = &[
("model.embed_tokens.weight", "token_embd.weight"),
("model.norm.weight", "output_norm.weight"),
("lm_head.weight", "output.weight"),
(
"model.layers.0.input_layernorm.weight",
"blk.0.attn_norm.weight",
),
(
"model.layers.15.post_attention_layernorm.weight",
"blk.15.ffn_norm.weight",
),
(
"model.layers.31.self_attn.q_proj.weight",
"blk.31.attn_q.weight",
),
(
"model.layers.7.self_attn.k_proj.weight",
"blk.7.attn_k.weight",
),
(
"model.layers.7.self_attn.v_proj.weight",
"blk.7.attn_v.weight",
),
(
"model.layers.7.self_attn.o_proj.weight",
"blk.7.attn_output.weight",
),
(
"model.layers.3.self_attn.q_norm.weight",
"blk.3.attn_q_norm.weight",
),
(
"model.layers.3.self_attn.k_norm.weight",
"blk.3.attn_k_norm.weight",
),
];
for &(hf, expected_gguf) in cases {
match map_tensor_name(hf) {
Some(MappedTensor::Dense { hf: h, gguf }) => {
assert_eq!(h, hf);
assert_eq!(gguf, expected_gguf);
}
other => {
panic!("map_tensor_name({hf:?}) = {other:?}, want Dense({expected_gguf:?})")
}
}
}
}
#[test]
fn minimax_m2_expert_tensor_mapping() {
let cases: &[(&str, u32, u32, ExpertRole, &str)] = &[
(
"model.layers.0.block_sparse_moe.experts.0.w1.weight",
0,
0,
ExpertRole::Gate,
"blk.0.ffn_gate_exps.weight",
),
(
"model.layers.0.block_sparse_moe.experts.0.w2.weight",
0,
0,
ExpertRole::Down,
"blk.0.ffn_down_exps.weight",
),
(
"model.layers.0.block_sparse_moe.experts.0.w3.weight",
0,
0,
ExpertRole::Up,
"blk.0.ffn_up_exps.weight",
),
(
"model.layers.15.block_sparse_moe.experts.7.w1.weight",
15,
7,
ExpertRole::Gate,
"blk.15.ffn_gate_exps.weight",
),
(
"model.layers.31.block_sparse_moe.experts.255.w3.weight",
31,
255,
ExpertRole::Up,
"blk.31.ffn_up_exps.weight",
),
];
for &(hf, exp_layer, exp_expert, exp_role, exp_stacked) in cases {
match map_tensor_name(hf) {
Some(MappedTensor::ExpertWeight {
hf: h,
layer,
expert,
role,
gguf_stacked,
}) => {
assert_eq!(h, hf);
assert_eq!(layer, exp_layer);
assert_eq!(expert, exp_expert);
assert_eq!(role, exp_role);
assert_eq!(gguf_stacked, exp_stacked);
}
other => panic!(
"map_tensor_name({hf:?}) = {other:?}, want ExpertWeight({exp_stacked:?})"
),
}
}
}
#[test]
fn minimax_m2_router_tensor_mapping() {
match map_tensor_name("model.layers.0.block_sparse_moe.gate.weight") {
Some(MappedTensor::Router { gguf, .. }) => {
assert_eq!(gguf, "blk.0.ffn_gate_inp.weight");
}
other => panic!("router gate: got {other:?}"),
}
match map_tensor_name("model.layers.5.block_sparse_moe.e_score_correction") {
Some(MappedTensor::Router { gguf, .. }) => {
assert_eq!(gguf, "blk.5.exp_probs_b.weight");
}
other => panic!("e_score_correction: got {other:?}"),
}
}
#[test]
fn minimax_m2_tensor_name_rejects_unknown_kinds() {
assert!(map_tensor_name("model.unknown.weight").is_none());
assert!(map_tensor_name("transformer.layers.0.attn.weight").is_none());
assert!(map_tensor_name("model.layers.0.self_attn.q_proj.bias").is_none());
assert!(map_tensor_name("model.layers.01.input_layernorm.weight").is_none());
assert!(map_tensor_name("model.layers.0.block_sparse_moe.experts.01.w1.weight").is_none());
assert!(map_tensor_name("model.layers.0.block_sparse_moe.experts.0.w4.weight").is_none());
assert!(map_tensor_name("model.layers.0.mlp.gate_proj.weight").is_none());
assert!(map_tensor_name("model.layers.0.mlp.up_proj.weight").is_none());
assert!(map_tensor_name("model.layers.0.mlp.down_proj.weight").is_none());
assert!(map_tensor_name("model.layers.0.unknown.weight").is_none());
}
#[test]
fn minimax_m2_metadata_built_from_config() {
let cfg = json!({
"_name_or_path": "MiniMaxAI/MiniMax-M2",
"hidden_size": 6144,
"num_hidden_layers": 80,
"intermediate_size": 9216,
"num_attention_heads": 64,
"num_key_value_heads": 8,
"max_position_embeddings": 196608,
"rms_norm_eps": 1.0e-6,
"rope_theta": 5_000_000.0,
"rotary_dim": 64,
"num_experts_per_tok": 8,
"num_local_experts": 256,
"quantization_config": {
"quant_method": "fp8",
"weight_block_size": [128, 128],
}
});
let kv = build_metadata(&cfg, 17 , None, None, None, None);
let by_key: std::collections::HashMap<_, _> =
kv.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
assert_eq!(kv.len(), 18);
assert_eq!(
by_key["general.architecture"],
MetaValue::String("minimax-m2".into())
);
assert!(
matches!(by_key.get("general.name"), Some(MetaValue::String(_))),
"general.name must be present"
);
assert_eq!(by_key["minimax-m2.context_length"], MetaValue::U32(196608));
assert_eq!(by_key["minimax-m2.embedding_length"], MetaValue::U32(6144));
assert_eq!(by_key["minimax-m2.block_count"], MetaValue::U32(80));
assert_eq!(
by_key["minimax-m2.feed_forward_length"],
MetaValue::U32(9216)
);
assert_eq!(
by_key["minimax-m2.expert_feed_forward_length"],
MetaValue::U32(9216)
);
assert_eq!(
by_key["minimax-m2.attention.head_count"],
MetaValue::U32(64)
);
assert_eq!(
by_key["minimax-m2.attention.head_count_kv"],
MetaValue::U32(8)
);
assert_eq!(
by_key["minimax-m2.attention.layer_norm_rms_epsilon"],
MetaValue::F32(1.0e-6)
);
assert_eq!(
by_key["minimax-m2.rope.freq_base"],
MetaValue::F32(5_000_000.0)
);
assert_eq!(
by_key["minimax-m2.rope.dimension_count"],
MetaValue::U32(64)
);
assert_eq!(by_key["minimax-m2.expert_count"], MetaValue::U32(256));
assert_eq!(by_key["minimax-m2.expert_used_count"], MetaValue::U32(8));
assert_eq!(by_key["general.file_type"], MetaValue::U32(17));
}
#[test]
fn minimax_m2_metadata_optional_key_defaults() {
let cfg = json!({
"hidden_size": 32,
"num_hidden_layers": 2,
"intermediate_size": 64,
"num_attention_heads": 4,
"max_position_embeddings": 2048,
"rms_norm_eps": 1.0e-5,
"rotary_dim": 8,
"num_experts": 4, });
let kv = build_metadata(&cfg, 0, None, None, None, None);
let by_key: std::collections::HashMap<_, _> =
kv.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
assert_eq!(
by_key["general.name"],
MetaValue::String("Model".into()),
"name defaults to title-cased 'Model'"
);
assert_eq!(
by_key["minimax-m2.attention.head_count_kv"],
MetaValue::U32(4),
"head_count_kv defaults to head_count"
);
assert_eq!(
by_key["minimax-m2.rope.freq_base"],
MetaValue::F32(10000.0),
"rope_theta defaults to 10000.0"
);
assert_eq!(
by_key["minimax-m2.expert_count"],
MetaValue::U32(4),
"num_experts (alias) accepted when num_local_experts absent"
);
assert!(
!by_key.contains_key("minimax-m2.expert_used_count"),
"expert_used_count omitted when num_experts_per_tok absent"
);
}
#[test]
fn minimax_m2_mapper_is_dtype_agnostic() {
let name = "model.layers.0.block_sparse_moe.experts.5.w1.weight";
match map_tensor_name(name) {
Some(MappedTensor::ExpertWeight {
layer,
expert,
role,
..
}) => {
assert_eq!(layer, 0);
assert_eq!(expert, 5);
assert_eq!(role, ExpertRole::Gate);
}
other => panic!("expert mapping: got {other:?}"),
}
}
}