use std::collections::HashMap;
use std::path::Path;
use crate::input::safetensors::{self};
use crate::ir::{DType, TensorRef};
use crate::progress::ProgressReporter;
use super::config::VisionConfig;
use super::VitConvertError;
#[derive(Debug, Clone)]
pub struct VitTensor {
pub gguf_name: String,
pub shape: Vec<usize>,
pub dtype: DType,
pub data: Vec<u8>,
}
pub fn hf_vit_name_to_gguf(hf_name: &str) -> Option<String> {
static STATIC_MAP: &[(&str, &str)] = &[
(
"model.vision_tower.embeddings.patch_embeddings.projection.weight",
"v.patch_embd.weight",
),
(
"model.vision_tower.embeddings.patch_embeddings.projection.bias",
"v.patch_embd.bias",
),
(
"model.vision_tower.embeddings.position_embeddings.weight",
"v.position_embd.weight",
),
(
"model.vision_tower.post_layernorm.weight",
"v.post_ln.weight",
),
("model.vision_tower.post_layernorm.bias", "v.post_ln.bias"),
("model.multi_modal_projector.linear_1.weight", "mm.0.weight"),
("model.multi_modal_projector.linear_1.bias", "mm.0.bias"),
("model.multi_modal_projector.linear_2.weight", "mm.2.weight"),
("model.multi_modal_projector.linear_2.bias", "mm.2.bias"),
];
for (hf, gguf) in STATIC_MAP {
if hf_name == *hf {
return Some((*gguf).to_string());
}
}
if let Some(rest) = hf_name.strip_prefix("model.visual.blocks.") {
let (layer_str, suffix) = rest.split_once('.')?;
let l: usize = layer_str.parse().ok()?;
let blk = format!("v.blk.{}", l);
let mapped = match suffix {
"attn.qkv.weight" => Some(format!("{}.attn_qkv.weight", blk)),
"attn.qkv.bias" => Some(format!("{}.attn_qkv.bias", blk)),
"attn.proj.weight" => Some(format!("{}.attn_out.weight", blk)),
"attn.proj.bias" => Some(format!("{}.attn_out.bias", blk)),
"mlp.linear_fc1.weight" => Some(format!("{}.ffn_up.weight", blk)),
"mlp.linear_fc1.bias" => Some(format!("{}.ffn_up.bias", blk)),
"mlp.linear_fc2.weight" => Some(format!("{}.ffn_down.weight", blk)),
"mlp.linear_fc2.bias" => Some(format!("{}.ffn_down.bias", blk)),
"norm1.weight" => Some(format!("{}.ln1.weight", blk)),
"norm1.bias" => Some(format!("{}.ln1.bias", blk)),
"norm2.weight" => Some(format!("{}.ln2.weight", blk)),
"norm2.bias" => Some(format!("{}.ln2.bias", blk)),
_ => None,
};
return mapped;
}
static QWEN36_GLOBAL_MAP: &[(&str, &str)] = &[
(
"model.visual.patch_embed.proj.weight",
"v.patch_embd.weight",
),
("model.visual.patch_embed.proj.bias", "v.patch_embd.bias"),
("model.visual.pos_embed.weight", "v.position_embd.weight"),
("model.visual.merger.linear_fc1.weight", "mm.0.weight"),
("model.visual.merger.linear_fc1.bias", "mm.0.bias"),
("model.visual.merger.linear_fc2.weight", "mm.2.weight"),
("model.visual.merger.linear_fc2.bias", "mm.2.bias"),
("model.visual.merger.norm.weight", "v.post_ln.weight"),
("model.visual.merger.norm.bias", "v.post_ln.bias"),
];
for (hf, gguf) in QWEN36_GLOBAL_MAP {
if hf_name == *hf {
return Some((*gguf).to_string());
}
}
if let Some(rest) = hf_name.strip_prefix("model.vision_tower.encoder.layer.") {
let (layer_str, suffix) = rest.split_once('.')?;
let l: usize = layer_str.parse().ok()?;
let blk = format!("v.blk.{}", l);
let mapped = match suffix {
"attention.q_proj.weight" => Some(format!("{}.attn_q.weight", blk)),
"attention.q_proj.bias" => Some(format!("{}.attn_q.bias", blk)),
"attention.k_proj.weight" => Some(format!("{}.attn_k.weight", blk)),
"attention.k_proj.bias" => Some(format!("{}.attn_k.bias", blk)),
"attention.v_proj.weight" => Some(format!("{}.attn_v.weight", blk)),
"attention.v_proj.bias" => Some(format!("{}.attn_v.bias", blk)),
"attention.output.dense.weight" => Some(format!("{}.attn_out.weight", blk)),
"attention.output.dense.bias" => Some(format!("{}.attn_out.bias", blk)),
"layer_norm1.weight" => Some(format!("{}.ln1.weight", blk)),
"layer_norm1.bias" => Some(format!("{}.ln1.bias", blk)),
"layer_norm2.weight" => Some(format!("{}.ln2.weight", blk)),
"layer_norm2.bias" => Some(format!("{}.ln2.bias", blk)),
"mlp.fc1.weight" => Some(format!("{}.ffn_up.weight", blk)),
"mlp.fc1.bias" => Some(format!("{}.ffn_up.bias", blk)),
"mlp.fc2.weight" => Some(format!("{}.ffn_down.weight", blk)),
"mlp.fc2.bias" => Some(format!("{}.ffn_down.bias", blk)),
_ => None,
};
return mapped;
}
None
}
pub(crate) fn vit_emission_is_f32(gguf_name: &str) -> bool {
if gguf_name.ends_with(".bias") {
return true;
}
if gguf_name.contains("ln1.weight")
|| gguf_name.contains("ln2.weight")
|| gguf_name.contains("attn_norm.weight")
|| gguf_name.contains("ffn_norm.weight")
|| gguf_name.contains("post_ffw_norm.weight")
|| gguf_name.contains("ffn_post_norm.weight")
|| gguf_name.contains("pre_ln.weight")
|| gguf_name.contains("post_ln.weight")
|| gguf_name.ends_with(".norm.weight")
{
return true;
}
if gguf_name == "v.position_embd.weight" {
return true;
}
false
}
pub(crate) fn ensure_f32_bytes(tensor: &TensorRef) -> Result<Vec<u8>, VitConvertError> {
match tensor.dtype {
DType::F32 => Ok((*tensor.data).clone()),
DType::F16 => {
let n = tensor.numel();
let mut out = Vec::with_capacity(n * 4);
for i in 0..n {
let b = &tensor.data[i * 2..(i + 1) * 2];
let h = half::f16::from_le_bytes([b[0], b[1]]);
out.extend_from_slice(&h.to_f32().to_le_bytes());
}
Ok(out)
}
DType::BF16 => {
let n = tensor.numel();
let mut out = Vec::with_capacity(n * 4);
for i in 0..n {
let b = &tensor.data[i * 2..(i + 1) * 2];
let bf = half::bf16::from_le_bytes([b[0], b[1]]);
out.extend_from_slice(&bf.to_f32().to_le_bytes());
}
Ok(out)
}
other => Err(VitConvertError::Safetensors(format!(
"unsupported dtype {:?} on vision tensor {:?} (ensure_f32_bytes)",
other, tensor.name
))),
}
}
fn ensure_f16_bytes(tensor: &TensorRef) -> Result<Vec<u8>, VitConvertError> {
match tensor.dtype {
DType::F16 => Ok((*tensor.data).clone()),
DType::F32 => {
let n = tensor.numel();
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
let b = &tensor.data[i * 4..(i + 1) * 4];
let f = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
let h = half::f16::from_f32(f);
out.extend_from_slice(&h.to_le_bytes());
}
Ok(out)
}
DType::BF16 => {
let n = tensor.numel();
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
let b = &tensor.data[i * 2..(i + 1) * 2];
let bf = half::bf16::from_le_bytes([b[0], b[1]]);
let h = half::f16::from_f32(bf.to_f32());
out.extend_from_slice(&h.to_le_bytes());
}
Ok(out)
}
other => Err(VitConvertError::Safetensors(format!(
"unsupported dtype {:?} on vision tensor {:?}",
other, tensor.name
))),
}
}
fn hf_qwen3vl_deepstack_to_gguf(hf_name: &str, deepstack_indexes: &[u32]) -> Option<String> {
let canon = hf_name.strip_prefix("model.").unwrap_or(hf_name);
let rest = canon.strip_prefix("visual.deepstack_merger_list.")?;
let mut parts = rest.splitn(3, '.');
let rel_idx_str = parts.next()?;
let component = parts.next()?;
let suffix = parts.next()?;
let rel_idx: usize = rel_idx_str.parse().ok()?;
if rel_idx >= deepstack_indexes.len() {
return None;
}
let abs_idx = deepstack_indexes[rel_idx];
let target = match component {
"norm" => "norm",
"linear_fc1" => "fc1",
"linear_fc2" => "fc2",
_ => return None,
};
Some(format!("v.deepstack.{}.{}.{}", abs_idx, target, suffix))
}
pub fn load_vision_tensors(
hf_repo_dir: &Path,
vision_config: &VisionConfig,
) -> Result<HashMap<String, VitTensor>, VitConvertError> {
let progress = ProgressReporter::new();
let tensor_map = safetensors::read_tensors(hf_repo_dir, &progress)
.map_err(|e| VitConvertError::Safetensors(e.to_string()))?;
let deepstack_indexes: Vec<u32> = vision_config
.deepstack_visual_indexes
.clone()
.unwrap_or_default();
let mut out: HashMap<String, VitTensor> = HashMap::new();
for (name, tensor) in tensor_map.iter() {
if let Some(gguf_name) = hf_qwen3vl_deepstack_to_gguf(name, &deepstack_indexes) {
let (data, dtype) = if vit_emission_is_f32(&gguf_name) {
(ensure_f32_bytes(tensor)?, DType::F32)
} else {
(ensure_f16_bytes(tensor)?, DType::F16)
};
out.insert(
gguf_name.clone(),
VitTensor {
gguf_name,
shape: tensor.shape.clone(),
dtype,
data,
},
);
continue;
}
let is_qwen3vl_patch_5d = (name == "model.visual.patch_embed.proj.weight"
|| name == "visual.patch_embed.proj.weight")
&& tensor.shape.len() == 5;
if is_qwen3vl_patch_5d {
let (slice0, slice1) = split_qwen3vl_patch_embed_temporal(tensor)?;
out.insert(
"v.patch_embd.weight".to_string(),
VitTensor {
gguf_name: "v.patch_embd.weight".to_string(),
shape: slice0.0,
dtype: DType::F16,
data: slice0.1.into(),
},
);
out.insert(
"v.patch_embd.weight.1".to_string(),
VitTensor {
gguf_name: "v.patch_embd.weight.1".to_string(),
shape: slice1.0,
dtype: DType::F16,
data: slice1.1.into(),
},
);
continue;
}
let mapped = hf_vit_name_to_gguf(name).or_else(|| {
if let Some(stripped) = name.strip_prefix("model.") {
hf_vit_name_to_gguf(stripped)
} else {
hf_vit_name_to_gguf(&format!("model.{}", name))
}
});
if let Some(gguf_name) = mapped {
let (data, dtype) = if vit_emission_is_f32(&gguf_name) {
(ensure_f32_bytes(tensor)?, DType::F32)
} else {
(ensure_f16_bytes(tensor)?, DType::F16)
};
out.insert(
gguf_name.clone(),
VitTensor {
gguf_name,
shape: tensor.shape.clone(),
dtype,
data,
},
);
}
}
Ok(out)
}
fn split_qwen3vl_patch_embed_temporal(
tensor: &TensorRef,
) -> Result<((Vec<usize>, Vec<u8>), (Vec<usize>, Vec<u8>)), VitConvertError> {
if tensor.shape.len() != 5 {
return Err(VitConvertError::Safetensors(
"patch_embed.proj.weight (expected 5-D)".to_string(),
));
}
let out = tensor.shape[0];
let inp = tensor.shape[1];
let t = tensor.shape[2];
let h = tensor.shape[3];
let w = tensor.shape[4];
if t != 2 {
return Err(VitConvertError::Safetensors(format!(
"patch_embed.proj.weight: expected temporal=2, got {}",
t
)));
}
let elem_size = tensor.dtype.element_size();
let hw_bytes = h * w * elem_size;
let mut s0_bytes = Vec::with_capacity(out * inp * h * w * elem_size);
let mut s1_bytes = Vec::with_capacity(out * inp * h * w * elem_size);
for o in 0..out {
for i in 0..inp {
let off0 = ((o * inp + i) * t + 0) * hw_bytes;
let off1 = ((o * inp + i) * t + 1) * hw_bytes;
s0_bytes.extend_from_slice(&tensor.data[off0..off0 + hw_bytes]);
s1_bytes.extend_from_slice(&tensor.data[off1..off1 + hw_bytes]);
}
}
let s0_f16 = match tensor.dtype {
DType::F16 => s0_bytes,
DType::BF16 => bf16_bytes_to_f16(&s0_bytes),
DType::F32 => f32_bytes_to_f16(&s0_bytes),
_ => {
return Err(VitConvertError::Safetensors(format!(
"patch_embed.proj.weight: unsupported dtype {:?}",
tensor.dtype
)))
}
};
let s1_f16 = match tensor.dtype {
DType::F16 => s1_bytes,
DType::BF16 => bf16_bytes_to_f16(&s1_bytes),
DType::F32 => f32_bytes_to_f16(&s1_bytes),
_ => unreachable!(), };
let split_shape = vec![out, inp, h, w];
Ok(((split_shape.clone(), s0_f16), (split_shape, s1_f16)))
}
fn bf16_bytes_to_f16(input: &[u8]) -> Vec<u8> {
let n = input.len() / 2;
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
let bf = u16::from_le_bytes([input[i * 2], input[i * 2 + 1]]);
let f32_bits = (bf as u32) << 16;
let f = f32::from_bits(f32_bits);
let h = half::f16::from_f32(f);
out.extend_from_slice(&h.to_le_bytes());
}
out
}
fn f32_bytes_to_f16(input: &[u8]) -> Vec<u8> {
let n = input.len() / 4;
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
let f = f32::from_le_bytes([
input[i * 4],
input[i * 4 + 1],
input[i * 4 + 2],
input[i * 4 + 3],
]);
let h = half::f16::from_f32(f);
out.extend_from_slice(&h.to_le_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_mappings_match_spec() {
assert_eq!(
hf_vit_name_to_gguf("model.vision_tower.embeddings.patch_embeddings.projection.weight"),
Some("v.patch_embd.weight".to_string())
);
assert_eq!(
hf_vit_name_to_gguf("model.vision_tower.embeddings.position_embeddings.weight"),
Some("v.position_embd.weight".to_string())
);
assert_eq!(
hf_vit_name_to_gguf("model.vision_tower.post_layernorm.weight"),
Some("v.post_ln.weight".to_string())
);
}
#[test]
fn fc1_maps_to_ffn_up_fc2_maps_to_ffn_down() {
let fc1 = hf_vit_name_to_gguf("model.vision_tower.encoder.layer.0.mlp.fc1.weight")
.expect("fc1 weight mapped");
let fc2 = hf_vit_name_to_gguf("model.vision_tower.encoder.layer.0.mlp.fc2.weight")
.expect("fc2 weight mapped");
assert_eq!(
fc1, "v.blk.0.ffn_up.weight",
"fc1 is the UP projection (hidden → intermediate)"
);
assert_eq!(
fc2, "v.blk.0.ffn_down.weight",
"fc2 is the DOWN projection (intermediate → hidden)"
);
}
#[test]
fn linear_1_maps_to_mm_0_linear_2_maps_to_mm_2() {
let l1 = hf_vit_name_to_gguf("model.multi_modal_projector.linear_1.weight")
.expect("linear_1 mapped");
let l2 = hf_vit_name_to_gguf("model.multi_modal_projector.linear_2.weight")
.expect("linear_2 mapped");
assert_eq!(l1, "mm.0.weight", "linear_1 = mm.0 (runs first)");
assert_eq!(
l2, "mm.2.weight",
"linear_2 = mm.2 (runs after GELU at mm.1)"
);
}
#[test]
fn per_layer_attn_and_ln_names_map_correctly() {
let cases = [
(
"model.vision_tower.encoder.layer.5.attention.q_proj.weight",
"v.blk.5.attn_q.weight",
),
(
"model.vision_tower.encoder.layer.5.attention.k_proj.bias",
"v.blk.5.attn_k.bias",
),
(
"model.vision_tower.encoder.layer.5.attention.v_proj.weight",
"v.blk.5.attn_v.weight",
),
(
"model.vision_tower.encoder.layer.5.attention.output.dense.weight",
"v.blk.5.attn_out.weight",
),
(
"model.vision_tower.encoder.layer.5.layer_norm1.weight",
"v.blk.5.ln1.weight",
),
(
"model.vision_tower.encoder.layer.5.layer_norm2.bias",
"v.blk.5.ln2.bias",
),
];
for (hf, expected) in cases {
let got = hf_vit_name_to_gguf(hf).unwrap_or_else(|| panic!("no mapping for {}", hf));
assert_eq!(got, expected, "mapping mismatch for {}", hf);
}
}
#[test]
fn non_vision_tensor_returns_none() {
assert!(hf_vit_name_to_gguf("model.layers.0.self_attn.q_proj.weight").is_none());
assert!(hf_vit_name_to_gguf("model.embed_tokens.weight").is_none());
assert!(hf_vit_name_to_gguf("lm_head.weight").is_none());
}
#[test]
fn patch_embd_name_preserved() {
let got =
hf_vit_name_to_gguf("model.vision_tower.embeddings.patch_embeddings.projection.weight")
.expect("mapped");
assert_eq!(got, "v.patch_embd.weight");
}
#[test]
fn ensure_f16_bytes_casts_f32_to_f16() {
let tensor = TensorRef {
name: "test".into(),
shape: vec![4],
dtype: DType::F32,
data: {
let mut v = Vec::new();
for x in &[1.0f32, 2.0, 3.0, 4.0] {
v.extend_from_slice(&x.to_le_bytes());
}
v.into()
},
};
let f16_bytes = ensure_f16_bytes(&tensor).unwrap();
assert_eq!(f16_bytes.len(), 4 * 2); for (i, chunk) in f16_bytes.chunks(2).enumerate() {
let h = half::f16::from_le_bytes([chunk[0], chunk[1]]);
assert_eq!(h.to_f32(), [1.0, 2.0, 3.0, 4.0][i]);
}
}
#[test]
fn ensure_f16_bytes_passthrough_for_f16_input() {
let tensor = TensorRef {
name: "test".into(),
shape: vec![2],
dtype: DType::F16,
data: std::sync::Arc::new(vec![0x00, 0x3c, 0x00, 0x40]), };
let out = ensure_f16_bytes(&tensor).unwrap();
assert_eq!(out, vec![0x00, 0x3c, 0x00, 0x40]);
}
#[test]
fn wedge4f_qwen3vl_merger_norm_maps_to_v_post_ln() {
let w = hf_vit_name_to_gguf("model.visual.merger.norm.weight").unwrap();
let b = hf_vit_name_to_gguf("model.visual.merger.norm.bias").unwrap();
assert_eq!(w, "v.post_ln.weight");
assert_eq!(b, "v.post_ln.bias");
}
#[test]
fn wedge4f_qwen3vl_merger_fc_maps_to_mm_0_and_mm_2() {
assert_eq!(
hf_vit_name_to_gguf("model.visual.merger.linear_fc1.weight"),
Some("mm.0.weight".to_string())
);
assert_eq!(
hf_vit_name_to_gguf("model.visual.merger.linear_fc2.weight"),
Some("mm.2.weight".to_string())
);
}
#[test]
fn wedge4f_qwen3vl_per_block_fused_qkv_name_preserved() {
assert_eq!(
hf_vit_name_to_gguf("model.visual.blocks.5.attn.qkv.weight"),
Some("v.blk.5.attn_qkv.weight".to_string())
);
assert_eq!(
hf_vit_name_to_gguf("model.visual.blocks.5.attn.qkv.bias"),
Some("v.blk.5.attn_qkv.bias".to_string())
);
assert_eq!(
hf_vit_name_to_gguf("model.visual.blocks.5.attn.proj.weight"),
Some("v.blk.5.attn_out.weight".to_string())
);
}
#[test]
fn wedge4f_qwen3vl_deepstack_relative_to_absolute_index_remap() {
let indexes = vec![5, 11, 17];
assert_eq!(
hf_qwen3vl_deepstack_to_gguf("visual.deepstack_merger_list.0.norm.weight", &indexes,),
Some("v.deepstack.5.norm.weight".to_string())
);
assert_eq!(
hf_qwen3vl_deepstack_to_gguf(
"visual.deepstack_merger_list.1.linear_fc1.weight",
&indexes,
),
Some("v.deepstack.11.fc1.weight".to_string())
);
assert_eq!(
hf_qwen3vl_deepstack_to_gguf(
"visual.deepstack_merger_list.2.linear_fc2.bias",
&indexes,
),
Some("v.deepstack.17.fc2.bias".to_string())
);
}
#[test]
fn wedge4f_qwen3vl_deepstack_with_model_prefix_supported() {
let indexes = vec![5];
assert_eq!(
hf_qwen3vl_deepstack_to_gguf(
"model.visual.deepstack_merger_list.0.norm.weight",
&indexes,
),
Some("v.deepstack.5.norm.weight".to_string())
);
}
#[test]
fn wedge4f_qwen3vl_deepstack_out_of_range_relative_returns_none() {
let indexes = vec![5, 11];
assert!(hf_qwen3vl_deepstack_to_gguf(
"visual.deepstack_merger_list.2.norm.weight",
&indexes,
)
.is_none());
}
#[test]
fn wedge4f_qwen3vl_deepstack_unknown_component_returns_none() {
let indexes = vec![5];
assert!(hf_qwen3vl_deepstack_to_gguf(
"visual.deepstack_merger_list.0.linear_fc3.weight",
&indexes,
)
.is_none());
}
#[test]
fn wedge4f_qwen3vl_deepstack_non_deepstack_returns_none() {
let indexes = vec![5, 11, 17];
assert!(
hf_qwen3vl_deepstack_to_gguf("visual.merger.linear_fc1.weight", &indexes,).is_none()
);
assert!(hf_qwen3vl_deepstack_to_gguf("visual.blocks.0.norm1.weight", &indexes,).is_none());
assert!(hf_qwen3vl_deepstack_to_gguf(
"model.vision_tower.encoder.layer.5.layer_norm1.weight",
&indexes,
)
.is_none());
}
#[test]
fn ensure_f16_bytes_casts_bf16_to_f16() {
let tensor = TensorRef {
name: "test".into(),
shape: vec![1],
dtype: DType::BF16,
data: std::sync::Arc::new(vec![0x80, 0x3f]),
};
let out = ensure_f16_bytes(&tensor).unwrap();
assert_eq!(out, vec![0x00, 0x3c]);
}
}