1use std::ops::Range;
17
18use burn::tensor::{Device, Int, Tensor, TensorData, activation::softmax, backend::Backend};
19use combs_formats::{ModelMetadata, ModelSource, VisionConfig};
20
21use crate::kv::{CacheConfig, KVCache};
22use crate::llama::{LlamaModel, linear, load_tensor};
23use crate::matmul::safe_matmul;
24use crate::norm::layer_norm;
25use crate::precision::{to_f32, to_float};
26use crate::traits::GenerativeModel;
27use crate::{ModelError, Result};
28
29struct SiglipLayer<B: Backend> {
31 ln1_w: Tensor<B, 1>,
32 ln1_b: Tensor<B, 1>,
33 q_w: Tensor<B, 2>,
34 q_b: Tensor<B, 1>,
35 k_w: Tensor<B, 2>,
36 k_b: Tensor<B, 1>,
37 v_w: Tensor<B, 2>,
38 v_b: Tensor<B, 1>,
39 o_w: Tensor<B, 2>,
40 o_b: Tensor<B, 1>,
41 ln2_w: Tensor<B, 1>,
42 ln2_b: Tensor<B, 1>,
43 fc1_w: Tensor<B, 2>,
44 fc1_b: Tensor<B, 1>,
45 fc2_w: Tensor<B, 2>,
46 fc2_b: Tensor<B, 1>,
47}
48
49struct SiglipEncoder<B: Backend> {
51 cfg: VisionConfig,
52 patch_w: Tensor<B, 2>,
54 patch_b: Tensor<B, 1>,
55 pos_embed: Tensor<B, 2>,
57 layers: Vec<SiglipLayer<B>>,
58 post_ln_w: Tensor<B, 1>,
59 post_ln_b: Tensor<B, 1>,
60 scale: f64,
61}
62
63fn gelu_tanh<B: Backend, const D: usize>(x: Tensor<B, D>) -> Tensor<B, D> {
65 let inner = (x.clone() + x.clone().powf_scalar(3.0).mul_scalar(0.044715))
66 .mul_scalar((2.0f64 / std::f64::consts::PI).sqrt());
67 x * inner.tanh().add_scalar(1.0).mul_scalar(0.5)
69}
70
71impl<B: Backend> SiglipEncoder<B> {
72 fn load(source: &dyn ModelSource, device: &Device<B>, cfg: &VisionConfig) -> Result<Self> {
73 let p = "model.vision_model";
74 let conv: Tensor<B, 4> = load_tensor(
76 source,
77 device,
78 &format!("{p}.embeddings.patch_embedding.weight"),
79 )?;
80 let patch_w = conv.reshape([
81 cfg.hidden_size,
82 3 * cfg.patch_size * cfg.patch_size,
83 ]);
84 let patch_b = load_tensor(source, device, &format!("{p}.embeddings.patch_embedding.bias"))?;
85 let pos_embed: Tensor<B, 2> = load_tensor(
86 source,
87 device,
88 &format!("{p}.embeddings.position_embedding.weight"),
89 )?;
90
91 let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
92 for i in 0..cfg.num_hidden_layers {
93 let lp = format!("{p}.encoder.layers.{i}");
94 layers.push(SiglipLayer {
95 ln1_w: load_tensor(source, device, &format!("{lp}.layer_norm1.weight"))?,
96 ln1_b: load_tensor(source, device, &format!("{lp}.layer_norm1.bias"))?,
97 q_w: load_tensor(source, device, &format!("{lp}.self_attn.q_proj.weight"))?,
98 q_b: load_tensor(source, device, &format!("{lp}.self_attn.q_proj.bias"))?,
99 k_w: load_tensor(source, device, &format!("{lp}.self_attn.k_proj.weight"))?,
100 k_b: load_tensor(source, device, &format!("{lp}.self_attn.k_proj.bias"))?,
101 v_w: load_tensor(source, device, &format!("{lp}.self_attn.v_proj.weight"))?,
102 v_b: load_tensor(source, device, &format!("{lp}.self_attn.v_proj.bias"))?,
103 o_w: load_tensor(source, device, &format!("{lp}.self_attn.out_proj.weight"))?,
104 o_b: load_tensor(source, device, &format!("{lp}.self_attn.out_proj.bias"))?,
105 ln2_w: load_tensor(source, device, &format!("{lp}.layer_norm2.weight"))?,
106 ln2_b: load_tensor(source, device, &format!("{lp}.layer_norm2.bias"))?,
107 fc1_w: load_tensor(source, device, &format!("{lp}.mlp.fc1.weight"))?,
108 fc1_b: load_tensor(source, device, &format!("{lp}.mlp.fc1.bias"))?,
109 fc2_w: load_tensor(source, device, &format!("{lp}.mlp.fc2.weight"))?,
110 fc2_b: load_tensor(source, device, &format!("{lp}.mlp.fc2.bias"))?,
111 });
112 }
113
114 Ok(SiglipEncoder {
115 scale: 1.0 / (cfg.head_dim() as f64).sqrt(),
116 cfg: cfg.clone(),
117 patch_w,
118 patch_b,
119 pos_embed,
120 layers,
121 post_ln_w: load_tensor(source, device, &format!("{p}.post_layernorm.weight"))?,
122 post_ln_b: load_tensor(source, device, &format!("{p}.post_layernorm.bias"))?,
123 })
124 }
125
126 fn attention(&self, layer: &SiglipLayer<B>, x: Tensor<B, 3>) -> Tensor<B, 3> {
128 let cfg = &self.cfg;
129 let [batch, seq, _] = x.dims();
130 let heads = cfg.num_attention_heads;
131 let head_dim = cfg.head_dim();
132
133 let q = linear(x.clone(), &layer.q_w, Some(&layer.q_b))
134 .reshape([batch, seq, heads, head_dim])
135 .swap_dims(1, 2);
136 let k = linear(x.clone(), &layer.k_w, Some(&layer.k_b))
137 .reshape([batch, seq, heads, head_dim])
138 .swap_dims(1, 2);
139 let v = linear(x, &layer.v_w, Some(&layer.v_b))
140 .reshape([batch, seq, heads, head_dim])
141 .swap_dims(1, 2);
142
143 let out_dtype = q.dtype();
146 let (q, k, v) = (to_f32(q), to_f32(k), to_f32(v));
147 let scores = safe_matmul(q, k.transpose()).mul_scalar(self.scale);
148 let ctx = to_float(safe_matmul(softmax(scores, 3), v), out_dtype);
149 let ctx = ctx.swap_dims(1, 2).reshape([batch, seq, heads * head_dim]);
150 linear(ctx, &layer.o_w, Some(&layer.o_b))
151 }
152
153 fn forward(&self, pixels: Tensor<B, 4>) -> Tensor<B, 3> {
155 let cfg = &self.cfg;
156 let p = cfg.patch_size;
157 let [_, c, h, w] = pixels.dims();
158 let (gh, gw) = (h / p, w / p);
159 debug_assert_eq!(c, 3);
160 debug_assert_eq!(h % p + w % p, 0);
161
162 let patches = pixels
165 .reshape([c, gh, p, gw, p])
166 .swap_dims(0, 1)
167 .swap_dims(1, 3)
168 .swap_dims(2, 3)
169 .reshape([gh * gw, c * p * p])
170 .unsqueeze_dim::<3>(0);
171 let mut x = linear(patches, &self.patch_w, Some(&self.patch_b));
172
173 let np = gh * gw;
175 x = x + self
176 .pos_embed
177 .clone()
178 .narrow(0, 0, np)
179 .reshape([1, np, cfg.hidden_size]);
180
181 for layer in &self.layers {
182 let h = layer_norm(
183 x.clone(),
184 layer.ln1_w.clone(),
185 layer.ln1_b.clone(),
186 cfg.layer_norm_eps,
187 );
188 x = x + self.attention(layer, h);
189 let h = layer_norm(
190 x.clone(),
191 layer.ln2_w.clone(),
192 layer.ln2_b.clone(),
193 cfg.layer_norm_eps,
194 );
195 let mlp = linear(
196 gelu_tanh(linear(h, &layer.fc1_w, Some(&layer.fc1_b))),
197 &layer.fc2_w,
198 Some(&layer.fc2_b),
199 );
200 x = x + mlp;
201 }
202
203 layer_norm(x, self.post_ln_w.clone(), self.post_ln_b.clone(), cfg.layer_norm_eps)
204 }
205}
206
207struct Connector<B: Backend> {
210 scale: usize,
211 proj: Tensor<B, 2>, }
213
214impl<B: Backend> Connector<B> {
215 fn pixel_shuffle(&self, x: Tensor<B, 3>) -> Tensor<B, 3> {
219 let s = self.scale;
220 let [b, seq, c] = x.dims();
221 let side = (seq as f64).sqrt() as usize;
222 assert_eq!(side * side, seq, "patch grid must be square");
223 x.reshape([b, side, side, c])
224 .reshape([b, side, side / s, c * s])
225 .swap_dims(1, 2) .reshape([b, side / s, side / s, c * s * s])
227 .swap_dims(1, 2) .reshape([b, seq / (s * s), c * s * s])
229 }
230
231 fn forward(&self, x: Tensor<B, 3>) -> Tensor<B, 3> {
232 linear(self.pixel_shuffle(x), &self.proj, None)
233 }
234}
235
236pub struct SmolVlmModel<B: Backend> {
238 metadata: ModelMetadata,
239 vision_cfg: VisionConfig,
240 vision: SiglipEncoder<B>,
241 connector: Connector<B>,
242 text: LlamaModel<B>,
243}
244
245impl<B: Backend> SmolVlmModel<B> {
246 fn image_features(&self, pixels: Tensor<B, 4>) -> Tensor<B, 3> {
249 self.connector.forward(self.vision.forward(pixels))
250 }
251}
252
253impl<B: Backend> GenerativeModel<B> for SmolVlmModel<B> {
254 fn metadata(&self) -> &ModelMetadata {
255 &self.metadata
256 }
257
258 fn load(source: &dyn ModelSource, device: &Device<B>) -> Result<Self> {
259 let metadata = source.metadata().clone();
260 let vision_cfg = metadata
261 .vision
262 .clone()
263 .ok_or_else(|| ModelError::MissingTensor("vision_config".to_string()))?;
264 let vision = SiglipEncoder::load(source, device, &vision_cfg)?;
265 let proj: Tensor<B, 2> = load_tensor(
266 source,
267 device,
268 "model.connector.modality_projection.proj.weight",
269 )?;
270 LlamaModel::<B>::expect_shape(
271 "model.connector.modality_projection.proj.weight",
272 &proj.dims(),
273 &[
274 metadata.hidden_size,
275 vision_cfg.hidden_size * vision_cfg.scale_factor * vision_cfg.scale_factor,
276 ],
277 )?;
278 let text = LlamaModel::<B>::load_with_prefix(source, device, "model.text_model")?;
279 Ok(SmolVlmModel {
280 connector: Connector {
281 scale: vision_cfg.scale_factor,
282 proj,
283 },
284 metadata,
285 vision_cfg,
286 vision,
287 text,
288 })
289 }
290
291 fn create_kv_cache(&self, config: &CacheConfig) -> Box<dyn KVCache<B>> {
292 self.text.create_kv_cache(config)
293 }
294
295 fn embed(&self, tokens: Tensor<B, 2, Int>) -> Tensor<B, 3> {
296 self.text.embed(tokens)
297 }
298
299 fn embed_multimodal(
300 &self,
301 tokens: Tensor<B, 2, Int>,
302 images: &[Tensor<B, 4>],
303 ) -> Result<Tensor<B, 3>> {
304 if images.is_empty() {
305 return Ok(self.text.embed(tokens));
306 }
307 let [_, seq] = tokens.dims();
308 let ids: Vec<i64> = tokens
309 .clone()
310 .into_data()
311 .convert::<i64>()
312 .to_vec()
313 .map_err(|e| ModelError::BadShape {
314 tensor: "tokens".to_string(),
315 expected: vec![1, seq],
316 got: vec![],
317 })
318 .unwrap_or_default();
319 if ids.len() != seq {
320 return Err(ModelError::BadShape {
321 tensor: "tokens".to_string(),
322 expected: vec![1, seq],
323 got: vec![ids.len()],
324 });
325 }
326
327 let image_id = self.vision_cfg.image_token_id as i64;
329 let span_len = self.vision_cfg.image_seq_len();
330 let mut spans: Vec<(usize, usize)> = Vec::new();
331 let mut i = 0;
332 while i < seq {
333 if ids[i] == image_id {
334 let start = i;
335 while i < seq && ids[i] == image_id {
336 i += 1;
337 }
338 spans.push((start, i));
339 } else {
340 i += 1;
341 }
342 }
343 if spans.len() != images.len() {
344 return Err(ModelError::UnsupportedMedia(format!(
345 "found {} image-token span(s) of len {span_len} but {} image(s) were provided",
346 spans.len(),
347 images.len()
348 )));
349 }
350
351 let base = self.text.embed(tokens);
352 let mut pieces: Vec<Tensor<B, 3>> = Vec::new();
353 let mut cursor = 0;
354 for (idx, (start, end)) in spans.iter().enumerate() {
355 if end - start != span_len {
356 return Err(ModelError::UnsupportedMedia(format!(
357 "image-token span has length {}, expected {span_len}",
358 end - start
359 )));
360 }
361 if *start > cursor {
362 pieces.push(base.clone().narrow(1, cursor, start - cursor));
363 }
364 pieces.push(self.image_features(images[idx].clone()));
365 cursor = *end;
366 }
367 if cursor < seq {
368 pieces.push(base.narrow(1, cursor, seq - cursor));
369 }
370 Ok(Tensor::cat(pieces, 1))
371 }
372
373 fn prefill(
374 &mut self,
375 input: Tensor<B, 3>,
376 cache: &mut dyn KVCache<B>,
377 pos: Range<u32>,
378 ) -> Tensor<B, 2> {
379 let [_, seq, _] = input.dims();
380 assert_eq!(
381 seq,
382 (pos.end - pos.start) as usize,
383 "prefill pos range must match the input sequence length"
384 );
385 let hidden = self.text.forward_hidden(input, cache, pos.start as usize);
386 self.text.last_logits(hidden)
387 }
388
389 fn decode(&mut self, input: Tensor<B, 3>, cache: &mut dyn KVCache<B>) -> Tensor<B, 2> {
390 let pos = cache.seq_len();
391 let hidden = self.text.forward_hidden(input, cache, pos);
392 self.text.last_logits(hidden)
393 }
394
395 fn prefill_hidden(
396 &mut self,
397 input: Tensor<B, 3>,
398 cache: &mut dyn KVCache<B>,
399 pos: Range<u32>,
400 ) -> Result<Tensor<B, 3>> {
401 self.text.prefill_hidden(input, cache, pos)
402 }
403
404 fn supports_hidden_states(&self) -> bool {
405 true
406 }
407
408 fn prefill_all_logits(
409 &mut self,
410 input: Tensor<B, 3>,
411 cache: &mut dyn KVCache<B>,
412 pos: Range<u32>,
413 ) -> Result<Tensor<B, 3>> {
414 self.text.prefill_all_logits(input, cache, pos)
415 }
416}
417
418pub fn image_prompt_expansion(image_seq_len: usize) -> String {
423 let mut s = String::with_capacity(image_seq_len * 8 + 64);
424 s.push_str("<fake_token_around_image><global-img>");
425 for _ in 0..image_seq_len {
426 s.push_str("<image>");
427 }
428 s.push_str("<fake_token_around_image>");
429 s
430}
431
432#[allow(dead_code)]
434fn token_ids<B: Backend>(tokens: Tensor<B, 2, Int>) -> Vec<i64> {
435 tokens
436 .into_data()
437 .convert::<i64>()
438 .to_vec()
439 .unwrap_or_default()
440}
441
442pub fn pixels_to_tensor<B: Backend>(
445 data: Vec<f32>,
446 shape: [usize; 4],
447 device: &Device<B>,
448) -> Tensor<B, 4> {
449 Tensor::from_data(TensorData::new(data, shape), device)
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 type TestBackend = burn::backend::NdArray<f32>;
456
457 #[test]
458 fn pixel_shuffle_matches_hf_ordering() {
459 let device = Default::default();
462 let data: Vec<f32> = (0..16).map(|v| v as f32).collect();
463 let x = Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [1, 16, 1]), &device);
464 let conn = Connector::<TestBackend> {
465 scale: 2,
466 proj: Tensor::eye(4, &device),
467 };
468 let out = conn.pixel_shuffle(x);
469 let got: Vec<f32> = out.into_data().to_vec().unwrap();
470 let expected: Vec<f32> = vec![
476 0.0, 1.0, 4.0, 5.0, 2.0, 3.0, 6.0, 7.0, 8.0, 9.0, 12.0, 13.0, 10.0, 11.0, 14.0, 15.0,
480 ];
481 assert_eq!(got, expected);
482 }
483
484 #[test]
485 fn image_prompt_expansion_shape() {
486 let s = image_prompt_expansion(64);
487 assert!(s.starts_with("<fake_token_around_image><global-img>"));
488 assert!(s.ends_with("<fake_token_around_image>"));
489 assert_eq!(s.matches("<image>").count(), 64);
490 }
491
492 #[test]
493 fn gelu_tanh_reference() {
494 let device = Default::default();
495 let x = Tensor::<TestBackend, 1>::from_data(TensorData::new(vec![0.0f32, 1.0, -1.0], [3]), &device);
496 let y: Vec<f32> = gelu_tanh(x).into_data().to_vec().unwrap();
497 assert!(y[0].abs() < 1e-5);
499 assert!((y[1] - 0.8412).abs() < 1e-3, "gelu(1) = {}", y[1]);
500 assert!((y[2] + 0.1588).abs() < 1e-3, "gelu(-1) = {}", y[2]);
501 }
502}