use crate::backends::gguf::types::MetaValue;
use crate::convert::arch::bake::{BakeOp, SplitHalf};
use crate::convert::arch::qwen35moe::ExpertKind;
use crate::convert::cli_driver::SplitOutput;
#[derive(Debug, Clone)]
pub struct Qwen35MoeFullCtx {
pub num_hidden_layers: usize,
pub num_experts: usize,
pub moe_intermediate_size: usize,
pub hidden_size: usize,
pub linear_num_key_heads: usize,
pub linear_num_value_heads: usize,
pub linear_key_head_dim: usize,
pub linear_value_head_dim: usize,
pub multimodal_wrapping: bool,
pub drop_mtp: bool,
}
impl Qwen35MoeFullCtx {
pub fn num_v_per_k(&self) -> usize {
self.linear_num_value_heads / self.linear_num_key_heads
}
}
#[derive(Debug, Clone)]
pub enum MappedTensor {
Direct(String),
DirectWithBake { gguf_name: String, bake: BakeOp },
SplitInto(Vec<SplitOutput>),
ExpertGroup {
gguf_name: String,
layer: usize,
expert_index: usize,
kind: ExpertKind,
},
Drop,
}
pub fn map_tensor_name(
hf_name: &str,
hf_shape: &[usize],
ctx: &Qwen35MoeFullCtx,
) -> Option<MappedTensor> {
let canonical = if ctx.multimodal_wrapping {
if let Some(stripped) = hf_name.strip_prefix("model.language_model.") {
Some(format!("model.{stripped}"))
} else if hf_name.starts_with("model.visual.") {
return Some(MappedTensor::Drop);
} else if hf_name == "lm_head.weight" || hf_name.starts_with("mtp.") {
None
} else {
return None;
}
} else {
None
};
let canonical_ref: &str = canonical.as_deref().unwrap_or(hf_name);
if let Some(rest) = canonical_ref.strip_prefix("mtp.") {
if ctx.drop_mtp {
return Some(MappedTensor::Drop);
}
return map_mtp(rest, hf_shape, ctx);
}
match canonical_ref {
"model.embed_tokens.weight" => {
return Some(MappedTensor::Direct("token_embd.weight".into()));
}
"model.norm.weight" => {
return Some(MappedTensor::DirectWithBake {
gguf_name: "output_norm.weight".into(),
bake: BakeOp::AddOne,
});
}
"lm_head.weight" => {
return Some(MappedTensor::Direct("output.weight".into()));
}
_ => {}
}
let stripped = canonical_ref.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..];
map_per_block(layer, rest, hf_shape, ctx)
}
fn map_per_block(
layer: usize,
rest: &str,
hf_shape: &[usize],
ctx: &Qwen35MoeFullCtx,
) -> Option<MappedTensor> {
let blk = |suffix: &str| format!("blk.{layer}.{suffix}");
if rest == "input_layernorm.weight" {
return Some(MappedTensor::DirectWithBake {
gguf_name: blk("attn_norm.weight"),
bake: BakeOp::AddOne,
});
}
if rest == "post_attention_layernorm.weight" {
return Some(MappedTensor::DirectWithBake {
gguf_name: blk("post_attention_norm.weight"),
bake: BakeOp::AddOne,
});
}
let full_attn_map = [
("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"),
];
for (suffix, gguf_suffix) in full_attn_map {
if rest == suffix {
return Some(MappedTensor::Direct(blk(gguf_suffix)));
}
}
if rest == "self_attn.q_norm.weight" {
return Some(MappedTensor::DirectWithBake {
gguf_name: blk("attn_q_norm.weight"),
bake: BakeOp::AddOne,
});
}
if rest == "self_attn.k_norm.weight" {
return Some(MappedTensor::DirectWithBake {
gguf_name: blk("attn_k_norm.weight"),
bake: BakeOp::AddOne,
});
}
if let Some(la_rest) = rest.strip_prefix("linear_attn.") {
return map_linear_attn(layer, la_rest, hf_shape, ctx);
}
if let Some(mlp_rest) = rest.strip_prefix("mlp.") {
return map_mlp(layer, mlp_rest, ctx);
}
None
}
fn map_linear_attn(
layer: usize,
la_rest: &str,
hf_shape: &[usize],
ctx: &Qwen35MoeFullCtx,
) -> Option<MappedTensor> {
let blk = |suffix: &str| format!("blk.{layer}.{suffix}");
let nk = ctx.linear_num_key_heads;
let nv_per_k = ctx.num_v_per_k();
let vd = ctx.linear_value_head_dim;
let kd = ctx.linear_key_head_dim;
let reorder_required = ctx.linear_num_key_heads != ctx.linear_num_value_heads;
match la_rest {
"norm.weight" => Some(MappedTensor::Direct(blk("ssm_norm.weight"))),
"dt_bias" => {
if reorder_required {
Some(MappedTensor::DirectWithBake {
gguf_name: blk("ssm_dt.bias"),
bake: BakeOp::ReorderVHeads {
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim: 1,
slice: None,
},
})
} else {
Some(MappedTensor::Direct(blk("ssm_dt.bias")))
}
}
"in_proj_a.weight" | "in_proj_b.weight" => {
let cols = if hf_shape.len() >= 2 { hf_shape[1] } else { 1 };
let gguf_name = if la_rest == "in_proj_a.weight" {
blk("ssm_alpha.weight")
} else {
blk("ssm_beta.weight")
};
if reorder_required {
Some(MappedTensor::DirectWithBake {
gguf_name,
bake: BakeOp::ReorderVHeads {
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim: cols,
slice: None,
},
})
} else {
Some(MappedTensor::Direct(gguf_name))
}
}
"A_log" => {
let neg_exp = BakeOp::NegExp;
let bake = if reorder_required {
BakeOp::Sequence(vec![
BakeOp::ReorderVHeads {
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim: 1,
slice: None,
},
neg_exp,
])
} else {
neg_exp
};
Some(MappedTensor::DirectWithBake {
gguf_name: blk("ssm_a"),
bake,
})
}
"conv1d.weight" => {
let kernel = if hf_shape.len() >= 3 {
hf_shape[2]
} else {
if hf_shape.len() >= 2 {
hf_shape[1]
} else {
1
}
};
let bake = if reorder_required {
let qk_channels = kd * nk * 2;
let v_off = qk_channels * kernel;
let v_len = nk * nv_per_k * vd * kernel;
BakeOp::Sequence(vec![
BakeOp::Squeeze,
BakeOp::ReorderVHeads {
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim: vd * kernel,
slice: Some(v_off..(v_off + v_len)),
},
])
} else {
BakeOp::Squeeze
};
Some(MappedTensor::DirectWithBake {
gguf_name: blk("ssm_conv1d.weight"),
bake,
})
}
"in_proj_qkv.weight" => {
let cols = if hf_shape.len() >= 2 { hf_shape[1] } else { 1 };
let nk = ctx.linear_num_key_heads;
let nv_per_k = ctx.num_v_per_k();
let qd = ctx.linear_key_head_dim;
let kd = ctx.linear_key_head_dim;
let vd = ctx.linear_value_head_dim;
let v_slice_start = (qd + kd) * nk * cols;
let v_slice_len = nv_per_k * vd * nk * cols;
Some(MappedTensor::DirectWithBake {
gguf_name: blk("attn_qkv.weight"),
bake: BakeOp::ReorderVHeads {
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim: vd * cols,
slice: Some(v_slice_start..(v_slice_start + v_slice_len)),
},
})
}
"in_proj_z.weight" => {
let cols = if hf_shape.len() >= 2 { hf_shape[1] } else { 1 };
let nk = ctx.linear_num_key_heads;
let nv_per_k = ctx.num_v_per_k();
let vd = ctx.linear_value_head_dim;
Some(MappedTensor::DirectWithBake {
gguf_name: blk("attn_gate.weight"),
bake: BakeOp::ReorderVHeads {
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim: vd * cols,
slice: None,
},
})
}
"out_proj.weight" => {
if hf_shape.len() < 2 {
return None;
}
let rows = hf_shape[0];
let nk = ctx.linear_num_key_heads;
let nv_per_k = ctx.num_v_per_k();
let vd = ctx.linear_value_head_dim;
if hf_shape[1] != nk * nv_per_k * vd {
return None;
}
Some(MappedTensor::DirectWithBake {
gguf_name: blk("ssm_out.weight"),
bake: BakeOp::ReorderVHeadsPerRow {
row_count: rows,
num_k_heads: nk,
num_v_per_k: nv_per_k,
head_dim_in_row: vd,
},
})
}
_ => None,
}
}
fn map_mlp(layer: usize, mlp_rest: &str, ctx: &Qwen35MoeFullCtx) -> Option<MappedTensor> {
let blk = |suffix: &str| format!("blk.{layer}.{suffix}");
match mlp_rest {
"gate.weight" => Some(MappedTensor::Direct(blk("ffn_gate_inp.weight"))),
"shared_expert.gate_proj.weight" => {
Some(MappedTensor::Direct(blk("ffn_gate_shexp.weight")))
}
"shared_expert.up_proj.weight" => Some(MappedTensor::Direct(blk("ffn_up_shexp.weight"))),
"shared_expert.down_proj.weight" => {
Some(MappedTensor::Direct(blk("ffn_down_shexp.weight")))
}
"shared_expert_gate.weight" => Some(MappedTensor::DirectWithBake {
gguf_name: blk("ffn_gate_inp_shexp.weight"),
bake: BakeOp::Squeeze,
}),
"experts.down_proj" | "experts.down_proj.weight" => {
Some(MappedTensor::Direct(blk("ffn_down_exps.weight")))
}
rest if rest.starts_with("experts.")
&& rest != "experts.gate_up_proj"
&& rest != "experts.gate_up_proj.weight"
&& rest != "experts.down_proj"
&& rest != "experts.down_proj.weight" =>
{
let expert_rest = rest.strip_prefix("experts.")?;
let dot = expert_rest.find('.')?;
let (expert_str, kind_with_dot) = expert_rest.split_at(dot);
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: blk(gguf_suffix),
layer,
expert_index,
kind,
})
}
"experts.gate_up_proj" | "experts.gate_up_proj.weight" => {
let n_expert = ctx.num_experts;
let n_ff = ctx.moe_intermediate_size;
let n_embd = ctx.hidden_size;
let two_nff = 2 * n_ff;
let gate_op = BakeOp::SplitAxisHalf {
outer_count: n_expert,
axis_size: two_nff,
inner_count: n_embd,
half: SplitHalf::First,
};
let up_op = BakeOp::SplitAxisHalf {
outer_count: n_expert,
axis_size: two_nff,
inner_count: n_embd,
half: SplitHalf::Second,
};
let gguf_shape = vec![n_embd, n_ff, n_expert];
Some(MappedTensor::SplitInto(vec![
SplitOutput {
gguf_name: blk("ffn_gate_exps.weight"),
gguf_shape: gguf_shape.clone(),
bake: gate_op,
},
SplitOutput {
gguf_name: blk("ffn_up_exps.weight"),
gguf_shape,
bake: up_op,
},
]))
}
_ => None,
}
}
fn map_mtp(rest: &str, hf_shape: &[usize], ctx: &Qwen35MoeFullCtx) -> Option<MappedTensor> {
let n_layer = ctx.num_hidden_layers;
let mtp_blk = |suffix: &str| format!("blk.{n_layer}.{suffix}");
match rest {
"fc.weight" => return Some(MappedTensor::Direct(mtp_blk("nextn.eh_proj.weight"))),
"pre_fc_norm_embedding.weight" => {
return Some(MappedTensor::DirectWithBake {
gguf_name: mtp_blk("nextn.enorm.weight"),
bake: BakeOp::AddOne,
});
}
"pre_fc_norm_hidden.weight" => {
return Some(MappedTensor::DirectWithBake {
gguf_name: mtp_blk("nextn.hnorm.weight"),
bake: BakeOp::AddOne,
});
}
"norm.weight" => {
return Some(MappedTensor::DirectWithBake {
gguf_name: mtp_blk("nextn.shared_head_norm.weight"),
bake: BakeOp::AddOne,
});
}
_ => {}
}
let layers_rest = rest.strip_prefix("layers.")?;
let dot = layers_rest.find('.')?;
let (bid_str, inner_with_dot) = layers_rest.split_at(dot);
let bid: usize = bid_str.parse().ok()?;
if bid.to_string() != bid_str {
return None;
}
let inner = &inner_with_dot[1..];
map_per_block(bid + n_layer, inner, hf_shape, ctx)
}
pub fn build_metadata(
ctx: &Qwen35MoeFullCtx,
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 text = effective_text_config(config);
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 = text
.get("hidden_size")
.and_then(|v| v.as_u64())
.expect("config missing hidden_size") as u32;
let n_layers_base = text
.get("num_hidden_layers")
.and_then(|v| v.as_u64())
.expect("config missing num_hidden_layers") as u32;
let n_mtp_raw = text
.get("mtp_num_hidden_layers")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let n_mtp = if ctx.drop_mtp { 0 } else { n_mtp_raw };
let n_layers = n_layers_base + n_mtp;
let n_head = text
.get("num_attention_heads")
.and_then(|v| v.as_u64())
.expect("config missing num_attention_heads") as u32;
let n_head_kv = text
.get("num_key_value_heads")
.and_then(|v| v.as_u64())
.map(|x| x as u32)
.unwrap_or(n_head);
let ctx_len = text
.get("max_position_embeddings")
.and_then(|v| v.as_u64())
.expect("config missing max_position_embeddings") as u32;
let rms_eps = text
.get("rms_norm_eps")
.and_then(|v| v.as_f64())
.expect("config missing rms_norm_eps") as f32;
let moe_ffn = text
.get("moe_intermediate_size")
.and_then(|v| v.as_u64())
.expect("config missing moe_intermediate_size") as u32;
let n_experts = text
.get("num_experts")
.or_else(|| text.get("num_local_experts"))
.and_then(|v| v.as_u64())
.expect("config missing num_experts") as u32;
let n_experts_used = text
.get("num_experts_per_tok")
.and_then(|v| v.as_u64())
.expect("config missing num_experts_per_tok") as u32;
let rope_theta = text
.get("rope_parameters")
.and_then(|rp| rp.get("rope_theta"))
.and_then(|v| v.as_f64())
.or_else(|| text.get("rope_theta").and_then(|v| v.as_f64()))
.unwrap_or(10000.0) as f32;
let linear_conv_kernel_dim = text
.get("linear_conv_kernel_dim")
.and_then(|v| v.as_u64())
.expect("config missing linear_conv_kernel_dim") as u32;
let linear_key_head_dim = text
.get("linear_key_head_dim")
.and_then(|v| v.as_u64())
.expect("config missing linear_key_head_dim") as u32;
let linear_num_key_heads = text
.get("linear_num_key_heads")
.and_then(|v| v.as_u64())
.expect("config missing linear_num_key_heads") as u32;
let linear_num_value_heads = text
.get("linear_num_value_heads")
.and_then(|v| v.as_u64())
.expect("config missing linear_num_value_heads") as u32;
let linear_value_head_dim = text
.get("linear_value_head_dim")
.and_then(|v| v.as_u64())
.expect("config missing linear_value_head_dim") as u32;
let ssm_inner_size = linear_value_head_dim * linear_num_value_heads;
let full_attn_interval = text
.get("full_attention_interval")
.and_then(|v| v.as_u64())
.unwrap_or(4) as u32;
let head_dim = text
.get("head_dim")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| (hidden_size as u64) / (n_head as u64)) as u32;
let partial_rotary_factor = text
.get("partial_rotary_factor")
.and_then(|v| v.as_f64())
.unwrap_or(0.25);
let rope_dim_count = ((head_dim as f64) * partial_rotary_factor) as u32;
let mut mrope_section: Vec<i32> = text
.get("rope_parameters")
.and_then(|rp| rp.get("mrope_section"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_i64().map(|n| n as i32))
.collect()
})
.or_else(|| {
text.get("mrope_section")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_i64().map(|n| n as i32))
.collect()
})
})
.unwrap_or_else(|| vec![11, 11, 10, 0]);
while mrope_section.len() < 4 {
mrope_section.push(0);
}
mrope_section.truncate(4);
let shared_expert_ffn = text
.get("shared_expert_intermediate_size")
.and_then(|v| v.as_u64())
.map(|x| x as u32);
let mut kvs: Vec<(String, MetaValue)> = emit_general_prelude(
"qwen35moe",
display_name,
&id_components,
size_label_override,
model_card,
sampling,
);
kvs.push(("qwen35moe.block_count".into(), MetaValue::U32(n_layers)));
kvs.push(("qwen35moe.context_length".into(), MetaValue::U32(ctx_len)));
kvs.push((
"qwen35moe.embedding_length".into(),
MetaValue::U32(hidden_size),
));
kvs.push((
"qwen35moe.attention.head_count".into(),
MetaValue::U32(n_head),
));
kvs.push((
"qwen35moe.attention.head_count_kv".into(),
MetaValue::U32(n_head_kv),
));
kvs.push((
"qwen35moe.rope.dimension_sections".into(),
MetaValue::ArrayI32(mrope_section),
));
kvs.push((
"qwen35moe.rope.freq_base".into(),
MetaValue::F32(rope_theta),
));
kvs.push((
"qwen35moe.attention.layer_norm_rms_epsilon".into(),
MetaValue::F32(rms_eps),
));
kvs.push(("qwen35moe.expert_count".into(), MetaValue::U32(n_experts)));
kvs.push((
"qwen35moe.expert_used_count".into(),
MetaValue::U32(n_experts_used),
));
kvs.push((
"qwen35moe.attention.key_length".into(),
MetaValue::U32(head_dim),
));
kvs.push((
"qwen35moe.attention.value_length".into(),
MetaValue::U32(head_dim),
));
kvs.push((
"qwen35moe.expert_feed_forward_length".into(),
MetaValue::U32(moe_ffn),
));
if let Some(s) = shared_expert_ffn {
kvs.push((
"qwen35moe.expert_shared_feed_forward_length".into(),
MetaValue::U32(s),
));
}
kvs.push((
"qwen35moe.ssm.conv_kernel".into(),
MetaValue::U32(linear_conv_kernel_dim),
));
kvs.push((
"qwen35moe.ssm.state_size".into(),
MetaValue::U32(linear_key_head_dim),
));
kvs.push((
"qwen35moe.ssm.group_count".into(),
MetaValue::U32(linear_num_key_heads),
));
kvs.push((
"qwen35moe.ssm.time_step_rank".into(),
MetaValue::U32(linear_num_value_heads),
));
kvs.push((
"qwen35moe.ssm.inner_size".into(),
MetaValue::U32(ssm_inner_size),
));
kvs.push((
"qwen35moe.full_attention_interval".into(),
MetaValue::U32(full_attn_interval),
));
kvs.push((
"qwen35moe.rope.dimension_count".into(),
MetaValue::U32(rope_dim_count),
));
if n_mtp > 0 {
kvs.push((
"qwen35moe.nextn_predict_layers".into(),
MetaValue::U32(n_mtp),
));
}
kvs.extend(emit_general_postlude(file_type));
let _ = ctx; kvs
}
fn effective_text_config(config: &serde_json::Value) -> &serde_json::Value {
config.get("text_config").unwrap_or(config)
}
#[cfg(test)]
mod tests {
use super::*;
fn vlm_ctx() -> Qwen35MoeFullCtx {
Qwen35MoeFullCtx {
num_hidden_layers: 80,
num_experts: 128,
moe_intermediate_size: 768,
hidden_size: 2048,
linear_num_key_heads: 16,
linear_num_value_heads: 32,
linear_key_head_dim: 128,
linear_value_head_dim: 128,
multimodal_wrapping: true,
drop_mtp: false,
}
}
fn text_only_ctx() -> Qwen35MoeFullCtx {
Qwen35MoeFullCtx {
multimodal_wrapping: false,
..vlm_ctx()
}
}
#[test]
fn globals_with_multimodal_prefix() {
let ctx = vlm_ctx();
match map_tensor_name("model.language_model.embed_tokens.weight", &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "token_embd.weight"),
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name("model.language_model.norm.weight", &[], &ctx) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "output_norm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name("lm_head.weight", &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "output.weight"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn globals_text_only() {
let ctx = text_only_ctx();
match map_tensor_name("model.embed_tokens.weight", &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "token_embd.weight"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn visual_tensors_drop_in_multimodal() {
let ctx = vlm_ctx();
match map_tensor_name("model.visual.blocks.0.attn.qkv.weight", &[], &ctx) {
Some(MappedTensor::Drop) => {}
other => panic!("expected Drop, got: {other:?}"),
}
}
#[test]
fn input_layernorm_gets_plus_one_bake() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.5.input_layernorm.weight",
&[],
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.5.attn_norm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn post_attention_layernorm_gets_plus_one_bake() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.3.post_attention_layernorm.weight",
&[],
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.3.post_attention_norm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_norm_does_not_get_plus_one() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.0.linear_attn.norm.weight",
&[],
&ctx,
) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "blk.0.ssm_norm.weight"),
other => panic!("expected Direct (no bake), got: {other:?}"),
}
}
#[test]
fn linear_attn_a_log_reorder_then_neg_exp() {
let ctx = vlm_ctx();
match map_tensor_name("model.language_model.layers.0.linear_attn.A_log", &[], &ctx) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.0.ssm_a");
match bake {
BakeOp::Sequence(ref ops) => {
assert_eq!(ops.len(), 2);
match ops[0] {
BakeOp::ReorderVHeads { head_dim, .. } => assert_eq!(head_dim, 1),
ref other => panic!("expected ReorderVHeads first, got {other:?}"),
}
assert_eq!(ops[1], BakeOp::NegExp);
}
other => panic!("expected Sequence, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_conv1d_squeeze_then_partial_reorder() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.0.linear_attn.conv1d.weight",
&[2048, 1, 4],
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.0.ssm_conv1d.weight");
match bake {
BakeOp::Sequence(ref ops) => {
assert_eq!(ops.len(), 2);
assert_eq!(ops[0], BakeOp::Squeeze);
match ops[1] {
BakeOp::ReorderVHeads {
head_dim,
ref slice,
..
} => {
assert_eq!(head_dim, 128 * 4); let r = slice.as_ref().expect("expected V-portion slice");
assert_eq!(r.start, 16384);
}
ref other => panic!("expected ReorderVHeads 2nd, got {other:?}"),
}
}
other => panic!("expected Sequence, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_in_proj_a_b_reorder_with_cols_head_dim() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.0.linear_attn.in_proj_a.weight",
&[32, 2048],
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.0.ssm_alpha.weight");
match bake {
BakeOp::ReorderVHeads {
head_dim, slice, ..
} => {
assert_eq!(head_dim, 2048); assert!(slice.is_none());
}
other => panic!("expected ReorderVHeads, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name(
"model.language_model.layers.0.linear_attn.in_proj_b.weight",
&[32, 2048],
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, .. }) => {
assert_eq!(gguf_name, "blk.0.ssm_beta.weight");
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_in_proj_qkv_v_only_reorder_with_slice() {
let ctx = vlm_ctx();
let hf_shape = [8192_usize, 2048];
match map_tensor_name(
"model.language_model.layers.0.linear_attn.in_proj_qkv.weight",
&hf_shape,
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.0.attn_qkv.weight");
match bake {
BakeOp::ReorderVHeads {
num_k_heads,
num_v_per_k,
head_dim,
slice,
} => {
assert_eq!(num_k_heads, 16);
assert_eq!(num_v_per_k, 2);
assert_eq!(head_dim, 128 * 2048); let expect_start = (128 + 128) * 16 * 2048;
let expect_len = 2 * 128 * 16 * 2048;
let r = slice.expect("expected sliced reorder");
assert_eq!(r.start, expect_start);
assert_eq!(r.end - r.start, expect_len);
}
other => panic!("expected ReorderVHeads, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_in_proj_z_full_reorder_no_slice() {
let ctx = vlm_ctx();
let hf_shape = [4096_usize, 2048];
match map_tensor_name(
"model.language_model.layers.0.linear_attn.in_proj_z.weight",
&hf_shape,
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.0.attn_gate.weight");
match bake {
BakeOp::ReorderVHeads {
num_k_heads,
num_v_per_k,
head_dim,
slice,
} => {
assert_eq!(num_k_heads, 16);
assert_eq!(num_v_per_k, 2);
assert_eq!(head_dim, 128 * 2048);
assert!(slice.is_none(), "in_proj_z reorders full buffer");
}
other => panic!("expected ReorderVHeads, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_out_proj_per_row_col_reorder() {
let ctx = vlm_ctx();
let hf_shape = [2048_usize, 4096];
match map_tensor_name(
"model.language_model.layers.0.linear_attn.out_proj.weight",
&hf_shape,
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.0.ssm_out.weight");
match bake {
BakeOp::ReorderVHeadsPerRow {
row_count,
num_k_heads,
num_v_per_k,
head_dim_in_row,
} => {
assert_eq!(row_count, 2048);
assert_eq!(num_k_heads, 16);
assert_eq!(num_v_per_k, 2);
assert_eq!(head_dim_in_row, 128);
}
other => panic!("expected ReorderVHeadsPerRow, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn linear_attn_out_proj_rejects_mismatched_cols() {
let ctx = vlm_ctx();
let hf_shape = [2048_usize, 3000];
assert!(map_tensor_name(
"model.language_model.layers.0.linear_attn.out_proj.weight",
&hf_shape,
&ctx,
)
.is_none());
}
#[test]
fn linear_attn_v_reorder_arms_no_longer_surface_unmapped_with_shape() {
let ctx = vlm_ctx();
assert!(map_tensor_name(
"model.language_model.layers.0.linear_attn.in_proj_qkv.weight",
&[8192, 2048],
&ctx,
)
.is_some());
assert!(map_tensor_name(
"model.language_model.layers.0.linear_attn.in_proj_z.weight",
&[4096, 2048],
&ctx,
)
.is_some());
assert!(map_tensor_name(
"model.language_model.layers.0.linear_attn.out_proj.weight",
&[2048, 4096],
&ctx,
)
.is_some());
}
#[test]
fn mlp_router_gate_direct() {
let ctx = vlm_ctx();
match map_tensor_name("model.language_model.layers.7.mlp.gate.weight", &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "blk.7.ffn_gate_inp.weight"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn mlp_shared_experts_direct() {
let ctx = vlm_ctx();
let direct_cases = [
(
"mlp.shared_expert.gate_proj.weight",
"ffn_gate_shexp.weight",
),
("mlp.shared_expert.up_proj.weight", "ffn_up_shexp.weight"),
(
"mlp.shared_expert.down_proj.weight",
"ffn_down_shexp.weight",
),
];
for (hf_suffix, gguf_suffix) in direct_cases {
let hf = format!("model.language_model.layers.2.{hf_suffix}");
match map_tensor_name(&hf, &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, format!("blk.2.{gguf_suffix}")),
other => panic!("unexpected for {hf}: {other:?}"),
}
}
match map_tensor_name(
"model.language_model.layers.2.mlp.shared_expert_gate.weight",
&[],
&ctx,
) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.2.ffn_gate_inp_shexp.weight");
assert_eq!(bake, BakeOp::Squeeze);
}
other => panic!("expected DirectWithBake Squeeze, got {other:?}"),
}
}
#[test]
fn mlp_experts_gate_up_proj_splits_via_split_axis_half() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.9.mlp.experts.gate_up_proj",
&[],
&ctx,
) {
Some(MappedTensor::SplitInto(outputs)) => {
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].gguf_name, "blk.9.ffn_gate_exps.weight");
assert_eq!(outputs[1].gguf_name, "blk.9.ffn_up_exps.weight");
assert_eq!(outputs[0].gguf_shape, vec![2048, 768, 128]);
assert_eq!(outputs[1].gguf_shape, vec![2048, 768, 128]);
match outputs[0].bake {
BakeOp::SplitAxisHalf { half, .. } => assert_eq!(half, SplitHalf::First),
ref other => panic!("expected SplitAxisHalf, got {other:?}"),
}
match outputs[1].bake {
BakeOp::SplitAxisHalf { half, .. } => assert_eq!(half, SplitHalf::Second),
ref other => panic!("expected SplitAxisHalf, got {other:?}"),
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn mlp_experts_down_proj_direct_rename() {
let ctx = vlm_ctx();
match map_tensor_name(
"model.language_model.layers.9.mlp.experts.down_proj",
&[],
&ctx,
) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "blk.9.ffn_down_exps.weight"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn mtp_helpers_remap_to_next_block_layer() {
let ctx = vlm_ctx();
match map_tensor_name("mtp.fc.weight", &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "blk.80.nextn.eh_proj.weight"),
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name("mtp.pre_fc_norm_embedding.weight", &[], &ctx) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.80.nextn.enorm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name("mtp.pre_fc_norm_hidden.weight", &[], &ctx) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.80.nextn.hnorm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name("mtp.norm.weight", &[], &ctx) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.80.nextn.shared_head_norm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn mtp_layer_body_tensors_remap_to_n_plus_bid() {
let ctx = vlm_ctx();
match map_tensor_name("mtp.layers.0.input_layernorm.weight", &[], &ctx) {
Some(MappedTensor::DirectWithBake { gguf_name, bake }) => {
assert_eq!(gguf_name, "blk.80.attn_norm.weight");
assert_eq!(bake, BakeOp::AddOne);
}
other => panic!("unexpected: {other:?}"),
}
match map_tensor_name("mtp.layers.0.self_attn.q_proj.weight", &[], &ctx) {
Some(MappedTensor::Direct(s)) => assert_eq!(s, "blk.80.attn_q.weight"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn unmapped_name_returns_none() {
let ctx = vlm_ctx();
assert!(map_tensor_name("model.totally_made_up_tensor.weight", &[], &ctx).is_none());
assert!(map_tensor_name("garbage", &[], &ctx).is_none());
}
}