use crate::backends::gguf::types::MetaValue;
pub fn map_tensor_name(hf_name: &str) -> Option<String> {
match hf_name {
"model.multi_modal_projector.mm_input_projection_weight"
| "model.multi_modal_projector.mm_input_projection.weight" => {
return Some("mm.input_projection.weight".to_string());
}
"model.multi_modal_projector.mm_soft_emb_norm.weight" => {
return Some("mm.soft_emb_norm.weight".to_string());
}
_ => {}
}
let v_prefix = "model.vision_tower.vision_model.";
let v_rest = hf_name.strip_prefix(v_prefix)?;
match v_rest {
"embeddings.patch_embedding.weight" => {
return Some("v.patch_embd.weight".to_string());
}
"embeddings.patch_embedding.bias" => {
return Some("v.patch_embd.bias".to_string());
}
"embeddings.position_embedding.weight" => {
return Some("v.position_embd.weight".to_string());
}
"post_layernorm.weight" => return Some("v.post_ln.weight".to_string()),
"post_layernorm.bias" => return Some("v.post_ln.bias".to_string()),
_ => {}
}
let after_layers = v_rest.strip_prefix("encoder.layers.")?;
let dot = after_layers.find('.')?;
let (layer_str, rest_with_dot) = after_layers.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 (sub, param) = split_weight_or_bias(rest)?;
let new_sub = match sub {
"layer_norm1" => "ln1",
"layer_norm2" => "ln2",
"self_attn.q_proj" => "attn_q",
"self_attn.k_proj" => "attn_k",
"self_attn.v_proj" => "attn_v",
"self_attn.out_proj" => "attn_out",
"mlp.fc1" => "ffn_up",
"mlp.fc2" => "ffn_down",
_ => return None,
};
Some(format!("v.blk.{layer}.{new_sub}.{param}"))
}
fn split_weight_or_bias(s: &str) -> Option<(&str, &str)> {
if let Some(head) = s.strip_suffix(".weight") {
Some((head, "weight"))
} else if let Some(head) = s.strip_suffix(".bias") {
Some((head, "bias"))
} else {
None
}
}
pub fn build_metadata(
vision_config: &serde_json::Value,
file_type: u32,
) -> Vec<(String, MetaValue)> {
let image_size = vision_config["image_size"]
.as_u64()
.expect("vision_config missing required key `image_size`") as u32;
let patch_size = vision_config["patch_size"]
.as_u64()
.expect("vision_config missing required key `patch_size`") as u32;
let hidden_size = vision_config["hidden_size"]
.as_u64()
.expect("vision_config missing required key `hidden_size`") as u32;
let ffn_len = vision_config["intermediate_size"]
.as_u64()
.expect("vision_config missing required key `intermediate_size`") as u32;
let n_layers = vision_config["num_hidden_layers"]
.as_u64()
.expect("vision_config missing required key `num_hidden_layers`") as u32;
let n_head = vision_config["num_attention_heads"]
.as_u64()
.expect("vision_config missing required key `num_attention_heads`") as u32;
let ln_eps = vision_config
.get("layer_norm_eps")
.and_then(|v| v.as_f64())
.unwrap_or(1.0e-6) as f32;
vec![
(
"general.architecture".into(),
MetaValue::String("clip".into()),
),
("general.file_type".into(), MetaValue::U32(file_type)),
("clip.has_vision_encoder".into(), MetaValue::Bool(true)),
("clip.has_audio_encoder".into(), MetaValue::Bool(false)),
(
"clip.projector_type".into(),
MetaValue::String("gemma3".into()),
),
("clip.use_gelu".into(), MetaValue::Bool(true)),
("clip.vision.image_size".into(), MetaValue::U32(image_size)),
("clip.vision.patch_size".into(), MetaValue::U32(patch_size)),
(
"clip.vision.embedding_length".into(),
MetaValue::U32(hidden_size),
),
(
"clip.vision.feed_forward_length".into(),
MetaValue::U32(ffn_len),
),
("clip.vision.block_count".into(), MetaValue::U32(n_layers)),
(
"clip.vision.attention.head_count".into(),
MetaValue::U32(n_head),
),
(
"clip.vision.attention.layer_norm_epsilon".into(),
MetaValue::F32(ln_eps),
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn gemma4_mmproj_tensor_name_round_trip() {
let cases: &[(&str, &str)] = &[
(
"model.vision_tower.vision_model.embeddings.patch_embedding.weight",
"v.patch_embd.weight",
),
(
"model.vision_tower.vision_model.embeddings.patch_embedding.bias",
"v.patch_embd.bias",
),
(
"model.vision_tower.vision_model.embeddings.position_embedding.weight",
"v.position_embd.weight",
),
(
"model.vision_tower.vision_model.post_layernorm.weight",
"v.post_ln.weight",
),
(
"model.vision_tower.vision_model.post_layernorm.bias",
"v.post_ln.bias",
),
(
"model.vision_tower.vision_model.encoder.layers.0.layer_norm1.weight",
"v.blk.0.ln1.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.0.layer_norm1.bias",
"v.blk.0.ln1.bias",
),
(
"model.vision_tower.vision_model.encoder.layers.13.layer_norm2.weight",
"v.blk.13.ln2.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.13.layer_norm2.bias",
"v.blk.13.ln2.bias",
),
(
"model.vision_tower.vision_model.encoder.layers.0.self_attn.q_proj.weight",
"v.blk.0.attn_q.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.0.self_attn.q_proj.bias",
"v.blk.0.attn_q.bias",
),
(
"model.vision_tower.vision_model.encoder.layers.7.self_attn.k_proj.weight",
"v.blk.7.attn_k.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.7.self_attn.v_proj.weight",
"v.blk.7.attn_v.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.7.self_attn.out_proj.weight",
"v.blk.7.attn_out.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.7.self_attn.out_proj.bias",
"v.blk.7.attn_out.bias",
),
(
"model.vision_tower.vision_model.encoder.layers.26.mlp.fc1.weight",
"v.blk.26.ffn_up.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.26.mlp.fc1.bias",
"v.blk.26.ffn_up.bias",
),
(
"model.vision_tower.vision_model.encoder.layers.26.mlp.fc2.weight",
"v.blk.26.ffn_down.weight",
),
(
"model.vision_tower.vision_model.encoder.layers.26.mlp.fc2.bias",
"v.blk.26.ffn_down.bias",
),
(
"model.multi_modal_projector.mm_input_projection_weight",
"mm.input_projection.weight",
),
(
"model.multi_modal_projector.mm_input_projection.weight",
"mm.input_projection.weight",
),
(
"model.multi_modal_projector.mm_soft_emb_norm.weight",
"mm.soft_emb_norm.weight",
),
];
for &(hf, expected_gguf) in cases {
let got = map_tensor_name(hf);
assert_eq!(
got.as_deref(),
Some(expected_gguf),
"map_tensor_name({hf:?}) = {got:?}, want Some({expected_gguf:?})"
);
}
}
#[test]
fn gemma4_mmproj_tensor_name_rejects_unknown_kinds() {
assert_eq!(map_tensor_name("model.embed_tokens.weight"), None);
assert_eq!(map_tensor_name("lm_head.weight"), None);
assert_eq!(
map_tensor_name("vision_tower.vision_model.embeddings.patch_embedding.weight"),
None
);
assert_eq!(
map_tensor_name("model.vision_tower.vision_model.pre_layernorm.weight"),
None
);
assert_eq!(
map_tensor_name("model.vision_tower.vision_model.encoder.layers.0.layer_norm3.weight"),
None
);
assert_eq!(
map_tensor_name("model.vision_tower.vision_model.encoder.layers.01.layer_norm1.weight"),
None
);
assert_eq!(
map_tensor_name("model.vision_tower.vision_model.encoder.layers..layer_norm1.weight"),
None
);
assert_eq!(
map_tensor_name("model.vision_tower.vision_model.encoder.layers.layer_norm1.weight"),
None
);
assert_eq!(
map_tensor_name("model.multi_modal_projector.unknown.weight"),
None
);
assert_eq!(
map_tensor_name(
"model.vision_tower.vision_model.encoder.layers.0.layer_norm1.running_mean"
),
None
);
}
#[test]
fn gemma4_mmproj_metadata_built_from_vision_config() {
let vcfg = json!({
"image_size": 896,
"patch_size": 14,
"hidden_size": 1152,
"intermediate_size": 4304,
"num_hidden_layers": 27,
"num_attention_heads": 16,
"layer_norm_eps": 1.0e-6,
});
let kv = build_metadata(&vcfg, 17 );
assert_eq!(kv.len(), 13, "Gemma4 mmproj emits 13 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("clip".into())
);
assert_eq!(by_key["general.file_type"], MetaValue::U32(17));
assert_eq!(by_key["clip.has_vision_encoder"], MetaValue::Bool(true));
assert_eq!(by_key["clip.has_audio_encoder"], MetaValue::Bool(false));
assert_eq!(
by_key["clip.projector_type"],
MetaValue::String("gemma3".into())
);
assert_eq!(by_key["clip.use_gelu"], MetaValue::Bool(true));
assert_eq!(by_key["clip.vision.image_size"], MetaValue::U32(896));
assert_eq!(by_key["clip.vision.patch_size"], MetaValue::U32(14));
assert_eq!(by_key["clip.vision.embedding_length"], MetaValue::U32(1152));
assert_eq!(
by_key["clip.vision.feed_forward_length"],
MetaValue::U32(4304)
);
assert_eq!(by_key["clip.vision.block_count"], MetaValue::U32(27));
assert_eq!(
by_key["clip.vision.attention.head_count"],
MetaValue::U32(16)
);
assert_eq!(
by_key["clip.vision.attention.layer_norm_epsilon"],
MetaValue::F32(1.0e-6)
);
}
#[test]
fn gemma4_mmproj_metadata_layer_norm_eps_default() {
let vcfg = json!({
"image_size": 224,
"patch_size": 14,
"hidden_size": 768,
"intermediate_size": 3072,
"num_hidden_layers": 12,
"num_attention_heads": 12,
});
let kv = build_metadata(&vcfg, 0);
let by_key: std::collections::HashMap<_, _> =
kv.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
assert_eq!(
by_key["clip.vision.attention.layer_norm_epsilon"],
MetaValue::F32(1.0e-6),
"layer_norm_eps defaults to 1e-6 per gemma.py:257"
);
assert_eq!(by_key["clip.vision.image_size"], MetaValue::U32(224));
assert_eq!(by_key["clip.vision.block_count"], MetaValue::U32(12));
}
}