1use crate::egemma2::{Mat, add_normed, gelu_mul, rms_into, rms_rows, softmax, vecf};
54use crate::ltxdit::{Shared, rows};
55use crate::media::RgbFrame;
56use crate::pool::Pool;
57use cortiq_core::CmfModel;
58use serde_json::Value;
59use std::path::Path;
60use std::sync::{Arc, Mutex};
61
62pub const BUDGETS: [usize; 5] = [70, 140, 280, 560, 1120];
64const PACK_PATCHES: usize = 10_080;
66const SCORE_BLOCK: usize = 8 << 20;
69const QBLOCK_MIN: usize = 256;
70const ITEM_QBLOCK: usize = 128;
72const GPU_MIN_PATCHES: usize = 4096;
75
76mod vprof {
78 use std::sync::atomic::{AtomicU64, Ordering};
79 pub const QKV: usize = 0;
80 pub const NORM_ROPE: usize = 1;
81 pub const CORE: usize = 2;
82 pub const O: usize = 3;
83 pub const MLP: usize = 4;
84 pub const GELU: usize = 5;
85 pub const NORMS: usize = 6;
86 const NAMES: [&str; 7] = [
87 "q/k/v",
88 "norm+rope",
89 "attention core",
90 "o",
91 "mlp",
92 "gelu",
93 "norms",
94 ];
95 static T: [AtomicU64; 7] = [const { AtomicU64::new(0) }; 7];
96 pub fn on() -> bool {
97 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
98 *ON.get_or_init(|| std::env::var("CMF_EGEMMA2_PROF").is_ok_and(|v| v == "1"))
99 }
100 pub fn add(i: usize, t: std::time::Instant) {
101 if on() {
102 T[i].fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
103 }
104 }
105 pub fn take() -> String {
106 NAMES
107 .iter()
108 .enumerate()
109 .map(|(i, n)| format!("{n} {:.1}", T[i].swap(0, Ordering::Relaxed) as f64 / 1e3))
110 .collect::<Vec<_>>()
111 .join(", ")
112 }
113}
114
115#[derive(Clone, Debug, PartialEq)]
119pub struct VisionProcessor {
120 pub patch: usize,
121 pub pool_k: usize,
122 pub image_budget: usize,
124 pub video_budget: usize,
126 pub video_fps: f64,
128 pub max_frames: usize,
130 pub overflow_truncate: bool,
132}
133
134impl Default for VisionProcessor {
135 fn default() -> Self {
136 VisionProcessor {
137 patch: 16,
138 pool_k: 3,
139 image_budget: 280,
140 video_budget: 140,
141 video_fps: 1.0,
142 max_frames: 32,
143 overflow_truncate: false,
144 }
145 }
146}
147
148impl VisionProcessor {
149 pub fn from_config(cfg: &Value) -> Self {
151 let mut p = VisionProcessor::default();
152 let u = |v: &Value, k: &str| v.get(k).and_then(|x| x.as_u64()).map(|x| x as usize);
153 let ip = &cfg["image_processor"];
154 let vp = &cfg["video_processor"];
155 if let Some(x) = u(ip, "patch_size") {
156 p.patch = x;
157 }
158 if let Some(x) = u(ip, "pooling_kernel_size") {
159 p.pool_k = x;
160 }
161 if let Some(x) = u(ip, "max_soft_tokens").or_else(|| u(cfg, "image_seq_length")) {
162 p.image_budget = x;
163 }
164 if let Some(x) = u(vp, "max_soft_tokens") {
165 p.video_budget = x;
166 }
167 if let Some(x) = vp.get("fps").and_then(|x| x.as_f64()) {
168 p.video_fps = x;
169 }
170 if let Some(x) = u(vp, "max_frames") {
171 p.max_frames = x;
172 }
173 p.overflow_truncate =
174 vp.get("overflow_strategy").and_then(|x| x.as_str()) == Some("truncate");
175 p
176 }
177
178 pub fn check_budget(budget: usize) -> Result<usize, String> {
180 if BUDGETS.contains(&budget) {
181 Ok(budget)
182 } else {
183 Err(format!(
184 "soft-token budget {budget}: use 70, 140, 280, 560 or 1120"
185 ))
186 }
187 }
188}
189
190pub fn target_size(
194 height: usize,
195 width: usize,
196 patch: usize,
197 max_patches: usize,
198 k: usize,
199) -> Result<(usize, usize), String> {
200 let total_px = (height * width) as f64;
201 let target_px = (max_patches * patch * patch) as f64;
202 let factor = (target_px / total_px).sqrt();
203 let (ih, iw) = (factor * height as f64, factor * width as f64);
204 let side = patch * k;
205 let mut th = (ih / side as f64).floor() as usize * side;
206 let mut tw = (iw / side as f64).floor() as usize * side;
207 if th == 0 && tw == 0 {
208 return Err(format!(
209 "a {width}x{height} image cannot be resized to a non-empty patch grid"
210 ));
211 }
212 let max_side = (max_patches / (k * k)) * side;
213 if th == 0 {
214 th = side;
215 tw = ((width as f64 / height as f64).floor() as usize * side).min(max_side);
216 } else if tw == 0 {
217 tw = side;
218 th = ((height as f64 / width as f64).floor() as usize * side).min(max_side);
219 }
220 if (th * tw) as f64 > target_px {
221 return Err(format!(
222 "resizing {width}x{height} to {tw}x{th} exceeds {max_patches} patches"
223 ));
224 }
225 Ok((th, tw))
226}
227
228fn cubic(x: f64) -> f64 {
232 const A: f64 = -0.5;
233 let x = x.abs();
234 if x < 1.0 {
235 ((A + 2.0) * x - (A + 3.0)) * x * x + 1.0
236 } else if x < 2.0 {
237 ((A * x - 5.0 * A) * x + 8.0 * A) * x - 4.0 * A
238 } else {
239 0.0
240 }
241}
242
243struct AxisTaps {
247 xmin: Vec<usize>,
248 xsize: Vec<usize>,
249 w: Vec<i16>,
250 stride: usize,
251 precision: u32,
252}
253
254fn axis_taps(in_size: usize, out_size: usize) -> AxisTaps {
255 let scale = in_size as f64 / out_size as f64;
256 let support = if scale >= 1.0 { 2.0 * scale } else { 2.0 };
257 let max_interp = support.ceil() as usize * 2 + 1;
258 let invscale = if scale >= 1.0 { 1.0 / scale } else { 1.0 };
259 let mut wf = vec![0f64; out_size * max_interp];
260 let mut xmin = vec![0usize; out_size];
261 let mut xsize = vec![0usize; out_size];
262 let mut wt_max = 0f64;
263 for i in 0..out_size {
264 let center = scale * (i as f64 + 0.5);
265 let lo = ((center - support + 0.5) as i64).max(0);
266 let hi = ((center + support + 0.5) as i64).min(in_size as i64);
267 let n = (hi - lo).clamp(0, max_interp as i64) as usize;
268 let row = &mut wf[i * max_interp..(i + 1) * max_interp];
269 let mut total = 0f64;
270 for (j, w) in row.iter_mut().enumerate().take(n) {
271 *w = cubic((j as f64 + lo as f64 - center + 0.5) * invscale);
272 total += *w;
273 }
274 if total != 0.0 {
275 for w in row.iter_mut().take(n) {
276 *w /= total;
277 }
278 }
279 for &w in row.iter() {
280 wt_max = wt_max.max(w);
281 }
282 xmin[i] = lo as usize;
283 xsize[i] = n;
284 }
285 let mut precision = 0u32;
286 while precision < 22 {
287 let next = (0.5 + wt_max * (1u64 << (precision + 1)) as f64) as i64;
288 if next >= 1 << 15 {
289 break;
290 }
291 precision += 1;
292 }
293 let mul = (1u64 << precision) as f64;
294 let w = wf
295 .iter()
296 .map(|&v| {
297 let v = v * mul;
298 (if v < 0.0 {
299 (-0.5 + v) as i32
300 } else {
301 (0.5 + v) as i32
302 }) as i16
303 })
304 .collect();
305 AxisTaps {
306 xmin,
307 xsize,
308 w,
309 stride: max_interp,
310 precision,
311 }
312}
313
314fn resample_axis(
317 src: &[u8],
318 outer: usize,
319 len_in: usize,
320 inner: usize,
321 taps: &AxisTaps,
322 len_out: usize,
323) -> Vec<u8> {
324 let mut out = vec![0u8; outer * len_out * inner];
325 let round = 1i64 << (taps.precision.max(1) - 1);
326 for o in 0..outer {
327 let s = &src[o * len_in * inner..(o + 1) * len_in * inner];
328 let d = &mut out[o * len_out * inner..(o + 1) * len_out * inner];
329 for i in 0..len_out {
330 let (x0, n) = (taps.xmin[i], taps.xsize[i]);
331 let w = &taps.w[i * taps.stride..i * taps.stride + n];
332 for c in 0..inner {
333 let mut acc = round;
334 for (j, &wj) in w.iter().enumerate() {
335 acc += s[(x0 + j) * inner + c] as i64 * wj as i64;
336 }
337 d[i * inner + c] = (acc >> taps.precision).clamp(0, 255) as u8;
338 }
339 }
340 }
341 out
342}
343
344pub fn resize_bicubic_aa(img: &RgbFrame, height: usize, width: usize) -> RgbFrame {
348 let (h0, w0) = (img.height, img.width);
349 let mut cur = img.data.clone();
350 if width != w0 {
351 cur = resample_axis(&cur, h0, w0, 3, &axis_taps(w0, width), width);
352 }
353 if height != h0 {
354 cur = resample_axis(&cur, 1, h0, width * 3, &axis_taps(h0, height), height);
355 }
356 RgbFrame {
357 width,
358 height,
359 data: cur,
360 }
361}
362
363#[derive(Clone, Debug)]
368pub struct VisionInput {
369 pub pw: usize,
370 pub ph: usize,
371 pub pixels: Vec<f32>,
372}
373
374impl VisionInput {
375 pub fn n_patches(&self) -> usize {
376 self.pw * self.ph
377 }
378
379 pub fn n_soft(&self, k: usize) -> usize {
381 (self.pw / k) * (self.ph / k)
382 }
383}
384
385pub fn patchify(img: &RgbFrame, patch: usize) -> VisionInput {
388 let (pw, ph) = (img.width / patch, img.height / patch);
389 let dim = 3 * patch * patch;
390 let scale = (1.0f64 / 255.0) as f32;
392 let mut pixels = vec![0f32; pw * ph * dim];
393 for py in 0..ph {
394 for px in 0..pw {
395 let dst = &mut pixels[(py * pw + px) * dim..(py * pw + px + 1) * dim];
396 for yy in 0..patch {
397 let row = (py * patch + yy) * img.width + px * patch;
398 let src = &img.data[row * 3..(row + patch) * 3];
399 for (d, &v) in dst[yy * patch * 3..(yy + 1) * patch * 3]
400 .iter_mut()
401 .zip(src)
402 {
403 *d = 2.0 * (v as f32 * scale - 0.5);
404 }
405 }
406 }
407 }
408 VisionInput { pw, ph, pixels }
409}
410
411impl VisionProcessor {
412 pub fn resized_size(
414 &self,
415 w: usize,
416 h: usize,
417 budget: usize,
418 ) -> Result<(usize, usize), String> {
419 let (th, tw) = target_size(
420 h,
421 w,
422 self.patch,
423 budget * self.pool_k * self.pool_k,
424 self.pool_k,
425 )?;
426 Ok((tw, th))
427 }
428
429 pub fn prepare_image(&self, img: &RgbFrame, budget: usize) -> Result<VisionInput, String> {
431 VisionProcessor::check_budget(budget)?;
432 let (tw, th) = self.resized_size(img.width, img.height, budget)?;
433 if (tw, th) == (img.width, img.height) {
434 return Ok(patchify(img, self.patch));
435 }
436 Ok(patchify(&resize_bicubic_aa(img, th, tw), self.patch))
437 }
438
439 pub fn prepare_frames(
442 &self,
443 frames: &[RgbFrame],
444 budget: usize,
445 ) -> Result<Vec<VisionInput>, String> {
446 let first = frames.first().ok_or("a video needs at least one frame")?;
447 if let Some((i, f)) = frames
448 .iter()
449 .enumerate()
450 .find(|(_, f)| (f.width, f.height) != (first.width, first.height))
451 {
452 return Err(format!(
453 "video frame {i} is {}x{}, frame 0 is {}x{}: all frames must share one size",
454 f.width, f.height, first.width, first.height
455 ));
456 }
457 VisionProcessor::check_budget(budget)?;
458 let (tw, th) = self.resized_size(first.width, first.height, budget)?;
459 Ok(frames
460 .iter()
461 .map(|f| {
462 if (tw, th) == (f.width, f.height) {
463 patchify(f, self.patch)
464 } else {
465 patchify(&resize_bicubic_aa(f, th, tw), self.patch)
466 }
467 })
468 .collect())
469 }
470
471 pub fn sample_indices(&self, total: usize, src_fps: Option<f64>) -> Vec<usize> {
477 if total == 0 {
478 return Vec::new();
479 }
480 let mut idx: Vec<usize> = match src_fps.filter(|f| *f > 0.0 && f.is_finite()) {
481 None => (0..total).collect(),
482 Some(f) => {
483 let step = f / self.video_fps;
484 let duration = total as f64 / f;
485 let n = ((duration * self.video_fps) as usize).max(1);
486 (0..n)
487 .map(|i| ((i as f64 * step) as usize).min(total - 1))
488 .collect()
489 }
490 };
491 let m = self.max_frames;
492 if m > 0 && idx.len() > m {
493 if self.overflow_truncate {
494 idx.truncate(m);
495 } else {
496 let last = idx.len() - 1;
499 let step = if m > 1 {
500 last as f64 / (m - 1) as f64
501 } else {
502 0.0
503 };
504 idx = (0..m)
505 .map(|i| {
506 let t = if i + 1 == m && m > 1 {
507 last
508 } else {
509 (i as f64 * step).floor() as usize
510 };
511 idx[t.min(last)]
512 })
513 .collect();
514 }
515 }
516 idx
517 }
518}
519
520#[derive(Clone, Debug)]
524pub struct DecodedVideo {
525 pub frames: Vec<RgbFrame>,
526 pub indices: Vec<usize>,
528 pub total_frames: usize,
529 pub src_fps: Option<f64>,
530 pub decoder: &'static str,
532}
533
534pub fn decode_video(
549 path: &Path,
550 fps: Option<f64>,
551 proc: &VisionProcessor,
552) -> Result<DecodedVideo, String> {
553 use crate::mimo_vision::VideoSource;
554 if path.is_dir() {
555 let src = VideoSource::frame_dir(path, fps.unwrap_or(1.0))?;
556 let total = src.frame_count();
557 let indices = proc.sample_indices(total, fps);
558 let frames = indices
559 .iter()
560 .map(|&i| src.read_frame(i))
561 .collect::<Result<Vec<_>, _>>()?;
562 return Ok(DecodedVideo {
563 frames,
564 indices,
565 total_frames: total,
566 src_fps: fps,
567 decoder: "frames-dir",
568 });
569 }
570 let ext = path
571 .extension()
572 .and_then(|e| e.to_str())
573 .unwrap_or("")
574 .to_ascii_lowercase();
575 if ext == "y4m" {
576 let src = VideoSource::open(path, fps)?;
577 let total = src.frame_count();
578 let f = src.fps();
579 let indices = proc.sample_indices(total, Some(f));
580 let frames = indices
581 .iter()
582 .map(|&i| src.read_frame(i))
583 .collect::<Result<Vec<_>, _>>()?;
584 return Ok(DecodedVideo {
585 frames,
586 indices,
587 total_frames: total,
588 src_fps: Some(f),
589 decoder: "y4m",
590 });
591 }
592 decode_with_ffmpeg(path, fps, proc)
593}
594
595struct Probe {
597 width: usize,
598 height: usize,
599 fps: f64,
600 frames: Option<usize>,
602 duration: Option<f64>,
603}
604
605fn ffprobe_stream(path: &Path) -> Result<Probe, String> {
606 let out = std::process::Command::new("ffprobe")
607 .args([
608 "-v",
609 "error",
610 "-select_streams",
611 "v:0",
612 "-show_entries",
613 "stream=width,height,avg_frame_rate,r_frame_rate,nb_frames,duration:format=duration",
614 "-of",
615 "json",
616 ])
617 .arg(path)
618 .output()
619 .map_err(|e| {
620 format!(
621 "{}: decoding this video needs ffmpeg/ffprobe on PATH ({e}); \
622 or pass a directory of frames or a .y4m",
623 path.display()
624 )
625 })?;
626 if !out.status.success() {
627 return Err(format!(
628 "ffprobe {}: {}",
629 path.display(),
630 String::from_utf8_lossy(&out.stderr).trim()
631 ));
632 }
633 let v: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("ffprobe json: {e}"))?;
634 let s = &v["streams"][0];
635 let w = s["width"].as_u64().ok_or("ffprobe: no video stream")? as usize;
636 let h = s["height"].as_u64().ok_or("ffprobe: no video stream")? as usize;
637 let rate = |k: &str| -> Option<f64> {
638 let r = s[k].as_str()?;
639 let (n, d) = r.split_once('/').unwrap_or((r, "1"));
640 let (n, d): (f64, f64) = (n.parse().ok()?, d.parse().ok()?);
641 (d > 0.0 && n > 0.0).then(|| n / d)
642 };
643 let fps = rate("avg_frame_rate")
644 .or_else(|| rate("r_frame_rate"))
645 .ok_or("ffprobe: the video has no frame rate")?;
646 let nb = s["nb_frames"]
647 .as_str()
648 .and_then(|x| x.parse::<usize>().ok())
649 .filter(|&n| n > 0);
650 let dur = s["duration"]
651 .as_str()
652 .or_else(|| v["format"]["duration"].as_str())
653 .and_then(|x| x.parse::<f64>().ok());
654 Ok(Probe {
655 width: w,
656 height: h,
657 fps,
658 frames: nb,
659 duration: dur,
660 })
661}
662
663fn decode_with_ffmpeg(
664 path: &Path,
665 fps_override: Option<f64>,
666 proc: &VisionProcessor,
667) -> Result<DecodedVideo, String> {
668 use std::io::Read;
669 let Probe {
670 width: w,
671 height: h,
672 fps: probed_fps,
673 frames: nb,
674 duration: dur,
675 } = ffprobe_stream(path)?;
676 let fps = fps_override.unwrap_or(probed_fps);
677 let total = nb
680 .or_else(|| dur.map(|d| (d * probed_fps).round() as usize))
681 .ok_or_else(|| format!("{}: frame count unknown", path.display()))?;
682 let indices = proc.sample_indices(total, Some(fps));
683 let last = *indices.last().ok_or("video has no frames")?;
684 let mut child = std::process::Command::new("ffmpeg")
685 .args(["-v", "error", "-nostdin", "-noautorotate", "-i"])
688 .arg(path)
689 .args([
690 "-map",
691 "0:v:0",
692 "-frames:v",
693 &(last + 1).to_string(),
694 "-f",
695 "rawvideo",
696 "-pix_fmt",
697 "rgb24",
698 "-",
699 ])
700 .stdout(std::process::Stdio::piped())
701 .stderr(std::process::Stdio::piped())
702 .spawn()
703 .map_err(|e| format!("ffmpeg: {e}"))?;
704 let mut stdout = child.stdout.take().ok_or("ffmpeg: no stdout")?;
705 let frame_bytes = w * h * 3;
706 let mut buf = vec![0u8; frame_bytes];
707 let mut frames = Vec::with_capacity(indices.len());
708 let mut kept = Vec::with_capacity(indices.len());
709 let mut want = indices.iter().peekable();
710 let mut i = 0usize;
711 while want.peek().is_some() {
712 match stdout.read_exact(&mut buf) {
713 Ok(()) => {}
714 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
715 Err(e) => return Err(format!("ffmpeg read: {e}")),
716 }
717 while want.peek() == Some(&&i) {
718 frames.push(RgbFrame::new(w, h, buf.clone())?);
719 kept.push(i);
720 want.next();
721 }
722 i += 1;
723 }
724 drop(stdout);
725 let status = child.wait().map_err(|e| format!("ffmpeg: {e}"))?;
726 if frames.is_empty() {
727 let mut err = String::new();
728 if let Some(mut e) = child.stderr.take() {
729 let _ = e.read_to_string(&mut err);
730 }
731 return Err(format!(
732 "ffmpeg decoded no frames from {} ({status}): {}",
733 path.display(),
734 err.trim()
735 ));
736 }
737 Ok(DecodedVideo {
738 frames,
739 indices: kept,
740 total_frames: total,
741 src_fps: Some(fps),
742 decoder: "ffmpeg",
743 })
744}
745
746struct VLayer {
749 q: Mat,
750 k: Mat,
751 v: Mat,
752 o: Mat,
753 q_norm: Vec<f32>,
754 k_norm: Vec<f32>,
755 in_norm: Vec<f32>,
756 post_attn_norm: Vec<f32>,
757 pre_ff_norm: Vec<f32>,
758 post_ff_norm: Vec<f32>,
759 gate: Mat,
760 up: Mat,
761 down: Mat,
762}
763
764pub struct VisionTower {
766 #[cfg(target_os = "macos")]
769 gpu: Option<crate::gpu_metal::egemma2::VisionGpu>,
770 host_only: std::sync::atomic::AtomicBool,
772 input_proj: Mat,
773 pos: Vec<f32>,
775 pos_rows: usize,
776 layers: Vec<VLayer>,
777 embed_proj: Mat,
778 hidden: usize,
779 heads: usize,
780 head_dim: usize,
781 inv_freq: Vec<f32>,
783 pub pool_k: usize,
784 pub out_dim: usize,
786 pool: Option<Arc<Pool>>,
787 busy: Mutex<()>,
788 gpu_min_patches: usize,
790}
791
792#[derive(Clone, Copy)]
794struct Seg {
795 off: usize,
796 pw: usize,
797 ph: usize,
798}
799
800impl VisionTower {
801 pub fn present(model: &CmfModel) -> bool {
803 model
804 .tensor("vision_tower.patch_embedder.input_proj.weight")
805 .is_some()
806 }
807
808 pub fn load(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
809 if !Self::present(model) {
810 return Err(
811 "this EmbeddingGemma 2 file has no vision tower (packed with CMF_EGEMMA2_TEXT_ONLY=1?)"
812 .into(),
813 );
814 }
815 let prov = model.header.provenance.clone().unwrap_or(Value::Null);
816 let vc = &prov["embedding_gemma2"]["config"]["vision_config"];
817 let g = |k: &str, d: u64| vc.get(k).and_then(|v| v.as_u64()).unwrap_or(d) as usize;
818 let hidden = g("hidden_size", 768);
819 let heads = g("num_attention_heads", 12);
820 let head_dim = g("head_dim", 64);
821 let n_layers = g("num_hidden_layers", 16);
822 let pool_k = g("pooling_kernel_size", 3);
823 let theta = vc["rope_parameters"]["rope_theta"]
824 .as_f64()
825 .unwrap_or(100.0) as f32;
826 if vc.get("use_clipped_linears").and_then(|v| v.as_bool()) == Some(true) {
827 return Err("vision tower with clipped linears is not supported".into());
828 }
829 if vc.get("standardize").and_then(|v| v.as_bool()) == Some(true) {
830 return Err("vision tower with standardization is not supported".into());
831 }
832 let dequant = !std::env::var("CMF_EGEMMA2_LOWMEM").is_ok_and(|v| v == "1");
835 let mut layers = Vec::with_capacity(n_layers);
836 for i in 0..n_layers {
837 let p = format!("vision_tower.encoder.layers.{i}");
838 let ld = |s: &str| Mat::load(model, &format!("{p}.{s}.linear.weight"), dequant);
839 let nv = |s: &str| vecf(model, &format!("{p}.{s}.weight"));
840 layers.push(VLayer {
841 q: ld("self_attn.q_proj")?,
842 k: ld("self_attn.k_proj")?,
843 v: ld("self_attn.v_proj")?,
844 o: ld("self_attn.o_proj")?,
845 q_norm: nv("self_attn.q_norm")?,
846 k_norm: nv("self_attn.k_norm")?,
847 in_norm: nv("input_layernorm")?,
848 post_attn_norm: nv("post_attention_layernorm")?,
849 pre_ff_norm: nv("pre_feedforward_layernorm")?,
850 post_ff_norm: nv("post_feedforward_layernorm")?,
851 gate: ld("mlp.gate_proj")?,
852 up: ld("mlp.up_proj")?,
853 down: ld("mlp.down_proj")?,
854 });
855 }
856 let pos_name = "vision_tower.patch_embedder.position_embedding_table";
857 let pos_shape = model
858 .tensor(pos_name)
859 .ok_or_else(|| format!("missing tensor {pos_name}"))?
860 .shape
861 .clone();
862 if pos_shape.len() != 3 || pos_shape[0] != 2 || pos_shape[2] != hidden {
863 return Err(format!("{pos_name}: unexpected shape {pos_shape:?}"));
864 }
865 let pos = vecf(model, pos_name)?;
866 let embed_proj = Mat::load(model, "embed_vision.embedding_projection.weight", true)?;
867 let quarter = head_dim / 4;
868 let inv_freq = (0..quarter)
870 .map(|i| 1.0f32 / theta.powf((2 * i) as f32 / (head_dim / 2) as f32))
871 .collect();
872 let gpu_min_patches = match std::env::var("CMF_EGEMMA2_VISION_GPU").as_deref() {
879 Ok("0") => usize::MAX,
880 Ok("1") => 256,
881 _ => GPU_MIN_PATCHES,
882 };
883 #[cfg_attr(not(target_os = "macos"), allow(unused_mut))]
885 let mut tower = VisionTower {
886 #[cfg(target_os = "macos")]
887 gpu: None,
888 host_only: std::sync::atomic::AtomicBool::new(false),
889 input_proj: Mat::load(
890 model,
891 "vision_tower.patch_embedder.input_proj.weight",
892 dequant,
893 )?,
894 pos,
895 pos_rows: pos_shape[1],
896 layers,
897 out_dim: embed_proj.rows(),
898 embed_proj,
899 hidden,
900 heads,
901 head_dim,
902 inv_freq,
903 pool_k,
904 pool,
905 busy: Mutex::new(()),
906 gpu_min_patches,
907 };
908 #[cfg(target_os = "macos")]
909 if dequant && crate::gpu_metal::egemma2::enabled() {
910 match tower.device_layers() {
911 Ok(g) => tower.gpu = Some(g),
912 Err(e) => {
913 tracing::info!("egemma2 vision: Metal layers off ({e}); the CPU runs them")
914 }
915 }
916 }
917 Ok(tower)
918 }
919
920 #[cfg(target_os = "macos")]
922 fn device_layers(&self) -> Result<crate::gpu_metal::egemma2::VisionGpu, String> {
923 use crate::gpu_metal::egemma2::{VLayerSpec, VisionGpu};
924 fn f(m: &Mat) -> Result<&[f32], String> {
925 m.f32_weights()
926 .ok_or_else(|| "a quantized matrix (low-memory mode)".to_string())
927 }
928 let specs = self
929 .layers
930 .iter()
931 .map(|l| {
932 Ok(VLayerSpec {
933 q: f(&l.q)?,
934 k: f(&l.k)?,
935 v: f(&l.v)?,
936 o: f(&l.o)?,
937 gate: f(&l.gate)?,
938 up: f(&l.up)?,
939 down: f(&l.down)?,
940 q_norm: &l.q_norm,
941 k_norm: &l.k_norm,
942 in_norm: &l.in_norm,
943 post_attn_norm: &l.post_attn_norm,
944 pre_ff_norm: &l.pre_ff_norm,
945 post_ff_norm: &l.post_ff_norm,
946 })
947 })
948 .collect::<Result<Vec<_>, String>>()?;
949 VisionGpu::new(
950 &specs,
951 self.hidden,
952 self.heads,
953 self.head_dim,
954 crate::egemma2::EPS as f32,
955 )
956 }
957
958 pub fn force_host(&self, on: bool) {
960 self.host_only
961 .store(on, std::sync::atomic::Ordering::Relaxed);
962 }
963
964 pub fn on_device(&self) -> bool {
966 #[cfg(target_os = "macos")]
967 let up = self.gpu.is_some();
968 #[cfg(not(target_os = "macos"))]
969 let up = false;
970 up && !self.host_only.load(std::sync::atomic::Ordering::Relaxed)
971 }
972
973 pub fn encode(&self, inputs: &[VisionInput]) -> Result<Vec<Vec<f32>>, String> {
976 let k = self.pool_k;
977 for (i, x) in inputs.iter().enumerate() {
978 if x.pw % k != 0 || x.ph % k != 0 || x.pw == 0 || x.ph == 0 {
979 return Err(format!(
980 "image {i}: a {}x{} patch grid does not pool by {k}",
981 x.pw, x.ph
982 ));
983 }
984 if x.pw > self.pos_rows || x.ph > self.pos_rows {
985 return Err(format!(
986 "image {i}: {}x{} patches exceed the {}-entry position table",
987 x.pw, x.ph, self.pos_rows
988 ));
989 }
990 }
991 let _g = self.busy.lock().unwrap_or_else(|e| e.into_inner());
992 let mut out = Vec::with_capacity(inputs.len());
993 let mut start = 0usize;
994 while start < inputs.len() {
995 let mut end = start;
996 let mut rows_n = 0usize;
997 while end < inputs.len()
998 && (end == start || rows_n + inputs[end].n_patches() <= PACK_PATCHES)
999 {
1000 rows_n += inputs[end].n_patches();
1001 end += 1;
1002 }
1003 out.extend(self.forward_packed(&inputs[start..end]));
1004 start = end;
1005 }
1006 Ok(out)
1007 }
1008
1009 fn forward_packed(&self, inputs: &[VisionInput]) -> Vec<Vec<f32>> {
1010 let t_fwd = std::time::Instant::now();
1011 let pool = self.pool.as_deref();
1012 let d = self.hidden;
1013 let mut segs = Vec::with_capacity(inputs.len());
1014 let mut off = 0usize;
1015 for x in inputs {
1016 segs.push(Seg {
1017 off,
1018 pw: x.pw,
1019 ph: x.ph,
1020 });
1021 off += x.n_patches();
1022 }
1023 let n = off;
1024 let pix: Vec<f32> = inputs
1026 .iter()
1027 .flat_map(|x| x.pixels.iter().copied())
1028 .collect();
1029 let mut h = self.input_proj.apply(&pix, n, pool);
1030 drop(pix);
1031 let (tx, ty) = self.pos.split_at(self.pos_rows * d);
1032 for s in &segs {
1033 for py in 0..s.ph {
1034 for px in 0..s.pw {
1035 let r = &mut h[(s.off + py * s.pw + px) * d..][..d];
1036 let (ex, ey) = (&tx[px * d..(px + 1) * d], &ty[py * d..(py + 1) * d]);
1037 for ((v, &a), &b) in r.iter_mut().zip(ex).zip(ey) {
1038 *v += a + b;
1039 }
1040 }
1041 }
1042 }
1043 let quarter = self.head_dim / 4;
1045 let mut cos = vec![0f32; n * 2 * quarter];
1046 let mut sin = vec![0f32; n * 2 * quarter];
1047 for s in &segs {
1048 for py in 0..s.ph {
1049 for px in 0..s.pw {
1050 let t = s.off + py * s.pw + px;
1051 for (axis, p) in [(0usize, px), (1, py)] {
1052 for (j, &f) in self.inv_freq.iter().enumerate() {
1053 let a = (p as f32 * f) as f64;
1054 cos[(t * 2 + axis) * quarter + j] = a.cos() as f32;
1055 sin[(t * 2 + axis) * quarter + j] = a.sin() as f32;
1056 }
1057 }
1058 }
1059 }
1060 }
1061 #[cfg(target_os = "macos")]
1062 let on_device = match self.gpu.as_ref().filter(|_| self.on_device()) {
1063 Some(g) => {
1064 let lens: Vec<usize> = segs.iter().map(|s| s.pw * s.ph).collect();
1065 match g.forward(&mut h, &lens, &cos, &sin) {
1066 Ok(()) => true,
1067 Err(e) => {
1068 tracing::warn!(
1069 "egemma2 vision: Metal layers failed ({e}); the CPU runs them"
1070 );
1071 false
1072 }
1073 }
1074 }
1075 None => false,
1076 };
1077 #[cfg(not(target_os = "macos"))]
1078 let on_device = false;
1079 for l in self.layers.iter().filter(|_| !on_device) {
1080 let t0 = std::time::Instant::now();
1081 let a = rms_rows(&h, Some(&l.in_norm), d, pool);
1082 vprof::add(vprof::NORMS, t0);
1083 let attn = self.attention(l, &a, n, &segs, &cos, &sin, pool);
1084 let t0 = std::time::Instant::now();
1085 add_normed(&mut h, &attn, &l.post_attn_norm, d, pool);
1086 let m = rms_rows(&h, Some(&l.pre_ff_norm), d, pool);
1087 vprof::add(vprof::NORMS, t0);
1088 let t0 = std::time::Instant::now();
1089 let mut g = l.gate.apply(&m, n, pool);
1090 let u = l.up.apply(&m, n, pool);
1091 vprof::add(vprof::MLP, t0);
1092 let t0 = std::time::Instant::now();
1093 gelu_mul(&mut g, &u, pool);
1094 drop(u);
1095 vprof::add(vprof::GELU, t0);
1096 let t0 = std::time::Instant::now();
1097 let f = l.down.apply(&g, n, pool);
1098 vprof::add(vprof::MLP, t0);
1099 let t0 = std::time::Instant::now();
1100 add_normed(&mut h, &f, &l.post_ff_norm, d, pool);
1101 vprof::add(vprof::NORMS, t0);
1102 }
1103 let k = self.pool_k;
1105 let root = (d as f32).sqrt();
1106 let inv_k2 = 1.0f32 / (k * k) as f32;
1107 if vprof::on() {
1108 eprintln!(
1109 "egemma2 vision: {n} patches / {} images in {:.1} ms — {}",
1110 segs.len(),
1111 t_fwd.elapsed().as_secs_f64() * 1e3,
1112 vprof::take()
1113 );
1114 }
1115 segs.iter()
1116 .map(|s| {
1117 let (cw, ch) = (s.pw / k, s.ph / k);
1118 let mut pooled = vec![0f32; cw * ch * d];
1119 for py in 0..s.ph {
1120 for px in 0..s.pw {
1121 let cell = (py / k) * cw + px / k;
1122 let src = &h[(s.off + py * s.pw + px) * d..][..d];
1123 for (o, &v) in pooled[cell * d..(cell + 1) * d].iter_mut().zip(src) {
1124 *o += v * inv_k2;
1125 }
1126 }
1127 }
1128 for v in pooled.iter_mut() {
1129 *v *= root;
1130 }
1131 let normed = rms_rows(&pooled, None, d, pool);
1132 self.embed_proj.apply(&normed, cw * ch, pool)
1133 })
1134 .collect()
1135 }
1136
1137 #[allow(clippy::too_many_arguments)]
1139 fn attention(
1140 &self,
1141 l: &VLayer,
1142 a: &[f32],
1143 n: usize,
1144 segs: &[Seg],
1145 cos: &[f32],
1146 sin: &[f32],
1147 pool: Option<&Pool>,
1148 ) -> Vec<f32> {
1149 let (hd, nh) = (self.head_dim, self.heads);
1150 let w = nh * hd;
1151 let t0 = std::time::Instant::now();
1152 let mut q = l.q.apply(a, n, pool);
1153 let mut k = l.k.apply(a, n, pool);
1154 let mut v = l.v.apply(a, n, pool);
1155 vprof::add(vprof::QKV, t0);
1156 let t0 = std::time::Instant::now();
1157 let quarter = hd / 4;
1158 {
1159 let (qp, kp, vp) = (
1160 Shared(q.as_mut_ptr()),
1161 Shared(k.as_mut_ptr()),
1162 Shared(v.as_mut_ptr()),
1163 );
1164 rows(pool, n, &|s, e| {
1165 let mut tmp = vec![0f32; hd];
1166 for t in s..e {
1167 let c = &cos[t * 2 * quarter..(t + 1) * 2 * quarter];
1168 let sn = &sin[t * 2 * quarter..(t + 1) * 2 * quarter];
1169 let rope = |x: &mut [f32]| {
1170 for axis in 0..2 {
1172 let part = &mut x[axis * 2 * quarter..(axis + 1) * 2 * quarter];
1173 let (cc, ss) = (
1174 &c[axis * quarter..(axis + 1) * quarter],
1175 &sn[axis * quarter..(axis + 1) * quarter],
1176 );
1177 for i in 0..quarter {
1178 let (x1, x2) = (part[i], part[i + quarter]);
1179 part[i] = x1 * cc[i] - x2 * ss[i];
1180 part[i + quarter] = x2 * cc[i] + x1 * ss[i];
1181 }
1182 }
1183 };
1184 let qr = unsafe { qp.at(t * w, w) };
1185 for hh in qr.chunks_exact_mut(hd) {
1186 rms_into(hh, Some(&l.q_norm), &mut tmp);
1187 hh.copy_from_slice(&tmp);
1188 rope(hh);
1189 }
1190 let kr = unsafe { kp.at(t * w, w) };
1191 for hh in kr.chunks_exact_mut(hd) {
1192 rms_into(hh, Some(&l.k_norm), &mut tmp);
1193 hh.copy_from_slice(&tmp);
1194 rope(hh);
1195 }
1196 let vr = unsafe { vp.at(t * w, w) };
1197 for hh in vr.chunks_exact_mut(hd) {
1198 rms_into(hh, None, &mut tmp);
1199 hh.copy_from_slice(&tmp);
1200 }
1201 }
1202 });
1203 }
1204 vprof::add(vprof::NORM_ROPE, t0);
1205 let t0 = std::time::Instant::now();
1206 let mut out = vec![0f32; n * w];
1207 for s in segs {
1208 let len = s.pw * s.ph;
1209 let rng = s.off * w..(s.off + len) * w;
1210 if len >= self.gpu_min_patches
1211 && self.attend_gpu(
1212 &q[rng.clone()],
1213 &k[rng.clone()],
1214 &v[rng.clone()],
1215 len,
1216 &mut out[rng.clone()],
1217 )
1218 {
1219 continue;
1220 }
1221 attend_host(
1222 &q[rng.clone()],
1223 &k[rng.clone()],
1224 &v[rng.clone()],
1225 len,
1226 nh,
1227 hd,
1228 &mut out[rng],
1229 pool,
1230 );
1231 }
1232 vprof::add(vprof::CORE, t0);
1233 let t0 = std::time::Instant::now();
1234 let r = l.o.apply(&out, n, pool);
1235 vprof::add(vprof::O, t0);
1236 r
1237 }
1238
1239 fn attend_gpu(&self, q: &[f32], k: &[f32], v: &[f32], len: usize, out: &mut [f32]) -> bool {
1242 if !crate::gpu::enabled_here() {
1243 return false;
1244 }
1245 let (hd, nh) = (self.head_dim, self.heads);
1246 let w = nh * hd;
1247 let pack = |x: &[f32]| {
1248 let mut p = vec![0f32; len * w];
1249 for t in 0..len {
1250 for hh in 0..nh {
1251 p[hh * len * hd + t * hd..][..hd].copy_from_slice(&x[t * w + hh * hd..][..hd]);
1252 }
1253 }
1254 p
1255 };
1256 let (qh, kh, vh) = (pack(q), pack(k), pack(v));
1257 crate::gpu::dit_attention(&qh, &kh, &vh, nh, nh, len, hd, 1.0, out)
1258 }
1259}
1260
1261#[allow(clippy::too_many_arguments)]
1265fn sgemm_view(
1266 a: &[f32],
1267 lda: usize,
1268 b: &[f32],
1269 ldb: usize,
1270 bt: bool,
1271 c: &mut [f32],
1272 ldc: usize,
1273 m: usize,
1274 n: usize,
1275 k: usize,
1276) {
1277 #[cfg(target_os = "macos")]
1278 if crate::egemma2::accelerate() {
1279 #[link(name = "Accelerate", kind = "framework")]
1280 unsafe extern "C" {
1281 fn cblas_sgemm(
1282 order: i32,
1283 ta: i32,
1284 tb: i32,
1285 m: i32,
1286 n: i32,
1287 k: i32,
1288 alpha: f32,
1289 a: *const f32,
1290 lda: i32,
1291 b: *const f32,
1292 ldb: i32,
1293 beta: f32,
1294 c: *mut f32,
1295 ldc: i32,
1296 );
1297 }
1298 assert!(a.len() >= (m - 1) * lda + k);
1299 assert!(if bt {
1300 b.len() >= (n - 1) * ldb + k
1301 } else {
1302 b.len() >= (k - 1) * ldb + n
1303 });
1304 assert!(c.len() >= (m - 1) * ldc + n);
1305 unsafe {
1307 cblas_sgemm(
1308 101,
1309 111,
1310 if bt { 112 } else { 111 },
1311 m as i32,
1312 n as i32,
1313 k as i32,
1314 1.0,
1315 a.as_ptr(),
1316 lda as i32,
1317 b.as_ptr(),
1318 ldb as i32,
1319 0.0,
1320 c.as_mut_ptr(),
1321 ldc as i32,
1322 );
1323 }
1324 return;
1325 }
1326 sgemm_view_portable(a, lda, b, ldb, bt, c, ldc, m, n, k);
1327}
1328
1329#[allow(clippy::too_many_arguments)]
1332fn sgemm_view_portable(
1333 a: &[f32],
1334 lda: usize,
1335 b: &[f32],
1336 ldb: usize,
1337 bt: bool,
1338 c: &mut [f32],
1339 ldc: usize,
1340 m: usize,
1341 n: usize,
1342 k: usize,
1343) {
1344 if m == 0 || n == 0 {
1345 return;
1346 }
1347 assert!(a.len() >= (m - 1) * lda + k && c.len() >= (m - 1) * ldc + n);
1348 let (rsb, csb) = if bt {
1350 assert!(b.len() >= (n - 1) * ldb + k);
1351 (1isize, ldb as isize)
1352 } else {
1353 assert!(k == 0 || b.len() >= (k - 1) * ldb + n);
1354 (ldb as isize, 1isize)
1355 };
1356 unsafe {
1358 matrixmultiply::sgemm(
1359 m,
1360 k,
1361 n,
1362 1.0,
1363 a.as_ptr(),
1364 lda as isize,
1365 1,
1366 b.as_ptr(),
1367 rsb,
1368 csb,
1369 0.0,
1370 c.as_mut_ptr(),
1371 ldc as isize,
1372 1,
1373 );
1374 }
1375}
1376
1377#[allow(clippy::too_many_arguments)]
1388fn attend_host(
1389 q: &[f32],
1390 k: &[f32],
1391 v: &[f32],
1392 len: usize,
1393 nh: usize,
1394 hd: usize,
1395 out: &mut [f32],
1396 pool: Option<&Pool>,
1397) {
1398 if pool.is_some() && !crate::egemma2::accelerate() {
1399 return attend_host_items(q, k, v, len, nh, hd, out, pool);
1400 }
1401 let w = nh * hd;
1402 let qblock = (SCORE_BLOCK / len).clamp(QBLOCK_MIN, len.max(1));
1403 let nblk = len.div_ceil(qblock);
1404 let steps = nh * nblk;
1405 let step = |t: usize| {
1407 let (h, b) = (t / nblk, t % nblk);
1408 let i0 = b * qblock;
1409 (h, i0, (i0 + qblock).min(len) - i0)
1410 };
1411 let mut bufs: [Vec<f32>; 3] = std::array::from_fn(|_| vec![0f32; qblock * len]);
1412 let scores = |t: usize, sc: &mut [f32]| {
1413 let (h, i0, nb) = step(t);
1414 sgemm_view(
1415 &q[i0 * w + h * hd..],
1416 w,
1417 &k[h * hd..],
1418 w,
1419 true,
1420 &mut sc[..nb * len],
1421 len,
1422 nb,
1423 len,
1424 hd,
1425 );
1426 };
1427 let outp = Shared(out.as_mut_ptr());
1428 let out_len = out.len();
1429 let mix = |t: usize, sc: &[f32]| {
1430 let (h, i0, nb) = step(t);
1431 let o = unsafe { outp.at(0, out_len) };
1433 sgemm_view(
1434 &sc[..nb * len],
1435 len,
1436 &v[h * hd..],
1437 w,
1438 false,
1439 &mut o[i0 * w + h * hd..],
1440 w,
1441 nb,
1442 hd,
1443 len,
1444 );
1445 };
1446 let soft = |t: usize, sc: &mut [f32]| {
1447 let (_, _, nb) = step(t);
1448 let sp = Shared(sc.as_mut_ptr());
1449 rows(pool, nb, &|s, e| {
1450 for i in s..e {
1451 softmax(unsafe { sp.at(i * len, len) });
1452 }
1453 });
1454 };
1455 if pool.is_none() || steps < 2 {
1456 let sc = &mut bufs[0];
1457 for t in 0..steps {
1458 scores(t, sc);
1459 soft(t, sc);
1460 mix(t, sc);
1461 }
1462 return;
1463 }
1464 scores(0, &mut bufs[0]);
1465 for t in 0..steps {
1466 let [b0, b1, b2] = &mut bufs;
1469 let (cur, next, prev) = match t % 3 {
1470 0 => (b0, b1, b2),
1471 1 => (b1, b2, b0),
1472 _ => (b2, b0, b1),
1473 };
1474 std::thread::scope(|sc| {
1475 sc.spawn(|| {
1476 if t + 1 < steps {
1477 scores(t + 1, next);
1478 }
1479 if t >= 1 {
1480 mix(t - 1, prev);
1481 }
1482 });
1483 soft(t, cur);
1484 });
1485 }
1486 mix(steps - 1, &bufs[(steps - 1) % 3]);
1487}
1488
1489#[allow(clippy::too_many_arguments)]
1496fn attend_host_items(
1497 q: &[f32],
1498 k: &[f32],
1499 v: &[f32],
1500 len: usize,
1501 nh: usize,
1502 hd: usize,
1503 out: &mut [f32],
1504 pool: Option<&Pool>,
1505) {
1506 if len == 0 {
1507 return;
1508 }
1509 let w = nh * hd;
1510 let qblock = ITEM_QBLOCK.min(len);
1511 let nblk = len.div_ceil(qblock);
1512 let outp = Shared(out.as_mut_ptr());
1513 rows(pool, nh * nblk, &|s, e| {
1514 let mut sc = vec![0f32; qblock * len];
1515 let mut ob = vec![0f32; qblock * hd];
1516 for t in s..e {
1517 let (h, i0) = (t / nblk, (t % nblk) * qblock);
1518 let nb = (i0 + qblock).min(len) - i0;
1519 let sc = &mut sc[..nb * len];
1520 sgemm_view(
1521 &q[i0 * w + h * hd..],
1522 w,
1523 &k[h * hd..],
1524 w,
1525 true,
1526 sc,
1527 len,
1528 nb,
1529 len,
1530 hd,
1531 );
1532 for row in sc.chunks_exact_mut(len) {
1533 softmax(row);
1534 }
1535 sgemm_view(sc, len, &v[h * hd..], w, false, &mut ob, hd, nb, hd, len);
1536 for (i, src) in ob[..nb * hd].chunks_exact(hd).enumerate() {
1537 unsafe { outp.at((i0 + i) * w + h * hd, hd) }.copy_from_slice(src);
1539 }
1540 }
1541 });
1542}
1543
1544#[cfg(test)]
1545mod tests {
1546 use super::*;
1547
1548 #[test]
1549 fn target_sizes_match_the_processor() {
1550 let cases = [
1552 ((512, 512, 280), (768, 768)),
1553 ((756, 1980, 280), (480, 1296)),
1554 ((941, 530, 280), (1056, 576)),
1555 ((48, 64, 280), (672, 912)),
1556 ((512, 512, 70), (384, 384)),
1557 ((512, 512, 140), (528, 528)),
1558 ((512, 512, 560), (1104, 1104)),
1559 ((512, 512, 1120), (1584, 1584)),
1560 ((360, 640, 140), (384, 720)),
1561 ];
1562 for ((h, w, b), want) in cases {
1563 assert_eq!(
1564 target_size(h, w, 16, b * 9, 3).unwrap(),
1565 want,
1566 "{w}x{h} @ {b}"
1567 );
1568 }
1569 let (h, w) = target_size(10, 10_000, 16, 280 * 9, 3).unwrap();
1571 assert_eq!(h, 48);
1572 assert!(w <= 280 * 48 && w % 48 == 0);
1573 }
1574
1575 #[test]
1576 fn resize_identity_and_constant() {
1577 let img = RgbFrame::new(4, 3, (0..36).map(|i| (i * 7) as u8).collect()).unwrap();
1578 assert_eq!(resize_bicubic_aa(&img, 3, 4), img);
1579 let c = RgbFrame::new(37, 23, vec![131; 37 * 23 * 3]).unwrap();
1581 for (h, w) in [(48, 96), (10, 7), (100, 3)] {
1582 let r = resize_bicubic_aa(&c, h, w);
1583 assert!(r.data.iter().all(|&v| v == 131), "{w}x{h}");
1584 }
1585 }
1586
1587 #[test]
1588 fn patchify_orders_rows_then_columns() {
1589 let mut data = vec![0u8; 32 * 16 * 3];
1591 for y in 0..16 {
1592 for x in 0..32 {
1593 data[(y * 32 + x) * 3] = if x < 16 { 0 } else { 255 };
1594 }
1595 }
1596 let p = patchify(&RgbFrame::new(32, 16, data).unwrap(), 16);
1597 assert_eq!((p.pw, p.ph), (2, 1));
1598 assert_eq!(p.pixels[0], -1.0);
1599 assert_eq!(p.pixels[768], 1.0);
1600 assert_eq!(p.pixels[1], -1.0); }
1602
1603 #[test]
1604 fn frames_sample_at_one_fps() {
1605 let p = VisionProcessor::default();
1606 assert_eq!(p.sample_indices(115, Some(25.0)), vec![0, 25, 50, 75]);
1608 assert_eq!(p.sample_indices(10, Some(25.0)), vec![0]);
1610 let all = p.sample_indices(100, None);
1612 assert_eq!(all.len(), 32);
1613 assert_eq!(all[0], 0);
1614 assert_eq!(*all.last().unwrap(), 99);
1615 assert_eq!(all[1], 3);
1617 assert_eq!(all[2], 6);
1618 let long = p.sample_indices(30 * 100, Some(30.0));
1620 assert_eq!(long.len(), 32);
1621 assert_eq!(long[0], 0);
1622 assert_eq!(*long.last().unwrap(), 99 * 30);
1623 }
1624
1625 #[test]
1626 fn strided_gemm_views_agree() {
1627 let (m, n, k, lda, ldb, ldc) = (5usize, 7usize, 9usize, 13usize, 11usize, 10usize);
1630 let a: Vec<f32> = (0..m * lda)
1631 .map(|i| ((i * 7 % 17) as f32 - 8.0) / 8.0)
1632 .collect();
1633 for bt in [true, false] {
1634 let blen = if bt { n * ldb } else { k * ldb };
1635 let b: Vec<f32> = (0..blen)
1636 .map(|i| ((i * 5 % 13) as f32 - 6.0) / 6.0)
1637 .collect();
1638 let mut want = vec![0f32; m * ldc];
1639 for r in 0..m {
1640 for c in 0..n {
1641 want[r * ldc + c] = (0..k)
1642 .map(|j| a[r * lda + j] * if bt { b[c * ldb + j] } else { b[j * ldb + c] })
1643 .sum();
1644 }
1645 }
1646 let mut c1 = vec![0f32; m * ldc];
1647 sgemm_view(&a, lda, &b, ldb, bt, &mut c1, ldc, m, n, k);
1648 let mut c2 = vec![0f32; m * ldc];
1649 sgemm_view_portable(&a, lda, &b, ldb, bt, &mut c2, ldc, m, n, k);
1650 for r in 0..m {
1651 for c in 0..n {
1652 let w = want[r * ldc + c];
1653 assert!((c1[r * ldc + c] - w).abs() < 1e-4, "bt={bt} accel");
1654 assert!((c2[r * ldc + c] - w).abs() < 1e-4, "bt={bt} portable");
1655 }
1656 }
1657 }
1658 }
1659
1660 #[test]
1661 fn host_attention_matches_naive() {
1662 let (len, nh, hd) = (300usize, 2usize, 8usize);
1667 let w = nh * hd;
1668 let f = |i: usize, s: usize| (((i * s) % 29) as f32 - 14.0) / 20.0;
1669 let q: Vec<f32> = (0..len * w).map(|i| f(i, 7)).collect();
1670 let k: Vec<f32> = (0..len * w).map(|i| f(i, 11)).collect();
1671 let v: Vec<f32> = (0..len * w).map(|i| f(i, 13)).collect();
1672 let mut want = vec![0f32; len * w];
1673 for h in 0..nh {
1674 for i in 0..len {
1675 let sc: Vec<f64> = (0..len)
1676 .map(|j| {
1677 (0..hd)
1678 .map(|c| (q[i * w + h * hd + c] * k[j * w + h * hd + c]) as f64)
1679 .sum()
1680 })
1681 .collect();
1682 let mx = sc.iter().cloned().fold(f64::MIN, f64::max);
1683 let e: Vec<f64> = sc.iter().map(|x| (x - mx).exp()).collect();
1684 let z: f64 = e.iter().sum();
1685 for c in 0..hd {
1686 want[i * w + h * hd + c] = (0..len)
1687 .map(|j| e[j] / z * v[j * w + h * hd + c] as f64)
1688 .sum::<f64>() as f32;
1689 }
1690 }
1691 }
1692 let pool = crate::pool::Pool::new(3);
1693 for (p, items) in [(None, false), (Some(&pool), false), (Some(&pool), true)] {
1694 let mut out = vec![0f32; len * w];
1695 if items {
1696 attend_host_items(&q, &k, &v, len, nh, hd, &mut out, p);
1697 } else {
1698 attend_host(&q, &k, &v, len, nh, hd, &mut out, p);
1699 }
1700 let err = out
1701 .iter()
1702 .zip(&want)
1703 .map(|(a, b)| (a - b).abs())
1704 .fold(0f32, f32::max);
1705 assert!(err < 1e-5, "max err {err}");
1706 }
1707 }
1708
1709 #[test]
1710 fn processor_config_is_read() {
1711 let cfg = serde_json::json!({
1712 "image_processor": {"max_soft_tokens": 560, "patch_size": 16, "pooling_kernel_size": 3},
1713 "video_processor": {"max_soft_tokens": 70, "fps": 2, "max_frames": 8, "overflow_strategy": "truncate"},
1714 });
1715 let p = VisionProcessor::from_config(&cfg);
1716 assert_eq!(p.image_budget, 560);
1717 assert_eq!(p.video_budget, 70);
1718 assert_eq!(p.video_fps, 2.0);
1719 assert_eq!(p.max_frames, 8);
1720 assert!(p.overflow_truncate);
1721 assert!(VisionProcessor::check_budget(300).is_err());
1722 }
1723}