use crate::backends::gguf::types::MetaValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MappedTensor {
Direct(String),
ExpertGroup {
gguf_name: String,
layer: usize,
expert_index: usize,
kind: ExpertKind,
},
Drop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpertKind {
Gate,
Up,
Down,
}
pub fn map_tensor_name(hf_name: &str) -> Option<MappedTensor> {
match hf_name {
"model.embed_tokens.weight" => {
return Some(MappedTensor::Direct("token_embd.weight".to_string()));
}
"model.norm.weight" => {
return Some(MappedTensor::Direct("output_norm.weight".to_string()));
}
"lm_head.weight" => {
return Some(MappedTensor::Direct("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: usize = layer_str.parse().ok()?;
if layer.to_string() != layer_str {
return None;
}
let rest = &rest_with_dot[1..];
let direct_suffix = match rest {
"input_layernorm.weight" => Some("attn_norm.weight"),
"post_attention_layernorm.weight" => Some("ffn_norm.weight"),
"self_attn.q_proj.weight" => Some("attn_q.weight"),
"self_attn.k_proj.weight" => Some("attn_k.weight"),
"self_attn.v_proj.weight" => Some("attn_v.weight"),
"self_attn.o_proj.weight" => Some("attn_output.weight"),
"self_attn.q_norm.weight" => Some("attn_q_norm.weight"),
"self_attn.k_norm.weight" => Some("attn_k_norm.weight"),
"mlp.gate.weight" => Some("ffn_gate_inp.weight"),
_ => None,
};
if let Some(suffix) = direct_suffix {
return Some(MappedTensor::Direct(format!("blk.{layer}.{suffix}")));
}
let expert_rest = rest.strip_prefix("mlp.experts.")?;
let dot2 = expert_rest.find('.')?;
let (expert_str, kind_with_dot) = expert_rest.split_at(dot2);
let expert_index: usize = expert_str.parse().ok()?;
if expert_index.to_string() != expert_str {
return None;
}
let kind_tail = &kind_with_dot[1..];
let (kind, gguf_suffix) = match kind_tail {
"gate_proj.weight" => (ExpertKind::Gate, "ffn_gate_exps.weight"),
"up_proj.weight" => (ExpertKind::Up, "ffn_up_exps.weight"),
"down_proj.weight" => (ExpertKind::Down, "ffn_down_exps.weight"),
_ => return None,
};
Some(MappedTensor::ExpertGroup {
gguf_name: format!("blk.{layer}.{gguf_suffix}"),
layer,
expert_index,
kind,
})
}
pub fn build_metadata(config: &serde_json::Value, file_type: u32) -> Vec<(String, MetaValue)> {
let name = config
.get("_name_or_path")
.and_then(|v| v.as_str())
.unwrap_or("model")
.to_string();
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 n_head_kv = config["num_key_value_heads"]
.as_u64()
.expect("config.json missing required key `num_key_value_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 moe_ffn = config["moe_intermediate_size"]
.as_u64()
.expect("config.json missing required key `moe_intermediate_size`")
as u32;
let n_experts = config["num_experts"]
.as_u64()
.expect("config.json missing required key `num_experts`") as u32;
let n_experts_used = config["num_experts_per_tok"]
.as_u64()
.expect("config.json missing required key `num_experts_per_tok`")
as u32;
let rope_theta = config
.get("rope_theta")
.and_then(|v| v.as_f64())
.unwrap_or(10000.0) as f32;
vec![
(
"general.architecture".into(),
MetaValue::String("qwen3moe".into()),
),
("general.name".into(), MetaValue::String(name)),
("qwen3moe.context_length".into(), MetaValue::U32(ctx_len)),
(
"qwen3moe.embedding_length".into(),
MetaValue::U32(hidden_size),
),
("qwen3moe.block_count".into(), MetaValue::U32(n_layers)),
(
"qwen3moe.feed_forward_length".into(),
MetaValue::U32(moe_ffn),
),
(
"qwen3moe.attention.head_count".into(),
MetaValue::U32(n_head),
),
(
"qwen3moe.attention.head_count_kv".into(),
MetaValue::U32(n_head_kv),
),
(
"qwen3moe.attention.layer_norm_rms_epsilon".into(),
MetaValue::F32(rms_eps),
),
("qwen3moe.rope.freq_base".into(), MetaValue::F32(rope_theta)),
("qwen3moe.expert_count".into(), MetaValue::U32(n_experts)),
(
"qwen3moe.expert_used_count".into(),
MetaValue::U32(n_experts_used),
),
(
"qwen3moe.expert_feed_forward_length".into(),
MetaValue::U32(moe_ffn),
),
("general.file_type".into(), MetaValue::U32(file_type)),
]
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn qwen35moe_direct_tensor_names_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.23.input_layernorm.weight",
"blk.23.attn_norm.weight",
),
(
"model.layers.47.input_layernorm.weight",
"blk.47.attn_norm.weight",
),
(
"model.layers.0.post_attention_layernorm.weight",
"blk.0.ffn_norm.weight",
),
(
"model.layers.7.self_attn.q_proj.weight",
"blk.7.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.7.self_attn.q_norm.weight",
"blk.7.attn_q_norm.weight",
),
(
"model.layers.7.self_attn.k_norm.weight",
"blk.7.attn_k_norm.weight",
),
(
"model.layers.3.mlp.gate.weight",
"blk.3.ffn_gate_inp.weight",
),
];
for &(hf, expected_gguf) in cases {
let got = map_tensor_name(hf);
assert_eq!(
got,
Some(MappedTensor::Direct(expected_gguf.to_string())),
"map_tensor_name({hf:?}) = {got:?}, want Direct({expected_gguf:?})"
);
}
}
#[test]
fn qwen35moe_expert_tensor_fusion_classified() {
let cases: &[(&str, usize, usize, ExpertKind, &str)] = &[
(
"model.layers.0.mlp.experts.0.gate_proj.weight",
0,
0,
ExpertKind::Gate,
"blk.0.ffn_gate_exps.weight",
),
(
"model.layers.12.mlp.experts.63.up_proj.weight",
12,
63,
ExpertKind::Up,
"blk.12.ffn_up_exps.weight",
),
(
"model.layers.47.mlp.experts.127.down_proj.weight",
47,
127,
ExpertKind::Down,
"blk.47.ffn_down_exps.weight",
),
(
"model.layers.15.mlp.experts.1.gate_proj.weight",
15,
1,
ExpertKind::Gate,
"blk.15.ffn_gate_exps.weight",
),
];
for &(hf, want_layer, want_expert, want_kind, want_gguf) in cases {
let got = map_tensor_name(hf);
match got {
Some(MappedTensor::ExpertGroup {
gguf_name,
layer,
expert_index,
kind,
}) => {
assert_eq!(layer, want_layer, "layer for {hf:?}");
assert_eq!(expert_index, want_expert, "expert_index for {hf:?}");
assert_eq!(kind, want_kind, "kind for {hf:?}");
assert_eq!(gguf_name, want_gguf, "gguf_name for {hf:?}");
}
other => panic!("map_tensor_name({hf:?}) = {other:?}, want ExpertGroup"),
}
}
}
#[test]
fn qwen35moe_router_vs_expert_gate_disambiguated() {
assert_eq!(
map_tensor_name("model.layers.5.mlp.gate.weight"),
Some(MappedTensor::Direct("blk.5.ffn_gate_inp.weight".into())),
"mlp.gate.weight must map to ffn_gate_inp (router), NOT ffn_gate_exps"
);
let expert = map_tensor_name("model.layers.5.mlp.experts.0.gate_proj.weight");
assert!(
matches!(
expert,
Some(MappedTensor::ExpertGroup {
kind: ExpertKind::Gate,
..
})
),
"experts.<E>.gate_proj.weight must map to ExpertGroup(Gate), got {expert:?}"
);
let router = match map_tensor_name("model.layers.5.mlp.gate.weight") {
Some(MappedTensor::Direct(n)) => n,
_ => panic!(),
};
let per_expert = match map_tensor_name("model.layers.5.mlp.experts.0.gate_proj.weight") {
Some(MappedTensor::ExpertGroup { gguf_name, .. }) => gguf_name,
_ => panic!(),
};
assert_ne!(
router, per_expert,
"router gate and per-expert gate MUST map to distinct GGUF tensors"
);
}
#[test]
fn qwen35moe_tensor_name_rejects_unknown_kinds() {
assert_eq!(map_tensor_name("model.unknown.weight"), None);
assert_eq!(map_tensor_name("transformer.layers.0.attn.weight"), None);
assert_eq!(
map_tensor_name("model.layers.0.self_attn.q_proj.bias"),
None
);
assert_eq!(
map_tensor_name("model.layers.01.self_attn.q_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers..self_attn.q_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.experts..gate_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.experts.00.gate_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.experts.0.unknown_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.shared_expert.gate_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.shared_expert.up_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.shared_expert.down_proj.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.mlp.shared_expert_gate.weight"),
None
);
assert_eq!(
map_tensor_name("model.layers.0.self_attn.rotary_emb.inv_freq"),
None
);
}
#[test]
fn qwen35moe_metadata_built_from_config() {
let cfg = json!({
"_name_or_path": "Qwen/Qwen3-30B-A3B",
"hidden_size": 2048,
"num_hidden_layers": 48,
"intermediate_size": 6144,
"moe_intermediate_size": 768,
"num_attention_heads": 32,
"num_key_value_heads": 4,
"max_position_embeddings": 32768,
"rms_norm_eps": 1.0e-6,
"rope_theta": 1_000_000.0,
"num_experts": 128,
"num_experts_per_tok": 8,
});
let kv = build_metadata(&cfg, 17 );
assert_eq!(kv.len(), 14, "Qwen3MoE emits 14 KV pairs at v1");
let by_key: std::collections::HashMap<_, _> =
kv.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
assert_eq!(
by_key["general.architecture"],
MetaValue::String("qwen3moe".into())
);
assert_eq!(
by_key["general.name"],
MetaValue::String("Qwen/Qwen3-30B-A3B".into())
);
assert_eq!(by_key["qwen3moe.context_length"], MetaValue::U32(32768));
assert_eq!(by_key["qwen3moe.embedding_length"], MetaValue::U32(2048));
assert_eq!(by_key["qwen3moe.block_count"], MetaValue::U32(48));
assert_eq!(
by_key["qwen3moe.feed_forward_length"],
MetaValue::U32(768),
"feed_forward_length must come from moe_intermediate_size, not intermediate_size"
);
assert_eq!(by_key["qwen3moe.attention.head_count"], MetaValue::U32(32));
assert_eq!(
by_key["qwen3moe.attention.head_count_kv"],
MetaValue::U32(4)
);
assert_eq!(
by_key["qwen3moe.attention.layer_norm_rms_epsilon"],
MetaValue::F32(1.0e-6)
);
assert_eq!(
by_key["qwen3moe.rope.freq_base"],
MetaValue::F32(1_000_000.0)
);
assert_eq!(by_key["qwen3moe.expert_count"], MetaValue::U32(128));
assert_eq!(by_key["qwen3moe.expert_used_count"], MetaValue::U32(8));
assert_eq!(
by_key["qwen3moe.expert_feed_forward_length"],
MetaValue::U32(768)
);
assert_eq!(by_key["general.file_type"], MetaValue::U32(17));
}
#[test]
fn qwen35moe_metadata_optional_key_defaults() {
let cfg = json!({
"hidden_size": 128,
"num_hidden_layers": 2,
"intermediate_size": 256,
"moe_intermediate_size": 64,
"num_attention_heads": 4,
"num_key_value_heads": 2,
"max_position_embeddings": 2048,
"rms_norm_eps": 1.0e-6,
"num_experts": 4,
"num_experts_per_tok": 2,
});
let kv = build_metadata(&cfg, 0);
assert_eq!(kv.len(), 14, "Qwen3MoE always emits 14 KV pairs");
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 'model' when _name_or_path absent"
);
assert_eq!(
by_key["qwen3moe.rope.freq_base"],
MetaValue::F32(10000.0),
"rope_theta defaults to 10000.0"
);
assert_eq!(
by_key["general.architecture"],
MetaValue::String("qwen3moe".into())
);
assert_eq!(by_key["general.file_type"], MetaValue::U32(0));
}
}