Skip to main content

cortiq_engine/
mimo_vision.rs

1//! MiMo-V2.6 vision: image and video preprocessing, prompt-id expansion for
2//! the image/video placeholders, and the MiMo ViT (`visual.*`).
3//!
4//! Reference semantics are the upstream processor (`MiMoProcessor`) and the
5//! HF `MiMoVisionTransformer`, with the merger taken as vLLM/sglang build it
6//! (RMSNorm, no biases — the checkpoint carries only `ln_q.weight` and the
7//! two merger matrices). Conventions that are easy to get wrong:
8//!
9//! * **Resize is `F.interpolate(bilinear, align_corners=False)` on 0..255**,
10//!   with no antialias and no rounding back to 8 bit, then ImageNet
11//!   mean/std *in 0..255 units*. `smart_resize` uses factor 32 and has an
12//!   upscale branch for a side below 32 that skips the aspect check.
13//! * **Every image is two identical frames** (T = 2), and each patch row is
14//!   the Conv3d kernel flattened as (c, tt, py, px).
15//! * **Rows are in merge-block order** (t, block row, block col, mh, mw).
16//!   Window blocks of type 1 run in *column* order: whole merge units are
17//!   listed by (t, block col, block row), the RoPE tables are permuted the
18//!   same way, and the band |i−j| ≤ 64 is taken over chunk-local indices in
19//!   the CURRENT order. Frames are independent chunks in every block.
20//! * **Sinks add to key 0's logit** (HF, vLLM `sinks_bias_key0`), they are
21//!   not an extra softmax column like the text model's sinks. Beyond query
22//!   64 key 0 is masked and the sink does nothing. `CMF_MIMO_VIT_SINK`
23//!   (`key0` | `column` | `off`) switches it for A/B only.
24//! * **GQA 32/8, head_dim 64 (`qk_channels`), not hidden/heads = 40.**
25//!
26//! Weights are read by source name from any CMF that carries them — the
27//! `<stem>.mm.cmf` companion or a single-file multimodal CMF — through
28//! [`MimoVit::from_model`]. Dense (F32/F16/BF16) matrices become exact f32
29//! GEMM operands; quantized ones (q4tp, q8_2f) stay mapped on the engine's
30//! kernels, CPU or GPU.
31//!
32//! The tower is unusually sensitive to weight quantization (G4.1, measured
33//! against the exact tower on 5 fixture images): q4tp gives a mean row cosine
34//! of 0.935, GPTQ-rounded q4tp 0.976, q8_2f 0.995 (CPU, int8 activations) to
35//! 0.998 (Vulkan). Every matrix group fails at q4tp on its own except the
36//! merger. The residual reaches ~4e5 in the last block, which is also why
37//! GEMM inputs are range-guarded (`Lin::mm`).
38
39use 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
58/// Name of the U8 blob that carries the checkpoint's full `config.json`.
59pub 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
66/// Full-attention frame chunks this process ran through `gpu::dit_attention`
67/// (so a timing on a live backend cannot be mistaken for the host path).
68pub fn gpu_attention_dispatches() -> usize {
69    GPU_ATTENTION_DISPATCHES.load(std::sync::atomic::Ordering::Relaxed)
70}
71
72// ─────────────────────────────── processor ───────────────────────────────
73
74/// The processor knobs, read from `config.json` (`processor_config` plus the
75/// vision geometry). Missing fields take the upstream processor's defaults.
76#[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    /// Sampling rate of video frames, frames per second of source time.
87    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    /// The values pinned by MiMo-V2.6-Flash's `config.json`.
96    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    /// Build from the full `config.json` value.
121    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        // `x or default` in the upstream processor: a null/0 takes the default.
142        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    /// `patch_size · merge_size`: every resized side is a multiple of it.
169    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
178/// Python's `round` (ties to even) for the non-negative values used here.
179fn 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
193/// MiMo's `smart_resize` (processor `MiMoProcessor.smart_resize`), returning
194/// `(height, width)`. Note the upscale branch: when the short side is below
195/// `factor` both sides are scaled up first and the aspect check is skipped.
196pub 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
243/// Per-output-index taps of torch's CPU `upsample_bilinear2d` along one axis
244/// (`align_corners=False`, scale from sizes, float opmath): `(i0, i1, w0, w1)`.
245fn 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
259/// `F.interpolate(x, size=(out_h,out_w), mode="bilinear",
260/// align_corners=False)` on a planar `[c][h][w]` f32 image, no antialias.
261/// Taps nest as torch does: interpolate along W on the two source rows,
262/// then along H.
263pub 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
296/// HWC u8 → planar CHW f32 in 0..255.
297fn 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
308/// Resize one frame to `(hb, wb)` and standardize it: `(x − mean) / std`.
309fn 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/// What a visual item is; it decides the placeholder and its expansion.
323#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324pub enum VisualKind {
325    Image,
326    Video,
327}
328
329/// One preprocessed image or video: the patch rows fed to the ViT, the
330/// patch grid, and (video) the per-frame timestamps in seconds.
331#[derive(Clone, Debug)]
332pub struct VisualInput {
333    pub kind: VisualKind,
334    /// `[grid_t · grid_h · grid_w, 3 · T · P · P]`, merge-block row order.
335    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    /// Video: one timestamp per frame after even padding (`2 · grid_t`).
342    pub timestamps: Vec<f32>,
343    /// Video: the source frame index of each entry of `timestamps`.
344    pub frame_indices: Vec<usize>,
345    /// The resized frame size `(height, width)`.
346    pub resized: (usize, usize),
347}
348
349impl VisualInput {
350    /// ViT rows (patches).
351    pub fn patches(&self) -> usize {
352        self.grid_t * self.grid_h * self.grid_w
353    }
354
355    /// LLM placeholder tokens for one temporal step.
356    pub fn tokens_per_step(&self) -> usize {
357        self.grid_h * self.grid_w / (self.merge_size * self.merge_size)
358    }
359
360    /// LLM placeholder tokens for the whole item.
361    pub fn tokens(&self) -> usize {
362        self.grid_t * self.tokens_per_step()
363    }
364
365    /// The "MM:SS" label of every temporal step (video only): the timestamp
366    /// of the first frame of each pair.
367    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
374/// Patchify `frames` (each planar CHW at `hb × wb`, count a multiple of T)
375/// exactly as `view(gt,T,C,gh/m,m,P,gw/m,m,P).permute(0,3,6,4,7,2,1,5,8)`.
376fn 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
420/// Preprocess one image: smart_resize (factor 32), bilinear resize on
421/// 0..255, standardize, duplicate to T frames, patchify. `max_pixels`
422/// overrides `image_max_pixels` (the `--image-max-pixels` knob).
423pub 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
455// ───────────────────────────────── video ─────────────────────────────────
456
457/// Frame count for `total` source frames at `video_fps` (sglang
458/// `smart_nframes` with the processor defaults): `total / fps_src · fps`,
459/// clamped to `[min_frames, max_frames]` and `total`, floored to even.
460pub 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
482/// The sampled frame indices and their timestamps:
483/// `unique(int64(linspace(0, total−1, n)))` and `float32(idx) / fps_src`.
484pub 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
508/// Per-frame pixel ceiling for `n_sampled` frames:
509/// `max(min_pixels, min(total_max · T // n, max_pixels))`.
510pub 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
516/// `f"{int(ts // 60):02d}:{int(ts % 60):02d}"` on the float32 timestamp,
517/// with torch's float floor-division/remainder. Minutes are not wrapped.
518pub 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/// The Y4M stream layout needed to seek to a frame.
525#[derive(Clone, Debug)]
526pub struct Y4mInfo {
527    pub width: usize,
528    pub height: usize,
529    pub fps: f64,
530    chroma: Chroma,
531    /// Byte offset of every frame's payload.
532    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/// A silent video given as decoded frames: a directory of images with an
555/// explicit frame rate, or a Y4M stream (`ffmpeg -i in.mp4 -pix_fmt yuv420p
556/// out.y4m`). mp4 decoding is out of scope for v1.
557#[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
565/// Natural-order key: digit runs compare numerically, so `f2` < `f10`.
566fn 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    /// Every image file of `dir` (png/jpg/jpeg/webp/gif/ppm), in natural
591    /// name order, played at `fps`.
592    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    /// Index a Y4M file (8-bit 4:2:0 / 4:2:2 / 4:4:4 / mono).
618    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    /// A directory (needs `fps`) or a `.y4m` file (its own rate; an explicit
714    /// `fps` overrides it).
715    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    /// Decode frame `idx` to RGB.
747    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
760/// BT.601 limited-range YUV → RGB with nearest-neighbour chroma — the
761/// swscale default for untagged 8-bit video. (The upstream processor gets
762/// frames from its own decoder; Y4M has no reference path to match.)
763fn 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
806/// Preprocess already-sampled frames with their timestamps: per-frame pixel
807/// budget from the sampled count, even padding (repeat the last frame and
808/// its timestamp), one smart_resize for all frames, standardize, and pair
809/// consecutive frames as temporal patches.
810pub 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
866/// Sample, decode and preprocess a video source.
867pub 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
880// ─────────────────────────────── prompt ids ──────────────────────────────
881
882/// Expand the rendered prompt's media placeholders.
883///
884/// The chat template renders one `<|vision_start|><|image_pad|><|vision_end|>`
885/// per image part, `…<|video_pad|>…` per video part and
886/// `<|mimo_audio_start|><|audio_pad|><|mimo_audio_end|>` per audio part.
887/// Like the upstream regex (`(?:pad)+`) a run of pads between the markers
888/// is one placeholder. Expansion, in order within each modality:
889/// * image → `[vs] + N×[image_pad] + [ve]`, N = the item's tokens;
890/// * video → `[video_start] + Σₜ(encode("MM:SS") + [vs] + n×[video_pad] +
891///   [ve]) + [video_end]`, the WHOLE triple replaced;
892/// * audio → `[as] + K×[audio_pad] + [ae]`, K from `audio_tokens`.
893///
894/// A placeholder/item count mismatch, or a pad token outside a
895/// placeholder, is an error.
896pub 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    // Length of a `start pad+ end` run at `p`, if one starts there.
907    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// ────────────────────────────────── ViT ──────────────────────────────────
991
992/// `vision_config` geometry.
993#[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    /// `vit_window_attn_types`: 1 = column order, anything else = row order.
1008    pub window_types: Vec<i64>,
1009    /// `visual_token_window_size`; `None` = no band (`≤ 0` upstream).
1010    pub window: Option<usize>,
1011    pub use_sink: bool,
1012    pub eps: f64,
1013    pub rope_theta: f32,
1014}
1015
1016impl MimoVisionConfig {
1017    /// From the full `config.json` (reads `vision_config`) or from the
1018    /// `vision_config` object itself.
1019    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/// How the per-head sinks enter the windowed blocks.
1118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1119pub enum SinkMode {
1120    /// HF / vLLM: `logit[h, i, key 0] += sinks[h]` (the default).
1121    Key0,
1122    /// sglang FA3: an extra softmax column carrying no value.
1123    Column,
1124    /// No sinks (A/B only).
1125    Off,
1126}
1127
1128impl SinkMode {
1129    /// `CMF_MIMO_VIT_SINK` = `key0` (default) | `column` | `off`.
1130    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
1139/// A named projection. The name keys the GPTQ Hessian capture: dense
1140/// weights run as f32 GEMM operands, which the `QTensor` hook never sees,
1141/// so the tower folds its own inputs while `gptq_capture` is active.
1142struct Lin {
1143    name: String,
1144    p: Proj,
1145}
1146
1147/// Activations above this are scaled down by a power of two before a GEMM.
1148/// The device GEMMs on matrix units stage their operands as f16 (max 65504),
1149/// and the last ViT block's down_proj input reaches ~1.1e5 (448² image) —
1150/// an inf, then NaN rows, on the default NVIDIA path. A power-of-two scale
1151/// is exact in f32, so the host result is bit-identical either way.
1152const 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
1190/// The MiMo vision transformer and its patch merger.
1191pub 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    /// Use `gpu::dit_attention` for the full blocks when a backend is live.
1203    gpu_attention: bool,
1204    /// `CMF_MIMO_VIT_TRACE=1`: per-block activation ranges on stderr.
1205    trace: bool,
1206}
1207
1208fn max_abs(x: &[f32]) -> f32 {
1209    x.iter().fold(0f32, |m, v| m.max(v.abs()))
1210}
1211
1212/// Whether `model` carries the vision tower.
1213pub fn has_vision(model: &CmfModel) -> bool {
1214    model.tensor(PATCH_EMBED).is_some()
1215}
1216
1217/// The checkpoint `config.json` stored in the file as [`MM_CONFIG_TENSOR`].
1218pub 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                // SAFETY: rows are disjoint across workers.
1270                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
1288/// `erf` by the Numerical Recipes rational form (1.2e-7 relative).
1289fn 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
1310/// Row permutation into column order: merge units listed by (t, b, a),
1311/// each unit's `m²` rows kept together. `perm[dst] = src`.
1312pub 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    /// Load from a CMF that carries `visual.*` and the `mm.config_json` blob.
1344    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    /// Load `visual.*` from `model` with an explicit config (`config.json`
1350    /// or its `vision_config`).
1351    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    /// Allow (default) or forbid the device attention for the full blocks.
1431    pub fn set_gpu_attention(&mut self, on: bool) {
1432        self.gpu_attention = on;
1433    }
1434
1435    /// The row-order RoPE tables `(cos, sin)`, `[rows, head_dim]` each,
1436    /// laid out `[h, w, h, w]` in quarters.
1437    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        // torch: 1 / theta ** (arange(0, dim, 2, f32) / dim), dim = hd/2.
1442        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    /// Encode one image or video: `[tokens, out_hidden]`, in the order the
1476    /// item's placeholders take them (raster (t, block row, block col)).
1477    pub fn forward(&self, input: &VisualInput) -> Result<Vec<f32>, String> {
1478        // Keep the fp32 oracle contract with default GPU settings as well:
1479        // cooperative GEMMs/attention otherwise round operands to fp16.
1480        // This also covers the dense patch projection and exact dev towers,
1481        // which do not carry a mapped model identity into their GEMM calls.
1482        #[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        // Merger: RMSNorm per row, four consecutive rows per merge unit.
1559        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        // Split and rotate: q [n, heads·hd], k/v [n, kv_heads·hd].
1601        {
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                            // SAFETY: each row writes only its own slice.
1621                            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    /// One frame chunk of a full block on the device, head-major panels in,
1686    /// token-major `[l, heads·hd]` out. `false` = not taken (no backend,
1687    /// refused, or too large for the n² score scratch).
1688    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    /// Exact full attention over one frame chunk: per head, query tiles
1733    /// through the engine GEMM, row softmax, P·V.
1734    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                                // SAFETY: disjoint score rows per worker.
1787                                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    /// Windowed attention over one frame chunk (|i−j| ≤ window in the
1817    /// current order), with the per-head sinks as `sink_mode` says.
1818    /// `window = None` means every key (a sink-carrying block without band).
1819    #[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                        // SAFETY: each query row writes only its own slice.
1876                        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
1888/// In-place softmax; `column` is an extra logit that joins the max and the
1889/// denominator but carries no value (sglang-style sink).
1890fn 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
1909/// Activation buffers reused across blocks.
1910struct Scratch {
1911    /// Seconds spent in full / windowed attention (trace only).
1912    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        // Upscale branch: 20 → 32, 300 → 480, no aspect check.
1955        assert_eq!(smart_resize(20, 300, 32, lo, hi).unwrap(), (32, 480));
1956        // Ties to even: 48/32 = 1.5 → 2, 80/32 = 2.5 → 2.
1957        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        // 5 frames: max(5, 8) clamps to 5 → floor even 4.
1971        assert_eq!(smart_nframes(5, 30.0, &cfg).unwrap(), 4);
1972        // 10 s at 30 fps → 10 → 10.
1973        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        // gh = 2, gw = 4 → one unit row, two unit columns.
1990        let p = column_permutation(1, 4, 4, 2);
1991        // units (a,b): (0,0)=0 (0,1)=1 (1,0)=2 (1,1)=3; column order 0,2,1,3.
1992        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        // 4x2 4:2:0 at 30000/1001 fps, 3 frames; frame k has luma 16+100k,
2002        // neutral chroma except frame 2 (U=V=200 → reddish-magenta).
2003        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        // 1.164383 · 100 = 116.4 → 116 on every channel.
2019        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        // An explicit rate overrides the header.
2027        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        // Already-expanded runs count as one placeholder, like the regex.
2074        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        // 2x4 frame, value = y*4+x in channel 0.
2093        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        // row dim = 3*2*1*1 = 6; channel 0 is values [0..2) of each row.
2100        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}