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