use crate::models::lazy_bert::{BertConfig, BertModel, BertWeights};
use crate::models::lazy_clip::{ClipVisionConfig, ClipVisionModel, ClipVisionWeights};
use fuel_ir::Shape;
use fuel_ir::error::Result;
use fuel_tensor::{lazy::Tensor, lazy::WeightStorage};
#[cfg(test)]
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ChineseClipConfig {
pub text: BertConfig,
pub vision: ClipVisionConfig,
pub projection_dim: usize,
pub logit_scale_init_value: f32,
pub image_size: usize,
}
impl ChineseClipConfig {
pub fn clip_vit_base_patch16() -> Self {
let text = BertConfig {
vocab_size: 21128,
hidden_size: 768,
num_hidden_layers: 12,
num_attention_heads: 12,
intermediate_size: 3072,
max_position_embeddings: 512,
type_vocab_size: 2,
layer_norm_eps: 1e-12,
};
let vision = ClipVisionConfig {
embed_dim: 768,
intermediate_size: 3072,
num_hidden_layers: 12,
num_attention_heads: 12,
projection_dim: 512,
num_channels: 3,
image_size: 224,
patch_size: 16,
};
Self {
text,
vision,
projection_dim: 512,
logit_scale_init_value: 2.6592,
image_size: 224,
}
}
}
#[derive(Debug, Clone)]
pub struct ChineseClipWeights {
pub text: BertWeights,
pub vision: ClipVisionWeights,
pub visual_projection: WeightStorage,
pub text_projection: WeightStorage,
pub logit_scale: f32,
}
#[derive(Debug, Clone)]
pub struct ChineseClipModel {
pub config: ChineseClipConfig,
pub weights: ChineseClipWeights,
}
impl ChineseClipModel {
pub fn text_model(&self) -> BertModel {
BertModel::new(self.config.text.clone(), self.weights.text.clone())
}
pub fn vision_model(&self) -> ClipVisionModel {
ClipVisionModel {
config: self.config.vision.clone(),
weights: self.weights.vision.clone(),
}
}
pub fn get_image_features(&self, image: &Tensor) -> Result<Tensor> {
let pooled = self.vision_model().forward(image)?;
self.weights.visual_projection.apply_linear(
&pooled,
self.config.vision.embed_dim,
self.config.projection_dim,
)
}
pub fn get_text_features(&self, input_ids: &[u32]) -> Result<Tensor> {
let hidden = self.text_model().forward(input_ids)?;
let h = self.config.text.hidden_size;
let cls = hidden
.narrow(1_usize, 0, 1)?
.reshape(Shape::from_dims(&[1, h]))?;
self.weights
.text_projection
.apply_linear(&cls, h, self.config.projection_dim)
}
pub fn contrastive_logits(
&self,
image_features: &Tensor,
text_features: &Tensor,
) -> Result<(Tensor, Tensor)> {
let image_normed = l2_normalize_last(image_features)?;
let text_normed = l2_normalize_last(text_features)?;
let logits = text_normed.matmul(&image_normed.permute([1, 0_usize])?)?;
let scale = self.weights.logit_scale.exp() as f64;
let logits_per_text = logits.mul_scalar(scale);
let logits_per_image = logits_per_text.permute([1, 0_usize])?;
Ok((logits_per_text, logits_per_image))
}
}
fn l2_normalize_last(x: &Tensor) -> Result<Tensor> {
x.l2_normalize(1_usize, 0.0)
}
impl ChineseClipWeights {
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
cfg: &ChineseClipConfig,
) -> Result<Self> {
use fuel_tensor::{lazy::load_tensor_as_f32, lazy::load_transposed_matrix_preserve_dtype};
let text = BertWeights::load_from_mmapped(st, &cfg.text)?;
let vision = ClipVisionWeights::load_from_mmapped(st, &cfg.vision, "vision_model.")?;
let text_projection = load_transposed_matrix_preserve_dtype(
st,
"text_projection.weight",
cfg.projection_dim,
cfg.text.hidden_size,
)?;
let visual_projection = load_transposed_matrix_preserve_dtype(
st,
"visual_projection.weight",
cfg.projection_dim,
cfg.vision.embed_dim,
)?;
let logit_scale = load_tensor_as_f32(st, "logit_scale")?
.first()
.copied()
.unwrap_or(0.0);
Ok(Self {
text,
vision,
visual_projection,
text_projection,
logit_scale,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::lazy_bert::BertLayerWeights;
use crate::models::lazy_clip::ClipEncoderLayerWeights;
use fuel_tensor::Device;
fn rng_seed(seed: u32) -> impl FnMut() -> f32 {
let mut s = seed;
move || {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.05
}
}
fn vec_of(n: usize, nb: &mut dyn FnMut() -> f32) -> Arc<[f32]> {
Arc::from((0..n).map(|_| nb()).collect::<Vec<_>>())
}
fn ws(n: usize, nb: &mut dyn FnMut() -> f32) -> WeightStorage {
WeightStorage::F32(vec_of(n, nb))
}
fn tiny_text_cfg() -> BertConfig {
BertConfig {
vocab_size: 32,
hidden_size: 8,
num_hidden_layers: 1,
num_attention_heads: 2,
intermediate_size: 16,
max_position_embeddings: 16,
type_vocab_size: 2,
layer_norm_eps: 1e-12,
}
}
fn tiny_vision_cfg() -> ClipVisionConfig {
ClipVisionConfig {
embed_dim: 8,
intermediate_size: 16,
num_hidden_layers: 1,
num_attention_heads: 2,
projection_dim: 4,
num_channels: 3,
image_size: 16,
patch_size: 8,
}
}
fn build_tiny_bert_layer(h: usize, ff: usize, nb: &mut dyn FnMut() -> f32) -> BertLayerWeights {
BertLayerWeights {
attn_q_w: vec_of(h * h, nb),
attn_q_b: vec_of(h, nb),
attn_k_w: vec_of(h * h, nb),
attn_k_b: vec_of(h, nb),
attn_v_w: vec_of(h * h, nb),
attn_v_b: vec_of(h, nb),
attn_out_w: vec_of(h * h, nb),
attn_out_b: vec_of(h, nb),
attn_ln_gamma: Arc::from(vec![1.0_f32; h]),
attn_ln_beta: Arc::from(vec![0.0_f32; h]),
ffn_in_w: vec_of(h * ff, nb),
ffn_in_b: vec_of(ff, nb),
ffn_out_w: vec_of(ff * h, nb),
ffn_out_b: vec_of(h, nb),
ffn_ln_gamma: Arc::from(vec![1.0_f32; h]),
ffn_ln_beta: Arc::from(vec![0.0_f32; h]),
}
}
fn build_tiny_clip_vision_layer(
e: usize,
ff: usize,
nb: &mut dyn FnMut() -> f32,
) -> ClipEncoderLayerWeights {
ClipEncoderLayerWeights {
ln1_gain: Arc::from(vec![1.0_f32; e]),
ln1_bias: Arc::from(vec![0.0_f32; e]),
q_proj: ws(e * e, nb),
q_proj_bias: vec_of(e, nb),
k_proj: ws(e * e, nb),
k_proj_bias: vec_of(e, nb),
v_proj: ws(e * e, nb),
v_proj_bias: vec_of(e, nb),
out_proj: ws(e * e, nb),
out_proj_bias: vec_of(e, nb),
ln2_gain: Arc::from(vec![1.0_f32; e]),
ln2_bias: Arc::from(vec![0.0_f32; e]),
fc1: ws(e * ff, nb),
fc1_bias: vec_of(ff, nb),
fc2: ws(ff * e, nb),
fc2_bias: vec_of(e, nb),
}
}
fn tiny_model() -> ChineseClipModel {
let text_cfg = tiny_text_cfg();
let vision_cfg = tiny_vision_cfg();
let projection_dim = 4;
let cfg = ChineseClipConfig {
text: text_cfg.clone(),
vision: vision_cfg.clone(),
projection_dim,
logit_scale_init_value: 2.6592,
image_size: 16,
};
let mut nb = rng_seed(2026);
let h = text_cfg.hidden_size;
let ff = text_cfg.intermediate_size;
let text = BertWeights {
word_embeddings: vec_of(text_cfg.vocab_size * h, &mut nb),
position_embeddings: vec_of(text_cfg.max_position_embeddings * h, &mut nb),
token_type_embeddings: vec_of(text_cfg.type_vocab_size * h, &mut nb),
emb_ln_gamma: Arc::from(vec![1.0_f32; h]),
emb_ln_beta: Arc::from(vec![0.0_f32; h]),
layers: (0..text_cfg.num_hidden_layers)
.map(|_| build_tiny_bert_layer(h, ff, &mut nb))
.collect(),
};
let e = vision_cfg.embed_dim;
let vff = vision_cfg.intermediate_size;
let vision = ClipVisionWeights {
patch_proj: vec_of(
e * vision_cfg.num_channels * vision_cfg.patch_size * vision_cfg.patch_size,
&mut nb,
),
class_embedding: vec_of(e, &mut nb),
position_embedding: vec_of((vision_cfg.num_patches() + 1) * e, &mut nb),
pre_ln_gain: Arc::from(vec![1.0_f32; e]),
pre_ln_bias: Arc::from(vec![0.0_f32; e]),
layers: (0..vision_cfg.num_hidden_layers)
.map(|_| build_tiny_clip_vision_layer(e, vff, &mut nb))
.collect(),
post_ln_gain: Arc::from(vec![1.0_f32; e]),
post_ln_bias: Arc::from(vec![0.0_f32; e]),
};
let weights = ChineseClipWeights {
text,
vision,
visual_projection: ws(e * projection_dim, &mut nb),
text_projection: ws(h * projection_dim, &mut nb),
logit_scale: 2.6592_f32.ln().max(0.0_f32),
};
ChineseClipModel {
config: cfg,
weights,
}
}
#[test]
fn presets_construct() {
let p = ChineseClipConfig::clip_vit_base_patch16();
assert_eq!(p.projection_dim, 512);
assert_eq!(p.text.vocab_size, 21128);
assert_eq!(p.vision.patch_size, 16);
}
#[test]
fn image_features_shape_and_finite() {
let model = tiny_model();
let image = Tensor::from_f32(
(0..(3 * 16 * 16))
.map(|i| (i as f32) * 0.01)
.collect::<Vec<_>>(),
Shape::from_dims(&[1, 3, 16, 16]),
&Device::cpu(),
)
.unwrap();
let f = model.get_image_features(&image).unwrap();
assert_eq!(f.shape().dims(), &[1, 4]);
for &v in &f.realize_f32() {
assert!(v.is_finite());
}
}
#[test]
fn text_features_shape_and_finite() {
let model = tiny_model();
let f = model.get_text_features(&[1_u32, 2, 3, 4]).unwrap();
assert_eq!(f.shape().dims(), &[1, 4]);
for &v in &f.realize_f32() {
assert!(v.is_finite());
}
}
#[test]
fn contrastive_logits_shape_and_finite() {
let model = tiny_model();
let anchor =
Tensor::from_f32(vec![0.0_f32], Shape::from_dims(&[1]), &Device::cpu()).unwrap();
let img_feats = anchor
.const_f32_like(
Arc::from(vec![0.1_f32, 0.2, 0.3, 0.4]),
Shape::from_dims(&[1, 4]),
)
.unwrap();
let txt_feats = anchor
.const_f32_like(
Arc::from(vec![0.5_f32, -0.2, 0.1, 0.3]),
Shape::from_dims(&[1, 4]),
)
.unwrap();
let (lpt, lpi) = model.contrastive_logits(&img_feats, &txt_feats).unwrap();
assert_eq!(lpt.shape().dims(), &[1, 1]);
assert_eq!(lpi.shape().dims(), &[1, 1]);
for &v in &lpt.realize_f32() {
assert!(v.is_finite());
}
}
}