Skip to main content

cortiq_engine/
videogen.rs

1//! End-to-end MiniMax-H3 text→(video + synchronized stereo audio):
2//! Qwen3-VL prompt encode → 4-step dual-schedule flow sampling over the
3//! packed DiT → ViT3D video decode and BigVGAN audio decode.
4//!
5//! Stages load and drop one at a time, as the image pipeline next door
6//! does: peak resident is one component, not their sum.
7//!
8//! ## Two clocks, four steps
9//!
10//! The sampler walks the VIDEO sigma grid — `simple` at shift 12, which
11//! at four steps is 1, 0.973, 0.923, 0.8, 0 — and the audio stream is
12//! integrated on its own remap of that grid (shift 3). Stepping both on
13//! the video grid is what a stock sampler does, and it is fine at
14//! twenty steps and audibly wrong at four: `Δσ_a` and `Δσ_v` differ by
15//! a factor of three over the last interval, and no per-step slope
16//! correction fixes a step that large. Hence `--stock-sampler`, which
17//! reproduces the broken behaviour on purpose, for comparison.
18
19use crate::audiovae::AudioVae;
20use crate::mmh3::{Layout, MiniMaxH3, time_shift_sigma};
21use crate::qwen3te::{ImageSpan, Qwen3Encoder};
22use crate::qwen3vis::{self, VisionTower};
23use crate::sampler::SplitMix64;
24use crate::tokenizer::Tokenizer;
25use crate::vae3d::VideoVae;
26use crate::vae3d::VideoVaeEncoder;
27use std::path::Path;
28use std::sync::Arc;
29
30pub const FPS: usize = 24;
31pub const AUDIO_LATENT_FPS: usize = 40;
32
33pub struct AnimParams {
34    pub width: usize,
35    pub height: usize,
36    /// Frames at 24 fps; snapped up to the model's 17k+5 grid.
37    pub frames: usize,
38    pub steps: usize,
39    pub seed: u64,
40    /// Integrate the audio on the video's grid, as a stock sampler
41    /// would. Wrong at four steps; kept for A/B.
42    pub stock_sampler: bool,
43    pub max_tokens: usize,
44    /// RGB in [0, 1] as `[3, h, w]` with its size — the clip's first
45    /// frame, and/or its last.
46    pub first_frame: Option<(Vec<f32>, usize, usize)>,
47    pub last_frame: Option<(Vec<f32>, usize, usize)>,
48    /// Extra reference frames with the pixel index each one stands for:
49    /// video-to-video as this architecture actually takes it — every
50    /// frame is a condition row pinned to its own time coordinate, the
51    /// same machinery `--first-frame`/`--last-frame` use for two.
52    pub mid_frames: Vec<((Vec<f32>, usize, usize), usize)>,
53    /// A LoRA adapter (.safetensors) applied at runtime, and how hard.
54    pub lora: Option<String>,
55    pub lora_strength: f32,
56    /// The published latent upscaler (.safetensors) and the factor to
57    /// apply: the denoised latent is resized by the learned net and the
58    /// VAE decodes at the larger size, so the 5 B-parameter decode →
59    /// pixel resize → encode round trip never happens.
60    pub upscale: Option<String>,
61    pub upscale_by: f32,
62    /// Chunk-causal (streaming) generation: latent frames per chunk, and
63    /// how many chunks a chunk may see — `sink` from the start of the
64    /// clip and a sliding `window` of recent ones. 0 chunks = the
65    /// bidirectional path. This is what a streaming adapter is trained
66    /// for, and it is what stops the activation cache growing with the
67    /// clip's length.
68    pub stream_chunk: usize,
69    pub stream_sink: usize,
70    pub stream_window: usize,
71}
72
73/// The vision-block token ids the H3 presentation flanks a picture with.
74const VISION_START: u32 = 151_652;
75const VISION_END: u32 = 151_653;
76
77/// Resize `[3, h, w]` RGB to the canvas. The first frame is a geometry
78/// anchor and is stretched; the last one follows and is cover-cropped,
79/// which is what the reference node does with each.
80pub fn fit_to_canvas(
81    rgb: &[f32],
82    h: usize,
83    w: usize,
84    out_h: usize,
85    out_w: usize,
86    crop: bool,
87) -> Vec<f32> {
88    // Cover-crop picks the largest centred rectangle of the source with
89    // the target's aspect; a stretch takes the whole thing.
90    let (sx0, sy0, sw, sh) = if crop {
91        let (tw, th) = (out_w as f64, out_h as f64);
92        let scale = (w as f64 / tw).min(h as f64 / th);
93        let (cw, ch) = ((tw * scale).round() as usize, (th * scale).round() as usize);
94        ((w - cw) / 2, (h - ch) / 2, cw.max(1), ch.max(1))
95    } else {
96        (0, 0, w, h)
97    };
98    let mut out = vec![0f32; 3 * out_h * out_w];
99    for c in 0..3 {
100        for y in 0..out_h {
101            let sy = ((y as f64 + 0.5) * sh as f64 / out_h as f64 - 0.5).max(0.0);
102            let y0 = sy.floor() as usize;
103            let y1 = (y0 + 1).min(sh - 1);
104            let fy = (sy - y0 as f64) as f32;
105            for x in 0..out_w {
106                let sx = ((x as f64 + 0.5) * sw as f64 / out_w as f64 - 0.5).max(0.0);
107                let x0 = sx.floor() as usize;
108                let x1 = (x0 + 1).min(sw - 1);
109                let fx = (sx - x0 as f64) as f32;
110                let p = |yy: usize, xx: usize| rgb[(c * h + sy0 + yy) * w + sx0 + xx];
111                let top = p(y0, x0) * (1.0 - fx) + p(y0, x1) * fx;
112                let bot = p(y1, x0) * (1.0 - fx) + p(y1, x1) * fx;
113                out[(c * out_h + y) * out_w + x] = top * (1.0 - fy) + bot * fy;
114            }
115        }
116    }
117    out
118}
119
120impl Default for AnimParams {
121    fn default() -> Self {
122        Self {
123            width: 512,
124            height: 288,
125            frames: 39,
126            steps: 4,
127            seed: 42,
128            stock_sampler: false,
129            max_tokens: 512,
130            first_frame: None,
131            last_frame: None,
132            mid_frames: Vec::new(),
133            lora: None,
134            lora_strength: 1.0,
135            upscale: None,
136            upscale_by: 2.0,
137            stream_chunk: 0,
138            stream_sink: 2,
139            stream_window: 2,
140        }
141    }
142}
143
144/// The rendered result: RGB in [0, 1] as `[3, frames, h, w]`, and
145/// stereo f32 in [-1, 1] as `[2, samples]`.
146pub struct Anim {
147    pub rgb: Vec<f32>,
148    pub frames: usize,
149    pub height: usize,
150    pub width: usize,
151    pub audio: Vec<f32>,
152    pub samples: usize,
153    pub sample_rate: usize,
154}
155
156/// Frame counts snap UP to 17k+5 — the grid the temporal VAE and the
157/// DiT's frame-span pattern agree on.
158pub fn align_frames(n: usize) -> usize {
159    let mut n = n.max(5);
160    while n % 17 != 5 {
161        n += 1;
162    }
163    n
164}
165
166pub fn video_latent_t(frames: usize) -> usize {
167    if frames <= 5 {
168        2
169    } else {
170        (frames - 5) / 17 * 5 + 2
171    }
172}
173
174/// `(frames, latent_t, audio_t)` for a requested length.
175pub fn temporal_shape(len: usize) -> (usize, usize, usize) {
176    let frames = align_frames(len);
177    let audio_t = ((frames as f64 / FPS as f64) * AUDIO_LATENT_FPS as f64).round() as usize;
178    (frames, video_latent_t(frames), audio_t)
179}
180
181/// The `simple` scheduler over `ModelSamplingDiscreteFlow(shift)`: the
182/// 1000-entry sigma table sampled at even strides, terminal 0 appended.
183pub fn sigmas(steps: usize, shift: f64) -> Vec<f64> {
184    let table = 1000usize;
185    let mut out: Vec<f64> = (0..steps)
186        .map(|x| {
187            let idx = table - 1 - x * table / steps;
188            let t = (idx + 1) as f64 / table as f64;
189            shift * t / (1.0 + (shift - 1.0) * t)
190        })
191        .collect();
192    out.push(0.0);
193    out
194}
195
196/// Shared with the DiT's condition-row noise blend.
197pub fn gauss_pub(n: usize, seed: u64) -> Vec<f32> {
198    gauss(n, seed)
199}
200
201fn gauss(n: usize, seed: u64) -> Vec<f32> {
202    let mut rng = SplitMix64::new(seed);
203    let mut u = || (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
204    let mut out = Vec::with_capacity(n);
205    while out.len() < n {
206        let (a, b) = (u().max(1e-300), u());
207        let r = (-2.0 * a.ln()).sqrt();
208        let ang = 2.0 * std::f64::consts::PI * b;
209        out.push((r * ang.cos()) as f32);
210        if out.len() < n {
211            out.push((r * ang.sin()) as f32);
212        }
213    }
214    out
215}
216
217/// Text→(video, audio) from a packaged `.cmf`.
218pub fn generate(
219    path: &Path,
220    prompt: &str,
221    p: &AnimParams,
222    mut progress: impl FnMut(&str, usize, usize),
223) -> Result<Anim, String> {
224    if p.width % 32 != 0 || p.height % 32 != 0 {
225        return Err("width/height must be multiples of 32".into());
226    }
227    // The wgpu wide-GEMM arm was measured WRONG on one driver stack
228    // (RTX PRO 6000: step-1 velocity rms off, step-2 NaN) and byte-
229    // healthy on another (2×RTX 5090, coop and plain arms within 0.5%
230    // of each other and 3.5% of the host render). Trust is therefore
231    // PER-STACK, decided by a parity probe on this file's own first
232    // qkv weight at DiT-scale activations — not by a hardcoded verdict
233    // either way. CMF_MMH3_GPU=1/0 still forces.
234    let use_gpu = match std::env::var("CMF_MMH3_GPU").ok().as_deref() {
235        Some("1") => true,
236        Some("0") => false,
237        _ => mmh3_gpu_parity_probe(path).unwrap_or(false),
238    };
239    if use_gpu {
240        generate_inner(path, prompt, p, &mut progress)
241    } else {
242        crate::gpu::cpu_scope(|| generate_inner(path, prompt, p, &mut progress))
243    }
244}
245
246/// GPU-vs-host parity on the packed DiT's first attention projection:
247/// the real q4tp bytes, activations spanning the modulation range
248/// (±2000 mixed with ±2), rms gate at 1e-2 — the measured failure was
249/// ~24%, honest drift is ~1e-5, so the gate has a decade of margin on
250/// each side. Any refusal (no adapter, dtype outside the kernel) is a
251/// clean "no": the host path is never wrong, only slower.
252fn mmh3_gpu_parity_probe(path: &Path) -> Result<bool, String> {
253    let model = Arc::new(
254        cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
255    );
256    // Any codec, not just q4tp: the probe's job is to check THIS file's
257    // device arm against the host, and a container packed as q8_2f has one
258    // too. Matching on dtype sent every non-q4tp build to the CPU for the
259    // whole render — measured at 357 s against 60 on the same card.
260    let Some(idx) = model
261        .tensors
262        .iter()
263        .position(|t| t.name.starts_with("dit.") && t.name.ends_with("attn.qkv_proj.weight"))
264    else {
265        tracing::info!("mmh3 GPU parity probe: no qkv weight — host path");
266        return Ok(false);
267    };
268    let entry = &model.tensors[idx];
269    let (rows, cols) = (entry.shape[0], entry.shape[1]);
270    let b = 64usize;
271    let mut xs = vec![0f32; b * cols];
272    for (i, v) in xs.iter_mut().enumerate() {
273        let base = ((i * 37 + 11) % 1009) as f32 / 1009.0 - 0.5;
274        *v = base * if i % 7 == 0 { 2000.0 } else { 2.0 };
275    }
276    let mut gpu = vec![0f32; b * rows];
277    if std::env::var("CMF_GPU_DEBUG").is_ok() {
278        eprintln!(
279            "mmh3 probe env: CMF_GPU={:?} enabled={} avail={}",
280            std::env::var("CMF_GPU").ok(),
281            crate::gpu::enabled(),
282            crate::gpu::backend_available(),
283        );
284    }
285    let qt = crate::qtensor::QTensor::from_model(&model, &entry.name.clone())?;
286    if !qt.device_matmat(&xs, b, &mut gpu) {
287        tracing::info!(
288            "mmh3 GPU parity probe: {:?} device GEMM refused ({rows}x{cols}) — host path",
289            entry.dtype
290        );
291        if std::env::var("CMF_GPU_DEBUG").is_ok() {
292            eprintln!(
293                "mmh3 probe: device GEMM refused {rows}x{cols} for {:?}",
294                entry.dtype
295            );
296        }
297        return Ok(false);
298    }
299    let host = {
300        let name = entry.name.clone();
301        let proj = crate::dit::Proj::from_model(&model, &name)?;
302        let mut out = vec![0f32; b * rows];
303        crate::gpu::cpu_scope(|| proj.matmat(&xs, b, &mut out, None));
304        out
305    };
306    let mut num = 0f64;
307    let mut den = 0f64;
308    for (g, h) in gpu.iter().zip(&host) {
309        num += ((g - h) as f64).powi(2);
310        den += (*h as f64).powi(2);
311    }
312    let rel = (num / den.max(1e-30)).sqrt();
313    let ok = rel < 1e-2;
314    tracing::info!(
315        "mmh3 GPU parity probe: rel rms {rel:.2e} → {}",
316        if ok { "device" } else { "host" }
317    );
318    if std::env::var("CMF_GPU_DEBUG").is_ok() {
319        eprintln!("mmh3 GPU parity probe: rel rms {rel:.2e}");
320    }
321    Ok(ok)
322}
323
324fn generate_inner(
325    path: &Path,
326    prompt: &str,
327    p: &AnimParams,
328    progress: &mut dyn FnMut(&str, usize, usize),
329) -> Result<Anim, String> {
330    let model = Arc::new(
331        cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
332    );
333    let (frames_total, latent_t, audio_t) = temporal_shape(p.frames);
334    let (lat_h, lat_w) = (p.height / 16, p.width / 16);
335
336    // ── prompt ──
337    // The H3 presentation is raw text: no chat template, no BOS, no
338    // special tokens at all.
339    // Stage clock. Half of a render is not the DiT — at 8 steps the
340    // denoiser is 63 s of 116 — and until this line existed there was
341    // no way to see which half anything went to.
342    let t_stage = std::time::Instant::now();
343    let mut marks: Vec<(&str, f32)> = Vec::new();
344    let lap = |marks: &mut Vec<(&'static str, f32)>, name: &'static str| {
345        let prev: f32 = marks.iter().map(|(_, v)| v).sum();
346        marks.push((name, t_stage.elapsed().as_secs_f32() - prev));
347    };
348    let vocab = model
349        .vocab
350        .as_deref()
351        .ok_or("packaged .cmf has no embedded tokenizer")?;
352    let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
353    // fl2va: every keyframe is presented as "<Picture i>: " and a
354    // vision block BEFORE the prompt, and separately conditions the DiT
355    // as a latent. Both halves come from the same picture.
356    let mut keyframes: Vec<(&(Vec<f32>, usize, usize), usize)> = p
357        .first_frame
358        .iter()
359        .map(|f| (f, 0usize))
360        .chain(
361            p.mid_frames
362                .iter()
363                .map(|(f, i)| (f, (*i).min(frames_total - 1))),
364        )
365        .chain(p.last_frame.iter().map(|f| (f, frames_total - 1)))
366        .collect();
367    // Condition rows are placed by time coordinate, so they have to arrive
368    // in time order and only once per frame — two references pinned to the
369    // same pixel index would be two rows claiming one moment.
370    keyframes.sort_by_key(|(_, i)| *i);
371    keyframes.dedup_by_key(|(_, i)| *i);
372    let mut ids: Vec<u32> = Vec::new();
373    let mut spans: Vec<ImageSpan> = Vec::new();
374    let mut embeds: Vec<Vec<f32>> = Vec::new();
375    let mut deepstack: Vec<Vec<f32>> = Vec::new();
376    let mut cond: Vec<Vec<f32>> = Vec::new();
377    let mut tags: Vec<u8> = Vec::new();
378
379    if !keyframes.is_empty() {
380        let tower = VisionTower::from_cmf(&model)?;
381        // An activation harvest (CMF_TE_ONLY) never denoises, and the
382        // VAE latent is the DiT's food alone — the frame's 3-D conv
383        // encode is 99.5 s of a 102.5 s M4 run. Skip it.
384        let te_only = std::env::var("CMF_TE_ONLY").as_deref() == Ok("1");
385        let venc = if te_only {
386            None
387        } else {
388            Some(VideoVaeEncoder::from_cmf(&model)?)
389        };
390        for (i, (frame, _)) in keyframes.iter().enumerate() {
391            let (src, sh, sw) = *frame;
392            // The picture the DiT sees is on the generation canvas; the
393            // one Qwen sees keeps its own resolution policy.
394            let fitted = fit_to_canvas(src, *sh, *sw, p.height, p.width, i > 0);
395            if let Some(venc) = &venc {
396                let (z, _, _) = venc.encode_frame(
397                    &fitted.iter().map(|&v| v * 2.0 - 1.0).collect::<Vec<_>>(),
398                    p.height,
399                    p.width,
400                );
401                cond.push(z);
402            }
403
404            for t in tok.encode(&format!("<Picture {}>: ", i + 1)) {
405                ids.push(t);
406                tags.push(1);
407            }
408            let (patches, gh, gw) = qwen3vis::preprocess(
409                &fitted,
410                p.height,
411                p.width,
412                tower.patch_size,
413                tower.temporal_patch,
414                tower.merge,
415            );
416            let (merged, deep) = tower.forward(&patches, gh, gw);
417            let n_img = merged.len() / tower.out_hidden;
418            // The whole block carries the VIDEO tag, the flanking
419            // markers included.
420            ids.push(VISION_START);
421            tags.push(0);
422            let start = ids.len();
423            for _ in 0..n_img {
424                ids.push(VISION_START); // a placeholder the embed replaces
425                tags.push(0);
426            }
427            ids.push(VISION_END);
428            tags.push(0);
429            spans.push(ImageSpan {
430                start,
431                len: n_img,
432                merged_h: gh / tower.merge,
433                merged_w: gw / tower.merge,
434            });
435            embeds.push(merged);
436            if deepstack.is_empty() {
437                deepstack = deep;
438            } else {
439                for (a, b) in deepstack.iter_mut().zip(deep) {
440                    a.extend_from_slice(&b);
441                }
442            }
443        }
444    }
445    for t in tok.encode(prompt) {
446        ids.push(t);
447        tags.push(1);
448    }
449    if ids.is_empty() {
450        ids.push(151643); // the pad id, as the reference does for ""
451        tags.push(1);
452    }
453    ids.truncate(p.max_tokens);
454    tags.truncate(ids.len());
455    lap(&mut marks, "prepare");
456    // The prompt encoder is a one-shot pass over 12 GB of weights; on a
457    // machine the file does not fit it streams from disk and its GEMMs
458    // run over any contention budget for reasons that are not contention.
459    // The kill stays disarmed until the encode is done (users on 24 GB
460    // Macs had to patch it out to keep the denoise loop on the GPU).
461    crate::gpu::mm_kill_arm(false);
462    progress("encode", 0, 1);
463    let states = {
464        let enc = Qwen3Encoder::from_cmf(&model)?;
465        enc.encode_with_images(&ids, &spans, &embeds, &deepstack)
466    };
467    // `CMF_TE_DUMP=<path>`: the conditioning as `[u64 n][u64 width]`
468    // then f32 rows. A stand-in encoder is only as good as the stream
469    // it hands the DiT, and that is measurable against the teacher's
470    // dump on the same prompt WITHOUT rendering a frame.
471    if let Ok(p) = std::env::var("CMF_TE_DUMP") {
472        let w = states.len() / ids.len().max(1);
473        let mut b = Vec::with_capacity(16 + states.len() * 4);
474        b.extend_from_slice(&(ids.len() as u64).to_le_bytes());
475        b.extend_from_slice(&(w as u64).to_le_bytes());
476        for v in &states {
477            b.extend_from_slice(&v.to_le_bytes());
478        }
479        std::fs::write(&p, &b).map_err(|e| format!("CMF_TE_DUMP {p}: {e}"))?;
480        eprintln!("te dump: {} tokens x {w} -> {p}", ids.len());
481    }
482    progress("encode", 1, 1);
483    lap(&mut marks, "text encode");
484    crate::gpu::mm_kill_arm(true);
485    // `CMF_TE_ONLY=1`: stop after the dump — an activation-harvest run
486    // (the ClipProj refit) wants hundreds of encodes and zero renders.
487    if std::env::var("CMF_TE_ONLY").as_deref() == Ok("1") {
488        return Err("CMF_TE_ONLY: encode dumped, render skipped".into());
489    }
490    // The prompt encoder and vision tower ran their once-per-generation
491    // pass; release their page cache so the denoise loop's DiT does not
492    // fight 12+ GB of dead weights for RAM. On a 24 GB Mac with the
493    // 25.7 GB full-encoder fl2va file this is the difference between
494    // denoise steps at DiT speed and 320 s/step of SSD thrash.
495    {
496        let dropped = model.advise_done(|n| n.starts_with("model.") || n.starts_with("vis."));
497        if dropped > 0 {
498            tracing::info!(
499                "encoder pages released after prompt encode: {} MB",
500                dropped / (1024 * 1024)
501            );
502        }
503    }
504
505    // ── denoise ──
506    let (mut video, audio) = {
507        // The adapter is read here, after the encoder's pages are gone:
508        // a rank-32 file for this DiT is 130 MB of f32 once expanded and
509        // there is no reason for it to share a peak with 12 GB of text
510        // tower on a 24 GB machine.
511        let bank = match p.lora.as_deref() {
512            None => None,
513            Some(path) => {
514                let k =
515                    crate::ltxlora::LoraBank::load(std::path::Path::new(path), p.lora_strength)?;
516                Some(k)
517            }
518        };
519        let dit = MiniMaxH3::from_cmf_lora(&model, bank.as_ref())?;
520        if let Some(k) = &bank {
521            let bound = dit.lora_bound();
522            // Say what did NOT land. An adaLN branch on a curve-form
523            // pack is the one real gap, and a user who sees "applied"
524            // while half the adapter sat out has been lied to.
525            let mut skipped: std::collections::BTreeMap<String, usize> = Default::default();
526            for name in k.keys() {
527                if !dit.lora_binds(name) {
528                    let fam = name
529                        .rsplit_once('.')
530                        .map(|(_, t)| {
531                            let head = name.split('.').next().unwrap_or("");
532                            format!("{head}…{t}")
533                        })
534                        .unwrap_or_else(|| name.to_string());
535                    *skipped.entry(fam).or_default() += 1;
536                }
537            }
538            let tail = if skipped.is_empty() {
539                String::new()
540            } else {
541                let parts: Vec<String> = skipped.iter().map(|(k, v)| format!("{k} ×{v}")).collect();
542                format!("; not applied: {}", parts.join(", "))
543            };
544            tracing::info!(
545                "lora: rank {}, {} branches, {} bound at strength {}{}",
546                k.rank(),
547                k.len(),
548                bound,
549                p.lora_strength,
550                tail
551            );
552            println!(
553                "lora: rank {}, {}/{} branches bound{}",
554                k.rank(),
555                bound,
556                k.len(),
557                tail
558            );
559        }
560        let kf: Vec<(usize, usize)> = keyframes
561            .iter()
562            .map(|&(_, idx)| (idx, frames_total))
563            .collect();
564        let layout = if kf.is_empty() {
565            Layout::t2va(ids.len(), latent_t, lat_h, lat_w, audio_t)
566        } else {
567            Layout::fl2va(ids.len(), latent_t, lat_h, lat_w, audio_t, &kf, &tags)
568        };
569        let text = dit.refine_text(&states, ids.len());
570        let mut v = gauss(dit.latents_dim * latent_t * lat_h * lat_w, p.seed);
571        let mut a = gauss(dit.audio_dim * 2 * audio_t, p.seed ^ 0x9E37_79B9_7F4A_7C15);
572        let sg = sigmas(p.steps, dit.shift_video);
573        let mut return_streaming: Option<(Vec<f32>, Vec<f32>)> = None;
574        // `CMF_ANIM_PROF=1`: the per-step rms of both streams and of
575        // their velocities. A run that is not denoising shows it here
576        // long before anything is written out.
577        let prof = std::env::var_os("CMF_ANIM_PROF").is_some();
578        let rms = |x: &[f32]| {
579            (x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64).sqrt()
580        };
581        if prof {
582            eprintln!(
583                "  text {} tok, refined rms {:.4}, sigmas {:?}",
584                ids.len(),
585                rms(&text),
586                sg.iter()
587                    .map(|v| (v * 1e4).round() / 1e4)
588                    .collect::<Vec<_>>()
589            );
590        }
591        // ── chunk-causal rollout ──
592        //
593        // Each chunk is denoised while seeing only the text, `sink` chunks
594        // from the start and a sliding `window` of recent ones, and it is
595        // conditioned on their finished frames at timestep 0 — the
596        // protocol a streaming adapter is trained for. The reference keeps
597        // that context in a KV cache; not packing the rows a chunk may not
598        // see produces the same attention pattern, and the cache is then
599        // an optimization of this rather than a prerequisite.
600        if p.stream_chunk > 0 && cond.is_empty() {
601            let hw = lat_h * lat_w / 4 * 4; // frame element count is c·h·w below
602            let _ = hw;
603            let vc = dit.latents_dim;
604            let ac = dit.audio_dim;
605            let vframe = lat_h * lat_w; // per channel, per latent frame
606            let mut chunks: Vec<std::ops::Range<usize>> = (0..latent_t)
607                .step_by(p.stream_chunk)
608                .map(|s| s..(s + p.stream_chunk).min(latent_t))
609                .collect();
610            // A tail shorter than half a chunk joins the one before it. A
611            // two-frame chunk with four frames of context is the one shape
612            // that reliably wandered off — a different street in the last
613            // half second — and it is an artefact of the arithmetic, not
614            // something anyone asked for.
615            if chunks.len() > 1 {
616                let tail = chunks[chunks.len() - 1].clone();
617                if tail.len() < p.stream_chunk.div_ceil(2) {
618                    chunks.pop();
619                    let last = chunks.len() - 1;
620                    chunks[last].end = tail.end;
621                }
622            }
623            // The audio grid runs at its own rate; split it in the same
624            // proportion so a chunk's sound is the sound of its frames.
625            let a_bound = |k: usize| (k * audio_t).div_ceil(latent_t.max(1));
626            let mut v_out = vec![0f32; v.len()];
627            let mut a_out = vec![0f32; a.len()];
628            let gather_v = |src: &[f32], idx: &[usize]| -> Vec<f32> {
629                let mut out = vec![0f32; vc * idx.len() * vframe];
630                for ci in 0..vc {
631                    for (n, &k) in idx.iter().enumerate() {
632                        let s = (ci * latent_t + k) * vframe;
633                        let d = (ci * idx.len() + n) * vframe;
634                        out[d..d + vframe].copy_from_slice(&src[s..s + vframe]);
635                    }
636                }
637                out
638            };
639            let gather_a = |src: &[f32], idx: &[usize]| -> Vec<f32> {
640                let mut out = vec![0f32; ac * 2 * idx.len()];
641                for ci in 0..ac {
642                    for ch in 0..2 {
643                        for (n, &i) in idx.iter().enumerate() {
644                            out[(ci * 2 + ch) * idx.len() + n] = src[(ci * 2 + ch) * audio_t + i];
645                        }
646                    }
647                }
648                out
649            };
650            for (ci_, cur) in chunks.iter().enumerate() {
651                // The sink and the window are counted in LATENT FRAMES, the
652                // unit the reference's KV cache holds: two frames pinned at
653                // the start and two trailing the current chunk, not two whole
654                // chunks of each. Counting them in chunks made a chunk attend
655                // to four chunks' worth of rows — which is both slower than
656                // the bidirectional path it replaces and not the pattern the
657                // adapter trained. `CMF_STREAM_UNIT=chunks` keeps the old
658                // reading for comparison.
659                let by_chunk = std::env::var("CMF_STREAM_UNIT").as_deref() == Ok("chunks");
660                let ctx_v: Vec<usize> = if by_chunk {
661                    let mut vis: Vec<usize> = (0..p.stream_sink.min(ci_)).collect();
662                    for j in ci_.saturating_sub(p.stream_window)..ci_ {
663                        if !vis.contains(&j) {
664                            vis.push(j);
665                        }
666                    }
667                    vis.sort_unstable();
668                    vis.iter().flat_map(|&j| chunks[j].clone()).collect()
669                } else {
670                    let done = cur.start; // every frame finished so far
671                    let mut f: Vec<usize> = (0..p.stream_sink.min(done)).collect();
672                    for k in done.saturating_sub(p.stream_window)..done {
673                        if !f.contains(&k) {
674                            f.push(k);
675                        }
676                    }
677                    f.sort_unstable();
678                    f
679                };
680                let cur_v: Vec<usize> = cur.clone().collect();
681                // Audio rows follow the video frames they belong to.
682                let mut ctx_a: Vec<usize> = Vec::new();
683                for &k in &ctx_v {
684                    for i in a_bound(k)..a_bound(k + 1) {
685                        if !ctx_a.contains(&i) {
686                            ctx_a.push(i);
687                        }
688                    }
689                }
690                ctx_a.sort_unstable();
691                let cur_a: Vec<usize> = (a_bound(cur.start)..a_bound(cur.end)).collect();
692                let clay = Layout::streaming(
693                    ids.len(),
694                    &tags,
695                    lat_h,
696                    lat_w,
697                    &ctx_v,
698                    &cur_v,
699                    &ctx_a,
700                    &cur_a,
701                );
702                let ctx_vx = gather_v(&v_out, &ctx_v);
703                let ctx_ax = gather_a(&a_out, &ctx_a);
704                let mut xv = gather_v(&v, &cur_v);
705                let mut xa = gather_a(&a, &cur_a);
706                for i in 0..p.steps {
707                    let (sv, sv_n) = (sg[i], sg[i + 1]);
708                    // [context frames | current frames], the order the
709                    // layout lays its rows out in.
710                    let mut vin = vec![0f32; vc * (ctx_v.len() + cur_v.len()) * vframe];
711                    let mut ain = vec![0f32; ac * 2 * (ctx_a.len() + cur_a.len())];
712                    let (nc, nk) = (ctx_v.len(), cur_v.len());
713                    for c in 0..vc {
714                        let d = c * (nc + nk) * vframe;
715                        vin[d..d + nc * vframe]
716                            .copy_from_slice(&ctx_vx[c * nc * vframe..(c + 1) * nc * vframe]);
717                        vin[d + nc * vframe..d + (nc + nk) * vframe]
718                            .copy_from_slice(&xv[c * nk * vframe..(c + 1) * nk * vframe]);
719                    }
720                    let (mc, mk) = (ctx_a.len(), cur_a.len());
721                    for c in 0..ac {
722                        for ch in 0..2 {
723                            let d = (c * 2 + ch) * (mc + mk);
724                            ain[d..d + mc]
725                                .copy_from_slice(&ctx_ax[(c * 2 + ch) * mc..(c * 2 + ch + 1) * mc]);
726                            ain[d + mc..d + mc + mk]
727                                .copy_from_slice(&xa[(c * 2 + ch) * mk..(c * 2 + ch + 1) * mk]);
728                        }
729                    }
730                    let (dv, da) = dit.forward(&clay, &text, &vin, &ain, sv, &[]);
731                    let step_v = (sv_n - sv) as f32;
732                    for (x, &d) in xv.iter_mut().zip(&dv) {
733                        *x += step_v * d;
734                    }
735                    let step_a = if p.stock_sampler {
736                        step_v
737                    } else {
738                        (time_shift_sigma(sv_n, dit.shift_video, dit.shift_audio)
739                            - time_shift_sigma(sv.max(1e-6), dit.shift_video, dit.shift_audio))
740                            as f32
741                    };
742                    for (x, &d) in xa.iter_mut().zip(&da) {
743                        *x += step_a * d;
744                    }
745                }
746                for c in 0..vc {
747                    for (n, &k) in cur_v.iter().enumerate() {
748                        let d = (c * latent_t + k) * vframe;
749                        let s = (c * cur_v.len() + n) * vframe;
750                        v_out[d..d + vframe].copy_from_slice(&xv[s..s + vframe]);
751                    }
752                }
753                for c in 0..ac {
754                    for ch in 0..2 {
755                        for (n, &i) in cur_a.iter().enumerate() {
756                            a_out[(c * 2 + ch) * audio_t + i] = xa[(c * 2 + ch) * cur_a.len() + n];
757                        }
758                    }
759                }
760                progress("stream", ci_ + 1, chunks.len());
761            }
762            if let Some(rep) = dit.lora_report() {
763                eprint!("{rep}");
764            }
765            return_streaming = Some((v_out, a_out));
766        }
767
768        for i in 0..p.steps {
769            if return_streaming.is_some() {
770                break;
771            }
772            let (sv, sv_n) = (sg[i], sg[i + 1]);
773            let (dv, da) = dit.forward(&layout, &text, &v, &a, sv, &cond);
774            let step_v = (sv_n - sv) as f32;
775            for (x, &d) in v.iter_mut().zip(&dv) {
776                *x += step_v * d;
777            }
778            let step_a = if p.stock_sampler {
779                step_v
780            } else {
781                (time_shift_sigma(sv_n, dit.shift_video, dit.shift_audio)
782                    - time_shift_sigma(sv.max(1e-6), dit.shift_video, dit.shift_audio))
783                    as f32
784            };
785            for (x, &d) in a.iter_mut().zip(&da) {
786                *x += step_a * d;
787            }
788            if prof {
789                eprintln!(
790                    "  step {i}: sv {sv:.4}->{sv_n:.4} v_vel {:.4} a_vel {:.4} | video {:.4} audio {:.4}",
791                    rms(&dv),
792                    rms(&da),
793                    rms(&v),
794                    rms(&a)
795                );
796            }
797            progress("denoise", i + 1, p.steps);
798        }
799        if let Some(rep) = dit.lora_report() {
800            eprint!("{rep}");
801        }
802        match return_streaming {
803            Some(pair) => pair,
804            None => (v, a),
805        }
806    };
807
808    lap(&mut marks, "denoise");
809
810    // ── the learned latent resize, when one was handed in ──
811    let (mut out_h, mut out_w) = (p.height, p.width);
812    let (mut lat_h, mut lat_w) = (lat_h, lat_w);
813    if let Some(path) = p.upscale.as_deref() {
814        progress("upscale", 0, 1);
815        let t = std::time::Instant::now();
816        let ups = crate::mmh3ups::LatentUpscaler::load(std::path::Path::new(path))?;
817        let z = crate::mmh3ups::Vol {
818            c: video.len() / (latent_t * lat_h * lat_w),
819            t: latent_t,
820            h: lat_h,
821            w: lat_w,
822            data: video,
823        };
824        // Snap to the VAE's own 16-pixel grid: the net takes any target,
825        // the decoder does not.
826        let f = p.upscale_by.max(1.0);
827        let nh = ((lat_h as f32 * f).round() as usize).max(lat_h);
828        let nw = ((lat_w as f32 * f).round() as usize).max(lat_w);
829        let big = ups.upscale(&z, nh, nw, None);
830        tracing::info!(
831            "latent upscale {}x{} -> {}x{} in {:.1}s",
832            lat_h,
833            lat_w,
834            nh,
835            nw,
836            t.elapsed().as_secs_f64()
837        );
838        out_h = out_h * nh / lat_h;
839        out_w = out_w * nw / lat_w;
840        lat_h = nh;
841        lat_w = nw;
842        video = big.data;
843        progress("upscale", 1, 1);
844        lap(&mut marks, "upscale");
845    }
846
847    // ── decode ──
848    progress("video vae", 0, 1);
849    let (rgb, out_frames) = {
850        let vae = VideoVae::from_cmf(&model)?;
851        vae.decode(&video, latent_t, lat_h, lat_w)
852    };
853    progress("video vae", 1, 1);
854    lap(&mut marks, "video vae");
855    progress("audio vae", 0, 1);
856    let (wave, samples, sr) = {
857        let vae = AudioVae::from_cmf(&model)?;
858        let c = audio.len() / (2 * audio_t);
859        let (w, n) = vae.decode(&audio, c, audio_t);
860        (w, n, vae.sample_rate)
861    };
862    progress("audio vae", 1, 1);
863    lap(&mut marks, "audio vae");
864    tracing::info!(
865        "stages: {}",
866        marks
867            .iter()
868            .map(|(n, v)| format!("{n} {v:.1}s"))
869            .collect::<Vec<_>>()
870            .join(" · ")
871    );
872    // Where a GEMM's wall time goes on unified memory: copies or kernel.
873    // The answer decides whether fusing blocks or tuning the kernel is
874    // the optimization worth doing.
875    #[cfg(target_os = "macos")]
876    if std::env::var("CMF_METAL_MMPROF").is_ok() {
877        use std::sync::atomic::Ordering::Relaxed;
878        let n = crate::gpu_metal::MM_N.load(Relaxed);
879        eprintln!(
880            "  q4tp mm x{n}: upload {:.1}s · submit+wait {:.1}s · readback {:.1}s",
881            crate::gpu_metal::MM_UP.load(Relaxed) as f64 / 1e6,
882            crate::gpu_metal::MM_GPU.load(Relaxed) as f64 / 1e6,
883            crate::gpu_metal::MM_DN.load(Relaxed) as f64 / 1e6,
884        );
885    }
886
887    // The VAE emits latent_t·4 frames; the request snapped to 17k+5,
888    // which is one fewer than a multiple of four plus the leading key
889    // frame, so trim rather than pad.
890    let keep = out_frames.min(frames_total);
891    Ok(Anim {
892        rgb: trim_frames(&rgb, out_frames, keep, out_h, out_w),
893        frames: keep,
894        height: out_h,
895        width: out_w,
896        audio: wave,
897        samples,
898        sample_rate: sr,
899    })
900}
901
902fn trim_frames(rgb: &[f32], have: usize, keep: usize, h: usize, w: usize) -> Vec<f32> {
903    if keep == have {
904        return rgb.to_vec();
905    }
906    let mut out = vec![0f32; 3 * keep * h * w];
907    for c in 0..3 {
908        let s = c * have * h * w;
909        let d = c * keep * h * w;
910        out[d..d + keep * h * w].copy_from_slice(&rgb[s..s + keep * h * w]);
911    }
912    out
913}
914
915#[cfg(test)]
916mod tests {
917    use super::*;
918
919    #[test]
920    fn the_four_step_schedule_is_the_references() {
921        let s = sigmas(4, 12.0);
922        let want = [1.0, 0.972_973, 0.923_077, 0.8, 0.0];
923        assert_eq!(s.len(), want.len());
924        for (g, w) in s.iter().zip(&want) {
925            assert!((g - w).abs() < 1e-6, "{s:?}");
926        }
927    }
928
929    #[test]
930    fn a_stretch_keeps_the_corners_and_a_crop_takes_the_middle() {
931        // A 4x2 ramp: value rises left to right, so the corners name
932        // themselves.
933        let (h, w) = (2usize, 4usize);
934        let mut rgb = vec![0f32; 3 * h * w];
935        for c in 0..3 {
936            for y in 0..h {
937                for x in 0..w {
938                    rgb[(c * h + y) * w + x] = x as f32 / (w - 1) as f32;
939                }
940            }
941        }
942        // Stretch to a square: the far edges survive.
943        let s = fit_to_canvas(&rgb, h, w, 4, 4, false);
944        assert!((s[0] - 0.0).abs() < 1e-6, "left edge");
945        assert!((s[3] - 1.0).abs() < 1e-6, "right edge");
946        // Cover-crop to a square takes the centre 2x2, so the extremes
947        // are gone and the span is narrower.
948        let c = fit_to_canvas(&rgb, h, w, 4, 4, true);
949        let (lo, hi) = c[..16]
950            .iter()
951            .fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
952        assert!(lo > 0.05, "crop kept the left edge: {lo}");
953        assert!(hi < 0.95, "crop kept the right edge: {hi}");
954    }
955
956    #[test]
957    fn frame_counts_snap_to_the_models_grid() {
958        // The grid is 5 + 17k: 5, 22, 39, 56, … 124.
959        assert_eq!(align_frames(1), 5);
960        assert_eq!(align_frames(39), 39);
961        assert_eq!(align_frames(41), 56);
962        assert_eq!(align_frames(124), 124);
963        assert_eq!(video_latent_t(124), 37);
964        assert_eq!(video_latent_t(39), 12);
965        let (f, lt, at) = temporal_shape(124);
966        assert_eq!((f, lt, at), (124, 37, 207));
967    }
968}