Skip to main content

runtime/models_v2/
clip.rs

1//! CLIP Model V2 - Clean implementation using solid abstractions
2//!
3//! This implements the CLIP (Contrastive Language-Image Pretraining) architecture including:
4//! - CLIP-ViT-B/32, CLIP-ViT-B/16, CLIP-ViT-L/14
5//!
6//! CLIP is a dual encoder model for vision-language tasks:
7//! - Text encoder: standard transformer with causal masking
8//! - Vision encoder: ViT-style with patch embeddings, CLS token, position embeddings
9//! - Projects both to shared embedding space
10//! - Outputs similarity scores between text and image embeddings
11//! - Uses contrastive learning (logit_scale parameter)
12
13use crate::model_config;
14use super::traits::*;
15use anyhow::Result;
16use serde::{Serialize, Deserialize};
17
18// Note: CLIP uses different hidden sizes for text and vision, but model_config! macro
19// requires hidden_size and num_hidden_layers. We use transformer_width as hidden_size
20// and transformer_layers as num_hidden_layers for the text model.
21model_config!(CLIPConfig {
22    // Text config - these are used by ModelConfig trait
23    vocab_size: usize = 49408,
24    hidden_size: usize = 512,           // Same as transformer_width
25    num_hidden_layers: usize = 12,      // Same as transformer_layers
26    context_length: usize = 77,
27    transformer_width: usize = 512,
28    transformer_heads: usize = 8,
29    transformer_layers: usize = 12,
30
31    // Vision config
32    image_resolution: usize = 224,
33    vision_layers: usize = 12,
34    vision_width: usize = 768,
35    vision_heads: usize = 12,
36    vision_patch_size: usize = 32,
37
38    // Shared config
39    embed_dim: usize = 512,             // Projection dimension
40    projection_dim: usize = 512,
41
42    // Additional config
43    layer_norm_eps: f32 = 1e-5,
44    attention_dropout: f32 = 0.0,
45    initializer_range: f32 = 0.02,
46});
47
48impl CLIPConfig {
49    /// Create CLIPConfig from GGUF model configuration
50    /// Note: GGUF files for CLIP are rare, but this provides compatibility
51    pub fn from_gguf_config(gguf: &crate::weight_loader_core::GGUFModelConfig) -> Self {
52        Self {
53            vocab_size: gguf.vocab_size,
54            hidden_size: gguf.hidden_size,
55            num_hidden_layers: gguf.num_hidden_layers,
56            transformer_width: gguf.hidden_size,
57            transformer_layers: gguf.num_hidden_layers,
58            transformer_heads: gguf.num_attention_heads,
59            vision_width: gguf.hidden_size,  // Approximate
60            vision_layers: gguf.num_hidden_layers,
61            vision_heads: gguf.num_attention_heads,
62            ..Default::default()
63        }
64    }
65
66    /// Get vision head dimension
67    pub fn vision_head_dim(&self) -> usize {
68        self.vision_width / self.vision_heads
69    }
70
71    /// Get text head dimension
72    pub fn text_head_dim(&self) -> usize {
73        self.transformer_width / self.transformer_heads
74    }
75
76    /// Get number of patches (excluding CLS token)
77    pub fn num_patches(&self) -> usize {
78        (self.image_resolution / self.vision_patch_size).pow(2)
79    }
80
81    /// Get number of positions (patches + CLS token)
82    pub fn num_positions(&self) -> usize {
83        self.num_patches() + 1
84    }
85}
86
87/// Main CLIP model implementation
88pub struct CLIPModelV2 {
89    config: CLIPConfig,
90    device: Device,
91    text_model: CLIPTextTransformer,
92    vision_model: CLIPVisionTransformer,
93    text_projection: Tensor,
94    visual_projection: Tensor,
95    logit_scale: Tensor,
96}
97
98/// CLIP Text Transformer (causal self-attention)
99pub struct CLIPTextTransformer {
100    embeddings: CLIPTextEmbeddings,
101    encoder: CLIPEncoder,
102    final_layer_norm: Tensor,
103    final_layer_norm_bias: Option<Tensor>,
104    config: CLIPConfig,
105    is_causal: bool,
106}
107
108/// CLIP Vision Transformer (bidirectional self-attention)
109pub struct CLIPVisionTransformer {
110    embeddings: CLIPVisionEmbeddings,
111    pre_layernorm: Tensor,
112    pre_layernorm_bias: Option<Tensor>,
113    encoder: CLIPEncoder,
114    post_layernorm: Tensor,
115    post_layernorm_bias: Option<Tensor>,
116    config: CLIPConfig,
117}
118
119/// Text embeddings: token + position
120pub struct CLIPTextEmbeddings {
121    token_embedding: Tensor,
122    position_embedding: Tensor,
123    config: CLIPConfig,
124}
125
126/// Vision embeddings: patch + class + position
127pub struct CLIPVisionEmbeddings {
128    patch_embedding: Tensor,      // Conv2D as linear projection
129    class_embedding: Tensor,      // [CLS] token
130    position_embedding: Tensor,
131    config: CLIPConfig,
132}
133
134/// Shared encoder structure for both text and vision
135pub struct CLIPEncoder {
136    layers: Vec<CLIPEncoderLayer>,
137    is_causal: bool,
138}
139
140/// Single encoder layer
141pub struct CLIPEncoderLayer {
142    self_attn: CLIPAttention,
143    layer_norm1: Tensor,
144    layer_norm1_bias: Option<Tensor>,
145    mlp: CLIPMLP,
146    layer_norm2: Tensor,
147    layer_norm2_bias: Option<Tensor>,
148    hidden_size: usize,
149}
150
151/// Multi-head attention
152pub struct CLIPAttention {
153    q_proj: Tensor,
154    k_proj: Tensor,
155    v_proj: Tensor,
156    out_proj: Tensor,
157    q_bias: Option<Tensor>,
158    k_bias: Option<Tensor>,
159    v_bias: Option<Tensor>,
160    out_bias: Option<Tensor>,
161    num_heads: usize,
162    head_dim: usize,
163    scale: f32,
164}
165
166/// MLP with QuickGELU activation
167pub struct CLIPMLP {
168    fc1: Tensor,
169    fc1_bias: Option<Tensor>,
170    fc2: Tensor,
171    fc2_bias: Option<Tensor>,
172    hidden_size: usize,
173    intermediate_size: usize,
174}
175
176impl Model for CLIPModelV2 {
177    type Config = CLIPConfig;
178
179    fn new(config: CLIPConfig) -> Result<Self> {
180        let device = Device::CPU;
181
182        let text_model = CLIPTextTransformer::new(&config, &device)?;
183        let vision_model = CLIPVisionTransformer::new(&config, &device)?;
184
185        // Projection matrices map to shared embedding space
186        // Note: These may need transposing when loading weights
187        let text_projection = ops_fn::zeros(
188            &[config.transformer_width, config.projection_dim],
189            DataType::Float32,
190            &device
191        )?;
192        let visual_projection = ops_fn::zeros(
193            &[config.vision_width, config.projection_dim],
194            DataType::Float32,
195            &device
196        )?;
197
198        // Learned temperature parameter (logit_scale = log(1/0.07) initially)
199        let logit_scale_data = vec![config.projection_dim as f32 * 0.01]; // Small init
200        let logit_scale = Tensor::from_f32_slice(&[2.6592], &[1], &device)?; // ln(1/0.07)
201
202        Ok(Self {
203            config,
204            device,
205            text_model,
206            vision_model,
207            text_projection,
208            visual_projection,
209            logit_scale,
210        })
211    }
212
213    fn from_weights(config: CLIPConfig, weights: ModelWeights) -> Result<Self> {
214        let mut model = Self::new(config)?;
215
216        // Load projection weights (transpose for matmul)
217        if let Some(w) = weights.get("text_projection.weight") {
218            model.text_projection = ops_fn::transpose(w)?;
219        } else if let Some(w) = weights.get("text_projection") {
220            // Some models store without .weight suffix
221            model.text_projection = ops_fn::transpose(w)?;
222        }
223
224        if let Some(w) = weights.get("visual_projection.weight") {
225            model.visual_projection = ops_fn::transpose(w)?;
226        } else if let Some(w) = weights.get("visual_projection") {
227            model.visual_projection = ops_fn::transpose(w)?;
228        }
229
230        if let Some(w) = weights.get("logit_scale") {
231            model.logit_scale = w.clone();
232        }
233
234        model.text_model.load_weights(&weights)?;
235        model.vision_model.load_weights(&weights)?;
236
237        Ok(model)
238    }
239
240    fn forward(&self, inputs: &ModelInputs) -> Result<ModelOutputs> {
241        match inputs {
242            ModelInputs::Multimodal { input_ids, pixel_values, .. } => {
243                // Encode text
244                let text_features = self.encode_text_internal(input_ids)?;
245
246                // Encode vision
247                let pixel_values = pixel_values.as_ref()
248                    .ok_or_else(|| anyhow::anyhow!("CLIP multimodal forward requires pixel_values"))?;
249                let image_features = self.encode_image_internal(pixel_values)?;
250
251                // Compute similarity with learned temperature
252                let logit_scale = ops_fn::exp(&self.logit_scale)?;
253                let logit_scale_val = logit_scale.to_candle()?.to_vec1::<f32>()?[0];
254
255                // logits_per_text = text_features @ image_features.T * logit_scale
256                let image_features_t = ops_fn::transpose(&image_features)?;
257                let logits_per_text = ops_fn::matmul(&text_features, &image_features_t)?;
258                let logits_per_text = ops_fn::scale(&logits_per_text, logit_scale_val)?;
259
260                // logits_per_image = logits_per_text.T
261                let logits_per_image = ops_fn::transpose(&logits_per_text)?;
262
263                Ok(ModelOutputs::CLIP {
264                    logits_per_text,
265                    logits_per_image,
266                    text_embeds: text_features,
267                    image_embeds: image_features,
268                })
269            },
270            ModelInputs::Text { input_ids, .. } => {
271                // Text-only encoding
272                let text_features = self.encode_text_internal(input_ids)?;
273
274                Ok(ModelOutputs::Embeddings {
275                    embeddings: text_features.clone(),
276                    pooled: Some(text_features),
277                })
278            },
279            ModelInputs::Image { pixel_values, .. } => {
280                // Image-only encoding
281                let image_features = self.encode_image_internal(pixel_values)?;
282
283                Ok(ModelOutputs::Embeddings {
284                    embeddings: image_features.clone(),
285                    pooled: Some(image_features),
286                })
287            },
288            _ => Err(anyhow::anyhow!("CLIP expects text, image, or multimodal input")),
289        }
290    }
291
292    fn generate(&self, _prompt: &str, _config: &GenerationConfig) -> Result<String> {
293        // CLIP is not a generative model
294        Err(anyhow::anyhow!(
295            "CLIP is a contrastive model for computing image-text similarity. \
296             It does not support text generation. Use encode_text() and encode_image() \
297             to compute embeddings, then compare them for similarity."
298        ))
299    }
300
301    fn config(&self) -> &Self::Config {
302        &self.config
303    }
304
305    fn memory_requirements(&self) -> MemoryRequirements {
306        // Text model parameters
307        let text_embedding_params = self.config.vocab_size * self.config.transformer_width +
308                                   self.config.context_length * self.config.transformer_width;
309        let text_layer_params = self.config.transformer_layers * (
310            4 * self.config.transformer_width * self.config.transformer_width + // attention
311            2 * self.config.transformer_width * (self.config.transformer_width * 4) // MLP
312        );
313
314        // Vision model parameters
315        let num_patches = self.config.num_patches();
316        let vision_embedding_params = 3 * self.config.vision_patch_size * self.config.vision_patch_size * self.config.vision_width +
317                                     self.config.vision_width + // class embedding
318                                     (num_patches + 1) * self.config.vision_width; // position embedding
319        let vision_layer_params = self.config.vision_layers * (
320            4 * self.config.vision_width * self.config.vision_width + // attention
321            2 * self.config.vision_width * (self.config.vision_width * 4) // MLP
322        );
323
324        // Projections
325        let projection_params = self.config.transformer_width * self.config.projection_dim +
326                               self.config.vision_width * self.config.projection_dim;
327
328        let total_params = text_embedding_params + text_layer_params +
329                          vision_embedding_params + vision_layer_params +
330                          projection_params;
331
332        let param_bytes = total_params * 4; // float32
333
334        MemoryRequirements {
335            gpu_memory: param_bytes,
336            cpu_memory: param_bytes / 4,
337            kv_cache_memory: 0, // CLIP doesn't use KV cache
338            peak_memory: param_bytes * 2, // Forward pass activations
339        }
340    }
341
342    fn to_device(&mut self, device: &Device) -> Result<()> {
343        self.device = device.clone();
344        self.text_projection = self.text_projection.to_device(device)?;
345        self.visual_projection = self.visual_projection.to_device(device)?;
346        self.logit_scale = self.logit_scale.to_device(device)?;
347        self.text_model.to_device(device)?;
348        self.vision_model.to_device(device)?;
349        Ok(())
350    }
351}
352
353impl CLIPModelV2 {
354    /// Encode text into normalized embeddings
355    pub fn encode_text(&self, input_ids: &Tensor) -> Result<Tensor> {
356        self.encode_text_internal(input_ids)
357    }
358
359    /// Encode image into normalized embeddings
360    pub fn encode_image(&self, pixel_values: &Tensor) -> Result<Tensor> {
361        self.encode_image_internal(pixel_values)
362    }
363
364    /// Compute similarity between text and image embeddings
365    pub fn compute_similarity(&self, text_features: &Tensor, image_features: &Tensor) -> Result<Tensor> {
366        let logit_scale = ops_fn::exp(&self.logit_scale)?;
367        let logit_scale_val = logit_scale.to_candle()?.to_vec1::<f32>()?[0];
368
369        let image_features_t = ops_fn::transpose(image_features)?;
370        let similarity = ops_fn::matmul(text_features, &image_features_t)?;
371        ops_fn::scale(&similarity, logit_scale_val)
372    }
373
374    /// Internal text encoding with projection and normalization
375    fn encode_text_internal(&self, input_ids: &Tensor) -> Result<Tensor> {
376        // Get text encoder output
377        let hidden_states = self.text_model.forward(input_ids)?;
378
379        // Pool: take features from the EOS token position (highest index in each sequence)
380        // For CLIP, this is typically the last non-padding token
381        let pooled = self.pool_text_features(&hidden_states, input_ids)?;
382
383        // Project to shared embedding space
384        let text_features = ops_fn::matmul(&pooled, &self.text_projection)?;
385
386        // L2 normalize
387        self.normalize_features(&text_features)
388    }
389
390    /// Internal image encoding with projection and normalization
391    fn encode_image_internal(&self, pixel_values: &Tensor) -> Result<Tensor> {
392        // Get vision encoder output (already pooled via CLS token)
393        let image_features = self.vision_model.forward(pixel_values)?;
394
395        // Project to shared embedding space
396        let image_features = ops_fn::matmul(&image_features, &self.visual_projection)?;
397
398        // L2 normalize
399        self.normalize_features(&image_features)
400    }
401
402    /// Pool text features by taking the EOS token position
403    fn pool_text_features(&self, hidden_states: &Tensor, input_ids: &Tensor) -> Result<Tensor> {
404        // In CLIP, text features are pooled from the EOS token position
405        // For simplicity, we take the last token in each sequence
406        let candle_hidden = hidden_states.to_candle()?;
407        let shape = candle_hidden.dims();
408
409        if shape.len() == 3 {
410            // [batch, seq, hidden] -> take last position -> [batch, hidden]
411            let seq_len = shape[1];
412            let pooled = candle_hidden.narrow(1, seq_len - 1, 1)?.squeeze(1)?;
413            Ok(Tensor::from_candle(pooled))
414        } else if shape.len() == 2 {
415            // Already [batch/seq, hidden] - just return as is
416            Ok(hidden_states.clone())
417        } else {
418            Err(anyhow::anyhow!("Invalid hidden states shape: {:?}", shape))
419        }
420    }
421
422    /// L2 normalize features
423    fn normalize_features(&self, features: &Tensor) -> Result<Tensor> {
424        ops_fn::normalize(features, 2, -1)
425    }
426}
427
428// ============================================================================
429// Text Transformer
430// ============================================================================
431
432impl CLIPTextTransformer {
433    fn new(config: &CLIPConfig, device: &Device) -> Result<Self> {
434        Ok(Self {
435            embeddings: CLIPTextEmbeddings::new(config, device)?,
436            encoder: CLIPEncoder::new_text(config, device)?,
437            final_layer_norm: ops_fn::zeros(&[config.transformer_width], DataType::Float32, device)?,
438            final_layer_norm_bias: None,
439            config: config.clone(),
440            is_causal: true,
441        })
442    }
443
444    fn forward(&self, input_ids: &Tensor) -> Result<Tensor> {
445        // Get embeddings (token + position)
446        let hidden_states = self.embeddings.forward(input_ids)?;
447
448        // Apply transformer layers with causal attention
449        let hidden_states = self.encoder.forward(&hidden_states, self.is_causal)?;
450
451        // Final layer norm
452        ops_fn::layer_norm(&hidden_states, &self.final_layer_norm, self.final_layer_norm_bias.as_ref(), self.config.layer_norm_eps)
453    }
454
455    fn load_weights(&mut self, weights: &ModelWeights) -> Result<()> {
456        if let Some(w) = weights.get("text_model.final_layer_norm.weight") {
457            self.final_layer_norm = w.clone();
458        }
459        if let Some(w) = weights.get("text_model.final_layer_norm.bias") {
460            self.final_layer_norm_bias = Some(w.clone());
461        }
462
463        self.embeddings.load_weights(weights)?;
464        self.encoder.load_weights(weights, "text_model")?;
465        Ok(())
466    }
467
468    fn to_device(&mut self, device: &Device) -> Result<()> {
469        self.final_layer_norm = self.final_layer_norm.to_device(device)?;
470        if let Some(ref mut b) = self.final_layer_norm_bias {
471            *b = b.to_device(device)?;
472        }
473        self.embeddings.to_device(device)?;
474        self.encoder.to_device(device)?;
475        Ok(())
476    }
477}
478
479// ============================================================================
480// Vision Transformer
481// ============================================================================
482
483impl CLIPVisionTransformer {
484    fn new(config: &CLIPConfig, device: &Device) -> Result<Self> {
485        Ok(Self {
486            embeddings: CLIPVisionEmbeddings::new(config, device)?,
487            pre_layernorm: ops_fn::zeros(&[config.vision_width], DataType::Float32, device)?,
488            pre_layernorm_bias: None,
489            encoder: CLIPEncoder::new_vision(config, device)?,
490            post_layernorm: ops_fn::zeros(&[config.vision_width], DataType::Float32, device)?,
491            post_layernorm_bias: None,
492            config: config.clone(),
493        })
494    }
495
496    fn forward(&self, pixel_values: &Tensor) -> Result<Tensor> {
497        // Get patch embeddings + CLS token + position embeddings
498        let hidden_states = self.embeddings.forward(pixel_values)?;
499
500        // Pre-layer norm (some CLIP variants)
501        let hidden_states = ops_fn::layer_norm(
502            &hidden_states,
503            &self.pre_layernorm,
504            self.pre_layernorm_bias.as_ref(),
505            self.config.layer_norm_eps
506        )?;
507
508        // Apply transformer layers with bidirectional attention
509        let hidden_states = self.encoder.forward(&hidden_states, false)?;
510
511        // Extract CLS token (first position)
512        let candle_hidden = hidden_states.to_candle()?;
513        let cls_hidden = candle_hidden.narrow(1, 0, 1)?.squeeze(1)?;
514        let cls_hidden = Tensor::from_candle(cls_hidden);
515
516        // Post-layer norm on CLS token
517        ops_fn::layer_norm(
518            &cls_hidden,
519            &self.post_layernorm,
520            self.post_layernorm_bias.as_ref(),
521            self.config.layer_norm_eps
522        )
523    }
524
525    fn load_weights(&mut self, weights: &ModelWeights) -> Result<()> {
526        if let Some(w) = weights.get("vision_model.pre_layrnorm.weight") {
527            self.pre_layernorm = w.clone();
528        } else if let Some(w) = weights.get("vision_model.pre_layernorm.weight") {
529            self.pre_layernorm = w.clone();
530        }
531        if let Some(w) = weights.get("vision_model.pre_layrnorm.bias") {
532            self.pre_layernorm_bias = Some(w.clone());
533        } else if let Some(w) = weights.get("vision_model.pre_layernorm.bias") {
534            self.pre_layernorm_bias = Some(w.clone());
535        }
536
537        if let Some(w) = weights.get("vision_model.post_layernorm.weight") {
538            self.post_layernorm = w.clone();
539        }
540        if let Some(w) = weights.get("vision_model.post_layernorm.bias") {
541            self.post_layernorm_bias = Some(w.clone());
542        }
543
544        self.embeddings.load_weights(weights)?;
545        self.encoder.load_weights(weights, "vision_model")?;
546        Ok(())
547    }
548
549    fn to_device(&mut self, device: &Device) -> Result<()> {
550        self.pre_layernorm = self.pre_layernorm.to_device(device)?;
551        self.post_layernorm = self.post_layernorm.to_device(device)?;
552        if let Some(ref mut b) = self.pre_layernorm_bias {
553            *b = b.to_device(device)?;
554        }
555        if let Some(ref mut b) = self.post_layernorm_bias {
556            *b = b.to_device(device)?;
557        }
558        self.embeddings.to_device(device)?;
559        self.encoder.to_device(device)?;
560        Ok(())
561    }
562}
563
564// ============================================================================
565// Embeddings
566// ============================================================================
567
568impl CLIPTextEmbeddings {
569    fn new(config: &CLIPConfig, device: &Device) -> Result<Self> {
570        Ok(Self {
571            token_embedding: ops_fn::zeros(
572                &[config.vocab_size, config.transformer_width],
573                DataType::Float32,
574                device
575            )?,
576            position_embedding: ops_fn::zeros(
577                &[config.context_length, config.transformer_width],
578                DataType::Float32,
579                device
580            )?,
581            config: config.clone(),
582        })
583    }
584
585    fn forward(&self, input_ids: &Tensor) -> Result<Tensor> {
586        // Token embeddings
587        let token_embeds = ops_fn::embedding(input_ids, &self.token_embedding)?;
588
589        // Position embeddings (learned, not sinusoidal)
590        let input_shape = input_ids.shape();
591        let seq_len = if input_shape.len() >= 2 { input_shape[1] } else { input_shape[0] };
592
593        // Slice position embeddings to match sequence length
594        let pos_candle = self.position_embedding.to_candle()?;
595        let pos_slice = pos_candle.narrow(0, 0, seq_len)?;
596        let pos_embeds = Tensor::from_candle(pos_slice);
597
598        // Add token and position embeddings
599        ops_fn::add(&token_embeds, &pos_embeds)
600    }
601
602    fn load_weights(&mut self, weights: &ModelWeights) -> Result<()> {
603        if let Some(w) = weights.get("text_model.embeddings.token_embedding.weight") {
604            self.token_embedding = w.clone();
605        }
606        if let Some(w) = weights.get("text_model.embeddings.position_embedding.weight") {
607            self.position_embedding = w.clone();
608        }
609        Ok(())
610    }
611
612    fn to_device(&mut self, device: &Device) -> Result<()> {
613        self.token_embedding = self.token_embedding.to_device(device)?;
614        self.position_embedding = self.position_embedding.to_device(device)?;
615        Ok(())
616    }
617}
618
619impl CLIPVisionEmbeddings {
620    fn new(config: &CLIPConfig, device: &Device) -> Result<Self> {
621        let num_positions = config.num_positions();
622
623        Ok(Self {
624            // Patch embedding: linear projection of flattened patches
625            // Input: [batch, 3, H, W] -> [batch, num_patches, vision_width]
626            // Implemented as conv2d with kernel_size=patch_size, stride=patch_size
627            patch_embedding: ops_fn::zeros(
628                &[config.vision_width, 3, config.vision_patch_size, config.vision_patch_size],
629                DataType::Float32,
630                device
631            )?,
632            class_embedding: ops_fn::zeros(
633                &[config.vision_width],
634                DataType::Float32,
635                device
636            )?,
637            position_embedding: ops_fn::zeros(
638                &[num_positions, config.vision_width],
639                DataType::Float32,
640                device
641            )?,
642            config: config.clone(),
643        })
644    }
645
646    fn forward(&self, pixel_values: &Tensor) -> Result<Tensor> {
647        let pixel_candle = pixel_values.to_candle()?;
648        let shape = pixel_candle.dims();
649
650        // Expected input: [batch, channels, height, width]
651        let batch_size = shape[0];
652
653        // Apply patch embedding via conv2d
654        // patch_embedding: [out_channels, in_channels, kH, kW] = [vision_width, 3, patch_size, patch_size]
655        let patch_candle = self.patch_embedding.to_candle()?;
656
657        // Conv2d with stride = kernel_size = patch_size
658        let patch_embeds = pixel_candle.conv2d(
659            &patch_candle,
660            self.config.vision_patch_size,  // padding
661            self.config.vision_patch_size,  // stride
662            1,  // dilation
663            1,  // groups
664        )?;
665
666        // Reshape: [batch, vision_width, H/patch, W/patch] -> [batch, num_patches, vision_width]
667        let patch_shape = patch_embeds.dims();
668        let num_patches = patch_shape[2] * patch_shape[3];
669        let patch_embeds = patch_embeds
670            .reshape(&[batch_size, self.config.vision_width, num_patches])?
671            .transpose(1, 2)?; // [batch, num_patches, vision_width]
672
673        // Prepend CLS token
674        let class_candle = self.class_embedding.to_candle()?;
675        let class_embeds = class_candle
676            .unsqueeze(0)? // [1, vision_width]
677            .unsqueeze(0)? // [1, 1, vision_width]
678            .broadcast_as(&[batch_size, 1, self.config.vision_width])?;
679
680        let embeddings = candle_core::Tensor::cat(&[&class_embeds, &patch_embeds], 1)?;
681
682        // Add position embeddings
683        let pos_candle = self.position_embedding.to_candle()?;
684        let seq_len = embeddings.dims()[1];
685        let pos_slice = pos_candle.narrow(0, 0, seq_len)?;
686        let embeddings = embeddings.broadcast_add(&pos_slice)?;
687
688        Ok(Tensor::from_candle(embeddings))
689    }
690
691    fn load_weights(&mut self, weights: &ModelWeights) -> Result<()> {
692        if let Some(w) = weights.get("vision_model.embeddings.patch_embedding.weight") {
693            self.patch_embedding = w.clone();
694        }
695        if let Some(w) = weights.get("vision_model.embeddings.class_embedding") {
696            self.class_embedding = w.clone();
697        }
698        if let Some(w) = weights.get("vision_model.embeddings.position_embedding.weight") {
699            self.position_embedding = w.clone();
700        }
701        Ok(())
702    }
703
704    fn to_device(&mut self, device: &Device) -> Result<()> {
705        self.patch_embedding = self.patch_embedding.to_device(device)?;
706        self.class_embedding = self.class_embedding.to_device(device)?;
707        self.position_embedding = self.position_embedding.to_device(device)?;
708        Ok(())
709    }
710}
711
712// ============================================================================
713// Encoder
714// ============================================================================
715
716impl CLIPEncoder {
717    fn new_text(config: &CLIPConfig, device: &Device) -> Result<Self> {
718        let mut layers = Vec::new();
719        for _ in 0..config.transformer_layers {
720            layers.push(CLIPEncoderLayer::new(
721                config.transformer_width,
722                config.transformer_heads,
723                config.layer_norm_eps,
724                device,
725            )?);
726        }
727        Ok(Self { layers, is_causal: true })
728    }
729
730    fn new_vision(config: &CLIPConfig, device: &Device) -> Result<Self> {
731        let mut layers = Vec::new();
732        for _ in 0..config.vision_layers {
733            layers.push(CLIPEncoderLayer::new(
734                config.vision_width,
735                config.vision_heads,
736                config.layer_norm_eps,
737                device,
738            )?);
739        }
740        Ok(Self { layers, is_causal: false })
741    }
742
743    fn forward(&self, hidden_states: &Tensor, is_causal: bool) -> Result<Tensor> {
744        let mut hidden_states = hidden_states.clone();
745        for layer in &self.layers {
746            hidden_states = layer.forward(&hidden_states, is_causal)?;
747        }
748        Ok(hidden_states)
749    }
750
751    fn load_weights(&mut self, weights: &ModelWeights, prefix: &str) -> Result<()> {
752        for (i, layer) in self.layers.iter_mut().enumerate() {
753            layer.load_weights(weights, &format!("{}.encoder.layers.{}", prefix, i))?;
754        }
755        Ok(())
756    }
757
758    fn to_device(&mut self, device: &Device) -> Result<()> {
759        for layer in &mut self.layers {
760            layer.to_device(device)?;
761        }
762        Ok(())
763    }
764}
765
766impl CLIPEncoderLayer {
767    fn new(hidden_size: usize, num_heads: usize, layer_norm_eps: f32, device: &Device) -> Result<Self> {
768        let intermediate_size = hidden_size * 4;
769
770        Ok(Self {
771            self_attn: CLIPAttention::new(hidden_size, num_heads, device)?,
772            layer_norm1: ops_fn::zeros(&[hidden_size], DataType::Float32, device)?,
773            layer_norm1_bias: None,
774            mlp: CLIPMLP::new(hidden_size, intermediate_size, device)?,
775            layer_norm2: ops_fn::zeros(&[hidden_size], DataType::Float32, device)?,
776            layer_norm2_bias: None,
777            hidden_size,
778        })
779    }
780
781    fn forward(&self, hidden_states: &Tensor, is_causal: bool) -> Result<Tensor> {
782        // Pre-norm architecture
783        let residual = hidden_states.clone();
784        let hidden_states = ops_fn::layer_norm(hidden_states, &self.layer_norm1, self.layer_norm1_bias.as_ref(), 1e-5)?;
785        let hidden_states = self.self_attn.forward(&hidden_states, is_causal)?;
786        let hidden_states = ops_fn::add(&residual, &hidden_states)?;
787
788        let residual = hidden_states.clone();
789        let hidden_states = ops_fn::layer_norm(&hidden_states, &self.layer_norm2, self.layer_norm2_bias.as_ref(), 1e-5)?;
790        let hidden_states = self.mlp.forward(&hidden_states)?;
791        ops_fn::add(&residual, &hidden_states)
792    }
793
794    fn load_weights(&mut self, weights: &ModelWeights, prefix: &str) -> Result<()> {
795        if let Some(w) = weights.get(&format!("{}.layer_norm1.weight", prefix)) {
796            self.layer_norm1 = w.clone();
797        }
798        if let Some(w) = weights.get(&format!("{}.layer_norm1.bias", prefix)) {
799            self.layer_norm1_bias = Some(w.clone());
800        }
801        if let Some(w) = weights.get(&format!("{}.layer_norm2.weight", prefix)) {
802            self.layer_norm2 = w.clone();
803        }
804        if let Some(w) = weights.get(&format!("{}.layer_norm2.bias", prefix)) {
805            self.layer_norm2_bias = Some(w.clone());
806        }
807        self.self_attn.load_weights(weights, &format!("{}.self_attn", prefix))?;
808        self.mlp.load_weights(weights, &format!("{}.mlp", prefix))?;
809        Ok(())
810    }
811
812    fn to_device(&mut self, device: &Device) -> Result<()> {
813        self.layer_norm1 = self.layer_norm1.to_device(device)?;
814        self.layer_norm2 = self.layer_norm2.to_device(device)?;
815        if let Some(ref mut b) = self.layer_norm1_bias {
816            *b = b.to_device(device)?;
817        }
818        if let Some(ref mut b) = self.layer_norm2_bias {
819            *b = b.to_device(device)?;
820        }
821        self.self_attn.to_device(device)?;
822        self.mlp.to_device(device)?;
823        Ok(())
824    }
825}
826
827// ============================================================================
828// Attention
829// ============================================================================
830
831impl CLIPAttention {
832    fn new(hidden_size: usize, num_heads: usize, device: &Device) -> Result<Self> {
833        let head_dim = hidden_size / num_heads;
834        let scale = 1.0 / (head_dim as f32).sqrt();
835
836        Ok(Self {
837            q_proj: ops_fn::zeros(&[hidden_size, hidden_size], DataType::Float32, device)?,
838            k_proj: ops_fn::zeros(&[hidden_size, hidden_size], DataType::Float32, device)?,
839            v_proj: ops_fn::zeros(&[hidden_size, hidden_size], DataType::Float32, device)?,
840            out_proj: ops_fn::zeros(&[hidden_size, hidden_size], DataType::Float32, device)?,
841            q_bias: None,
842            k_bias: None,
843            v_bias: None,
844            out_bias: None,
845            num_heads,
846            head_dim,
847            scale,
848        })
849    }
850
851    fn forward(&self, hidden_states: &Tensor, is_causal: bool) -> Result<Tensor> {
852        let shape = hidden_states.shape();
853        let (batch_size, seq_len, _) = if shape.len() == 3 {
854            (shape[0], shape[1], shape[2])
855        } else if shape.len() == 2 {
856            (1, shape[0], shape[1])
857        } else {
858            return Err(anyhow::anyhow!("Invalid hidden_states shape: {:?}", shape));
859        };
860
861        // Project Q, K, V (with transpose for matmul)
862        let query = ops_fn::matmul(hidden_states, &self.q_proj)?;
863        let key = ops_fn::matmul(hidden_states, &self.k_proj)?;
864        let value = ops_fn::matmul(hidden_states, &self.v_proj)?;
865
866        // Add biases if present
867        let query = if let Some(ref bias) = self.q_bias {
868            ops_fn::add(&query, bias)?
869        } else {
870            query
871        };
872        let key = if let Some(ref bias) = self.k_bias {
873            ops_fn::add(&key, bias)?
874        } else {
875            key
876        };
877        let value = if let Some(ref bias) = self.v_bias {
878            ops_fn::add(&value, bias)?
879        } else {
880            value
881        };
882
883        // Reshape for multi-head attention
884        // [batch, seq, hidden] -> [batch, heads, seq, head_dim]
885        let q_candle = query.to_candle()?;
886        let k_candle = key.to_candle()?;
887        let v_candle = value.to_candle()?;
888
889        let q_reshaped = q_candle
890            .reshape(&[batch_size, seq_len, self.num_heads, self.head_dim])?
891            .transpose(1, 2)?;
892        let k_reshaped = k_candle
893            .reshape(&[batch_size, seq_len, self.num_heads, self.head_dim])?
894            .transpose(1, 2)?;
895        let v_reshaped = v_candle
896            .reshape(&[batch_size, seq_len, self.num_heads, self.head_dim])?
897            .transpose(1, 2)?;
898
899        // Scaled dot-product attention
900        let k_t = k_reshaped.transpose(2, 3)?;
901
902        // Make tensors contiguous for matmul
903        let q_contiguous = q_reshaped.contiguous()?;
904        let k_contiguous = k_t.contiguous()?;
905
906        let scores = q_contiguous.matmul(&k_contiguous)?;
907        let scaled_scores = (scores * (self.scale as f64))?;
908
909        // Apply causal mask if needed (for text encoder)
910        let masked_scores = if is_causal {
911            let device = scaled_scores.device();
912            let mut mask_data = vec![0.0f32; seq_len * seq_len];
913            for i in 0..seq_len {
914                for j in 0..seq_len {
915                    if j > i {
916                        mask_data[i * seq_len + j] = f32::NEG_INFINITY;
917                    }
918                }
919            }
920            let causal_mask = candle_core::Tensor::from_vec(mask_data, &[1, 1, seq_len, seq_len], device)?;
921            scaled_scores.broadcast_add(&causal_mask)?
922        } else {
923            scaled_scores
924        };
925
926        // Softmax
927        let attention_weights = candle_nn::ops::softmax_last_dim(&masked_scores)?;
928
929        // Apply attention to values
930        let v_contiguous = v_reshaped.contiguous()?;
931        let attn_output = attention_weights.matmul(&v_contiguous)?;
932
933        // Reshape back: [batch, heads, seq, head_dim] -> [batch, seq, hidden]
934        let attn_output = attn_output
935            .transpose(1, 2)?
936            .reshape(&[batch_size, seq_len, self.num_heads * self.head_dim])?;
937
938        let attn_output = Tensor::from_candle(attn_output);
939
940        // Output projection
941        let output = ops_fn::matmul(&attn_output, &self.out_proj)?;
942        if let Some(ref bias) = self.out_bias {
943            ops_fn::add(&output, bias)
944        } else {
945            Ok(output)
946        }
947    }
948
949    fn load_weights(&mut self, weights: &ModelWeights, prefix: &str) -> Result<()> {
950        // Load projection weights (transpose for matmul)
951        if let Some(w) = weights.get(&format!("{}.q_proj.weight", prefix)) {
952            self.q_proj = ops_fn::transpose(w)?;
953        }
954        if let Some(w) = weights.get(&format!("{}.k_proj.weight", prefix)) {
955            self.k_proj = ops_fn::transpose(w)?;
956        }
957        if let Some(w) = weights.get(&format!("{}.v_proj.weight", prefix)) {
958            self.v_proj = ops_fn::transpose(w)?;
959        }
960        if let Some(w) = weights.get(&format!("{}.out_proj.weight", prefix)) {
961            self.out_proj = ops_fn::transpose(w)?;
962        }
963
964        // Load biases
965        if let Some(w) = weights.get(&format!("{}.q_proj.bias", prefix)) {
966            self.q_bias = Some(w.clone());
967        }
968        if let Some(w) = weights.get(&format!("{}.k_proj.bias", prefix)) {
969            self.k_bias = Some(w.clone());
970        }
971        if let Some(w) = weights.get(&format!("{}.v_proj.bias", prefix)) {
972            self.v_bias = Some(w.clone());
973        }
974        if let Some(w) = weights.get(&format!("{}.out_proj.bias", prefix)) {
975            self.out_bias = Some(w.clone());
976        }
977        Ok(())
978    }
979
980    fn to_device(&mut self, device: &Device) -> Result<()> {
981        self.q_proj = self.q_proj.to_device(device)?;
982        self.k_proj = self.k_proj.to_device(device)?;
983        self.v_proj = self.v_proj.to_device(device)?;
984        self.out_proj = self.out_proj.to_device(device)?;
985        if let Some(ref mut b) = self.q_bias { *b = b.to_device(device)?; }
986        if let Some(ref mut b) = self.k_bias { *b = b.to_device(device)?; }
987        if let Some(ref mut b) = self.v_bias { *b = b.to_device(device)?; }
988        if let Some(ref mut b) = self.out_bias { *b = b.to_device(device)?; }
989        Ok(())
990    }
991}
992
993// ============================================================================
994// MLP
995// ============================================================================
996
997impl CLIPMLP {
998    fn new(hidden_size: usize, intermediate_size: usize, device: &Device) -> Result<Self> {
999        Ok(Self {
1000            fc1: ops_fn::zeros(&[hidden_size, intermediate_size], DataType::Float32, device)?,
1001            fc1_bias: None,
1002            fc2: ops_fn::zeros(&[intermediate_size, hidden_size], DataType::Float32, device)?,
1003            fc2_bias: None,
1004            hidden_size,
1005            intermediate_size,
1006        })
1007    }
1008
1009    fn forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
1010        // FC1
1011        let hidden_states = ops_fn::matmul(hidden_states, &self.fc1)?;
1012        let hidden_states = if let Some(ref bias) = self.fc1_bias {
1013            ops_fn::add(&hidden_states, bias)?
1014        } else {
1015            hidden_states
1016        };
1017
1018        // QuickGELU activation: x * sigmoid(1.702 * x)
1019        let hidden_states = self.quick_gelu(&hidden_states)?;
1020
1021        // FC2
1022        let hidden_states = ops_fn::matmul(&hidden_states, &self.fc2)?;
1023        if let Some(ref bias) = self.fc2_bias {
1024            ops_fn::add(&hidden_states, bias)
1025        } else {
1026            Ok(hidden_states)
1027        }
1028    }
1029
1030    /// QuickGELU: x * sigmoid(1.702 * x)
1031    fn quick_gelu(&self, x: &Tensor) -> Result<Tensor> {
1032        let x_candle = x.to_candle()?;
1033        // x * sigmoid(1.702 * x)
1034        let scaled = (&x_candle * 1.702)?;
1035        let sigmoid = candle_nn::ops::sigmoid(&scaled)?;
1036        let result = (&x_candle * &sigmoid)?;
1037        Ok(Tensor::from_candle(result))
1038    }
1039
1040    fn load_weights(&mut self, weights: &ModelWeights, prefix: &str) -> Result<()> {
1041        if let Some(w) = weights.get(&format!("{}.fc1.weight", prefix)) {
1042            self.fc1 = ops_fn::transpose(w)?;
1043        }
1044        if let Some(w) = weights.get(&format!("{}.fc1.bias", prefix)) {
1045            self.fc1_bias = Some(w.clone());
1046        }
1047        if let Some(w) = weights.get(&format!("{}.fc2.weight", prefix)) {
1048            self.fc2 = ops_fn::transpose(w)?;
1049        }
1050        if let Some(w) = weights.get(&format!("{}.fc2.bias", prefix)) {
1051            self.fc2_bias = Some(w.clone());
1052        }
1053        Ok(())
1054    }
1055
1056    fn to_device(&mut self, device: &Device) -> Result<()> {
1057        self.fc1 = self.fc1.to_device(device)?;
1058        self.fc2 = self.fc2.to_device(device)?;
1059        if let Some(ref mut b) = self.fc1_bias { *b = b.to_device(device)?; }
1060        if let Some(ref mut b) = self.fc2_bias { *b = b.to_device(device)?; }
1061        Ok(())
1062    }
1063}
1064
1065// ============================================================================
1066// Tests
1067// ============================================================================
1068
1069#[cfg(test)]
1070mod tests {
1071    use super::*;
1072
1073    #[test]
1074    fn test_clip_config_creation() {
1075        let config = CLIPConfig::default();
1076        assert_eq!(config.vocab_size(), 49408);
1077        assert_eq!(config.hidden_size(), 512);
1078        assert_eq!(config.num_layers(), 12);
1079        assert_eq!(config.num_patches(), 49); // (224/32)^2
1080        assert_eq!(config.num_positions(), 50); // patches + CLS
1081    }
1082
1083    #[test]
1084    fn test_clip_model_creation() {
1085        let config = CLIPConfig {
1086            vocab_size: 1000,
1087            hidden_size: 64,
1088            num_hidden_layers: 2,
1089            transformer_width: 64,
1090            transformer_layers: 2,
1091            transformer_heads: 4,
1092            vision_width: 64,
1093            vision_layers: 2,
1094            vision_heads: 4,
1095            vision_patch_size: 16,
1096            image_resolution: 64,
1097            projection_dim: 64,
1098            embed_dim: 64,
1099            ..Default::default()
1100        };
1101
1102        let model = CLIPModelV2::new(config).unwrap();
1103        assert_eq!(model.config().vocab_size(), 1000);
1104    }
1105
1106    #[test]
1107    fn test_clip_text_forward() {
1108        let config = CLIPConfig {
1109            vocab_size: 100,
1110            hidden_size: 32,
1111            num_hidden_layers: 1,
1112            transformer_width: 32,
1113            transformer_layers: 1,
1114            transformer_heads: 4,
1115            context_length: 16,
1116            vision_width: 32,
1117            vision_layers: 1,
1118            vision_heads: 4,
1119            vision_patch_size: 8,
1120            image_resolution: 32,
1121            projection_dim: 32,
1122            embed_dim: 32,
1123            ..Default::default()
1124        };
1125
1126        let model = CLIPModelV2::new(config).unwrap();
1127
1128        // Create text input
1129        let input_ids = ops_fn::zeros(&[2, 8], DataType::Int64, &Device::CPU).unwrap();
1130        let inputs = ModelInputs::text(input_ids);
1131
1132        let outputs = model.forward(&inputs).unwrap();
1133        match outputs {
1134            ModelOutputs::Embeddings { embeddings, .. } => {
1135                assert_eq!(embeddings.shape()[0], 2); // batch
1136                assert_eq!(embeddings.shape()[1], 32); // projection_dim
1137            }
1138            _ => panic!("Expected embeddings output"),
1139        }
1140    }
1141
1142    #[test]
1143    fn test_clip_generate_returns_error() {
1144        let config = CLIPConfig::default();
1145        let model = CLIPModelV2::new(config).unwrap();
1146        let gen_config = GenerationConfig::default();
1147
1148        let result = model.generate("test", &gen_config);
1149        assert!(result.is_err());
1150        assert!(result.unwrap_err().to_string().contains("contrastive model"));
1151    }
1152}