1use crate::pool::Pool;
11use crate::qtensor::QTensor;
12use crate::tokenizer::Tokenizer;
13use base64::Engine as _;
14use cortiq_core::CmfModel;
15use image::imageops::{self, FilterType};
16use image::{Rgb, RgbImage};
17use serde_json::Value;
18use std::fmt;
19use std::sync::Arc;
20use std::sync::atomic::{AtomicUsize, Ordering};
21
22static GPU_ATTENTION_DISPATCHES: AtomicUsize = AtomicUsize::new(0);
26static GPU_DENSE_GEMM_DISPATCHES: AtomicUsize = AtomicUsize::new(0);
27
28pub fn gpu_attention_dispatches() -> usize {
29 GPU_ATTENTION_DISPATCHES.load(Ordering::Relaxed)
30}
31
32pub fn gpu_dense_gemm_dispatches() -> usize {
33 GPU_DENSE_GEMM_DISPATCHES.load(Ordering::Relaxed)
34}
35
36pub const TEXT: i8 = -1;
39pub const IMAGE_START: i8 = 0;
40pub const IMAGE: i8 = 1;
41pub const IMAGE_NEW_LINE: i8 = 2;
42pub const IMAGE_END: i8 = 3;
43
44#[derive(Clone, Debug, PartialEq)]
48pub struct VisionConfig {
49 pub vision_n_layers: usize,
50 pub vision_dim: usize,
51 pub vision_n_heads: usize,
52 pub vision_inter_dim: usize,
53 pub vision_patch_size: usize,
54 pub vision_rope_theta: f32,
55 pub vision_downsample_ratio: usize,
56 pub vision_max_n_token: usize,
57 pub vision_min_pixels: usize,
58 pub vision_max_wh_ratio: Option<f64>,
59 pub text_dim: usize,
60 pub image_token_id: u32,
61}
62
63impl Default for VisionConfig {
64 fn default() -> Self {
65 Self {
66 vision_n_layers: 0,
67 vision_dim: 1024,
68 vision_n_heads: 16,
69 vision_inter_dim: 2816,
70 vision_patch_size: 14,
71 vision_rope_theta: 10_000.0,
72 vision_downsample_ratio: 3,
73 vision_max_n_token: 1024,
74 vision_min_pixels: 544 * 544,
75 vision_max_wh_ratio: None,
76 text_dim: 5120,
77 image_token_id: 129_264,
78 }
79 }
80}
81
82impl VisionConfig {
83 pub fn from_source(source: &Value) -> Result<Self, String> {
85 let mut out = Self::default();
86 let vision = source.get("vision_config").unwrap_or(source);
87 let empty = Value::Null;
88 let text = source.get("text_config").unwrap_or(&empty);
89 let usize_field = |object: &Value, key: &str, old: usize| {
90 object
91 .get(key)
92 .and_then(Value::as_u64)
93 .map(|v| v as usize)
94 .unwrap_or(old)
95 };
96 let f32_field = |object: &Value, key: &str, old: f32| {
97 object
98 .get(key)
99 .and_then(Value::as_f64)
100 .map(|v| v as f32)
101 .unwrap_or(old)
102 };
103 out.vision_n_layers = usize_field(vision, "num_hidden_layers", out.vision_n_layers);
104 out.vision_dim = usize_field(vision, "hidden_size", out.vision_dim);
105 out.vision_n_heads = usize_field(vision, "num_attention_heads", out.vision_n_heads);
106 out.vision_inter_dim = usize_field(vision, "intermediate_size", out.vision_inter_dim);
107 out.vision_patch_size = usize_field(vision, "patch_size", out.vision_patch_size);
108 out.vision_rope_theta = f32_field(vision, "rope_theta", out.vision_rope_theta);
109 out.vision_downsample_ratio =
110 usize_field(vision, "downsample_ratio", out.vision_downsample_ratio);
111 out.vision_max_n_token = usize_field(vision, "max_image_tokens", out.vision_max_n_token);
112 out.vision_min_pixels = usize_field(vision, "min_pixels", out.vision_min_pixels);
113 out.vision_max_wh_ratio = vision.get("max_wh_ratio").and_then(Value::as_f64);
114 out.text_dim = usize_field(text, "hidden_size", out.text_dim);
115 out.image_token_id = source
116 .get("image_token_id")
117 .and_then(Value::as_u64)
118 .or_else(|| vision.get("image_token_id").and_then(Value::as_u64))
119 .map(|v| v as u32)
120 .unwrap_or(out.image_token_id);
121 out.validate()?;
122 Ok(out)
123 }
124
125 pub fn validate(&self) -> Result<(), String> {
126 if self.vision_n_layers == 0 {
127 return Ok(());
128 }
129 if self.vision_dim == 0
130 || self.vision_n_heads == 0
131 || self.vision_dim % self.vision_n_heads != 0
132 || self.vision_dim / self.vision_n_heads % 2 != 0
133 {
134 return Err(format!(
135 "invalid vision attention geometry dim={} heads={}",
136 self.vision_dim, self.vision_n_heads
137 ));
138 }
139 if self.vision_patch_size == 0 || self.vision_downsample_ratio == 0 {
140 return Err("vision patch_size and downsample_ratio must be non-zero".to_string());
141 }
142 if self.vision_max_n_token < 4 {
143 return Err("vision max_image_tokens must be at least 4".to_string());
144 }
145 if self.text_dim == 0 {
146 return Err("text hidden_size must be non-zero for the aligner".to_string());
147 }
148 Ok(())
149 }
150
151 pub fn vision_enabled(&self) -> bool {
152 self.vision_n_layers > 0
153 }
154
155 pub fn head_dim(&self) -> usize {
156 self.vision_dim / self.vision_n_heads
157 }
158
159 pub fn rope_dim(&self) -> usize {
160 self.head_dim() / 2
161 }
162}
163
164#[derive(Clone, Debug)]
167pub struct ImageInput {
168 pub start: usize,
169 pub patches: Vec<f32>,
170 pub n_vit_h: usize,
171 pub n_vit_w: usize,
172 pub n_llm_h: usize,
173 pub n_llm_w: usize,
174 pub types: Vec<i8>,
175}
176
177impl ImageInput {
178 pub fn image_positions(&self) -> usize {
179 self.types.iter().filter(|&&kind| kind == IMAGE).count()
180 }
181
182 pub fn span_len(&self) -> usize {
183 self.types.len()
184 }
185}
186
187#[derive(Clone, Debug)]
190pub struct PreparedVlInputs {
191 pub token_ids: Vec<u32>,
192 pub token_types: Vec<i8>,
193 pub images: Vec<ImageInput>,
194}
195
196pub fn num_image_tokens(n_llm_h: usize, n_llm_w: usize) -> usize {
198 n_llm_h.saturating_mul(n_llm_w + 1).saturating_add(2)
199}
200
201pub fn llm_grid(
203 best_height: usize,
204 best_width: usize,
205 patch_size: usize,
206 downsample_ratio: usize,
207) -> (usize, usize) {
208 (
209 (best_height / patch_size).div_ceil(downsample_ratio),
210 (best_width / patch_size).div_ceil(downsample_ratio),
211 )
212}
213
214pub fn solve_resize_ratio(
216 height: usize,
217 width: usize,
218 patch_size: usize,
219 downsample_ratio: usize,
220 max_n_token: usize,
221) -> (usize, usize) {
222 solve_resize_ratio_f64(
223 height.max(1) as f64,
224 width.max(1) as f64,
225 patch_size,
226 downsample_ratio,
227 max_n_token,
228 )
229}
230
231fn solve_resize_ratio_f64(
232 height_f: f64,
233 width_f: f64,
234 patch_size: usize,
235 downsample_ratio: usize,
236 max_n_token: usize,
237) -> (usize, usize) {
238 let aspect = height_f / width_f;
239 let max_w_float = (((max_n_token.saturating_sub(2)) as f64 / aspect) + 0.25).sqrt() - 0.5;
240 let max_h_float = max_w_float * aspect;
241 let cell = patch_size.saturating_mul(downsample_ratio).max(1);
242 if max_w_float < 1.0 {
243 return (max_n_token.saturating_sub(2) / 2 * cell, cell);
244 }
245 if max_h_float < 1.0 {
246 return (cell, max_n_token.saturating_sub(3) * cell);
247 }
248 let beta = (max_w_float.floor() * cell as f64 / width_f)
249 .min(max_h_float.floor() * cell as f64 / height_f);
250 let best_h =
251 ((height_f * beta / patch_size.max(1) as f64).floor() as usize).saturating_mul(patch_size);
252 let best_w =
253 ((width_f * beta / patch_size.max(1) as f64).floor() as usize).saturating_mul(patch_size);
254 (best_h.max(patch_size), best_w.max(patch_size))
255}
256
257pub fn safe_resize(
259 height: usize,
260 width: usize,
261 mut best_height: usize,
262 mut best_width: usize,
263 patch_size: usize,
264 downsample_ratio: usize,
265 max_n_token: usize,
266) -> Result<(usize, usize, usize, usize), String> {
267 let (mut n_llm_h, mut n_llm_w) =
268 llm_grid(best_height, best_width, patch_size, downsample_ratio);
269 if num_image_tokens(n_llm_h, n_llm_w) > max_n_token {
270 let (h, w) = solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token);
271 best_height = h;
272 best_width = w;
273 (n_llm_h, n_llm_w) = llm_grid(best_height, best_width, patch_size, downsample_ratio);
274 if num_image_tokens(n_llm_h, n_llm_w) > max_n_token {
275 return Err(format!(
276 "image grid {}x{} costs {} tokens, cap {}",
277 n_llm_h,
278 n_llm_w,
279 num_image_tokens(n_llm_h, n_llm_w),
280 max_n_token
281 ));
282 }
283 }
284 Ok((n_llm_h, n_llm_w, best_height, best_width))
285}
286
287pub fn plan_image_grid(
289 width: usize,
290 height: usize,
291 config: &VisionConfig,
292) -> Result<(usize, usize, usize, usize), String> {
293 config.validate()?;
294 if width == 0 || height == 0 {
295 return Err("image dimensions must be non-zero".to_string());
296 }
297 let mut width_f = width as f64;
298 let mut height_f = height as f64;
299 if let Some(max_ratio) = config.vision_max_wh_ratio.filter(|v| *v > 0.0) {
300 if width_f > height_f * max_ratio {
301 width_f = height_f * max_ratio;
302 }
303 }
304 if width_f * height_f < config.vision_min_pixels as f64 {
305 let scale = (config.vision_min_pixels as f64 / (width_f * height_f)).sqrt();
306 width_f = (width_f * scale) as usize as f64;
307 height_f = (height_f * scale) as usize as f64;
308 }
309 let p = config.vision_patch_size;
310 let mut best_width = (width_f.ceil() as usize)
311 .max(1)
312 .div_ceil(p)
313 .saturating_mul(p);
314 let mut best_height = (height_f.ceil() as usize)
315 .max(1)
316 .div_ceil(p)
317 .saturating_mul(p);
318 let (mut n_h, mut n_w) = llm_grid(best_height, best_width, p, config.vision_downsample_ratio);
319 if num_image_tokens(n_h, n_w) > config.vision_max_n_token {
320 let (h, w) = solve_resize_ratio_f64(
321 height_f,
322 width_f,
323 p,
324 config.vision_downsample_ratio,
325 config.vision_max_n_token,
326 );
327 best_height = h;
328 best_width = w;
329 (n_h, n_w) = llm_grid(best_height, best_width, p, config.vision_downsample_ratio);
330 if num_image_tokens(n_h, n_w) > config.vision_max_n_token {
331 return Err(format!(
332 "image grid {}x{} costs {} tokens, cap {}",
333 n_h,
334 n_w,
335 num_image_tokens(n_h, n_w),
336 config.vision_max_n_token
337 ));
338 }
339 }
340 Ok((n_h, n_w, best_height, best_width))
341}
342
343pub fn image_token_types(n_llm_h: usize, n_llm_w: usize) -> Vec<i8> {
346 let mut types = Vec::with_capacity(num_image_tokens(n_llm_h, n_llm_w));
347 types.push(IMAGE_START);
348 for _ in 0..n_llm_h {
349 types.extend(std::iter::repeat_n(IMAGE, n_llm_w));
350 types.push(IMAGE_NEW_LINE);
351 }
352 types.push(IMAGE_END);
353 types
354}
355
356pub fn load_image_bytes(record: &Value) -> Result<Vec<u8>, String> {
359 let map = record
360 .as_object()
361 .ok_or_else(|| "image record must be an object".to_string())?;
362 if let Some(data) = map.get("data") {
363 if let Some(s) = data.as_str() {
364 return base64::engine::general_purpose::STANDARD
365 .decode(s)
366 .map_err(|e| format!("invalid base64 image data: {e}"));
367 }
368 }
369 if let Some(source) = map.get("source").and_then(Value::as_object) {
370 if let Some(data) = source.get("data").and_then(Value::as_str) {
371 return base64::engine::general_purpose::STANDARD
372 .decode(data)
373 .map_err(|e| format!("invalid base64 Anthropic image data: {e}"));
374 }
375 if let Some(url) = source.get("url").and_then(Value::as_str) {
376 return load_image_bytes(&serde_json::json!({"url": url}));
377 }
378 }
379 let url = map.get("url").and_then(Value::as_str).ok_or_else(|| {
380 format!(
381 "image record has no data/source/url (keys: {:?})",
382 map.keys()
383 )
384 })?;
385 if let Some((header, payload)) = url.split_once(',').filter(|(h, _)| h.starts_with("data:")) {
386 if !header.contains(";base64") {
387 return Err(format!("unsupported data URL encoding: {header}"));
388 }
389 return base64::engine::general_purpose::STANDARD
390 .decode(payload)
391 .map_err(|e| format!("invalid data URL image: {e}"));
392 }
393 if url.starts_with("http://") || url.starts_with("https://") {
394 let response = ureq::get(url)
395 .timeout(std::time::Duration::from_secs(30))
396 .call()
397 .map_err(|e| format!("image download failed: {e}"))?;
398 let mut reader = response.into_reader();
399 let mut bytes = Vec::new();
400 std::io::Read::read_to_end(&mut reader, &mut bytes)
401 .map_err(|e| format!("image download read failed: {e}"))?;
402 return Ok(bytes);
403 }
404 std::fs::read(url).map_err(|e| format!("image path '{url}' could not be read: {e}"))
405}
406
407fn resize_fit(image: &RgbImage, width: u32, height: u32) -> RgbImage {
408 let scale = (width as f64 / image.width() as f64).min(height as f64 / image.height() as f64);
409 let resized_width = (image.width() as f64 * scale).round().max(1.0) as u32;
410 let resized_height = (image.height() as f64 * scale).round().max(1.0) as u32;
411 imageops::resize(image, resized_width, resized_height, FilterType::CatmullRom)
412}
413
414fn pad_to(image: &RgbImage, width: u32, height: u32) -> RgbImage {
415 let resized = resize_fit(image, width, height);
416 let mut output = RgbImage::from_pixel(width, height, Rgb([127, 127, 127]));
417 let left = (width.saturating_sub(resized.width())) / 2;
418 let top = (height.saturating_sub(resized.height())) / 2;
419 imageops::overlay(&mut output, &resized, i64::from(left), i64::from(top));
420 output
421}
422
423#[inline]
428fn bf16_roundtrip(value: f32) -> f32 {
429 let bits = value.to_bits();
430 let round = 0x7fff + ((bits >> 16) & 1);
431 f32::from_bits((bits.wrapping_add(round) & 0xffff_0000))
432}
433
434pub fn load_image(
436 record: &Value,
437 config: &VisionConfig,
438) -> Result<(Vec<f32>, usize, usize, usize, usize), String> {
439 config.validate()?;
440 if !config.vision_enabled() {
441 return Err("image input requires a model with vision_n_layers > 0".to_string());
442 }
443 let bytes = load_image_bytes(record)?;
444 let decoded = image::load_from_memory(&bytes)
445 .map_err(|e| format!("image decode failed: {e}"))?
446 .to_rgb8();
447 let (width, height) = decoded.dimensions();
448 let (n_llm_h, n_llm_w, best_height, best_width) =
449 plan_image_grid(width as usize, height as usize, config)?;
450 let p = config.vision_patch_size as u32;
451 let target_width = best_width as u32;
452 let target_height = best_height as u32;
453 let transformed = if config
454 .vision_max_wh_ratio
455 .is_some_and(|ratio| width as f64 >= ratio * height as f64)
456 {
457 imageops::resize(
458 &decoded,
459 target_width,
460 target_height,
461 FilterType::CatmullRom,
462 )
463 } else {
464 pad_to(&decoded, target_width, target_height)
465 };
466 let n_vit_h = best_height / config.vision_patch_size;
467 let n_vit_w = best_width / config.vision_patch_size;
468 let patch_values = config
469 .vision_patch_size
470 .saturating_mul(config.vision_patch_size)
471 .saturating_mul(3);
472 let mut patches =
473 Vec::with_capacity(n_vit_h.saturating_mul(n_vit_w).saturating_mul(patch_values));
474 for patch_y in 0..n_vit_h {
477 for patch_x in 0..n_vit_w {
478 for channel in 0..3 {
479 for dy in 0..config.vision_patch_size {
480 for dx in 0..config.vision_patch_size {
481 let pixel = transformed.get_pixel(
482 (patch_x * config.vision_patch_size + dx) as u32,
483 (patch_y * config.vision_patch_size + dy) as u32,
484 );
485 let value = (pixel[channel] as f32 / 255.0 - 0.5) / 0.5;
486 patches.push(bf16_roundtrip(value));
487 }
488 }
489 }
490 }
491 }
492 debug_assert_eq!(patches.len(), n_vit_h * n_vit_w * patch_values);
493 Ok((patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w))
494}
495
496pub fn prepare_vl_inputs(
500 prompt: &str,
501 images: &[Value],
502 tokenizer: &Tokenizer,
503 config: &VisionConfig,
504) -> Result<PreparedVlInputs, String> {
505 config.validate()?;
506 let image_token_id = config.image_token_id;
507 if let Some(placeholder_id) = tokenizer.token_to_id(crate::dsv41_encoding::IMAGE_PLACEHOLDER) {
508 if placeholder_id != image_token_id {
509 return Err(format!(
510 "tokenizer image placeholder id {} != config image_token_id {}",
511 placeholder_id, image_token_id
512 ));
513 }
514 }
515 let prompt_tokens = tokenizer.encode(prompt);
516 let placeholders = prompt_tokens
517 .iter()
518 .filter(|&&token| token == image_token_id)
519 .count();
520 if placeholders != images.len() {
521 return Err(format!(
522 "found {placeholders} image tokens but received {} images",
523 images.len()
524 ));
525 }
526 if placeholders > 0 && !config.vision_enabled() {
527 return Err("prompt contains images but the model has no vision tower".to_string());
528 }
529 let mut token_ids = Vec::with_capacity(prompt_tokens.len());
530 let mut token_types = Vec::with_capacity(prompt_tokens.len());
531 let mut image_inputs = Vec::with_capacity(images.len());
532 let mut image_index = 0;
533 for token in prompt_tokens {
534 if token != image_token_id {
535 token_ids.push(token);
536 token_types.push(TEXT);
537 continue;
538 }
539 let (patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w) =
540 load_image(&images[image_index], config)?;
541 let types = image_token_types(n_llm_h, n_llm_w);
542 image_inputs.push(ImageInput {
543 start: token_ids.len(),
544 patches,
545 n_vit_h,
546 n_vit_w,
547 n_llm_h,
548 n_llm_w,
549 types: types.clone(),
550 });
551 token_ids.extend(std::iter::repeat_n(image_token_id, types.len()));
552 token_types.extend(types);
553 image_index += 1;
554 }
555 Ok(PreparedVlInputs {
556 token_ids,
557 token_types,
558 images: image_inputs,
559 })
560}
561
562pub struct VisionLinear {
563 pub weight: QTensor,
564 pub bias: Option<Vec<f32>>,
565}
566
567impl fmt::Debug for VisionLinear {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 f.debug_struct("VisionLinear")
570 .field("rows", &self.weight.rows())
571 .field("cols", &self.weight.cols())
572 .field("has_bias", &self.bias.is_some())
573 .finish()
574 }
575}
576
577impl VisionLinear {
578 fn new(weight: QTensor, bias: Option<Vec<f32>>) -> Result<Self, String> {
579 if let Some(values) = &bias {
580 if values.len() != weight.rows() {
581 return Err(format!(
582 "linear bias length {} != output rows {}",
583 values.len(),
584 weight.rows()
585 ));
586 }
587 }
588 Ok(Self { weight, bias })
589 }
590
591 fn apply_many(&self, input: &[f32], batch: usize, output: &mut [f32], pool: Option<&Pool>) {
592 assert_eq!(input.len(), batch * self.weight.cols());
593 assert!(output.len() >= batch * self.weight.rows());
594
595 let rows = self.weight.rows();
600 let cols = self.weight.cols();
601 if batch >= 8
602 && batch.saturating_mul(rows).saturating_mul(cols) >= (1 << 22)
603 && crate::gpu::enabled_here()
604 && let Some(weight) = self.weight.as_f32()
605 && crate::gpu::gemm_nt_f32(input, weight, output, batch, cols, rows)
606 {
607 GPU_DENSE_GEMM_DISPATCHES.fetch_add(1, Ordering::Relaxed);
608 if let Some(bias) = &self.bias {
609 for row in output[..batch * rows].chunks_exact_mut(rows) {
610 for (value, &b) in row.iter_mut().zip(bias) {
611 *value += b;
612 }
613 }
614 }
615 return;
616 }
617 self.weight.matmat(input, batch, output, pool);
618 if let Some(bias) = &self.bias {
619 for row in output[..batch * self.weight.rows()].chunks_exact_mut(self.weight.rows()) {
620 for (value, &b) in row.iter_mut().zip(bias) {
621 *value += b;
622 }
623 }
624 }
625 }
626}
627
628#[derive(Debug)]
629pub struct VisionAttention {
630 pub wqkv: VisionLinear,
631 pub wo: VisionLinear,
632}
633
634#[derive(Debug)]
635pub struct VisionMlp {
636 pub w1: VisionLinear,
637 pub w2: VisionLinear,
638}
639
640#[derive(Debug)]
641pub struct VisionBlock {
642 pub norm1: Vec<f32>,
643 pub attn: VisionAttention,
644 pub norm2: Vec<f32>,
645 pub mlp: VisionMlp,
646}
647
648#[derive(Debug)]
650pub struct VisionModel {
651 pub config: VisionConfig,
652 pub patch_embed: VisionLinear,
653 pub blocks: Vec<VisionBlock>,
654 pub norm: Vec<f32>,
655 pub aligner_w1: VisionLinear,
656 pub aligner_w2: VisionLinear,
657 pub image_start: Vec<f32>,
658 pub image_end: Vec<f32>,
659 pub image_newline: Vec<f32>,
660}
661
662fn load_tensor(model: &Arc<CmfModel>, name: &str) -> Result<QTensor, String> {
663 QTensor::from_model(model, name)
664}
665
666fn load_vector(model: &Arc<CmfModel>, name: &str, expected: usize) -> Result<Vec<f32>, String> {
667 let entry = model
668 .tensor(name)
669 .ok_or_else(|| format!("tensor '{name}' not found"))?;
670 if entry.shape.iter().product::<usize>() != expected {
671 return Err(format!(
672 "tensor '{name}' has shape {:?}, expected {} elements",
673 entry.shape, expected
674 ));
675 }
676 let mut data = vec![0.0f32; expected];
677 cortiq_core::quant::dequant_tensor(entry, model.entry_bytes(entry), &mut data)?;
678 Ok(data)
679}
680
681fn load_optional_vector(
682 model: &Arc<CmfModel>,
683 name: &str,
684 expected: usize,
685) -> Result<Option<Vec<f32>>, String> {
686 model
687 .tensor(name)
688 .map(|_| load_vector(model, name, expected))
689 .transpose()
690}
691
692fn required_norm(model: &Arc<CmfModel>, name: &str, dim: usize) -> Result<Vec<f32>, String> {
693 load_vector(model, name, dim)
694}
695
696fn padded_vit_grid(n_vit_h: usize, n_vit_w: usize, ratio: usize) -> (usize, usize) {
700 (
701 n_vit_h.div_ceil(ratio) * ratio,
702 n_vit_w.div_ceil(ratio) * ratio,
703 )
704}
705
706fn unfold_padded(
712 x: &[f32],
713 n_vit_h: usize,
714 n_vit_w: usize,
715 vision_dim: usize,
716 ratio: usize,
717) -> (Vec<f32>, usize, usize) {
718 let (h, w) = padded_vit_grid(n_vit_h, n_vit_w, ratio);
719 let rows = (h / ratio) * (w / ratio);
720 let in_dim = vision_dim * ratio * ratio;
721 debug_assert_eq!(x.len(), n_vit_h * n_vit_w * vision_dim);
722 let mut input = vec![0.0f32; rows * in_dim];
723 for block_y in 0..h / ratio {
724 for block_x in 0..w / ratio {
725 let row = block_y * (w / ratio) + block_x;
726 let mut at = 0;
727 for channel in 0..vision_dim {
730 for dy in 0..ratio {
731 for dx in 0..ratio {
732 let patch_y = block_y * ratio + dy;
733 let patch_x = block_x * ratio + dx;
734 input[row * in_dim + at] = if patch_y < n_vit_h && patch_x < n_vit_w {
735 let patch = patch_y * n_vit_w + patch_x;
736 x[patch * vision_dim + channel]
737 } else {
738 0.0
739 };
740 at += 1;
741 }
742 }
743 }
744 }
745 }
746 (input, h / ratio, w / ratio)
747}
748
749impl VisionModel {
750 pub fn from_model(model: &Arc<CmfModel>, config: VisionConfig) -> Result<Self, String> {
754 config.validate()?;
755 if !config.vision_enabled() {
756 return Err("cannot load a disabled vision tower".to_string());
757 }
758 let patch_in = 3 * config.vision_patch_size * config.vision_patch_size;
759 let patch_embed = VisionLinear::new(
760 load_tensor(model, "vision.patch_embed.proj.weight")?,
761 load_optional_vector(model, "vision.patch_embed.proj.bias", config.vision_dim)?,
762 )?;
763 if patch_embed.weight.rows() != config.vision_dim || patch_embed.weight.cols() != patch_in {
764 return Err(format!(
765 "patch embedding shape {}x{}, expected {}x{}",
766 patch_embed.weight.rows(),
767 patch_embed.weight.cols(),
768 config.vision_dim,
769 patch_in
770 ));
771 }
772 let mut blocks = Vec::with_capacity(config.vision_n_layers);
773 for layer in 0..config.vision_n_layers {
774 let prefix = format!("vision.blocks.{layer}");
775 let norm1 = required_norm(model, &format!("{prefix}.norm1.weight"), config.vision_dim)?;
776 let wqkv = VisionLinear::new(
777 load_tensor(model, &format!("{prefix}.attn.wqkv.weight"))?,
778 load_optional_vector(
779 model,
780 &format!("{prefix}.attn.wqkv.bias"),
781 3 * config.vision_dim,
782 )?,
783 )?;
784 let wo = VisionLinear::new(
785 load_tensor(model, &format!("{prefix}.attn.wo.weight"))?,
786 load_optional_vector(model, &format!("{prefix}.attn.wo.bias"), config.vision_dim)?,
787 )?;
788 let norm2 = required_norm(model, &format!("{prefix}.norm2.weight"), config.vision_dim)?;
789 let w1 = VisionLinear::new(
790 load_tensor(model, &format!("{prefix}.mlp.w1.weight"))?,
791 load_optional_vector(
792 model,
793 &format!("{prefix}.mlp.w1.bias"),
794 2 * config.vision_inter_dim,
795 )?,
796 )?;
797 let w2 = VisionLinear::new(
798 load_tensor(model, &format!("{prefix}.mlp.w2.weight"))?,
799 load_optional_vector(model, &format!("{prefix}.mlp.w2.bias"), config.vision_dim)?,
800 )?;
801 if wqkv.weight.rows() != 3 * config.vision_dim
802 || wqkv.weight.cols() != config.vision_dim
803 || wo.weight.rows() != config.vision_dim
804 || wo.weight.cols() != config.vision_dim
805 || w1.weight.rows() != 2 * config.vision_inter_dim
806 || w1.weight.cols() != config.vision_dim
807 || w2.weight.rows() != config.vision_dim
808 || w2.weight.cols() != config.vision_inter_dim
809 {
810 return Err(format!(
811 "vision block {layer} has a non-reference linear shape"
812 ));
813 }
814 blocks.push(VisionBlock {
815 norm1,
816 attn: VisionAttention { wqkv, wo },
817 norm2,
818 mlp: VisionMlp { w1, w2 },
819 });
820 }
821 let norm = required_norm(model, "vision.norm.weight", config.vision_dim)?;
822 let aligner_in = config.vision_dim * config.vision_downsample_ratio.pow(2);
823 let aligner_w1 = VisionLinear::new(
824 load_tensor(model, "aligner.w1.weight")?,
825 load_optional_vector(model, "aligner.w1.bias", config.text_dim)?,
826 )?;
827 let aligner_w2 = VisionLinear::new(
828 load_tensor(model, "aligner.w2.weight")?,
829 load_optional_vector(model, "aligner.w2.bias", config.text_dim)?,
830 )?;
831 if aligner_w1.weight.rows() != config.text_dim
832 || aligner_w1.weight.cols() != aligner_in
833 || aligner_w2.weight.rows() != config.text_dim
834 || aligner_w2.weight.cols() != config.text_dim
835 {
836 return Err("aligner linear shapes do not match vision config".to_string());
837 }
838 let image_start = load_vector(model, "image_start", config.text_dim)?;
839 let image_end = load_vector(model, "image_end", config.text_dim)?;
840 let image_newline = load_vector(model, "image_newline", config.text_dim)?;
841 Ok(Self {
842 config,
843 patch_embed,
844 blocks,
845 norm,
846 aligner_w1,
847 aligner_w2,
848 image_start,
849 image_end,
850 image_newline,
851 })
852 }
853
854 pub fn encode_image(
858 &self,
859 image: &ImageInput,
860 pool: Option<&Pool>,
861 ) -> Result<Vec<f32>, String> {
862 if image.n_vit_h == 0 || image.n_vit_w == 0 {
863 return Err("image patch grid must be non-empty".to_string());
864 }
865 let patch_dim = 3 * self.config.vision_patch_size * self.config.vision_patch_size;
866 let n = image.n_vit_h * image.n_vit_w;
867 if image.patches.len() != n * patch_dim {
868 return Err(format!(
869 "patch payload has {} values, expected {}",
870 image.patches.len(),
871 n * patch_dim
872 ));
873 }
874 let mut x = vec![0.0f32; n * self.config.vision_dim];
875 self.patch_embed.apply_many(&image.patches, n, &mut x, pool);
876 let (cos, sin) = get_vision_cos_sin(
877 image.n_vit_h,
878 image.n_vit_w,
879 self.config.rope_dim(),
880 self.config.vision_rope_theta,
881 );
882 for block in &self.blocks {
883 let mut normed = vec![0.0f32; x.len()];
884 for (src, dst) in x
885 .chunks_exact(self.config.vision_dim)
886 .zip(normed.chunks_exact_mut(self.config.vision_dim))
887 {
888 rms_norm(src, &block.norm1, 1e-6, dst);
889 }
890 let attention = attention_forward(
891 &normed,
892 &block.attn,
893 &cos,
894 &sin,
895 self.config.vision_dim,
896 self.config.vision_n_heads,
897 pool,
898 );
899 for (dst, update) in x
900 .chunks_exact_mut(self.config.vision_dim)
901 .zip(attention.chunks_exact(self.config.vision_dim))
902 {
903 for (v, &u) in dst.iter_mut().zip(update) {
904 *v += u;
905 }
906 }
907 let mut normed = vec![0.0f32; x.len()];
908 for (src, dst) in x
909 .chunks_exact(self.config.vision_dim)
910 .zip(normed.chunks_exact_mut(self.config.vision_dim))
911 {
912 rms_norm(src, &block.norm2, 1e-6, dst);
913 }
914 let inter = block.mlp.w1.weight.rows() / 2;
915 let mut hidden = vec![0.0f32; n * 2 * inter];
916 block.mlp.w1.apply_many(&normed, n, &mut hidden, pool);
917 let mut activated = vec![0.0f32; n * inter];
918 for (source, output) in hidden
919 .chunks_exact(2 * inter)
920 .zip(activated.chunks_exact_mut(inter))
921 {
922 for i in 0..inter {
923 let gate = source[i];
924 output[i] = gate / (1.0 + (-gate).exp()) * source[inter + i];
925 }
926 }
927 let mut mlp_out = vec![0.0f32; x.len()];
928 block.mlp.w2.apply_many(&activated, n, &mut mlp_out, pool);
929 for (dst, update) in x
930 .chunks_exact_mut(self.config.vision_dim)
931 .zip(mlp_out.chunks_exact(self.config.vision_dim))
932 {
933 for (v, &u) in dst.iter_mut().zip(update) {
934 *v += u;
935 }
936 }
937 }
938 for src in x.chunks_exact_mut(self.config.vision_dim).take(n) {
939 let copy = src.to_vec();
940 rms_norm(©, &self.norm, 1e-6, src);
941 }
942 self.align(image, &x, pool)
943 }
944
945 pub fn fill_image_span(
949 &self,
950 image: &ImageInput,
951 span: &mut [f32],
952 pool: Option<&Pool>,
953 ) -> Result<(), String> {
954 let dim = self.config.text_dim;
955 if span.len() != image.types.len() * dim {
956 return Err(format!(
957 "image span has {} values, expected {}",
958 span.len(),
959 image.types.len() * dim
960 ));
961 }
962 let embeds = self.encode_image(image, pool)?;
963 let mut image_row = 0;
964 for (kind, row) in image.types.iter().zip(span.chunks_exact_mut(dim)) {
965 match *kind {
966 IMAGE_START => row.copy_from_slice(&self.image_start),
967 IMAGE_END => row.copy_from_slice(&self.image_end),
968 IMAGE_NEW_LINE => row.copy_from_slice(&self.image_newline),
969 IMAGE => {
970 let source = embeds
971 .get(image_row * dim..(image_row + 1) * dim)
972 .ok_or_else(|| "aligner/image token count mismatch".to_string())?;
973 row.copy_from_slice(source);
974 image_row += 1;
975 }
976 TEXT => return Err("TEXT type cannot occur inside an image span".to_string()),
977 other => return Err(format!("unknown image token type {other}")),
978 }
979 }
980 if image_row != embeds.len() / dim {
981 return Err("aligner produced a different number of image rows".to_string());
982 }
983 Ok(())
984 }
985
986 pub fn image_span(&self, image: &ImageInput, pool: Option<&Pool>) -> Result<Vec<f32>, String> {
991 let mut span = vec![0.0f32; image.types.len() * self.config.text_dim];
992 self.fill_image_span(image, &mut span, pool)?;
993 Ok(span)
994 }
995
996 fn align(
997 &self,
998 image: &ImageInput,
999 x: &[f32],
1000 pool: Option<&Pool>,
1001 ) -> Result<Vec<f32>, String> {
1002 let r = self.config.vision_downsample_ratio;
1003 let (input, out_h, out_w) =
1004 unfold_padded(x, image.n_vit_h, image.n_vit_w, self.config.vision_dim, r);
1005 let rows = out_h * out_w;
1006 let mut out = vec![0.0f32; rows * self.config.text_dim];
1007 let mut hidden = vec![0.0f32; rows * self.config.text_dim];
1008 self.aligner_w1.apply_many(&input, rows, &mut hidden, pool);
1009 for value in &mut hidden {
1010 *value = gelu_exact(*value);
1011 }
1012 self.aligner_w2.apply_many(&hidden, rows, &mut out, pool);
1013 if out.len() != image.image_positions() * self.config.text_dim {
1014 return Err(format!(
1015 "aligner produced {} rows but span requests {} image positions",
1016 rows,
1017 image.image_positions()
1018 ));
1019 }
1020 Ok(out)
1021 }
1022}
1023
1024pub fn get_vision_cos_sin(n_h: usize, n_w: usize, dim: usize, theta: f32) -> (Vec<f32>, Vec<f32>) {
1027 let mut inv_freq = Vec::with_capacity(dim / 2);
1028 for i in (0..dim).step_by(2) {
1029 inv_freq.push(1.0 / theta.powf(i as f32 / dim.max(1) as f32));
1030 }
1031 let mut cos = Vec::with_capacity(n_h * n_w * dim);
1032 let mut sin = Vec::with_capacity(n_h * n_w * dim);
1033 for h in 0..n_h {
1034 for w in 0..n_w {
1035 for &position in &[h as f32, w as f32] {
1036 for &frequency in &inv_freq {
1037 let angle = position * frequency;
1038 cos.push(angle.cos());
1039 sin.push(angle.sin());
1040 }
1041 }
1042 }
1043 }
1044 debug_assert_eq!(cos.len(), n_h * n_w * dim);
1045 debug_assert_eq!(sin.len(), cos.len());
1046 (cos, sin)
1047}
1048
1049pub fn apply_rotary(x: &mut [f32], cos: &[f32], sin: &[f32]) {
1050 assert_eq!(x.len(), cos.len() * 2);
1051 let half = x.len() / 2;
1052 let left = x[..half].to_vec();
1053 let right = x[half..].to_vec();
1054 for i in 0..half {
1055 x[i] = left[i] * cos[i] - right[i] * sin[i];
1056 x[half + i] = right[i] * cos[i] + left[i] * sin[i];
1057 }
1058}
1059
1060pub fn rms_norm(input: &[f32], weight: &[f32], eps: f32, output: &mut [f32]) {
1061 assert_eq!(input.len(), weight.len());
1062 assert!(output.len() >= input.len());
1063 let mean = input.iter().map(|v| v * v).sum::<f32>() / input.len().max(1) as f32;
1064 let scale = (mean + eps).sqrt().recip();
1065 for ((dst, &value), &factor) in output.iter_mut().zip(input).zip(weight) {
1066 *dst = value * scale * factor;
1067 }
1068}
1069
1070fn gelu_exact(value: f32) -> f32 {
1071 let sign = if value < 0.0 { -1.0 } else { 1.0 };
1074 let x = value.abs() / std::f32::consts::SQRT_2;
1075 let t = 1.0 / (1.0 + 0.3275911 * x);
1076 let polynomial = (((((1.061_405_4 * t - 1.453_152_1) * t) + 1.421_413_8) * t - 0.284_496_72)
1077 * t
1078 + 0.254_829_6)
1079 * t;
1080 let erf = sign * (1.0 - polynomial * (-x * x).exp());
1081 0.5 * value * (1.0 + erf)
1082}
1083
1084fn attention_forward(
1085 input: &[f32],
1086 attention: &VisionAttention,
1087 cos: &[f32],
1088 sin: &[f32],
1089 dim: usize,
1090 heads: usize,
1091 pool: Option<&Pool>,
1092) -> Vec<f32> {
1093 let n = input.len() / dim;
1094 let head_dim = dim / heads;
1095 let rope_dim = head_dim / 2;
1096 let mut qkv = vec![0.0f32; n * 3 * dim];
1101 attention.wqkv.apply_many(input, n, &mut qkv, pool);
1102 let mut q = vec![0.0f32; n * dim];
1103 let mut k = vec![0.0f32; n * dim];
1104 let mut v = vec![0.0f32; n * dim];
1105 for (token, source) in qkv.chunks_exact(3 * dim).enumerate() {
1106 q[token * dim..(token + 1) * dim].copy_from_slice(&source[..dim]);
1107 k[token * dim..(token + 1) * dim].copy_from_slice(&source[dim..2 * dim]);
1108 v[token * dim..(token + 1) * dim].copy_from_slice(&source[2 * dim..]);
1109 let cos_row = &cos[token * rope_dim..(token + 1) * rope_dim];
1110 let sin_row = &sin[token * rope_dim..(token + 1) * rope_dim];
1111 for head in 0..heads {
1112 let offset = token * dim + head * head_dim;
1113 apply_rotary(&mut q[offset..offset + head_dim], cos_row, sin_row);
1114 apply_rotary(&mut k[offset..offset + head_dim], cos_row, sin_row);
1115 }
1116 }
1117 let scale = (head_dim as f32).sqrt().recip();
1118
1119 if n >= 128 && crate::gpu::enabled_here() {
1126 let panel_len = n * heads * head_dim;
1127 let mut qh = vec![0.0f32; panel_len];
1128 let mut kh = vec![0.0f32; panel_len];
1129 let mut vh = vec![0.0f32; panel_len];
1130 for token in 0..n {
1131 for head in 0..heads {
1132 let src = token * dim + head * head_dim;
1133 let dst = head * n * head_dim + token * head_dim;
1134 qh[dst..dst + head_dim].copy_from_slice(&q[src..src + head_dim]);
1135 kh[dst..dst + head_dim].copy_from_slice(&k[src..src + head_dim]);
1136 vh[dst..dst + head_dim].copy_from_slice(&v[src..src + head_dim]);
1137 }
1138 }
1139 let mut context = vec![0.0f32; panel_len];
1140 if crate::gpu::dit_attention(
1141 &qh,
1142 &kh,
1143 &vh,
1144 heads,
1145 heads,
1146 n,
1147 head_dim,
1148 scale,
1149 &mut context,
1150 ) {
1151 GPU_ATTENTION_DISPATCHES.fetch_add(1, Ordering::Relaxed);
1152 let mut output = vec![0.0f32; n * dim];
1153 attention.wo.apply_many(&context, n, &mut output, pool);
1154 return output;
1155 }
1156 }
1157
1158 let mut context = vec![0.0f32; n * dim];
1159 let mut scores = vec![0.0f32; n];
1160 for head in 0..heads {
1161 for query in 0..n {
1162 let qrow = &q[query * dim + head * head_dim..query * dim + (head + 1) * head_dim];
1163 let mut max_score = f32::NEG_INFINITY;
1164 for key in 0..n {
1165 let krow = &k[key * dim + head * head_dim..key * dim + (head + 1) * head_dim];
1166 let score = qrow.iter().zip(krow).map(|(a, b)| a * b).sum::<f32>() * scale;
1167 scores[key] = score;
1168 max_score = max_score.max(score);
1169 }
1170 let mut denominator = 0.0f32;
1171 for score in &mut scores {
1172 *score = (*score - max_score).exp();
1173 denominator += *score;
1174 }
1175 let inv = denominator.recip();
1176 let out =
1177 &mut context[query * dim + head * head_dim..query * dim + (head + 1) * head_dim];
1178 for key in 0..n {
1179 let probability = scores[key] * inv;
1180 let vrow = &v[key * dim + head * head_dim..key * dim + (head + 1) * head_dim];
1181 for (dst, &value) in out.iter_mut().zip(vrow) {
1182 *dst += probability * value;
1183 }
1184 }
1185 }
1186 }
1187 let mut output = vec![0.0f32; n * dim];
1188 attention.wo.apply_many(&context, n, &mut output, pool);
1189 output
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use super::*;
1195
1196 #[test]
1197 fn image_grid_and_types_match_reference() {
1198 let config = VisionConfig {
1199 vision_n_layers: 1,
1200 ..VisionConfig::default()
1201 };
1202 let (h, w, best_h, best_w) = plan_image_grid(544, 544, &config).unwrap();
1203 assert_eq!((h, w), (13, 13));
1204 assert_eq!((best_h, best_w), (546, 546));
1205 assert_eq!(num_image_tokens(h, w), 184);
1206 let types = image_token_types(h, w);
1207 assert_eq!(types.first(), Some(&IMAGE_START));
1208 assert_eq!(types.last(), Some(&IMAGE_END));
1209 assert_eq!(types.iter().filter(|&&v| v == IMAGE_NEW_LINE).count(), h);
1210 assert_eq!(types.iter().filter(|&&v| v == IMAGE).count(), h * w);
1211 }
1212
1213 #[test]
1214 fn rope_grid_has_reference_first_rows() {
1215 let (cos, sin) = get_vision_cos_sin(2, 2, 4, 10_000.0);
1216 assert_eq!(&cos[..4], &[1.0, 1.0, 1.0, 1.0]);
1217 assert_eq!(&sin[..4], &[0.0, 0.0, 0.0, 0.0]);
1218 assert!((cos[6] - 0.5403023).abs() < 1e-6);
1219 assert!((sin[6] - 0.8414710).abs() < 1e-6);
1220 }
1221
1222 #[test]
1223 fn exact_gelu_and_rms_are_finite() {
1224 assert!((gelu_exact(1.0) - 0.8413447).abs() < 2e-6);
1225 let mut out = [0.0; 2];
1226 rms_norm(&[3.0, 4.0], &[1.0, 2.0], 1e-6, &mut out);
1227 assert!(out.iter().all(|v| v.is_finite()));
1228 let scale = (12.5_f32 + 1e-6).sqrt().recip();
1229 assert!((out[0] - 3.0 * scale).abs() < 1e-4);
1230 assert!((out[1] - 8.0 * scale).abs() < 1e-4);
1231 }
1232
1233 #[test]
1234 fn aligner_zero_pads_nonmultiple_vit_grid() {
1235 assert_eq!(padded_vit_grid(35, 46, 3), (36, 48));
1240 assert_eq!(llm_grid(36 * 14, 48 * 14, 14, 3), (12, 16));
1241 assert_eq!(
1242 12 * 16,
1243 image_token_types(12, 16)
1244 .iter()
1245 .filter(|&&v| v == IMAGE)
1246 .count()
1247 );
1248
1249 let x: Vec<f32> = (0..8).map(|v| v as f32).collect();
1253 let (windows, out_h, out_w) = unfold_padded(&x, 2, 4, 1, 3);
1254 assert_eq!((out_h, out_w), (1, 2));
1255 assert_eq!(
1256 &windows[..9],
1257 &[0.0, 1.0, 2.0, 4.0, 5.0, 6.0, 0.0, 0.0, 0.0]
1258 );
1259 assert_eq!(
1260 &windows[9..],
1261 &[3.0, 0.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0]
1262 );
1263 }
1264}