#[derive(Debug, Clone)]
pub struct VisionConfig {
pub image_size: u32,
pub patch_size: u32,
pub n_patches: usize,
pub d_model: usize,
pub n_heads: usize,
pub n_layers: usize,
pub spatial_merge_size: usize,
pub global_attn_every: usize,
pub window_size: usize,
pub mlp_ratio: usize,
pub use_gelu: bool,
pub d_decoder: usize,
pub d_mlp: usize,
}
impl VisionConfig {
pub fn qwen3_vl_7b() -> Self {
let image_size = 448u32;
let patch_size = 16u32;
let n_patches = ((image_size / patch_size) as usize).pow(2);
let d_model = 1152usize;
let mlp_ratio = 4usize;
Self {
image_size,
patch_size,
n_patches,
d_model,
n_heads: 16,
n_layers: 27,
spatial_merge_size: 2,
global_attn_every: 4,
window_size: 16,
mlp_ratio,
use_gelu: true,
d_decoder: 3584,
d_mlp: d_model * mlp_ratio,
}
}
pub fn visual_tokens(&self) -> usize {
self.n_patches / (self.spatial_merge_size * self.spatial_merge_size)
}
pub fn head_dim(&self) -> usize {
self.d_model / self.n_heads
}
pub fn validate(&self) -> Result<(), super::VisionError> {
if self.patch_size == 0 {
return Err(super::VisionError::InvalidConfig(
"patch_size must be > 0".into(),
));
}
if self.image_size % self.patch_size != 0 {
return Err(super::VisionError::InvalidConfig(format!(
"image_size {} must be divisible by patch_size {}",
self.image_size, self.patch_size
)));
}
let expected_n_patches = ((self.image_size / self.patch_size) as usize).pow(2);
if self.n_patches != expected_n_patches {
return Err(super::VisionError::InvalidConfig(format!(
"n_patches {} inconsistent with image_size={} patch_size={}; expected {}",
self.n_patches, self.image_size, self.patch_size, expected_n_patches
)));
}
if self.n_patches % (self.spatial_merge_size * self.spatial_merge_size) != 0 {
return Err(super::VisionError::InvalidConfig(format!(
"n_patches {} must be divisible by spatial_merge_size^2={}",
self.n_patches,
self.spatial_merge_size * self.spatial_merge_size
)));
}
if self.d_model == 0 {
return Err(super::VisionError::InvalidConfig(
"d_model must be > 0".into(),
));
}
if self.n_heads == 0 || self.d_model % self.n_heads != 0 {
return Err(super::VisionError::InvalidConfig(format!(
"d_model {} must be divisible by n_heads {}",
self.d_model, self.n_heads
)));
}
if self.n_layers == 0 {
return Err(super::VisionError::InvalidConfig(
"n_layers must be > 0".into(),
));
}
if self.d_decoder == 0 {
return Err(super::VisionError::InvalidConfig(
"d_decoder must be > 0".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qwen3_vl_7b_defaults_are_consistent() {
let cfg = VisionConfig::qwen3_vl_7b();
assert_eq!(cfg.image_size, 448);
assert_eq!(cfg.patch_size, 16);
assert_eq!(cfg.n_patches, 784); assert_eq!(cfg.d_model, 1152);
assert_eq!(cfg.n_heads, 16);
assert_eq!(cfg.n_layers, 27);
assert_eq!(cfg.spatial_merge_size, 2);
assert_eq!(cfg.visual_tokens(), 196); assert_eq!(cfg.head_dim(), 72); assert_eq!(cfg.d_mlp, 4608); cfg.validate().expect("default config validates");
}
#[test]
fn validation_rejects_indivisible_image_patch() {
let mut cfg = VisionConfig::qwen3_vl_7b();
cfg.image_size = 449; assert!(cfg.validate().is_err());
}
#[test]
fn validation_rejects_zero_d_model() {
let mut cfg = VisionConfig::qwen3_vl_7b();
cfg.d_model = 0;
assert!(cfg.validate().is_err());
}
#[test]
fn validation_rejects_misaligned_heads() {
let mut cfg = VisionConfig::qwen3_vl_7b();
cfg.n_heads = 7; assert!(cfg.validate().is_err());
}
#[test]
fn visual_tokens_count() {
let cfg = VisionConfig::qwen3_vl_7b();
assert_eq!(cfg.visual_tokens(), 196);
}
}