1use crate::dit::Proj;
40use crate::media::RgbFrame;
41use crate::pool::Pool;
42use crate::tokenizer::Tokenizer;
43use cortiq_core::CmfModel;
44use serde_json::Value;
45use std::path::{Path, PathBuf};
46use std::sync::Arc;
47
48pub const VISION_START_ID: u32 = 151652;
49pub const VISION_END_ID: u32 = 151653;
50pub const IMAGE_PAD_ID: u32 = 151655;
51pub const VIDEO_PAD_ID: u32 = 151656;
52pub const AUDIO_PAD_ID: u32 = 151669;
53pub const VIDEO_START_ID: u32 = 151670;
54pub const VIDEO_END_ID: u32 = 151671;
55pub const AUDIO_START_ID: u32 = 151673;
56pub const AUDIO_END_ID: u32 = 151674;
57
58pub const MM_CONFIG_TENSOR: &str = "mm.config_json";
60
61const PATCH_EMBED: &str = "visual.patch_embed.proj.weight";
62
63static GPU_ATTENTION_DISPATCHES: std::sync::atomic::AtomicUsize =
64 std::sync::atomic::AtomicUsize::new(0);
65
66pub fn gpu_attention_dispatches() -> usize {
69 GPU_ATTENTION_DISPATCHES.load(std::sync::atomic::Ordering::Relaxed)
70}
71
72#[derive(Clone, Debug)]
77pub struct MimoProcessorConfig {
78 pub patch_size: usize,
79 pub merge_size: usize,
80 pub temporal_patch_size: usize,
81 pub image_min_pixels: usize,
82 pub image_max_pixels: usize,
83 pub video_min_pixels: usize,
84 pub video_max_pixels: usize,
85 pub video_total_max_pixels: usize,
86 pub fps: f64,
88 pub min_frames: usize,
89 pub max_frames: usize,
90 pub mean: [f32; 3],
91 pub std: [f32; 3],
92}
93
94impl Default for MimoProcessorConfig {
95 fn default() -> Self {
97 Self {
98 patch_size: 16,
99 merge_size: 2,
100 temporal_patch_size: 2,
101 image_min_pixels: 8192,
102 image_max_pixels: 8_388_608,
103 video_min_pixels: 8192,
104 video_max_pixels: 8_388_608,
105 video_total_max_pixels: 268_435_456,
106 fps: 1.0,
107 min_frames: 8,
108 max_frames: 3600,
109 mean: [123.675, 116.28, 103.53],
110 std: [58.395, 57.12, 57.375],
111 }
112 }
113}
114
115fn get_usize(v: Option<&Value>, key: &str) -> Option<usize> {
116 v?.get(key)?.as_u64().map(|n| n as usize)
117}
118
119impl MimoProcessorConfig {
120 pub fn from_config(cfg: &Value) -> Result<Self, String> {
122 let pc = cfg.get("processor_config");
123 let vc = cfg.get("vision_config");
124 let patch_size = get_usize(pc, "patch_size")
125 .or_else(|| get_usize(vc, "patch_size"))
126 .unwrap_or(16);
127 let merge_size = get_usize(pc, "merge_size")
128 .or_else(|| get_usize(vc, "spatial_merge_size"))
129 .unwrap_or(2);
130 let temporal_patch_size = get_usize(pc, "temporal_patch_size")
131 .or_else(|| get_usize(vc, "temporal_patch_size"))
132 .unwrap_or(2);
133 if let Some(r) = get_usize(pc, "temporal_compression_ratio") {
134 if r != 1 {
135 return Err(format!(
136 "temporal_compression_ratio {r} is not supported (MiMo pins 1)"
137 ));
138 }
139 }
140 let unit = patch_size * merge_size;
141 let nz = |k: &str, d: usize| get_usize(pc, k).filter(|&v| v > 0).unwrap_or(d);
143 let fps = pc
144 .and_then(|p| p.get("fps"))
145 .and_then(Value::as_f64)
146 .filter(|&v| v > 0.0)
147 .unwrap_or(2.0);
148 let out = Self {
149 patch_size,
150 merge_size,
151 temporal_patch_size,
152 image_min_pixels: nz("image_min_pixels", 4 * unit * unit),
153 image_max_pixels: nz("image_max_pixels", 4096 * unit * unit),
154 video_min_pixels: nz("video_min_pixels", 4 * unit * unit),
155 video_max_pixels: nz("video_max_pixels", 4096 * unit * unit),
156 video_total_max_pixels: nz("video_total_max_pixels", 16384 * unit * unit),
157 fps,
158 min_frames: nz("min_frames", 8),
159 max_frames: nz("max_frames", 256),
160 ..Self::default()
161 };
162 if out.patch_size == 0 || out.merge_size == 0 || out.temporal_patch_size == 0 {
163 return Err("processor patch/merge/temporal sizes must be positive".into());
164 }
165 Ok(out)
166 }
167
168 pub fn factor(&self) -> usize {
170 self.patch_size * self.merge_size
171 }
172
173 pub fn patch_dim(&self) -> usize {
174 3 * self.temporal_patch_size * self.patch_size * self.patch_size
175 }
176}
177
178fn py_round(x: f64) -> f64 {
180 let f = x.floor();
181 let d = x - f;
182 if d > 0.5 {
183 f + 1.0
184 } else if d < 0.5 {
185 f
186 } else if f % 2.0 == 0.0 {
187 f
188 } else {
189 f + 1.0
190 }
191}
192
193pub fn smart_resize(
197 height: usize,
198 width: usize,
199 factor: usize,
200 min_pixels: usize,
201 max_pixels: usize,
202) -> Result<(usize, usize), String> {
203 if height == 0 || width == 0 || factor == 0 {
204 return Err(format!(
205 "smart_resize needs positive sizes, got {height}x{width} factor {factor}"
206 ));
207 }
208 let (mut h, mut w) = (height, width);
209 let short = h.min(w);
210 if short < factor {
211 let scale = factor as f64 / short as f64;
212 h = py_round(h as f64 * scale) as usize;
213 w = py_round(w as f64 * scale) as usize;
214 } else {
215 let aspect = h.max(w) as f64 / h.min(w) as f64;
216 if aspect > 200.0 {
217 return Err(format!(
218 "absolute aspect ratio must be smaller than 200, got {aspect}"
219 ));
220 }
221 }
222 let f = factor as f64;
223 let mut hb = py_round(h as f64 / f) as usize * factor;
224 let mut wb = py_round(w as f64 / f) as usize * factor;
225 let area = (h as f64) * (w as f64);
226 if hb * wb > max_pixels {
227 let beta = (area / max_pixels as f64).sqrt();
228 hb = (h as f64 / beta / f).floor() as usize * factor;
229 wb = (w as f64 / beta / f).floor() as usize * factor;
230 } else if hb * wb < min_pixels {
231 let beta = (min_pixels as f64 / area).sqrt();
232 hb = (h as f64 * beta / f).ceil() as usize * factor;
233 wb = (w as f64 * beta / f).ceil() as usize * factor;
234 }
235 if hb == 0 || wb == 0 {
236 return Err(format!(
237 "smart_resize of {height}x{width} collapsed to {hb}x{wb} (pixel bounds {min_pixels}..{max_pixels})"
238 ));
239 }
240 Ok((hb, wb))
241}
242
243fn linear_taps(in_size: usize, out_size: usize) -> Vec<(usize, usize, f32, f32)> {
246 let scale = in_size as f32 / out_size as f32;
247 (0..out_size)
248 .map(|i| {
249 let real = scale * (i as f32 + 0.5) - 0.5;
250 let real = if real < 0.0 { 0.0 } else { real };
251 let i0 = (real.floor() as usize).min(in_size - 1);
252 let lambda = (real - i0 as f32).clamp(0.0, 1.0);
253 let i1 = if i0 < in_size - 1 { i0 + 1 } else { i0 };
254 (i0, i1, 1.0 - lambda, lambda)
255 })
256 .collect()
257}
258
259pub fn resize_bilinear(
264 src: &[f32],
265 channels: usize,
266 in_h: usize,
267 in_w: usize,
268 out_h: usize,
269 out_w: usize,
270) -> Vec<f32> {
271 assert_eq!(
272 src.len(),
273 channels * in_h * in_w,
274 "resize_bilinear: bad input size"
275 );
276 let ty = linear_taps(in_h, out_h);
277 let tx = linear_taps(in_w, out_w);
278 let mut out = vec![0f32; channels * out_h * out_w];
279 for c in 0..channels {
280 let plane = &src[c * in_h * in_w..(c + 1) * in_h * in_w];
281 let dst = &mut out[c * out_h * out_w..(c + 1) * out_h * out_w];
282 for (y, &(y0, y1, wy0, wy1)) in ty.iter().enumerate() {
283 let r0 = &plane[y0 * in_w..(y0 + 1) * in_w];
284 let r1 = &plane[y1 * in_w..(y1 + 1) * in_w];
285 let row = &mut dst[y * out_w..(y + 1) * out_w];
286 for (x, &(x0, x1, wx0, wx1)) in tx.iter().enumerate() {
287 let t0 = r0[x0] * wx0 + r0[x1] * wx1;
288 let t1 = r1[x0] * wx0 + r1[x1] * wx1;
289 row[x] = t0 * wy0 + t1 * wy1;
290 }
291 }
292 }
293 out
294}
295
296fn frame_to_chw(frame: &RgbFrame) -> Vec<f32> {
298 let n = frame.width * frame.height;
299 let mut out = vec![0f32; 3 * n];
300 for (i, px) in frame.data.chunks_exact(3).enumerate() {
301 out[i] = px[0] as f32;
302 out[n + i] = px[1] as f32;
303 out[2 * n + i] = px[2] as f32;
304 }
305 out
306}
307
308fn resize_normalize(frame: &RgbFrame, hb: usize, wb: usize, cfg: &MimoProcessorConfig) -> Vec<f32> {
310 let chw = frame_to_chw(frame);
311 let mut r = resize_bilinear(&chw, 3, frame.height, frame.width, hb, wb);
312 let plane = hb * wb;
313 for c in 0..3 {
314 let (m, s) = (cfg.mean[c], cfg.std[c]);
315 for v in &mut r[c * plane..(c + 1) * plane] {
316 *v = (*v - m) / s;
317 }
318 }
319 r
320}
321
322#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324pub enum VisualKind {
325 Image,
326 Video,
327}
328
329#[derive(Clone, Debug)]
332pub struct VisualInput {
333 pub kind: VisualKind,
334 pub rows: Vec<f32>,
336 pub grid_t: usize,
337 pub grid_h: usize,
338 pub grid_w: usize,
339 pub patch_dim: usize,
340 pub merge_size: usize,
341 pub timestamps: Vec<f32>,
343 pub frame_indices: Vec<usize>,
345 pub resized: (usize, usize),
347}
348
349impl VisualInput {
350 pub fn patches(&self) -> usize {
352 self.grid_t * self.grid_h * self.grid_w
353 }
354
355 pub fn tokens_per_step(&self) -> usize {
357 self.grid_h * self.grid_w / (self.merge_size * self.merge_size)
358 }
359
360 pub fn tokens(&self) -> usize {
362 self.grid_t * self.tokens_per_step()
363 }
364
365 pub fn timestamp_labels(&self) -> Vec<String> {
368 (0..self.grid_t)
369 .filter_map(|t| self.timestamps.get(2 * t).map(|&ts| format_timestamp(ts)))
370 .collect()
371 }
372}
373
374fn patchify(
377 frames: &[&[f32]],
378 hb: usize,
379 wb: usize,
380 cfg: &MimoProcessorConfig,
381) -> Result<(Vec<f32>, usize, usize, usize), String> {
382 let (p, m, tp) = (cfg.patch_size, cfg.merge_size, cfg.temporal_patch_size);
383 if frames.is_empty() || frames.len() % tp != 0 {
384 return Err(format!(
385 "{} frames is not a positive multiple of temporal_patch_size {tp}",
386 frames.len()
387 ));
388 }
389 if hb % (p * m) != 0 || wb % (p * m) != 0 {
390 return Err(format!("frame {hb}x{wb} is not a multiple of {}", p * m));
391 }
392 let (gt, gh, gw) = (frames.len() / tp, hb / p, wb / p);
393 let dim = cfg.patch_dim();
394 let mut rows = Vec::with_capacity(gt * gh * gw * dim);
395 let plane = hb * wb;
396 for t in 0..gt {
397 for a in 0..gh / m {
398 for b in 0..gw / m {
399 for mh in 0..m {
400 for mw in 0..m {
401 let y0 = (a * m + mh) * p;
402 let x0 = (b * m + mw) * p;
403 for c in 0..3 {
404 for tt in 0..tp {
405 let f = frames[t * tp + tt];
406 for py in 0..p {
407 let base = c * plane + (y0 + py) * wb + x0;
408 rows.extend_from_slice(&f[base..base + p]);
409 }
410 }
411 }
412 }
413 }
414 }
415 }
416 }
417 Ok((rows, gt, gh, gw))
418}
419
420pub fn prepare_image(
424 frame: &RgbFrame,
425 cfg: &MimoProcessorConfig,
426 max_pixels: Option<usize>,
427) -> Result<VisualInput, String> {
428 let max_px = max_pixels.unwrap_or(cfg.image_max_pixels);
429 let (hb, wb) = smart_resize(
430 frame.height,
431 frame.width,
432 cfg.factor(),
433 cfg.image_min_pixels,
434 max_px,
435 )?;
436 let norm = resize_normalize(frame, hb, wb, cfg);
437 let frames: Vec<&[f32]> = (0..cfg.temporal_patch_size)
438 .map(|_| norm.as_slice())
439 .collect();
440 let (rows, grid_t, grid_h, grid_w) = patchify(&frames, hb, wb, cfg)?;
441 Ok(VisualInput {
442 kind: VisualKind::Image,
443 rows,
444 grid_t,
445 grid_h,
446 grid_w,
447 patch_dim: cfg.patch_dim(),
448 merge_size: cfg.merge_size,
449 timestamps: Vec::new(),
450 frame_indices: Vec::new(),
451 resized: (hb, wb),
452 })
453}
454
455pub fn smart_nframes(
461 total: usize,
462 video_fps: f64,
463 cfg: &MimoProcessorConfig,
464) -> Result<usize, String> {
465 const FRAME_FACTOR: f64 = 2.0;
466 if !(video_fps > 0.0) || !video_fps.is_finite() {
467 return Err(format!("video fps must be positive, got {video_fps}"));
468 }
469 let min_frames = (cfg.min_frames as f64 / FRAME_FACTOR).ceil() * FRAME_FACTOR;
470 let max_frames = (cfg.max_frames as f64 / FRAME_FACTOR).floor() * FRAME_FACTOR;
471 let n = total as f64 / video_fps * cfg.fps;
472 let n = n.max(min_frames).min(max_frames).min(total as f64);
473 let n = (n / FRAME_FACTOR).floor() * FRAME_FACTOR;
474 if !(FRAME_FACTOR <= n && n <= total as f64) {
475 return Err(format!(
476 "nframes should in interval [2, {total}], but got {n} (video has {total} frames)"
477 ));
478 }
479 Ok(n as usize)
480}
481
482pub fn sample_frames(
485 total: usize,
486 video_fps: f64,
487 cfg: &MimoProcessorConfig,
488) -> Result<(Vec<usize>, Vec<f32>), String> {
489 let n = smart_nframes(total, video_fps, cfg)?;
490 let stop = (total - 1) as f64;
491 let step = stop / (n - 1) as f64;
492 let mut idx: Vec<usize> = (0..n)
493 .map(|i| {
494 if i == n - 1 {
495 stop
496 } else {
497 (i as f64 * step).floor()
498 }
499 })
500 .map(|v| v as usize)
501 .collect();
502 idx.dedup();
503 let fps32 = video_fps as f32;
504 let ts = idx.iter().map(|&i| i as f32 / fps32).collect();
505 Ok((idx, ts))
506}
507
508pub fn video_max_pixels(n_sampled: usize, cfg: &MimoProcessorConfig) -> usize {
511 let per_frame = cfg.video_total_max_pixels * cfg.temporal_patch_size / n_sampled.max(1);
512 cfg.video_min_pixels
513 .max(per_frame.min(cfg.video_max_pixels))
514}
515
516pub fn format_timestamp(ts: f32) -> String {
519 let rem = ts % 60.0;
520 let minutes = ((ts - rem) / 60.0).floor();
521 format!("{:02}:{:02}", minutes as i64, rem as i64)
522}
523
524#[derive(Clone, Debug)]
526pub struct Y4mInfo {
527 pub width: usize,
528 pub height: usize,
529 pub fps: f64,
530 chroma: Chroma,
531 offsets: Vec<u64>,
533}
534
535#[derive(Clone, Copy, Debug, PartialEq, Eq)]
536enum Chroma {
537 C420,
538 C422,
539 C444,
540 Mono,
541}
542
543impl Chroma {
544 fn plane_dims(self, w: usize, h: usize) -> (usize, usize) {
545 match self {
546 Chroma::C420 => (w.div_ceil(2), h.div_ceil(2)),
547 Chroma::C422 => (w.div_ceil(2), h),
548 Chroma::C444 => (w, h),
549 Chroma::Mono => (0, 0),
550 }
551 }
552}
553
554#[derive(Clone, Debug)]
558pub enum VideoSource {
559 FrameDir { frames: Vec<PathBuf>, fps: f64 },
560 Y4m { path: PathBuf, info: Y4mInfo },
561}
562
563const FRAME_EXTS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif", "ppm"];
564
565fn natural_key(s: &str) -> Vec<(u8, u128, String)> {
567 let mut out = Vec::new();
568 let mut chars = s.chars().peekable();
569 while let Some(&c) = chars.peek() {
570 let mut run = String::new();
571 if c.is_ascii_digit() {
572 while let Some(&d) = chars.peek().filter(|d| d.is_ascii_digit()) {
573 run.push(d);
574 chars.next();
575 }
576 let v = run.parse::<u128>().unwrap_or(u128::MAX);
577 out.push((0, v, run));
578 } else {
579 while let Some(&d) = chars.peek().filter(|d| !d.is_ascii_digit()) {
580 run.push(d);
581 chars.next();
582 }
583 out.push((1, 0, run));
584 }
585 }
586 out
587}
588
589impl VideoSource {
590 pub fn frame_dir(dir: &Path, fps: f64) -> Result<Self, String> {
593 if !(fps > 0.0) || !fps.is_finite() {
594 return Err(format!(
595 "frame directory needs a positive --video-fps, got {fps}"
596 ));
597 }
598 let mut frames: Vec<PathBuf> = std::fs::read_dir(dir)
599 .map_err(|e| format!("{}: {e}", dir.display()))?
600 .filter_map(|e| e.ok().map(|e| e.path()))
601 .filter(|p| {
602 p.is_file()
603 && p.extension()
604 .and_then(|e| e.to_str())
605 .is_some_and(|e| FRAME_EXTS.contains(&e.to_ascii_lowercase().as_str()))
606 })
607 .collect();
608 frames.sort_by_cached_key(|p| {
609 natural_key(&p.file_name().unwrap_or_default().to_string_lossy())
610 });
611 if frames.is_empty() {
612 return Err(format!("{}: no image frames found", dir.display()));
613 }
614 Ok(VideoSource::FrameDir { frames, fps })
615 }
616
617 pub fn y4m(path: &Path) -> Result<Self, String> {
619 use std::io::{BufRead, BufReader, Seek, SeekFrom};
620 let file = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
621 let file_len = file.metadata().map_err(|e| e.to_string())?.len();
622 let mut rd = BufReader::new(file);
623 let mut line = Vec::new();
624 rd.read_until(b'\n', &mut line).map_err(|e| e.to_string())?;
625 let header = std::str::from_utf8(&line)
626 .map_err(|_| "Y4M header is not text".to_string())?
627 .trim_end();
628 let mut parts = header.split(' ');
629 if parts.next() != Some("YUV4MPEG2") {
630 return Err(format!("{}: not a YUV4MPEG2 stream", path.display()));
631 }
632 let (mut w, mut h, mut fps, mut chroma) = (0usize, 0usize, 0f64, Chroma::C420);
633 for p in parts {
634 let (tag, val) = p.split_at(1.min(p.len()));
635 match tag {
636 "W" => w = val.parse().map_err(|_| format!("bad Y4M width '{val}'"))?,
637 "H" => h = val.parse().map_err(|_| format!("bad Y4M height '{val}'"))?,
638 "F" => {
639 let (n, d) = val
640 .split_once(':')
641 .ok_or_else(|| format!("bad Y4M rate '{val}'"))?;
642 let n: f64 = n.parse().map_err(|_| format!("bad Y4M rate '{val}'"))?;
643 let d: f64 = d.parse().map_err(|_| format!("bad Y4M rate '{val}'"))?;
644 fps = n / d;
645 }
646 "C" => {
647 chroma = match val {
648 "420" | "420jpeg" | "420paldv" | "420mpeg2" => Chroma::C420,
649 "422" => Chroma::C422,
650 "444" => Chroma::C444,
651 "mono" => Chroma::Mono,
652 other => {
653 return Err(format!(
654 "Y4M colorspace C{other} is not supported (8-bit 420/422/444/mono)"
655 ));
656 }
657 }
658 }
659 "I" => {
660 if val != "p" && val != "?" {
661 return Err(format!("interlaced Y4M (I{val}) is not supported"));
662 }
663 }
664 _ => {}
665 }
666 }
667 if w == 0 || h == 0 || !(fps > 0.0) || !fps.is_finite() {
668 return Err(format!("{}: Y4M header lacks W/H/F", path.display()));
669 }
670 let (cw, ch) = chroma.plane_dims(w, h);
671 let payload = (w * h + 2 * cw * ch) as u64;
672 let mut offsets = Vec::new();
673 let mut pos = line.len() as u64;
674 loop {
675 line.clear();
676 let got = rd.read_until(b'\n', &mut line).map_err(|e| e.to_string())?;
677 if got == 0 {
678 break;
679 }
680 if !line.starts_with(b"FRAME") {
681 return Err(format!(
682 "{}: bad frame marker at byte {pos}",
683 path.display()
684 ));
685 }
686 pos += got as u64;
687 if pos + payload > file_len {
688 return Err(format!(
689 "{}: truncated frame {}",
690 path.display(),
691 offsets.len()
692 ));
693 }
694 offsets.push(pos);
695 pos += payload;
696 rd.seek(SeekFrom::Start(pos)).map_err(|e| e.to_string())?;
697 }
698 if offsets.is_empty() {
699 return Err(format!("{}: Y4M stream has no frames", path.display()));
700 }
701 Ok(VideoSource::Y4m {
702 path: path.to_path_buf(),
703 info: Y4mInfo {
704 width: w,
705 height: h,
706 fps,
707 chroma,
708 offsets,
709 },
710 })
711 }
712
713 pub fn open(path: &Path, fps: Option<f64>) -> Result<Self, String> {
716 if path.is_dir() {
717 let fps = fps.ok_or_else(|| {
718 format!("{}: a frame directory needs --video-fps", path.display())
719 })?;
720 return Self::frame_dir(path, fps);
721 }
722 let mut src = Self::y4m(path)?;
723 if let (Some(f), VideoSource::Y4m { info, .. }) = (fps, &mut src) {
724 if !(f > 0.0) {
725 return Err(format!("--video-fps must be positive, got {f}"));
726 }
727 info.fps = f;
728 }
729 Ok(src)
730 }
731
732 pub fn frame_count(&self) -> usize {
733 match self {
734 VideoSource::FrameDir { frames, .. } => frames.len(),
735 VideoSource::Y4m { info, .. } => info.offsets.len(),
736 }
737 }
738
739 pub fn fps(&self) -> f64 {
740 match self {
741 VideoSource::FrameDir { fps, .. } => *fps,
742 VideoSource::Y4m { info, .. } => info.fps,
743 }
744 }
745
746 pub fn read_frame(&self, idx: usize) -> Result<RgbFrame, String> {
748 match self {
749 VideoSource::FrameDir { frames, .. } => {
750 let p = frames
751 .get(idx)
752 .ok_or_else(|| format!("frame {idx} out of range"))?;
753 crate::media::read_rgb(p)
754 }
755 VideoSource::Y4m { path, info } => read_y4m_frame(path, info, idx),
756 }
757 }
758}
759
760fn read_y4m_frame(path: &Path, info: &Y4mInfo, idx: usize) -> Result<RgbFrame, String> {
764 use std::io::{Read, Seek, SeekFrom};
765 let off = *info
766 .offsets
767 .get(idx)
768 .ok_or_else(|| format!("Y4M frame {idx} out of range"))?;
769 let (w, h) = (info.width, info.height);
770 let (cw, ch) = info.chroma.plane_dims(w, h);
771 let mut buf = vec![0u8; w * h + 2 * cw * ch];
772 let mut f = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
773 f.seek(SeekFrom::Start(off)).map_err(|e| e.to_string())?;
774 f.read_exact(&mut buf)
775 .map_err(|e| format!("{}: {e}", path.display()))?;
776 let (yp, rest) = buf.split_at(w * h);
777 let (up, vp) = rest.split_at(cw * ch);
778 let mut rgb = vec![0u8; w * h * 3];
779 for y in 0..h {
780 for x in 0..w {
781 let yy = yp[y * w + x] as f32 - 16.0;
782 let (u, v) = match info.chroma {
783 Chroma::Mono => (0.0, 0.0),
784 Chroma::C444 => (up[y * cw + x] as f32 - 128.0, vp[y * cw + x] as f32 - 128.0),
785 Chroma::C422 => (
786 up[y * cw + x / 2] as f32 - 128.0,
787 vp[y * cw + x / 2] as f32 - 128.0,
788 ),
789 Chroma::C420 => (
790 up[(y / 2) * cw + x / 2] as f32 - 128.0,
791 vp[(y / 2) * cw + x / 2] as f32 - 128.0,
792 ),
793 };
794 let r = 1.164_383 * yy + 1.596_027 * v;
795 let g = 1.164_383 * yy - 0.391_762 * u - 0.812_968 * v;
796 let b = 1.164_383 * yy + 2.017_232 * u;
797 let o = (y * w + x) * 3;
798 rgb[o] = r.round().clamp(0.0, 255.0) as u8;
799 rgb[o + 1] = g.round().clamp(0.0, 255.0) as u8;
800 rgb[o + 2] = b.round().clamp(0.0, 255.0) as u8;
801 }
802 }
803 RgbFrame::new(w, h, rgb)
804}
805
806pub fn prepare_video_frames(
811 frames: &[RgbFrame],
812 timestamps: &[f32],
813 frame_indices: &[usize],
814 cfg: &MimoProcessorConfig,
815 max_pixels: Option<usize>,
816) -> Result<VisualInput, String> {
817 if frames.is_empty() || frames.len() != timestamps.len() {
818 return Err(format!(
819 "video has {} frames but {} timestamps",
820 frames.len(),
821 timestamps.len()
822 ));
823 }
824 let (h, w) = (frames[0].height, frames[0].width);
825 if let Some(bad) = frames.iter().position(|f| f.height != h || f.width != w) {
826 return Err(format!(
827 "video frame {bad} is {}x{}, frame 0 is {w}x{h}: all frames must share one size",
828 frames[bad].width, frames[bad].height
829 ));
830 }
831 let n = frames.len();
832 let budget = video_max_pixels(n, cfg);
833 let max_px = max_pixels.map_or(budget, |m| m.min(budget).max(cfg.video_min_pixels));
834 let (hb, wb) = smart_resize(h, w, cfg.factor(), cfg.video_min_pixels, max_px)?;
835 let tp = cfg.temporal_patch_size;
836 let padded = n.div_ceil(tp) * tp;
837 let mut ts = timestamps.to_vec();
838 let mut fi = frame_indices.to_vec();
839 let mut normed: Vec<Vec<f32>> = frames
840 .iter()
841 .map(|f| resize_normalize(f, hb, wb, cfg))
842 .collect();
843 while normed.len() < padded {
844 normed.push(normed[n - 1].clone());
845 ts.push(timestamps[n - 1]);
846 if let Some(&last) = frame_indices.last() {
847 fi.push(last);
848 }
849 }
850 let refs: Vec<&[f32]> = normed.iter().map(|v| v.as_slice()).collect();
851 let (rows, grid_t, grid_h, grid_w) = patchify(&refs, hb, wb, cfg)?;
852 Ok(VisualInput {
853 kind: VisualKind::Video,
854 rows,
855 grid_t,
856 grid_h,
857 grid_w,
858 patch_dim: cfg.patch_dim(),
859 merge_size: cfg.merge_size,
860 timestamps: ts,
861 frame_indices: fi,
862 resized: (hb, wb),
863 })
864}
865
866pub fn prepare_video(
868 src: &VideoSource,
869 cfg: &MimoProcessorConfig,
870 max_pixels: Option<usize>,
871) -> Result<VisualInput, String> {
872 let (idx, ts) = sample_frames(src.frame_count(), src.fps(), cfg)?;
873 let frames = idx
874 .iter()
875 .map(|&i| src.read_frame(i))
876 .collect::<Result<Vec<_>, _>>()?;
877 prepare_video_frames(&frames, &ts, &idx, cfg, max_pixels)
878}
879
880pub fn expand_prompt_ids(
897 ids: &[u32],
898 images: &[&VisualInput],
899 videos: &[&VisualInput],
900 audio_tokens: &[usize],
901 tok: &Tokenizer,
902) -> Result<Vec<u32>, String> {
903 let mut out = Vec::with_capacity(ids.len());
904 let (mut ni, mut nv, mut na) = (0usize, 0usize, 0usize);
905 let mut p = 0usize;
906 let run = |p: usize, start: u32, pad: u32, end: u32| -> Option<usize> {
908 if ids.get(p) != Some(&start) || ids.get(p + 1) != Some(&pad) {
909 return None;
910 }
911 let mut q = p + 1;
912 while ids.get(q) == Some(&pad) {
913 q += 1;
914 }
915 (ids.get(q) == Some(&end)).then_some(q + 1 - p)
916 };
917 while p < ids.len() {
918 if let Some(len) = run(p, VISION_START_ID, IMAGE_PAD_ID, VISION_END_ID) {
919 let item = images.get(ni).ok_or_else(|| {
920 format!(
921 "prompt has more image placeholders than the {} images given",
922 images.len()
923 )
924 })?;
925 ni += 1;
926 out.push(VISION_START_ID);
927 out.extend(std::iter::repeat_n(IMAGE_PAD_ID, item.tokens()));
928 out.push(VISION_END_ID);
929 p += len;
930 } else if let Some(len) = run(p, VISION_START_ID, VIDEO_PAD_ID, VISION_END_ID) {
931 let item = videos.get(nv).ok_or_else(|| {
932 format!(
933 "prompt has more video placeholders than the {} videos given",
934 videos.len()
935 )
936 })?;
937 nv += 1;
938 let labels = item.timestamp_labels();
939 if labels.len() != item.grid_t {
940 return Err(format!(
941 "video has {} timestamps for {} temporal steps",
942 item.timestamps.len(),
943 item.grid_t
944 ));
945 }
946 out.push(VIDEO_START_ID);
947 for label in &labels {
948 out.extend(tok.encode(label));
949 out.push(VISION_START_ID);
950 out.extend(std::iter::repeat_n(VIDEO_PAD_ID, item.tokens_per_step()));
951 out.push(VISION_END_ID);
952 }
953 out.push(VIDEO_END_ID);
954 p += len;
955 } else if let Some(len) = run(p, AUDIO_START_ID, AUDIO_PAD_ID, AUDIO_END_ID) {
956 let k = *audio_tokens.get(na).ok_or_else(|| {
957 format!(
958 "prompt has more audio placeholders than the {} audios given",
959 audio_tokens.len()
960 )
961 })?;
962 na += 1;
963 out.push(AUDIO_START_ID);
964 out.extend(std::iter::repeat_n(AUDIO_PAD_ID, k));
965 out.push(AUDIO_END_ID);
966 p += len;
967 } else {
968 let id = ids[p];
969 if id == IMAGE_PAD_ID || id == VIDEO_PAD_ID || id == AUDIO_PAD_ID {
970 return Err(format!(
971 "media pad token {id} at position {p} is outside a placeholder"
972 ));
973 }
974 out.push(id);
975 p += 1;
976 }
977 }
978 if ni != images.len() || nv != videos.len() || na != audio_tokens.len() {
979 return Err(format!(
980 "placeholder/data mismatch: prompt has {ni} image, {nv} video, {na} audio \
981 placeholders; request has {} images, {} videos, {} audios",
982 images.len(),
983 videos.len(),
984 audio_tokens.len()
985 ));
986 }
987 Ok(out)
988}
989
990#[derive(Clone, Debug)]
994pub struct MimoVisionConfig {
995 pub depth: usize,
996 pub hidden: usize,
997 pub intermediate: usize,
998 pub heads: usize,
999 pub kv_heads: usize,
1000 pub head_dim: usize,
1001 pub patch_size: usize,
1002 pub temporal_patch_size: usize,
1003 pub merge_size: usize,
1004 pub in_channels: usize,
1005 pub out_hidden: usize,
1006 pub fullatt: Vec<usize>,
1007 pub window_types: Vec<i64>,
1009 pub window: Option<usize>,
1011 pub use_sink: bool,
1012 pub eps: f64,
1013 pub rope_theta: f32,
1014}
1015
1016impl MimoVisionConfig {
1017 pub fn from_json(v: &Value) -> Result<Self, String> {
1020 let vc = v.get("vision_config").unwrap_or(v);
1021 let req = |k: &str| {
1022 vc.get(k)
1023 .and_then(Value::as_u64)
1024 .map(|n| n as usize)
1025 .ok_or_else(|| format!("vision_config lacks integer '{k}'"))
1026 };
1027 let opt = |k: &str, d: usize| vc.get(k).and_then(Value::as_u64).map_or(d, |n| n as usize);
1028 let depth = req("depth")?;
1029 let heads = req("num_heads")?;
1030 let act = vc
1031 .get("hidden_act")
1032 .and_then(Value::as_str)
1033 .unwrap_or("silu");
1034 if act != "silu" {
1035 return Err(format!("MiMo ViT MLP needs hidden_act 'silu', got '{act}'"));
1036 }
1037 let fullatt = match vc.get("fullatt_block_indexes") {
1038 Some(Value::Array(a)) => a
1039 .iter()
1040 .map(|x| {
1041 x.as_u64()
1042 .map(|n| n as usize)
1043 .ok_or("non-integer fullatt index")
1044 })
1045 .collect::<Result<Vec<_>, _>>()?,
1046 _ => Vec::new(),
1047 };
1048 let window_types = match vc.get("vit_window_attn_types") {
1049 Some(Value::Array(a)) if !a.is_empty() => a
1050 .iter()
1051 .map(|x| x.as_i64().ok_or("non-integer vit_window_attn_types entry"))
1052 .collect::<Result<Vec<_>, _>>()?,
1053 _ => vec![-1; depth],
1054 };
1055 if window_types.len() != depth {
1056 return Err(format!(
1057 "vit_window_attn_types has {} entries for depth {depth}",
1058 window_types.len()
1059 ));
1060 }
1061 let window = vc
1062 .get("visual_token_window_size")
1063 .and_then(Value::as_i64)
1064 .filter(|&w| w > 0)
1065 .map(|w| w as usize);
1066 let cfg = Self {
1067 depth,
1068 hidden: req("hidden_size")?,
1069 intermediate: req("intermediate_size")?,
1070 heads,
1071 kv_heads: opt("num_key_value_heads", heads),
1072 head_dim: opt("qk_channels", 64),
1073 patch_size: req("patch_size")?,
1074 temporal_patch_size: req("temporal_patch_size")?,
1075 merge_size: opt("spatial_merge_size", 2),
1076 in_channels: vc
1077 .get("in_channels")
1078 .or_else(|| vc.get("in_chans"))
1079 .and_then(Value::as_u64)
1080 .map_or(3, |n| n as usize),
1081 out_hidden: req("out_hidden_size")?,
1082 fullatt,
1083 window_types,
1084 window,
1085 use_sink: vc.get("use_sink").and_then(Value::as_bool).unwrap_or(false),
1086 eps: vc
1087 .get("rms_norm_eps")
1088 .and_then(Value::as_f64)
1089 .unwrap_or(1e-6),
1090 rope_theta: 10_000.0,
1091 };
1092 if cfg.kv_heads == 0 || cfg.heads % cfg.kv_heads != 0 {
1093 return Err(format!(
1094 "heads {} not divisible by kv heads {}",
1095 cfg.heads, cfg.kv_heads
1096 ));
1097 }
1098 if cfg.head_dim % 4 != 0 || cfg.head_dim == 0 {
1099 return Err(format!(
1100 "head_dim {} must be a positive multiple of 4",
1101 cfg.head_dim
1102 ));
1103 }
1104 if cfg.in_channels != 3 {
1105 return Err(format!(
1106 "in_channels {} (only RGB is supported)",
1107 cfg.in_channels
1108 ));
1109 }
1110 if cfg.fullatt.iter().any(|&i| i >= depth) {
1111 return Err("fullatt_block_indexes exceeds depth".into());
1112 }
1113 Ok(cfg)
1114 }
1115}
1116
1117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1119pub enum SinkMode {
1120 Key0,
1122 Column,
1124 Off,
1126}
1127
1128impl SinkMode {
1129 pub fn from_env() -> Self {
1131 match std::env::var("CMF_MIMO_VIT_SINK").as_deref() {
1132 Ok("column") => SinkMode::Column,
1133 Ok("off") => SinkMode::Off,
1134 _ => SinkMode::Key0,
1135 }
1136 }
1137}
1138
1139struct Lin {
1143 name: String,
1144 p: Proj,
1145}
1146
1147const F16_HEADROOM: f32 = 16384.0;
1153
1154impl Lin {
1155 fn mm(&self, x: &[f32], n: usize, out: &mut [f32], pool: Option<&Pool>) {
1156 if crate::gptq_capture::capturing() && matches!(self.p, Proj::F32 { .. }) {
1157 crate::gptq_capture::accumulate(&self.name, x, n, self.p.cols());
1158 }
1159 let peak = max_abs(x);
1160 if peak > F16_HEADROOM && peak.is_finite() {
1161 let k = (peak / F16_HEADROOM).log2().ceil() as i32;
1162 let (down, up) = (2f32.powi(-k), 2f32.powi(k));
1163 let xs: Vec<f32> = x.iter().map(|v| v * down).collect();
1164 self.p.matmat(&xs, n, out, pool);
1165 for v in out.iter_mut() {
1166 *v *= up;
1167 }
1168 } else {
1169 self.p.matmat(x, n, out, pool);
1170 }
1171 }
1172}
1173
1174struct Block {
1175 norm1: Vec<f32>,
1176 norm2: Vec<f32>,
1177 qkv: Lin,
1178 qkv_b: Vec<f32>,
1179 proj: Lin,
1180 proj_b: Vec<f32>,
1181 gate: Lin,
1182 gate_b: Vec<f32>,
1183 up: Lin,
1184 up_b: Vec<f32>,
1185 down: Lin,
1186 down_b: Vec<f32>,
1187 sinks: Option<Vec<f32>>,
1188}
1189
1190pub struct MimoVit {
1192 pub cfg: MimoVisionConfig,
1193 patch: Proj,
1194 blocks: Vec<Block>,
1195 ln_q: Vec<f32>,
1196 mlp0: Lin,
1197 mlp0_b: Option<Vec<f32>>,
1198 mlp2: Lin,
1199 mlp2_b: Option<Vec<f32>>,
1200 sink_mode: SinkMode,
1201 pool: Option<Arc<Pool>>,
1202 gpu_attention: bool,
1204 trace: bool,
1206}
1207
1208fn max_abs(x: &[f32]) -> f32 {
1209 x.iter().fold(0f32, |m, v| m.max(v.abs()))
1210}
1211
1212pub fn has_vision(model: &CmfModel) -> bool {
1214 model.tensor(PATCH_EMBED).is_some()
1215}
1216
1217pub fn read_mm_config(model: &CmfModel) -> Result<Value, String> {
1219 let bytes = model
1220 .tensor_bytes(MM_CONFIG_TENSOR)
1221 .map_err(|_| format!("file lacks the '{MM_CONFIG_TENSOR}' config blob"))?;
1222 serde_json::from_slice(bytes).map_err(|e| format!("{MM_CONFIG_TENSOR}: {e}"))
1223}
1224
1225fn load_vec(model: &CmfModel, name: &str, len: usize) -> Result<Vec<f32>, String> {
1226 let v = crate::dit::cmf_f32(model, name)?;
1227 if v.len() != len {
1228 return Err(format!(
1229 "tensor '{name}' has {} values, expected {len}",
1230 v.len()
1231 ));
1232 }
1233 Ok(v)
1234}
1235
1236fn load_opt_vec(model: &CmfModel, name: &str, len: usize) -> Result<Option<Vec<f32>>, String> {
1237 if model.tensor(name).is_none() {
1238 return Ok(None);
1239 }
1240 load_vec(model, name, len).map(Some)
1241}
1242
1243fn load_proj(model: &Arc<CmfModel>, name: &str, rows: usize, cols: usize) -> Result<Lin, String> {
1244 let entry = model
1245 .tensor(name)
1246 .ok_or_else(|| format!("missing tensor '{name}'"))?;
1247 if entry.shape.as_slice() != [rows, cols] {
1248 return Err(format!(
1249 "tensor '{name}' shape {:?} != expected [{rows}, {cols}]",
1250 entry.shape
1251 ));
1252 }
1253 Ok(Lin {
1254 name: name.to_string(),
1255 p: Proj::from_model(model, name)?,
1256 })
1257}
1258
1259fn rms_norm_rows(x: &[f32], w: &[f32], eps: f64, out: &mut [f32], pool: Option<&Pool>) {
1260 let d = w.len();
1261 let rows = x.len() / d;
1262 let dst = crate::pool::SendMut::new(out.as_mut_ptr());
1263 let f = |lo: usize, hi: usize| {
1264 for r in lo..hi {
1265 let xr = &x[r * d..(r + 1) * d];
1266 let ss = xr.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / d as f64;
1267 let inv = 1.0 / (ss + eps).sqrt();
1268 for (j, (&v, &g)) in xr.iter().zip(w).enumerate() {
1269 unsafe { *dst.at(r * d + j) = (v as f64 * inv) as f32 * g };
1271 }
1272 }
1273 };
1274 match pool {
1275 Some(p) => p.run_rows(rows, &f),
1276 None => f(0, rows),
1277 }
1278}
1279
1280fn add_bias(x: &mut [f32], b: &[f32]) {
1281 for row in x.chunks_exact_mut(b.len()) {
1282 for (v, &bb) in row.iter_mut().zip(b) {
1283 *v += bb;
1284 }
1285 }
1286}
1287
1288fn erf(x: f64) -> f64 {
1290 let z = x.abs();
1291 let t = 1.0 / (1.0 + 0.5 * z);
1292 let ans = t
1293 * (-z * z - 1.265_512_23
1294 + t * (1.000_023_68
1295 + t * (0.374_091_96
1296 + t * (0.096_784_18
1297 + t * (-0.186_288_06
1298 + t * (0.278_868_07
1299 + t * (-1.135_203_98
1300 + t * (1.488_515_87
1301 + t * (-0.822_152_23 + t * 0.170_872_77)))))))))
1302 .exp();
1303 if x >= 0.0 { 1.0 - ans } else { ans - 1.0 }
1304}
1305
1306fn gelu_erf(v: f32) -> f32 {
1307 (0.5 * v as f64 * (1.0 + erf(v as f64 / std::f64::consts::SQRT_2))) as f32
1308}
1309
1310pub fn column_permutation(grid_t: usize, grid_h: usize, grid_w: usize, merge: usize) -> Vec<usize> {
1313 let (ua, ub, mm) = (grid_h / merge, grid_w / merge, merge * merge);
1314 let mut perm = Vec::with_capacity(grid_t * grid_h * grid_w);
1315 for t in 0..grid_t {
1316 for b in 0..ub {
1317 for a in 0..ua {
1318 let unit = t * ua * ub + a * ub + b;
1319 perm.extend(unit * mm..(unit + 1) * mm);
1320 }
1321 }
1322 }
1323 perm
1324}
1325
1326fn gather_rows(x: &[f32], perm: &[usize], d: usize) -> Vec<f32> {
1327 let mut out = vec![0f32; x.len()];
1328 for (dst, &src) in perm.iter().enumerate() {
1329 out[dst * d..(dst + 1) * d].copy_from_slice(&x[src * d..(src + 1) * d]);
1330 }
1331 out
1332}
1333
1334fn scatter_rows(x: &[f32], perm: &[usize], d: usize) -> Vec<f32> {
1335 let mut out = vec![0f32; x.len()];
1336 for (src, &dst) in perm.iter().enumerate() {
1337 out[dst * d..(dst + 1) * d].copy_from_slice(&x[src * d..(src + 1) * d]);
1338 }
1339 out
1340}
1341
1342impl MimoVit {
1343 pub fn from_model(model: &Arc<CmfModel>) -> Result<Self, String> {
1345 let cfg = read_mm_config(model)?;
1346 Self::from_model_with_config(model, &cfg)
1347 }
1348
1349 pub fn from_model_with_config(model: &Arc<CmfModel>, config: &Value) -> Result<Self, String> {
1352 let c = MimoVisionConfig::from_json(config)?;
1353 let (hid, inter, hd) = (c.hidden, c.intermediate, c.head_dim);
1354 let (qd, kvd) = (c.heads * hd, c.kv_heads * hd);
1355 let patch_dim = c.in_channels * c.temporal_patch_size * c.patch_size * c.patch_size;
1356 let pe = model
1357 .tensor(PATCH_EMBED)
1358 .ok_or_else(|| format!("missing tensor '{PATCH_EMBED}'"))?;
1359 let want = [
1360 hid,
1361 c.in_channels,
1362 c.temporal_patch_size,
1363 c.patch_size,
1364 c.patch_size,
1365 ];
1366 if pe.shape.as_slice() != want && pe.shape.as_slice() != [hid, patch_dim] {
1367 return Err(format!(
1368 "'{PATCH_EMBED}' shape {:?} != {:?}",
1369 pe.shape, want
1370 ));
1371 }
1372 let patch = Proj::f32(load_vec(model, PATCH_EMBED, hid * patch_dim)?, patch_dim);
1373 let mut blocks = Vec::with_capacity(c.depth);
1374 for i in 0..c.depth {
1375 let p = format!("visual.blocks.{i}");
1376 let full = c.fullatt.contains(&i);
1377 let sink_name = format!("{p}.attn.sinks");
1378 let sinks = if c.use_sink && !full {
1379 Some(load_vec(model, &sink_name, c.heads)?)
1380 } else {
1381 None
1382 };
1383 blocks.push(Block {
1384 norm1: load_vec(model, &format!("{p}.norm1.weight"), hid)?,
1385 norm2: load_vec(model, &format!("{p}.norm2.weight"), hid)?,
1386 qkv: load_proj(model, &format!("{p}.attn.qkv.weight"), qd + 2 * kvd, hid)?,
1387 qkv_b: load_vec(model, &format!("{p}.attn.qkv.bias"), qd + 2 * kvd)?,
1388 proj: load_proj(model, &format!("{p}.attn.proj.weight"), hid, qd)?,
1389 proj_b: load_vec(model, &format!("{p}.attn.proj.bias"), hid)?,
1390 gate: load_proj(model, &format!("{p}.mlp.gate_proj.weight"), inter, hid)?,
1391 gate_b: load_vec(model, &format!("{p}.mlp.gate_proj.bias"), inter)?,
1392 up: load_proj(model, &format!("{p}.mlp.up_proj.weight"), inter, hid)?,
1393 up_b: load_vec(model, &format!("{p}.mlp.up_proj.bias"), inter)?,
1394 down: load_proj(model, &format!("{p}.mlp.down_proj.weight"), hid, inter)?,
1395 down_b: load_vec(model, &format!("{p}.mlp.down_proj.bias"), hid)?,
1396 sinks,
1397 });
1398 }
1399 let mw = hid * c.merge_size * c.merge_size;
1400 if model.tensor("visual.merger.ln_q.bias").is_some() {
1401 return Err(
1402 "visual.merger.ln_q.bias present: the MiMo merger norm is an RMSNorm".into(),
1403 );
1404 }
1405 let vit = Self {
1406 ln_q: load_vec(model, "visual.merger.ln_q.weight", hid)?,
1407 mlp0: load_proj(model, "visual.merger.mlp.0.weight", mw, mw)?,
1408 mlp0_b: load_opt_vec(model, "visual.merger.mlp.0.bias", mw)?,
1409 mlp2: load_proj(model, "visual.merger.mlp.2.weight", c.out_hidden, mw)?,
1410 mlp2_b: load_opt_vec(model, "visual.merger.mlp.2.bias", c.out_hidden)?,
1411 cfg: c,
1412 patch,
1413 blocks,
1414 sink_mode: SinkMode::from_env(),
1415 pool: Pool::from_env(),
1416 gpu_attention: std::env::var("CMF_MIMO_VIT_GPU_ATTN").as_deref() != Ok("0"),
1417 trace: std::env::var("CMF_MIMO_VIT_TRACE").is_ok_and(|v| v != "0"),
1418 };
1419 Ok(vit)
1420 }
1421
1422 pub fn set_sink_mode(&mut self, mode: SinkMode) {
1423 self.sink_mode = mode;
1424 }
1425
1426 pub fn sink_mode(&self) -> SinkMode {
1427 self.sink_mode
1428 }
1429
1430 pub fn set_gpu_attention(&mut self, on: bool) {
1432 self.gpu_attention = on;
1433 }
1434
1435 fn rope_tables(&self, gt: usize, gh: usize, gw: usize) -> (Vec<f32>, Vec<f32>) {
1438 let hd = self.cfg.head_dim;
1439 let quarter = hd / 4;
1440 let m = self.cfg.merge_size;
1441 let dim = (hd / 2) as f32;
1443 let inv: Vec<f32> = (0..quarter)
1444 .map(|i| 1.0f32 / self.cfg.rope_theta.powf((2 * i) as f32 / dim))
1445 .collect();
1446 let n = gt * gh * gw;
1447 let (mut cos, mut sin) = (vec![0f32; n * hd], vec![0f32; n * hd]);
1448 let mut r = 0usize;
1449 for _t in 0..gt {
1450 for a in 0..gh / m {
1451 for b in 0..gw / m {
1452 for mh in 0..m {
1453 for mw in 0..m {
1454 let (hp, wp) = ((a * m + mh) as f32, (b * m + mw) as f32);
1455 for d in 0..hd {
1456 let q = d % (hd / 2);
1457 let ang = if q < quarter {
1458 hp * inv[q]
1459 } else {
1460 wp * inv[q - quarter]
1461 };
1462 let (s, c) = (ang as f64).sin_cos();
1463 cos[r * hd + d] = c as f32;
1464 sin[r * hd + d] = s as f32;
1465 }
1466 r += 1;
1467 }
1468 }
1469 }
1470 }
1471 }
1472 (cos, sin)
1473 }
1474
1475 pub fn forward(&self, input: &VisualInput) -> Result<Vec<f32>, String> {
1478 #[cfg(feature = "gpu")]
1483 let _precision = crate::gpu_wgpu::MimoF32Gemm::for_tower();
1484 let c = &self.cfg;
1485 let m = c.merge_size;
1486 let (gt, gh, gw) = (input.grid_t, input.grid_h, input.grid_w);
1487 if gt == 0 || gh == 0 || gw == 0 || gh % m != 0 || gw % m != 0 {
1488 return Err(format!(
1489 "vision grid {gt}x{gh}x{gw} is empty or not {m}-aligned"
1490 ));
1491 }
1492 let patch_dim = c.in_channels * c.temporal_patch_size * c.patch_size * c.patch_size;
1493 let n = gt * gh * gw;
1494 if input.rows.len() != n * patch_dim {
1495 return Err(format!(
1496 "vision input has {} values, expected {n} rows × {patch_dim}",
1497 input.rows.len()
1498 ));
1499 }
1500 let hid = c.hidden;
1501 let pool = self.pool.as_deref();
1502 let mut x = vec![0f32; n * hid];
1503 self.patch.matmat(&input.rows, n, &mut x, pool);
1504
1505 let hd = c.head_dim;
1506 let (cos_r, sin_r) = self.rope_tables(gt, gh, gw);
1507 let perm = column_permutation(gt, gh, gw, m);
1508 let cos_c = gather_rows(&cos_r, &perm, hd);
1509 let sin_c = gather_rows(&sin_r, &perm, hd);
1510 let chunk = gh * gw;
1511 let mut scratch = Scratch::new(n, c);
1512 let t_blocks = std::time::Instant::now();
1513 for (i, blk) in self.blocks.iter().enumerate() {
1514 let ty = c.window_types[i];
1515 let prev = if i > 0 {
1516 c.window_types[i - 1]
1517 } else {
1518 i64::MIN
1519 };
1520 if ty == 1 && (i == 0 || prev != 1) {
1521 x = gather_rows(&x, &perm, hid);
1522 }
1523 if i > 0 && ty != 1 && prev == 1 {
1524 x = scatter_rows(&x, &perm, hid);
1525 }
1526 let (cs, sn) = if ty == 1 {
1527 (&cos_c, &sin_c)
1528 } else {
1529 (&cos_r, &sin_r)
1530 };
1531 let full = c.fullatt.contains(&i);
1532 self.block(blk, &mut x, n, chunk, cs, sn, full, &mut scratch)?;
1533 if self.trace {
1534 eprintln!(
1535 "mimo-vit block {i:2} ({}): max|x| {:.1}, max|down in| {:.1}",
1536 if full {
1537 "full"
1538 } else if ty == 1 {
1539 "col "
1540 } else {
1541 "row "
1542 },
1543 max_abs(&x),
1544 max_abs(&scratch.gate)
1545 );
1546 }
1547 }
1548
1549 if self.trace {
1550 let total = t_blocks.elapsed().as_secs_f64();
1551 eprintln!(
1552 "mimo-vit {n} rows: blocks {total:.2} s = full attention {:.2} s + windowed attention {:.2} s + projections/norms {:.2} s",
1553 scratch.t_full,
1554 scratch.t_band,
1555 total - scratch.t_full - scratch.t_band
1556 );
1557 }
1558 let groups = n / (m * m);
1560 let mw = hid * m * m;
1561 let mut xn = vec![0f32; n * hid];
1562 rms_norm_rows(&x, &self.ln_q, c.eps, &mut xn, pool);
1563 drop(x);
1564 let mut h1 = vec![0f32; groups * mw];
1565 self.mlp0.mm(&xn, groups, &mut h1, pool);
1566 if let Some(b) = &self.mlp0_b {
1567 add_bias(&mut h1, b);
1568 }
1569 for v in &mut h1 {
1570 *v = gelu_erf(*v);
1571 }
1572 let mut out = vec![0f32; groups * c.out_hidden];
1573 self.mlp2.mm(&h1, groups, &mut out, pool);
1574 if let Some(b) = &self.mlp2_b {
1575 add_bias(&mut out, b);
1576 }
1577 Ok(out)
1578 }
1579
1580 #[allow(clippy::too_many_arguments)]
1581 fn block(
1582 &self,
1583 b: &Block,
1584 x: &mut [f32],
1585 n: usize,
1586 chunk: usize,
1587 cos: &[f32],
1588 sin: &[f32],
1589 full: bool,
1590 s: &mut Scratch,
1591 ) -> Result<(), String> {
1592 let c = &self.cfg;
1593 let pool = self.pool.as_deref();
1594 let (hid, hd) = (c.hidden, c.head_dim);
1595 let (qd, kvd) = (c.heads * hd, c.kv_heads * hd);
1596 let width = qd + 2 * kvd;
1597 rms_norm_rows(x, &b.norm1, c.eps, &mut s.norm, pool);
1598 b.qkv.mm(&s.norm, n, &mut s.qkv, pool);
1599 add_bias(&mut s.qkv, &b.qkv_b);
1600 {
1602 let (qp, kp, vp) = (
1603 crate::pool::SendMut::new(s.q.as_mut_ptr()),
1604 crate::pool::SendMut::new(s.k.as_mut_ptr()),
1605 crate::pool::SendMut::new(s.v.as_mut_ptr()),
1606 );
1607 let qkv = &s.qkv;
1608 let half = hd / 2;
1609 let f = |lo: usize, hi: usize| {
1610 for r in lo..hi {
1611 let src = &qkv[r * width..(r + 1) * width];
1612 let (cr, sr) = (&cos[r * hd..(r + 1) * hd], &sin[r * hd..(r + 1) * hd]);
1613 let rot = |head: &[f32], dst: crate::pool::SendMut, off: usize| {
1614 for d in 0..hd {
1615 let rh = if d < half {
1616 -head[d + half]
1617 } else {
1618 head[d - half]
1619 };
1620 unsafe { *dst.at(off + d) = head[d] * cr[d] + rh * sr[d] };
1622 }
1623 };
1624 for h in 0..c.heads {
1625 rot(&src[h * hd..(h + 1) * hd], qp, r * qd + h * hd);
1626 }
1627 for h in 0..c.kv_heads {
1628 rot(&src[qd + h * hd..qd + (h + 1) * hd], kp, r * kvd + h * hd);
1629 }
1630 for j in 0..kvd {
1631 unsafe { *vp.at(r * kvd + j) = src[qd + kvd + j] };
1632 }
1633 }
1634 };
1635 match pool {
1636 Some(p) => p.run_rows(n, &f),
1637 None => f(0, n),
1638 }
1639 }
1640 let sinks = match self.sink_mode {
1641 SinkMode::Off => None,
1642 _ => b.sinks.as_deref(),
1643 };
1644 let window = if full { None } else { c.window };
1645 s.attn.fill(0.0);
1646 let t_attn = std::time::Instant::now();
1647 for c0 in (0..n).step_by(chunk) {
1648 if window.is_none() && sinks.is_none() {
1649 if self.full_attention_gpu(&s.q, &s.k, &s.v, c0, chunk, &mut s.attn) {
1650 continue;
1651 }
1652 self.full_attention_cpu(&s.q, &s.k, &s.v, c0, chunk, &mut s.attn);
1653 } else {
1654 self.band_attention_cpu(&s.q, &s.k, &s.v, c0, chunk, window, sinks, &mut s.attn);
1655 }
1656 }
1657 let dt = t_attn.elapsed().as_secs_f64();
1658 if window.is_none() && sinks.is_none() {
1659 s.t_full += dt;
1660 } else {
1661 s.t_band += dt;
1662 }
1663 b.proj.mm(&s.attn, n, &mut s.proj, pool);
1664 add_bias(&mut s.proj, &b.proj_b);
1665 for (a, &p) in x.iter_mut().zip(&s.proj) {
1666 *a += p;
1667 }
1668 rms_norm_rows(x, &b.norm2, c.eps, &mut s.norm, pool);
1669 b.gate.mm(&s.norm, n, &mut s.gate, pool);
1670 b.up.mm(&s.norm, n, &mut s.up, pool);
1671 add_bias(&mut s.gate, &b.gate_b);
1672 add_bias(&mut s.up, &b.up_b);
1673 for (g, &u) in s.gate.iter_mut().zip(&s.up) {
1674 *g = (*g / (1.0 + (-*g).exp())) * u;
1675 }
1676 b.down.mm(&s.gate, n, &mut s.proj, pool);
1677 add_bias(&mut s.proj, &b.down_b);
1678 for (a, &p) in x.iter_mut().zip(&s.proj) {
1679 *a += p;
1680 }
1681 debug_assert_eq!(s.proj.len(), n * hid);
1682 Ok(())
1683 }
1684
1685 fn full_attention_gpu(
1689 &self,
1690 q: &[f32],
1691 k: &[f32],
1692 v: &[f32],
1693 c0: usize,
1694 l: usize,
1695 out: &mut [f32],
1696 ) -> bool {
1697 let c = &self.cfg;
1698 if !self.gpu_attention
1699 || l < 128
1700 || l * l * 4 > (1usize << 30)
1701 || !crate::gpu::enabled_here()
1702 {
1703 return false;
1704 }
1705 let (nh, nkv, hd) = (c.heads, c.kv_heads, c.head_dim);
1706 let (qd, kvd) = (nh * hd, nkv * hd);
1707 let mut qh = vec![0f32; nh * l * hd];
1708 let mut kh = vec![0f32; nkv * l * hd];
1709 let mut vh = vec![0f32; nkv * l * hd];
1710 for p in 0..l {
1711 let r = c0 + p;
1712 for h in 0..nh {
1713 qh[(h * l + p) * hd..(h * l + p + 1) * hd]
1714 .copy_from_slice(&q[r * qd + h * hd..r * qd + (h + 1) * hd]);
1715 }
1716 for h in 0..nkv {
1717 kh[(h * l + p) * hd..(h * l + p + 1) * hd]
1718 .copy_from_slice(&k[r * kvd + h * hd..r * kvd + (h + 1) * hd]);
1719 vh[(h * l + p) * hd..(h * l + p + 1) * hd]
1720 .copy_from_slice(&v[r * kvd + h * hd..r * kvd + (h + 1) * hd]);
1721 }
1722 }
1723 let scale = (hd as f32).powf(-0.5);
1724 let dst = &mut out[c0 * qd..(c0 + l) * qd];
1725 let ok = crate::gpu::dit_attention(&qh, &kh, &vh, nh, nkv, l, hd, scale, dst);
1726 if ok {
1727 GPU_ATTENTION_DISPATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1728 }
1729 ok
1730 }
1731
1732 fn full_attention_cpu(
1735 &self,
1736 q: &[f32],
1737 k: &[f32],
1738 v: &[f32],
1739 c0: usize,
1740 l: usize,
1741 out: &mut [f32],
1742 ) {
1743 let c = &self.cfg;
1744 let pool = self.pool.as_deref();
1745 let (nh, nkv, hd) = (c.heads, c.kv_heads, c.head_dim);
1746 let (qd, kvd) = (nh * hd, nkv * hd);
1747 let group = nh / nkv;
1748 let scale = (hd as f32).powf(-0.5);
1749 let tile = l.min(1024);
1750 let mut kh = vec![0f32; l * hd];
1751 let mut vt = vec![0f32; hd * l];
1752 let mut qh = vec![0f32; tile * hd];
1753 let mut sc = vec![0f32; tile * l];
1754 let mut oh = vec![0f32; tile * hd];
1755 for g in 0..nkv {
1756 for j in 0..l {
1757 let r = c0 + j;
1758 kh[j * hd..(j + 1) * hd]
1759 .copy_from_slice(&k[r * kvd + g * hd..r * kvd + (g + 1) * hd]);
1760 for d in 0..hd {
1761 vt[d * l + j] = v[r * kvd + g * hd + d];
1762 }
1763 }
1764 for h in g * group..(g + 1) * group {
1765 for t0 in (0..l).step_by(tile) {
1766 let tq = tile.min(l - t0);
1767 for i in 0..tq {
1768 let r = c0 + t0 + i;
1769 for d in 0..hd {
1770 qh[i * hd + d] = q[r * qd + h * hd + d] * scale;
1771 }
1772 }
1773 crate::fcd_ops::gemm_nt(
1774 &qh[..tq * hd],
1775 &kh,
1776 &mut sc[..tq * l],
1777 tq,
1778 hd,
1779 l,
1780 pool,
1781 );
1782 {
1783 let sp = crate::pool::SendMut::new(sc.as_mut_ptr());
1784 let f = |lo: usize, hi: usize| {
1785 for i in lo..hi {
1786 let row =
1788 unsafe { std::slice::from_raw_parts_mut(sp.at(i * l), l) };
1789 softmax_row(row, None);
1790 }
1791 };
1792 match pool {
1793 Some(p) => p.run_rows(tq, &f),
1794 None => f(0, tq),
1795 }
1796 }
1797 crate::fcd_ops::gemm_nt(
1798 &sc[..tq * l],
1799 &vt,
1800 &mut oh[..tq * hd],
1801 tq,
1802 l,
1803 hd,
1804 pool,
1805 );
1806 for i in 0..tq {
1807 let r = c0 + t0 + i;
1808 out[r * qd + h * hd..r * qd + (h + 1) * hd]
1809 .copy_from_slice(&oh[i * hd..(i + 1) * hd]);
1810 }
1811 }
1812 }
1813 }
1814 }
1815
1816 #[allow(clippy::too_many_arguments)]
1820 fn band_attention_cpu(
1821 &self,
1822 q: &[f32],
1823 k: &[f32],
1824 v: &[f32],
1825 c0: usize,
1826 l: usize,
1827 window: Option<usize>,
1828 sinks: Option<&[f32]>,
1829 out: &mut [f32],
1830 ) {
1831 let c = &self.cfg;
1832 let (nh, nkv, hd) = (c.heads, c.kv_heads, c.head_dim);
1833 let (qd, kvd) = (nh * hd, nkv * hd);
1834 let group = nh / nkv;
1835 let scale = (hd as f32).powf(-0.5);
1836 let w = window.unwrap_or(l);
1837 let mode = self.sink_mode;
1838 let op = crate::pool::SendMut::new(out.as_mut_ptr());
1839 let f = |lo: usize, hi: usize| {
1840 let mut sc = vec![0f32; (2 * w + 1).min(l)];
1841 let mut acc = vec![0f32; hd];
1842 for li in lo..hi {
1843 let r = c0 + li;
1844 let j0 = li.saturating_sub(w);
1845 let j1 = (li + w).min(l - 1);
1846 for h in 0..nh {
1847 let g = h / group;
1848 let qi = &q[r * qd + h * hd..r * qd + (h + 1) * hd];
1849 let row = &mut sc[..j1 - j0 + 1];
1850 for (s, j) in row.iter_mut().zip(j0..=j1) {
1851 let kr = c0 + j;
1852 let kj = &k[kr * kvd + g * hd..kr * kvd + (g + 1) * hd];
1853 *s = crate::attention::dot_f32(qi, kj) * scale;
1854 }
1855 let column = match (mode, sinks) {
1856 (SinkMode::Key0, Some(sk)) => {
1857 if j0 == 0 {
1858 row[0] += sk[h];
1859 }
1860 None
1861 }
1862 (SinkMode::Column, Some(sk)) => Some(sk[h]),
1863 _ => None,
1864 };
1865 softmax_row(row, column);
1866 acc.fill(0.0);
1867 for (&p, j) in row.iter().zip(j0..=j1) {
1868 let kr = c0 + j;
1869 let vj = &v[kr * kvd + g * hd..kr * kvd + (g + 1) * hd];
1870 for (a, &vv) in acc.iter_mut().zip(vj) {
1871 *a += p * vv;
1872 }
1873 }
1874 for (d, &a) in acc.iter().enumerate() {
1875 unsafe { *op.at(r * qd + h * hd + d) = a };
1877 }
1878 }
1879 }
1880 };
1881 match self.pool.as_deref() {
1882 Some(p) => p.run_rows(l, &f),
1883 None => f(0, l),
1884 }
1885 }
1886}
1887
1888fn softmax_row(row: &mut [f32], column: Option<f32>) {
1891 let mut mx = row.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1892 if let Some(s) = column {
1893 mx = mx.max(s);
1894 }
1895 let mut den = 0f32;
1896 for v in row.iter_mut() {
1897 *v = (*v - mx).exp();
1898 den += *v;
1899 }
1900 if let Some(s) = column {
1901 den += (s - mx).exp();
1902 }
1903 let inv = 1.0 / den;
1904 for v in row.iter_mut() {
1905 *v *= inv;
1906 }
1907}
1908
1909struct Scratch {
1911 t_full: f64,
1913 t_band: f64,
1914 norm: Vec<f32>,
1915 qkv: Vec<f32>,
1916 q: Vec<f32>,
1917 k: Vec<f32>,
1918 v: Vec<f32>,
1919 attn: Vec<f32>,
1920 proj: Vec<f32>,
1921 gate: Vec<f32>,
1922 up: Vec<f32>,
1923}
1924
1925impl Scratch {
1926 fn new(n: usize, c: &MimoVisionConfig) -> Self {
1927 let (qd, kvd) = (c.heads * c.head_dim, c.kv_heads * c.head_dim);
1928 Self {
1929 t_full: 0.0,
1930 t_band: 0.0,
1931 norm: vec![0f32; n * c.hidden],
1932 qkv: vec![0f32; n * (qd + 2 * kvd)],
1933 q: vec![0f32; n * qd],
1934 k: vec![0f32; n * kvd],
1935 v: vec![0f32; n * kvd],
1936 attn: vec![0f32; n * qd],
1937 proj: vec![0f32; n * c.hidden],
1938 gate: vec![0f32; n * c.intermediate],
1939 up: vec![0f32; n * c.intermediate],
1940 }
1941 }
1942}
1943
1944#[cfg(test)]
1945mod tests {
1946 use super::*;
1947
1948 #[test]
1949 fn smart_resize_matches_worked_examples() {
1950 let (lo, hi) = (8192, 8_388_608);
1951 assert_eq!(smart_resize(448, 448, 32, lo, hi).unwrap(), (448, 448));
1952 assert_eq!(smart_resize(352, 640, 32, lo, hi).unwrap(), (352, 640));
1953 assert_eq!(smart_resize(768, 1024, 32, lo, hi).unwrap(), (768, 1024));
1954 assert_eq!(smart_resize(20, 300, 32, lo, hi).unwrap(), (32, 480));
1956 assert_eq!(smart_resize(48, 80, 32, 0, hi).unwrap(), (64, 64));
1958 assert!(smart_resize(32, 32 * 201, 32, lo, hi).is_err());
1959 }
1960
1961 #[test]
1962 fn identity_resize_is_exact() {
1963 let src: Vec<f32> = (0..3 * 5 * 7).map(|v| v as f32).collect();
1964 assert_eq!(resize_bilinear(&src, 3, 5, 7, 5, 7), src);
1965 }
1966
1967 #[test]
1968 fn frame_counts_follow_smart_nframes() {
1969 let cfg = MimoProcessorConfig::default();
1970 assert_eq!(smart_nframes(5, 30.0, &cfg).unwrap(), 4);
1972 assert_eq!(smart_nframes(300, 30.0, &cfg).unwrap(), 10);
1974 assert!(smart_nframes(1, 1.0, &cfg).is_err());
1975 let (idx, ts) = sample_frames(8, 1.0, &cfg).unwrap();
1976 assert_eq!(idx, (0..8).collect::<Vec<_>>());
1977 assert_eq!(ts[7], 7.0);
1978 }
1979
1980 #[test]
1981 fn timestamps_format_like_python() {
1982 assert_eq!(format_timestamp(7.0), "00:07");
1983 assert_eq!(format_timestamp(65.9), "01:05");
1984 assert_eq!(format_timestamp(6000.0), "100:00");
1985 }
1986
1987 #[test]
1988 fn column_permutation_lists_units_by_column() {
1989 let p = column_permutation(1, 4, 4, 2);
1991 let units: Vec<usize> = p.chunks(4).map(|c| c[0] / 4).collect();
1993 assert_eq!(units, vec![0, 2, 1, 3]);
1994 }
1995
1996 #[test]
1997 fn y4m_source_indexes_and_decodes_frames() {
1998 let dir = std::env::temp_dir().join(format!("mimo-y4m-{}", std::process::id()));
1999 std::fs::create_dir_all(&dir).unwrap();
2000 let path = dir.join("clip.y4m");
2001 let mut bytes = b"YUV4MPEG2 W4 H2 F30000:1001 Ip A1:1 C420jpeg XYSCSS=420JPEG\n".to_vec();
2004 for k in 0..3u8 {
2005 bytes.extend_from_slice(b"FRAME\n");
2006 bytes.extend(std::iter::repeat_n(16 + 100 * k.min(2), 8));
2007 let c = if k == 2 { 200 } else { 128 };
2008 bytes.extend(std::iter::repeat_n(c, 2 * 2));
2009 }
2010 std::fs::write(&path, &bytes).unwrap();
2011 let src = VideoSource::open(&path, None).unwrap();
2012 assert_eq!(src.frame_count(), 3);
2013 assert!((src.fps() - 30000.0 / 1001.0).abs() < 1e-12);
2014 let f0 = src.read_frame(0).unwrap();
2015 assert_eq!((f0.width, f0.height), (4, 2));
2016 assert!(f0.data.iter().all(|&v| v == 0), "Y=16 is black");
2017 let f1 = src.read_frame(1).unwrap();
2018 assert!(f1.data.iter().all(|&v| v == 116), "{:?}", &f1.data[..3]);
2020 let f2 = src.read_frame(2).unwrap();
2021 assert!(
2022 f2.data[0] > f2.data[1] && f2.data[2] > f2.data[1],
2023 "{:?}",
2024 &f2.data[..3]
2025 );
2026 assert_eq!(VideoSource::open(&path, Some(2.0)).unwrap().fps(), 2.0);
2028 std::fs::remove_dir_all(&dir).ok();
2029 }
2030
2031 #[test]
2032 fn frame_directory_sorts_naturally() {
2033 let dir = std::env::temp_dir().join(format!("mimo-frames-{}", std::process::id()));
2034 std::fs::create_dir_all(&dir).unwrap();
2035 for (i, name) in ["f10.ppm", "f2.ppm", "f1.ppm", "notes.txt"]
2036 .iter()
2037 .enumerate()
2038 {
2039 let mut b = b"P6\n1 1\n255\n".to_vec();
2040 b.extend_from_slice(&[i as u8, 0, 0]);
2041 std::fs::write(dir.join(name), b).unwrap();
2042 }
2043 let src = VideoSource::open(&dir, Some(1.0)).unwrap();
2044 assert_eq!(src.frame_count(), 3);
2045 let order: Vec<u8> = (0..3).map(|i| src.read_frame(i).unwrap().data[0]).collect();
2046 assert_eq!(order, vec![2, 1, 0], "f1, f2, f10");
2047 assert!(
2048 VideoSource::open(&dir, None).is_err(),
2049 "a directory needs a rate"
2050 );
2051 std::fs::remove_dir_all(&dir).ok();
2052 }
2053
2054 #[test]
2055 fn expansion_counts_and_rejects_mismatches() {
2056 let tok = Tokenizer::byte_level();
2057 let img = VisualInput {
2058 kind: VisualKind::Image,
2059 rows: Vec::new(),
2060 grid_t: 1,
2061 grid_h: 4,
2062 grid_w: 6,
2063 patch_dim: 1536,
2064 merge_size: 2,
2065 timestamps: Vec::new(),
2066 frame_indices: Vec::new(),
2067 resized: (64, 96),
2068 };
2069 let ids = [1, VISION_START_ID, IMAGE_PAD_ID, VISION_END_ID, 2];
2070 let out = expand_prompt_ids(&ids, &[&img], &[], &[], &tok).unwrap();
2071 assert_eq!(out.len(), 2 + 2 + 6);
2072 assert_eq!(out.iter().filter(|&&t| t == IMAGE_PAD_ID).count(), 6);
2073 let again = expand_prompt_ids(&out, &[&img], &[], &[], &tok).unwrap();
2075 assert_eq!(again, out);
2076 assert!(expand_prompt_ids(&ids, &[], &[], &[], &tok).is_err());
2077 assert!(expand_prompt_ids(&ids, &[&img, &img], &[], &[], &tok).is_err());
2078 assert!(expand_prompt_ids(&[IMAGE_PAD_ID], &[], &[], &[], &tok).is_err());
2079 let audio = [AUDIO_START_ID, AUDIO_PAD_ID, AUDIO_END_ID];
2080 let a = expand_prompt_ids(&audio, &[], &[], &[32], &tok).unwrap();
2081 assert_eq!(a.len(), 34);
2082 }
2083
2084 #[test]
2085 fn patchify_orders_rows_by_merge_block() {
2086 let cfg = MimoProcessorConfig {
2087 patch_size: 1,
2088 merge_size: 2,
2089 temporal_patch_size: 2,
2090 ..MimoProcessorConfig::default()
2091 };
2092 let mut f = vec![0f32; 3 * 8];
2094 for i in 0..8 {
2095 f[i] = i as f32;
2096 }
2097 let (rows, gt, gh, gw) = patchify(&[&f, &f], 2, 4, &cfg).unwrap();
2098 assert_eq!((gt, gh, gw), (1, 2, 4));
2099 let ch0: Vec<f32> = rows.chunks(6).map(|r| r[0]).collect();
2101 assert_eq!(ch0, vec![0., 1., 4., 5., 2., 3., 6., 7.]);
2102 }
2103}