use anyhow::{anyhow, Context, Result};
use mlx_native::gguf::GgufFile;
use mlx_native::{MlxBuffer, MlxDevice};
use crate::serve::header::LoadProgress;
use super::Qwen3VlTextConfig;
pub struct Qwen3VlTextLayerWeights {
pub attn_norm: MlxBuffer,
pub attn_q: MlxBuffer,
pub attn_k: MlxBuffer,
pub attn_v: MlxBuffer,
pub attn_q_norm: MlxBuffer,
pub attn_k_norm: MlxBuffer,
pub attn_output: MlxBuffer,
pub ffn_norm: MlxBuffer,
pub ffn_gate: MlxBuffer,
pub ffn_up: MlxBuffer,
pub ffn_down: MlxBuffer,
}
pub struct Qwen3VlTextWeights {
pub token_embd: MlxBuffer,
pub layers: Vec<Qwen3VlTextLayerWeights>,
pub output_norm: MlxBuffer,
pub output: Option<MlxBuffer>,
pub tied_word_embeddings: bool,
pub hidden_size: usize,
pub vocab_size: usize,
pub num_hidden_layers: usize,
}
impl Qwen3VlTextWeights {
pub fn load_from_gguf(
gguf: &GgufFile,
cfg: &Qwen3VlTextConfig,
device: &MlxDevice,
progress: &mut LoadProgress,
) -> Result<Self> {
let hidden = cfg.hidden_size as usize;
let n_kv_heads = cfg.num_key_value_heads as usize;
let head_dim = cfg.head_dim as usize;
let kv_dim = n_kv_heads * head_dim;
let intermediate = cfg.intermediate_size as usize;
let vocab = cfg.vocab_size as usize;
let token_embd = gguf
.load_tensor_f32("token_embd.weight", device)
.map_err(|e| anyhow!("token_embd.weight load (as F32): {e}"))?;
validate_shape("token_embd.weight", &token_embd, &[vocab, hidden])?;
let output_norm = gguf
.load_tensor_f32("output_norm.weight", device)
.map_err(|e| anyhow!("output_norm.weight load: {e}"))?;
validate_shape("output_norm.weight", &output_norm, &[hidden])?;
let output = if cfg.tied_word_embeddings {
None
} else {
let buf = gguf
.load_tensor("output.weight", device)
.map_err(|e| anyhow!("output.weight load: {e}"))?;
validate_shape("output.weight", &buf, &[vocab, hidden])?;
Some(buf)
};
let mut layers = Vec::with_capacity(cfg.num_hidden_layers as usize);
for il in 0..cfg.num_hidden_layers as usize {
let layer = load_layer(gguf, device, il, hidden, kv_dim, head_dim, intermediate)
.with_context(|| format!("Qwen3-VL text LM: layer {il}"))?;
layers.push(layer);
progress.on_layer(il + 1);
}
progress.finish();
Ok(Self {
token_embd,
layers,
output_norm,
output,
tied_word_embeddings: cfg.tied_word_embeddings,
hidden_size: hidden,
vocab_size: vocab,
num_hidden_layers: cfg.num_hidden_layers as usize,
})
}
}
fn validate_shape(name: &str, buf: &MlxBuffer, expected: &[usize]) -> Result<()> {
let actual = buf.shape();
if actual != expected {
return Err(anyhow!(
"{name}: shape mismatch — got {actual:?}, expected {expected:?}"
));
}
Ok(())
}
fn validate_quantized_proj_type(gguf: &mlx_native::gguf::GgufFile, name: &str) -> Result<()> {
use mlx_native::ops::quantized_matmul_ggml::GgmlType;
let info = gguf
.tensor_info(name)
.ok_or_else(|| anyhow!("{name}: tensor info missing (loader can't validate ggml_type)"))?;
match info.ggml_type {
GgmlType::Q4_0 | GgmlType::Q4_K | GgmlType::Q5_K | GgmlType::Q6_K | GgmlType::Q8_0 => {
Ok(())
}
other => Err(anyhow!(
"{name}: ggml_type {other:?} is not a quantized projection \
type (expected Q4_0/Q4_K/Q5_K/Q6_K/Q8_0). Loading would \
route through the wrong qmatmul kernel and produce garbage."
)),
}
}
fn load_layer(
gguf: &GgufFile,
device: &MlxDevice,
il: usize,
hidden: usize,
kv_dim: usize,
head_dim: usize,
intermediate: usize,
) -> Result<Qwen3VlTextLayerWeights> {
let attn_norm = gguf
.load_tensor_f32(&format!("blk.{il}.attn_norm.weight"), device)
.map_err(|e| anyhow!("blk.{il}.attn_norm.weight: {e}"))?;
validate_shape("attn_norm", &attn_norm, &[hidden])?;
let attn_q_name = format!("blk.{il}.attn_q.weight");
validate_quantized_proj_type(gguf, &attn_q_name)?;
let attn_q = gguf
.load_tensor(&attn_q_name, device)
.map_err(|e| anyhow!("{attn_q_name}: {e}"))?;
validate_shape("attn_q", &attn_q, &[hidden, hidden])?;
let attn_k_name = format!("blk.{il}.attn_k.weight");
validate_quantized_proj_type(gguf, &attn_k_name)?;
let attn_k = gguf
.load_tensor(&attn_k_name, device)
.map_err(|e| anyhow!("{attn_k_name}: {e}"))?;
validate_shape("attn_k", &attn_k, &[kv_dim, hidden])?;
let attn_v_name = format!("blk.{il}.attn_v.weight");
validate_quantized_proj_type(gguf, &attn_v_name)?;
let attn_v = gguf
.load_tensor(&attn_v_name, device)
.map_err(|e| anyhow!("{attn_v_name}: {e}"))?;
validate_shape("attn_v", &attn_v, &[kv_dim, hidden])?;
let attn_q_norm = gguf
.load_tensor_f32(&format!("blk.{il}.attn_q_norm.weight"), device)
.map_err(|e| anyhow!("blk.{il}.attn_q_norm.weight: {e}"))?;
validate_shape("attn_q_norm", &attn_q_norm, &[head_dim])?;
let attn_k_norm = gguf
.load_tensor_f32(&format!("blk.{il}.attn_k_norm.weight"), device)
.map_err(|e| anyhow!("blk.{il}.attn_k_norm.weight: {e}"))?;
validate_shape("attn_k_norm", &attn_k_norm, &[head_dim])?;
let attn_output_name = format!("blk.{il}.attn_output.weight");
validate_quantized_proj_type(gguf, &attn_output_name)?;
let attn_output = gguf
.load_tensor(&attn_output_name, device)
.map_err(|e| anyhow!("{attn_output_name}: {e}"))?;
validate_shape("attn_output", &attn_output, &[hidden, hidden])?;
let ffn_norm = gguf
.load_tensor_f32(&format!("blk.{il}.ffn_norm.weight"), device)
.map_err(|e| anyhow!("blk.{il}.ffn_norm.weight: {e}"))?;
validate_shape("ffn_norm", &ffn_norm, &[hidden])?;
let ffn_gate_name = format!("blk.{il}.ffn_gate.weight");
validate_quantized_proj_type(gguf, &ffn_gate_name)?;
let ffn_gate = gguf
.load_tensor(&ffn_gate_name, device)
.map_err(|e| anyhow!("{ffn_gate_name}: {e}"))?;
validate_shape("ffn_gate", &ffn_gate, &[intermediate, hidden])?;
let ffn_up_name = format!("blk.{il}.ffn_up.weight");
validate_quantized_proj_type(gguf, &ffn_up_name)?;
let ffn_up = gguf
.load_tensor(&ffn_up_name, device)
.map_err(|e| anyhow!("{ffn_up_name}: {e}"))?;
validate_shape("ffn_up", &ffn_up, &[intermediate, hidden])?;
let ffn_down_name = format!("blk.{il}.ffn_down.weight");
validate_quantized_proj_type(gguf, &ffn_down_name)?;
let ffn_down = gguf
.load_tensor(&ffn_down_name, device)
.map_err(|e| anyhow!("{ffn_down_name}: {e}"))?;
validate_shape("ffn_down", &ffn_down, &[hidden, intermediate])?;
Ok(Qwen3VlTextLayerWeights {
attn_norm,
attn_q,
attn_k,
attn_v,
attn_q_norm,
attn_k_norm,
attn_output,
ffn_norm,
ffn_gate,
ffn_up,
ffn_down,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_real_qwen3vl_2b_gguf_when_operator_gated() {
if std::env::var("HF2Q_QWEN3VL_LM_LOAD").ok().as_deref() != Some("1") {
eprintln!("skip: HF2Q_QWEN3VL_LM_LOAD!=1");
return;
}
let p =
std::path::PathBuf::from("/opt/hf2q/.cfa-archive/wedge4f-out/qwen3-vl-2b-q4_0.gguf");
if !p.exists() {
eprintln!("skip: real GGUF fixture not present at {}", p.display());
return;
}
let gguf = GgufFile::open(&p).expect("open real Qwen3-VL-2B GGUF");
let cfg = Qwen3VlTextConfig::from_gguf(&gguf).expect("parse config");
let device = MlxDevice::new().expect("Metal device init");
let mut progress = LoadProgress::new(false, 0, cfg.num_hidden_layers as usize);
let weights = Qwen3VlTextWeights::load_from_gguf(&gguf, &cfg, &device, &mut progress)
.expect("load weights from real Qwen3-VL-2B GGUF");
assert_eq!(weights.num_hidden_layers, 28);
assert_eq!(weights.hidden_size, 2048);
assert_eq!(weights.vocab_size, 151936);
assert!(
weights.tied_word_embeddings,
"Qwen3-VL-2B has tied word embeddings"
);
assert!(
weights.output.is_none(),
"tied → no dedicated output buffer"
);
assert_eq!(weights.layers.len(), 28);
let l0 = &weights.layers[0];
assert_eq!(l0.attn_norm.shape(), &[2048]);
assert_eq!(l0.attn_q.shape(), &[2048, 2048]);
assert_eq!(l0.attn_k.shape(), &[1024, 2048]);
assert_eq!(l0.attn_v.shape(), &[1024, 2048]);
assert_eq!(l0.attn_q_norm.shape(), &[128]);
assert_eq!(l0.attn_k_norm.shape(), &[128]);
assert_eq!(l0.attn_output.shape(), &[2048, 2048]);
assert_eq!(l0.ffn_norm.shape(), &[2048]);
assert_eq!(l0.ffn_gate.shape(), &[6144, 2048]);
assert_eq!(l0.ffn_up.shape(), &[6144, 2048]);
assert_eq!(l0.ffn_down.shape(), &[2048, 6144]);
}
}