Skip to main content

cortiq_engine/
vae3d.rs

1//! MiniMax-H3's video VAE decoder: a ViT3D, not a conv stack.
2//!
3//! 36 transformer blocks over the latent grid, each latent cell one
4//! token, and a single linear that expands every token into a
5//! 4×16×16×3 block of pixels. The encoder half is a 3-D causal CNN and
6//! is not packed — text-to-video never runs it.
7//!
8//! ## Tiling is not an optimization here
9//!
10//! The reference decodes in 256-pixel spatial tiles and 17-frame
11//! temporal clips ALWAYS — `tiling=True` is the constructor default and
12//! `decode_tiled` just forwards to `decode`. Because the decoder is
13//! global attention, a tile sees a different context than the whole
14//! frame would, so the tiling is part of the model's output, not a
15//! memory strategy layered on top of it. Both schedules are reproduced
16//! exactly: `split_tiles` down to the last overlap unit, and the
17//! clip/token-drop bookkeeping that makes a chunk emit 17 frames and
18//! carry 5 over.
19
20use std::sync::atomic::{AtomicU64, Ordering};
21
22/// Where a VAE decode actually goes. The DiT had a profiler for months
23/// while this stage — the LARGER half of a render at low step counts,
24/// 37.8 s against the denoiser's 19.2 — had none, so every hour of
25/// tuning went to the half that was already instrumented.
26pub static VAE3D_PROF: [AtomicU64; 8] = [
27    AtomicU64::new(0),
28    AtomicU64::new(0),
29    AtomicU64::new(0),
30    AtomicU64::new(0),
31    AtomicU64::new(0),
32    AtomicU64::new(0),
33    AtomicU64::new(0),
34    AtomicU64::new(0),
35];
36
37pub(crate) fn vae3d_prof_on() -> bool {
38    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
39    *ON.get_or_init(|| std::env::var("CMF_VAE3D_PROF").is_ok())
40}
41
42fn vprof(slot: usize, t: std::time::Instant) {
43    if vae3d_prof_on() {
44        VAE3D_PROF[slot].fetch_add(t.elapsed().as_micros() as u64, Ordering::Relaxed);
45    }
46}
47
48/// One line per phase, sorted by cost.
49pub fn vae3d_prof_report() -> Option<String> {
50    if !vae3d_prof_on() {
51        return None;
52    }
53    const NAMES: [&str; 8] = [
54        "norm rows",
55        "qkv gemm",
56        "repack+rope",
57        "attention",
58        "out gemm",
59        "ffn",
60        "head+proj",
61        "pixel shuffle",
62    ];
63    let mut v: Vec<(u64, &str)> = VAE3D_PROF
64        .iter()
65        .map(|a| a.load(Ordering::Relaxed))
66        .zip(NAMES)
67        .collect();
68    let total: u64 = v.iter().map(|(u, _)| u).sum();
69    if total == 0 {
70        return None;
71    }
72    v.sort_by(|a, b| b.0.cmp(&a.0));
73    let mut out = format!("vae3d phases (total {:.1} s):\n", total as f64 / 1e6);
74    for (us, name) in v {
75        out.push_str(&format!(
76            "  {name:<16} {:>6.1} s  {:>5.1}%\n",
77            us as f64 / 1e6,
78            100.0 * us as f64 / total as f64
79        ));
80    }
81    Some(out)
82}
83
84use crate::dit::Proj;
85use crate::pool::Pool;
86use cortiq_core::CmfModel;
87use std::sync::Arc;
88
89const IMAGENET_MEAN: [f32; 3] = [0.485, 0.456, 0.406];
90const IMAGENET_STD: [f32; 3] = [0.229, 0.224, 0.225];
91
92/// Pixel tile edge and the smallest overlap `split_tiles` will accept.
93const TILE: usize = 256;
94const TILE_OVERLAP_MIN: usize = 64;
95/// Temporal clip in frames, and the tokens dropped off the encoder's
96/// tail that the decoder must therefore re-manufacture.
97const CLIP_LENGTH: usize = 17;
98const TOKEN_DROP: usize = 3;
99
100struct Block {
101    norm1: Vec<f32>,
102    norm2: Vec<f32>,
103    scale1: Vec<f32>,
104    scale2: Vec<f32>,
105    qkv: Proj, // [3·dim, dim], PER HEAD interleaved
106    qkv_b: Vec<f32>,
107    out: Proj,
108    out_b: Vec<f32>,
109    w1: Proj, // [2·4·dim, dim]
110    w1_b: Vec<f32>,
111    w2: Proj, // [dim, 4·dim]
112    w2_b: Vec<f32>,
113}
114
115pub struct VideoVae {
116    post_quant: Proj,
117    post_quant_b: Vec<f32>,
118    x_embed: Proj,
119    x_embed_b: Vec<f32>,
120    registers: Vec<f32>, // [n_reg, dim]
121    blocks: Vec<Block>,
122    norm_out_w: Vec<f32>,
123    norm_out_b: Vec<f32>,
124    proj_out: Proj,
125    proj_out_b: Vec<f32>,
126    latents_mean: Vec<f32>,
127    latents_std: Vec<f32>,
128    pool: Option<Arc<Pool>>,
129    dim: usize,
130    heads: usize,
131    head_dim: usize,
132    z_channels: usize,
133    patch: usize,
134    patch_t: usize,
135    n_reg: usize,
136    rope_theta: f32,
137    rope_dim: usize,
138    /// Position axes the rotation covers: (t, h, w) on the video decoder,
139    /// (h, w) on MiniMax-H3-Image's.
140    rope_axes: usize,
141    eps: f64,
142}
143
144fn layer_norm(x: &[f32], w: &[f32], b: &[f32], eps: f64, dst: &mut [f32]) {
145    let n = x.len() as f64;
146    let mean = x.iter().map(|&v| v as f64).sum::<f64>() / n;
147    let var = x.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / n;
148    let inv = 1.0 / (var + eps).sqrt();
149    for (((d, &v), &g), &bb) in dst.iter_mut().zip(x).zip(w).zip(b) {
150        *d = ((v as f64 - mean) * inv) as f32 * g + bb;
151    }
152}
153
154/// RMSNorm with a weight, no bias.
155fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
156    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
157    let inv = 1.0 / (ss + eps).sqrt();
158    for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
159        *d = (v as f64 * inv) as f32 * g;
160    }
161}
162
163/// RMSNorm with NO affine — the decoder's q/k norms are weightless.
164thread_local! {
165    /// The check's reference arm re-enters `attention`; without this it
166    /// checks its own check, forever.
167    static CHECKING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
168}
169
170fn rms_norm_plain(x: &mut [f32], eps: f64) {
171    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
172    let inv = 1.0 / (ss + eps).sqrt();
173    for v in x.iter_mut() {
174        *v = (*v as f64 * inv) as f32;
175    }
176}
177
178fn silu(v: f32) -> f32 {
179    v / (1.0 + (-v).exp())
180}
181
182/// Rows across the pool, or straight through when there is none. The
183/// DiT has had this since the start; this decoder was still walking its
184/// norms and residuals on one thread — 3.3 s of a 38 s stage.
185fn rows_par(pool: Option<&crate::pool::Pool>, n: usize, f: &(dyn Fn(usize, usize) + Sync)) {
186    match pool {
187        Some(pl) => pl.run_rows(n, f),
188        None => f(0, n),
189    }
190}
191
192struct SendPtr(*mut f32);
193unsafe impl Send for SendPtr {}
194unsafe impl Sync for SendPtr {}
195impl SendPtr {
196    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
197    #[allow(clippy::mut_from_ref)]
198    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
199        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
200    }
201}
202
203fn softmax_inplace(row: &mut [f32]) {
204    let mx = row.iter().cloned().fold(f32::MIN, f32::max);
205    let mut den = 0f32;
206    for r in row.iter_mut() {
207        *r = (*r - mx).exp();
208        den += *r;
209    }
210    if den > 0.0 {
211        let inv = 1.0 / den;
212        for r in row.iter_mut() {
213            *r *= inv;
214        }
215    }
216}
217
218/// `(starts, lens, overlaps)` for one axis — the reference's schedule,
219/// which grows the overlaps rather than the tile count so every tile is
220/// exactly `TILE` wide.
221fn split_tiles(input_len: usize, ratio: usize) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
222    if TILE >= input_len {
223        return (vec![0], vec![input_len], Vec::new());
224    }
225    let mut n = input_len.div_ceil(TILE);
226    let (mut overlaps, remaining);
227    loop {
228        overlaps = vec![TILE_OVERLAP_MIN; n - 1];
229        let total: usize = overlaps.iter().sum();
230        if TILE * n < total + input_len {
231            n += 1;
232            continue;
233        }
234        remaining = TILE * n - total - input_len;
235        break;
236    }
237    for i in 0..remaining / ratio {
238        overlaps[i % (n - 1)] += ratio;
239    }
240    let mut starts = vec![0usize];
241    for i in 0..n - 1 {
242        starts.push(starts[i] + TILE - overlaps[i]);
243    }
244    (starts, vec![TILE; n], overlaps)
245}
246
247impl VideoVae {
248    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
249        let cfg: serde_json::Value = serde_json::from_slice(
250            model
251                .tensor_bytes("vvae.config_json")
252                .map_err(|e| e.to_string())?,
253        )
254        .map_err(|e| format!("vvae.config_json: {e}"))?;
255        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
256        let f32v = |n: &str| crate::dit::cmf_f32(model, n);
257        let n = u("num_layers", 36);
258        let mut blocks = Vec::with_capacity(n);
259        for l in 0..n {
260            let p = format!("vvae.blocks.{l}");
261            blocks.push(Block {
262                norm1: f32v(&format!("{p}.norm1"))?,
263                norm2: f32v(&format!("{p}.norm2"))?,
264                scale1: f32v(&format!("{p}.scale1"))?,
265                scale2: f32v(&format!("{p}.scale2"))?,
266                qkv: Proj::from_model(model, &format!("{p}.attn.to_qkv.weight"))?,
267                qkv_b: f32v(&format!("{p}.attn.to_qkv.bias"))?,
268                out: Proj::from_model(model, &format!("{p}.attn.to_out.weight"))?,
269                out_b: f32v(&format!("{p}.attn.to_out.bias"))?,
270                w1: Proj::from_model(model, &format!("{p}.ff.w1.weight"))?,
271                w1_b: f32v(&format!("{p}.ff.w1.bias"))?,
272                w2: Proj::from_model(model, &format!("{p}.ff.w2.weight"))?,
273                w2_b: f32v(&format!("{p}.ff.w2.bias"))?,
274            });
275        }
276        let dim = u("dim", 2048);
277        let heads = u("heads", 32);
278        Ok(Self {
279            post_quant: Proj::from_model(model, "vvae.post_quant_conv.weight")?,
280            post_quant_b: f32v("vvae.post_quant_conv.bias")?,
281            x_embed: Proj::from_model(model, "vvae.x_embedder.weight")?,
282            x_embed_b: f32v("vvae.x_embedder.bias")?,
283            registers: f32v("vvae.register_tokens")?,
284            blocks,
285            norm_out_w: f32v("vvae.norm_out.weight")?,
286            norm_out_b: f32v("vvae.norm_out.bias")?,
287            proj_out: Proj::from_model(model, "vvae.proj_out.weight")?,
288            proj_out_b: f32v("vvae.proj_out.bias")?,
289            latents_mean: f32v("vvae.latents_mean")?,
290            latents_std: f32v("vvae.latents_std")?,
291            pool: Pool::from_env(),
292            dim,
293            heads,
294            head_dim: dim / heads,
295            z_channels: u("z_channels", 24),
296            patch: u("patch_size", 16),
297            patch_t: u("patch_size_t", 4),
298            n_reg: u("num_register_tokens", 4),
299            rope_theta: cfg["rope_theta"].as_f64().unwrap_or(100.0) as f32,
300            rope_dim: ((dim / heads) as f64 * cfg["rope_dim_ratio"].as_f64().unwrap_or(0.75))
301                as usize,
302            rope_axes: u("rope_dims", 3).clamp(2, 3),
303            eps: cfg["eps"].as_f64().unwrap_or(1e-5),
304        })
305    }
306
307    /// `arange(0.5, n)/n · 2 − 1` — the normalized cell centre of one
308    /// axis, so a tile's coordinates depend on the TILE's extent and
309    /// not on the frame's.
310    fn axis(n: usize) -> Vec<f32> {
311        (0..n)
312            .map(|i| 2.0 * ((i as f32 + 0.5) / n as f32) - 1.0)
313            .collect()
314    }
315
316    /// `[S, rope_dim/2]` angles: the position axes × `rope_dim/(2·axes)`
317    /// frequencies each, scaled by 2π — (t, h, w) on the video decoder,
318    /// (h, w) on the image one. Suffix tokens sit at the origin and get
319    /// zeros.
320    fn rope_angles(&self, t: usize, h: usize, w: usize, suffix: usize) -> Vec<f32> {
321        let ax = self.rope_axes;
322        let k = self.rope_dim / (2 * ax); // frequencies per axis
323        let inv: Vec<f32> = (0..k)
324            .map(|i| {
325                1.0 / self
326                    .rope_theta
327                    .powf(i as f32 * 2.0 * ax as f32 / self.rope_dim as f32)
328            })
329            .collect();
330        let (ta, ha, wa) = (Self::axis(t), Self::axis(h), Self::axis(w));
331        let tau = 2.0 * std::f32::consts::PI;
332        let mut out = Vec::with_capacity((t * h * w + suffix) * ax * k);
333        for ti in 0..t {
334            for hi in 0..h {
335                for wi in 0..w {
336                    let coords = [ta[ti], ha[hi], wa[wi]];
337                    for &c in &coords[3 - ax..] {
338                        for &f in &inv {
339                            out.push(tau * c * f);
340                        }
341                    }
342                }
343            }
344        }
345        out.extend(std::iter::repeat_n(0.0, suffix * ax * k));
346        out
347    }
348
349    fn attention(&self, qkv: &[f32], n: usize, attn: &mut [f32], angles: &[f32]) {
350        let (nh, hd, dim) = (self.heads, self.head_dim, self.dim);
351        let pairs = angles.len() / n; // = rope_dim / 2
352        let scale = 1.0 / (hd as f32).sqrt();
353        let pool = self.pool.as_deref();
354        // Device path: the same `dit_qk`/`dit_softmax`/`dit_pv` chain the
355        // DiT rides, so the n×n score plane stays on the card. This VAE
356        // is the larger half of a render's wall (201 s of 475 on an RTX
357        // 5090), and it was materializing those scores per head on the
358        // host. CMF_VAE3D_ATTN=cpu forces the loop below.
359        if std::env::var("CMF_VAE3D_ATTN").as_deref() != Ok("cpu")
360            && crate::gpu::enabled_here()
361            && n >= 256
362        {
363            // A CMF_VAE3D_CHECK harness lived here and was WRONG: its
364            // reference arm re-entered this very function (so it
365            // recursed through its own check) and its device arm was
366            // never verified to have written anything, so its verdict
367            // — "both layouts identical, 7.2765 from the host" — was
368            // just max|host| against two buffers of zeros. It measured
369            // itself. A replacement must assert the device arm wrote
370            // before it compares, and must call the host repack
371            // DIRECTLY rather than through the dispatcher.
372            // A sound version of the harness the previous one failed to
373            // be: a re-entry guard (its reference arm calls back into
374            // this function), and a sentinel that proves the device arm
375            // WROTE before its numbers are believed.
376            //
377            // VERDICT, and it moves the search a long way: the device
378            // arm writes EVERY element (wrote=3680256/3680256) and it
379            // writes ZEROS — `l1-host` comes back exactly equal to
380            // `host|max|`, which is the distance from an all-zero
381            // buffer. So the whole device attention is empty for this
382            // decoder, and the split's addressing, its bias and its
383            // layout are all downstream of a stage that has already
384            // produced nothing. That also explains why every layout
385            // experiment agreed: zero does not depend on addressing.
386            //
387            // Already eliminated, so nobody re-walks them:
388            //  * the split/qk-norm encoder IS submitted before attention
389            //    reads the planes (`c.queue.submit` at the end of that
390            //    block) — the planes are not merely un-flushed;
391            //  * the scratch slots are grow-only and the DiT ran first
392            //    at LARGER shapes, so nothing here is undersized;
393            //  * the split's dispatch grid is 14376 groups in x, well
394            //    under the 65535 bound, so it is not rounding to zero;
395            //  * `dit_attention_packed_src` returned true, so no stage
396            //    refused — the chain ran and produced zeros.
397            //
398            // What is left: the three planes the split writes are read
399            // back as zeros while the SAME kernels, fed the same three
400            // planes uploaded from the host (`resident=false`, the
401            // shipped VAE path), are correct at this very hd=64. So the
402            // difference is the hand-off of the planes themselves, not
403            // the attention math. Next: read `sc.dq` back right after
404            // the split and compare it against the host repack — one
405            // buffer, one dispatch, no attention involved.
406            if std::env::var("CMF_VAE3D_CHECK").as_deref() == Ok("1") && !CHECKING.with(|c| c.get())
407            {
408                CHECKING.with(|c| c.set(true));
409                const SENT: f32 = -12345.0;
410                let mut d1 = vec![SENT; attn.len()];
411                let mut d0 = vec![SENT; attn.len()];
412                let ok1 = crate::gpu::vae_attention_packed_layout(
413                    qkv,
414                    nh,
415                    n,
416                    hd,
417                    scale,
418                    angles,
419                    self.eps as f32,
420                    &mut d1,
421                    1,
422                );
423                let ok0 = crate::gpu::vae_attention_packed_layout(
424                    qkv,
425                    nh,
426                    n,
427                    hd,
428                    scale,
429                    angles,
430                    self.eps as f32,
431                    &mut d0,
432                    0,
433                );
434                let wrote = |v: &[f32]| v.iter().filter(|&&x| x != SENT).count();
435                let mut host = vec![0f32; attn.len()];
436                let saved = std::env::var("CMF_VAE3D_ATTN").ok();
437                // SAFETY: single-threaded diagnostic, guarded above.
438                unsafe { std::env::set_var("CMF_VAE3D_ATTN", "cpu") };
439                self.attention(qkv, n, &mut host, angles);
440                match &saved {
441                    Some(v) => unsafe { std::env::set_var("CMF_VAE3D_ATTN", v) },
442                    None => unsafe { std::env::remove_var("CMF_VAE3D_ATTN") },
443                }
444                // The split ALONE: no norm, no RoPE, no attention. If
445                // this mismatches, the hand-off is the whole story.
446                let mut sq = vec![SENT; nh * n * hd];
447                let oks = crate::gpu::dit_split_only(qkv, nh, n, hd, 1, None, &mut sq);
448                let mut sqn = vec![SENT; nh * n * hd];
449                let okn = crate::gpu::dit_split_only(
450                    qkv,
451                    nh,
452                    n,
453                    hd,
454                    1,
455                    Some((angles, self.eps as f32)),
456                    &mut sqn,
457                );
458                let mut hq = vec![0f32; nh * n * hd];
459                for p in 0..n {
460                    for h in 0..nh {
461                        let b = p * 3 * dim + h * 3 * hd;
462                        hq[(h * n + p) * hd..(h * n + p) * hd + hd]
463                            .copy_from_slice(&qkv[b..b + hd]);
464                    }
465                }
466                let sq_wrote = sq.iter().filter(|&&x| x != SENT).count();
467                let sq_diff = sq
468                    .iter()
469                    .zip(&hq)
470                    .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
471                // The same q plane after norm+RoPE, against the host's
472                // own loop on a copy of it.
473                let mut hqn = hq.clone();
474                let pairs = angles.len() / n;
475                for p in 0..n {
476                    for h in 0..nh {
477                        let r = &mut hqn[(h * n + p) * hd..(h * n + p) * hd + hd];
478                        rms_norm_plain(r, self.eps);
479                        for j in 0..pairs {
480                            let (sn, cs) = angles[p * pairs + j].sin_cos();
481                            let (a, b) = (r[j], r[j + pairs]);
482                            r[j] = a * cs - b * sn;
483                            r[j + pairs] = a * sn + b * cs;
484                        }
485                    }
486                }
487                let n_wrote = sqn.iter().filter(|&&x| x != SENT).count();
488                let n_diff = sqn
489                    .iter()
490                    .zip(&hqn)
491                    .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
492                eprintln!(
493                    "vae split-only: ok={oks} wrote={sq_wrote}/{} maxdiff={sq_diff:.4e} host|max|={:.4e} || +norm: ok={okn} wrote={n_wrote} maxdiff={n_diff:.4e} host|max|={:.4e}",
494                    sq.len(),
495                    hq.iter().fold(0f32, |m, v| m.max(v.abs())),
496                    hqn.iter().fold(0f32, |m, v| m.max(v.abs())),
497                );
498                let md = |a: &[f32], b: &[f32]| {
499                    a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
500                };
501                static ONCE: std::sync::Once = std::sync::Once::new();
502                ONCE.call_once(|| {
503                    eprintln!(
504                        "vae attn check: ok={ok1}/{ok0} wrote={}/{} of {} | host|max| {:.4e} | l1-host {:.4e} | l0-host {:.4e} | l1-l0 {:.4e}",
505                        wrote(&d1), wrote(&d0), attn.len(),
506                        host.iter().fold(0f32, |m, v| m.max(v.abs())),
507                        md(&d1, &host), md(&d0, &host), md(&d1, &d0),
508                    )
509                });
510                CHECKING.with(|c| c.set(false));
511            }
512            if std::env::var("CMF_VAE3D_SPLIT").as_deref() == Ok("1")
513                && crate::gpu::vae_attention_packed(
514                    qkv,
515                    nh,
516                    n,
517                    hd,
518                    scale,
519                    angles,
520                    self.eps as f32,
521                    attn,
522                )
523            {
524                return;
525            }
526            let t_rp = std::time::Instant::now();
527            let mut qa = vec![0f32; nh * n * hd];
528            let mut ka = vec![0f32; nh * n * hd];
529            let mut va = vec![0f32; nh * n * hd];
530            {
531                let (pq, pk, pv) = (
532                    SendPtr(qa.as_mut_ptr()),
533                    SendPtr(ka.as_mut_ptr()),
534                    SendPtr(va.as_mut_ptr()),
535                );
536                let fill = |lo: usize, hi: usize| {
537                    for p in lo..hi {
538                        for h in 0..nh {
539                            let base = p * 3 * dim + h * 3 * hd;
540                            let dst = (h * n + p) * hd;
541                            // SAFETY: disjoint token ranges per worker,
542                            // and each token owns its own head slots.
543                            let (q, k, v) =
544                                unsafe { (pq.row(dst, hd), pk.row(dst, hd), pv.row(dst, hd)) };
545                            q.copy_from_slice(&qkv[base..base + hd]);
546                            k.copy_from_slice(&qkv[base + hd..base + 2 * hd]);
547                            v.copy_from_slice(&qkv[base + 2 * hd..base + 3 * hd]);
548                            // q and k carry the same norm + rotation the
549                            // host loop applies; v is untouched.
550                            for x in [&mut *q, &mut *k] {
551                                rms_norm_plain(x, self.eps);
552                                for j in 0..pairs {
553                                    let (sn, cs) = angles[p * pairs + j].sin_cos();
554                                    let (a, b) = (x[j], x[j + pairs]);
555                                    x[j] = a * cs - b * sn;
556                                    x[j + pairs] = a * sn + b * cs;
557                                }
558                            }
559                        }
560                    }
561                };
562                match pool {
563                    Some(pl) => pl.run_rows(n, &fill),
564                    None => fill(0, n),
565                }
566            }
567            vprof(2, t_rp);
568            let t_at = std::time::Instant::now();
569            if crate::gpu::dit_attention(&qa, &ka, &va, nh, nh, n, hd, scale, attn) {
570                vprof(3, t_at);
571                return;
572            }
573        }
574        let mut qh = vec![0f32; n * hd];
575        let mut kh = vec![0f32; n * hd];
576        let mut vt = vec![0f32; hd * n];
577        let mut scores = vec![0f32; n * n];
578        let mut oh = vec![0f32; n * hd];
579        for h in 0..nh {
580            for p in 0..n {
581                // to_qkv is viewed [.., heads, 3·hd] then chunked, so a
582                // head's q, k and v are adjacent — not three planes.
583                let base = p * 3 * dim + h * 3 * hd;
584                qh[p * hd..(p + 1) * hd].copy_from_slice(&qkv[base..base + hd]);
585                kh[p * hd..(p + 1) * hd].copy_from_slice(&qkv[base + hd..base + 2 * hd]);
586                for d in 0..hd {
587                    vt[d * n + p] = qkv[base + 2 * hd + d];
588                }
589            }
590            for (buf, _) in [(&mut qh, 0), (&mut kh, 1)] {
591                for p in 0..n {
592                    let x = &mut buf[p * hd..(p + 1) * hd];
593                    rms_norm_plain(x, self.eps);
594                    for j in 0..pairs {
595                        let (s, c) = angles[p * pairs + j].sin_cos();
596                        let (a, b) = (x[j], x[j + pairs]);
597                        x[j] = a * c - b * s;
598                        x[j + pairs] = a * s + b * c;
599                    }
600                }
601            }
602            for v in qh.iter_mut() {
603                *v *= scale;
604            }
605            crate::fcd_ops::gemm_nt(&qh, &kh, &mut scores, n, hd, n, pool);
606            let sp = SendPtr(scores.as_mut_ptr());
607            let soft = |lo: usize, hi: usize| {
608                for r in lo..hi {
609                    // SAFETY: workers own disjoint score rows.
610                    softmax_inplace(unsafe { sp.row(r * n, n) });
611                }
612            };
613            match pool {
614                Some(pl) => pl.run_rows(n, &soft),
615                None => soft(0, n),
616            }
617            crate::fcd_ops::gemm_nt(&scores, &vt, &mut oh, n, n, hd, pool);
618            for p in 0..n {
619                attn[p * dim + h * hd..p * dim + (h + 1) * hd]
620                    .copy_from_slice(&oh[p * hd..(p + 1) * hd]);
621            }
622        }
623    }
624
625    /// A tile's latents `[z_channels, t, h, w]` as the decoder's token
626    /// stream `[n, dim]`: the patches through the post-quant conv and the
627    /// embedder, then the register tokens and one all-zero token.
628    fn embed_tile(&self, z: &[f32], t: usize, h: usize, w: usize) -> Vec<f32> {
629        let pool = self.pool.as_deref();
630        let dim = self.dim;
631        let np = t * h * w;
632        let suffix = 1 + self.n_reg;
633        let n = np + suffix;
634
635        // [C, T, H, W] → [T·H·W, C], then the 1×1×1 post-quant conv and
636        // the embedder, which are both plain linears at this point.
637        let zc = self.z_channels;
638        let mut rows = vec![0f32; np * zc];
639        for c in 0..zc {
640            for i in 0..np {
641                rows[i * zc + c] = z[c * np + i];
642            }
643        }
644        let mut pq = vec![0f32; np * zc];
645        self.post_quant.matmat(&rows, np, &mut pq, pool);
646        for r in pq.chunks_exact_mut(zc) {
647            for (v, &b) in r.iter_mut().zip(&self.post_quant_b) {
648                *v += b;
649            }
650        }
651        let mut x = vec![0f32; n * dim];
652        self.x_embed.matmat(&pq, np, &mut x[..np * dim], pool);
653        for r in x[..np * dim].chunks_exact_mut(dim) {
654            for (v, &b) in r.iter_mut().zip(&self.x_embed_b) {
655                *v += b;
656            }
657        }
658        // register tokens, then one all-zero token
659        x[np * dim..(np + self.n_reg) * dim].copy_from_slice(&self.registers);
660        x
661    }
662
663    /// One tile-clip: latents `[z_channels, t, h, w]` → pixels
664    /// `[3, t·4, h·16, w·16]`, both channel-major.
665    fn decode_tile(&self, z: &[f32], t: usize, h: usize, w: usize) -> Vec<f32> {
666        let pool = self.pool.as_deref();
667        let dim = self.dim;
668        let np = t * h * w;
669        let suffix = 1 + self.n_reg;
670        let n = np + suffix;
671        let mut x = self.embed_tile(z, t, h, w);
672
673        let angles = self.rope_angles(t, h, w, suffix);
674        let mut xn = vec![0f32; n * dim];
675        let mut qkv = vec![0f32; n * 3 * dim];
676        let mut attn = vec![0f32; n * dim];
677        let mut proj = vec![0f32; n * dim];
678        let inner = 4 * dim;
679        for blk in &self.blocks {
680            let t = std::time::Instant::now();
681            {
682                let px = SendPtr(xn.as_mut_ptr());
683                rows_par(pool, n, &|lo, hi| {
684                    for p in lo..hi {
685                        // SAFETY: workers own disjoint token rows.
686                        rms_norm_into(&x[p * dim..(p + 1) * dim], &blk.norm1, self.eps, unsafe {
687                            px.row(p * dim, dim)
688                        });
689                    }
690                });
691            }
692            vprof(0, t);
693            let t = std::time::Instant::now();
694            // The whole attention half on the card: qkv GEMM, bias, the
695            // weightless q/k norm, RoPE, attention, output projection —
696            // nothing crossing the bus between them. The DiT's chain cut
697            // its step 29%; this decoder spent 18.2 s of 26.1 on the
698            // same six steps. `CMF_VAE3D_FUSE=0` restores the host chain.
699            if std::env::var("CMF_GPU_DEBUG").is_ok() {
700                static ONCE: std::sync::Once = std::sync::Once::new();
701                ONCE.call_once(|| {
702                    eprintln!(
703                        "vae3d shape: n={n} dim={dim} heads={} head_dim={} pairs={}",
704                        self.heads,
705                        self.head_dim,
706                        angles.len() / n.max(1)
707                    )
708                });
709            }
710            let mut fused = false;
711            // Default since the split kernel's encoder started reaching
712            // the queue: this stage 26.1 s → 16.6 s and a 2-step render
713            // 57.1 s → 46.9 s, at 2.33e-3 rel rms from the host chain
714            // (max 8/255 on 3% of pixels) — four times inside the gate
715            // the DiT's own device path is held to. `CMF_VAE3D_FUSE=0`
716            // restores the host chain.
717            //
718            // It read 0.296 rel rms and 97% of pixels for most of a day,
719            // and none of that was this code: the split never ran, so V
720            // was zeros and P·V was zero. Zero does not depend on
721            // addressing, which is why every layout experiment agreed
722            // with every other and sent the search the wrong way.
723            //
724            // Narrowed to four stages, with everything else measured
725            // and cleared (`CMF_VAE3D_CHECK=1` runs the harness):
726            //   * the device attention returns ZEROS — proven with a
727            //     sentinel fill, so "it wrote" is a fact, not a guess;
728            //   * the split reproduces the host repack EXACTLY at this
729            //     head-interleaved layout (maxdiff 0.0000e0);
730            //   * qk-norm + RoPE match the host loop to 3.8e-6 against
731            //     a magnitude of 6.87 — f32 rounding;
732            //   * the plane pickup is sound: with an empty slice
733            //     `Scratch::ensure` asks for 0 bytes, its `cap >= need`
734            //     arm holds for any live slot, and both sides name the
735            //     same slots with the same usage.
736            // So the zeros are made in QK, softmax, PV or the unstack,
737            // at nh=32 hd=64 n=1797 — shapes at which that same code is
738            // correct when the planes arrive by upload instead
739            // (`resident=false`, the shipped path). Give each of those
740            // four private buffers, one at a time; the first that comes
741            // back zero is the answer. Worth 26.1 s → 16.1 s on this stage
742            // and 57.6 s → 47.2 s on a 2-step render once it is right.
743            //
744            // What is known: the error survives with both GEMMs on the
745            // host (`CMF_VAE3D_SPLIT=1` reproduces it byte for byte), so
746            // it lives in the split or the qk-norm kernel, not in the
747            // resident hand-off. Teaching BOTH kernels this panel's
748            // layout (head-interleaved, mode=1) and its bias changed the
749            // output by nothing — which should be impossible at nh=32,
750            // where the two addressings differ for every head above the
751            // first. That contradiction is the thread to pull: the
752            // one-shot probe in `packed_src` fires on the DiT's first
753            // call and never reaches the VAE's, so instrument per layout.
754            if std::env::var("CMF_VAE3D_FUSE").as_deref() != Ok("0")
755                && crate::gpu::enabled_here()
756                && n >= 256
757            {
758                if let (Proj::Q(q), Proj::Q(o)) = (&blk.qkv, &blk.out) {
759                    if let (Some((m, i)), Some((_, oi))) =
760                        (q.mapped_device_gemm(), o.mapped_device_gemm())
761                    {
762                        fused = crate::gpu::vae_qkv_attn_out(
763                            m,
764                            i,
765                            oi,
766                            &xn,
767                            n,
768                            dim,
769                            self.heads,
770                            self.head_dim,
771                            1.0 / (self.head_dim as f32).sqrt(),
772                            &angles,
773                            self.eps as f32,
774                            &blk.qkv_b,
775                            &mut proj,
776                        );
777                    }
778                }
779            }
780            if fused {
781                vprof(1, t);
782            }
783            if !fused {
784                blk.qkv.matmat(&xn, n, &mut qkv, pool);
785                {
786                    let pq = SendPtr(qkv.as_mut_ptr());
787                    rows_par(pool, n, &|lo, hi| {
788                        for p in lo..hi {
789                            // SAFETY: disjoint token rows.
790                            for (v, &b) in unsafe { pq.row(p * 3 * dim, 3 * dim) }
791                                .iter_mut()
792                                .zip(&blk.qkv_b)
793                            {
794                                *v += b;
795                            }
796                        }
797                    });
798                }
799                vprof(1, t);
800                self.attention(&qkv, n, &mut attn, &angles);
801                let t = std::time::Instant::now();
802                blk.out.matmat(&attn, n, &mut proj, pool);
803                vprof(4, t);
804            }
805            {
806                let pxx = SendPtr(x.as_mut_ptr());
807                rows_par(pool, n, &|lo, hi| {
808                    for p in lo..hi {
809                        let r = &proj[p * dim..(p + 1) * dim];
810                        // SAFETY: workers own disjoint token rows.
811                        let dst = unsafe { pxx.row(p * dim, dim) };
812                        for (i, v) in dst.iter_mut().enumerate() {
813                            *v += (r[i] + blk.out_b[i]) * blk.scale1[i];
814                        }
815                    }
816                });
817            }
818            let t = std::time::Instant::now();
819            {
820                let px = SendPtr(xn.as_mut_ptr());
821                rows_par(pool, n, &|lo, hi| {
822                    for p in lo..hi {
823                        // SAFETY: workers own disjoint token rows.
824                        rms_norm_into(&x[p * dim..(p + 1) * dim], &blk.norm2, self.eps, unsafe {
825                            px.row(p * dim, dim)
826                        });
827                    }
828                });
829            }
830            vprof(0, t);
831            let t_ffn = std::time::Instant::now();
832            // Device-resident FFN when both weights are q4tp: fc1 →
833            // SwiGLU (with this VAE's gate/up bias) → fc2 without the
834            // intermediate panel crossing the bus twice per block.
835            // CMF_VAE3D_FFN=cpu forces the host chain.
836            if std::env::var("CMF_VAE3D_FFN").as_deref() != Ok("cpu")
837                && crate::gpu::enabled_here()
838                && n >= 64
839            {
840                if let (Proj::Q(q1), Proj::Q(q2)) = (&blk.w1, &blk.w2) {
841                    if let (Some((m, i1)), Some((_, i2))) =
842                        (q1.mapped_device_gemm(), q2.mapped_device_gemm())
843                    {
844                        let mut fout = vec![0f32; n * dim];
845                        if crate::gpu::q4tp_ffn_packed(
846                            m,
847                            i1,
848                            i2,
849                            &xn,
850                            n,
851                            dim,
852                            inner,
853                            Some(&blk.w1_b),
854                            &mut fout,
855                        ) {
856                            {
857                                let pxx = SendPtr(x.as_mut_ptr());
858                                rows_par(pool, n, &|lo, hi| {
859                                    for p in lo..hi {
860                                        let r = &fout[p * dim..(p + 1) * dim];
861                                        // SAFETY: disjoint token rows.
862                                        let dst = unsafe { pxx.row(p * dim, dim) };
863                                        for (i, v) in dst.iter_mut().enumerate() {
864                                            *v += (r[i] + blk.w2_b[i]) * blk.scale2[i];
865                                        }
866                                    }
867                                });
868                            }
869                            vprof(5, t_ffn);
870                            continue;
871                        }
872                    }
873                }
874            }
875            let mut gu = vec![0f32; n * 2 * inner];
876            blk.w1.matmat(&xn, n, &mut gu, pool);
877            let mut act = vec![0f32; n * inner];
878            for p in 0..n {
879                let r = &gu[p * 2 * inner..(p + 1) * 2 * inner];
880                for i in 0..inner {
881                    act[p * inner + i] =
882                        silu(r[i] + blk.w1_b[i]) * (r[inner + i] + blk.w1_b[inner + i]);
883                }
884            }
885            blk.w2.matmat(&act, n, &mut proj, pool);
886            {
887                let pxx = SendPtr(x.as_mut_ptr());
888                rows_par(pool, n, &|lo, hi| {
889                    for p in lo..hi {
890                        let r = &proj[p * dim..(p + 1) * dim];
891                        // SAFETY: disjoint token rows.
892                        let dst = unsafe { pxx.row(p * dim, dim) };
893                        for (i, v) in dst.iter_mut().enumerate() {
894                            *v += (r[i] + blk.w2_b[i]) * blk.scale2[i];
895                        }
896                    }
897                });
898            }
899            vprof(5, t_ffn);
900        }
901
902        self.head_tile(&x, t, h, w)
903    }
904
905    /// The decoder's output layer over a tile's token stream: the final
906    /// norm and projection of the patch tokens, unpatchified to
907    /// `[3, t·pt, h·ps, w·ps]`.
908    fn head_tile(&self, x: &[f32], t: usize, h: usize, w: usize) -> Vec<f32> {
909        let pool = self.pool.as_deref();
910        let dim = self.dim;
911        let np = t * h * w;
912        let (pt, ps) = (self.patch_t, self.patch);
913        let od = 3 * pt * ps * ps;
914        let t_head = std::time::Instant::now();
915        let mut head = vec![0f32; np * dim];
916        {
917            let ph = SendPtr(head.as_mut_ptr());
918            rows_par(pool, np, &|lo, hi| {
919                for p in lo..hi {
920                    // SAFETY: disjoint token rows.
921                    layer_norm(
922                        &x[p * dim..(p + 1) * dim],
923                        &self.norm_out_w,
924                        &self.norm_out_b,
925                        self.eps,
926                        unsafe { ph.row(p * dim, dim) },
927                    );
928                }
929            });
930        }
931        let mut out = vec![0f32; np * od];
932        self.proj_out.matmat(&head, np, &mut out, pool);
933        for r in out.chunks_exact_mut(od) {
934            for (v, &b) in r.iter_mut().zip(&self.proj_out_b) {
935                *v += b;
936            }
937        }
938
939        vprof(6, t_head);
940        let t_px = std::time::Instant::now();
941        // [T,H,W, 3,pt,ps,ps] → [3, T·pt, H·ps, W·ps]
942        let (ot, oh, ow) = (t * pt, h * ps, w * ps);
943        let mut px = vec![0f32; 3 * ot * oh * ow];
944        for ti in 0..t {
945            for hi in 0..h {
946                for wi in 0..w {
947                    let src =
948                        &out[((ti * h + hi) * w + wi) * od..((ti * h + hi) * w + wi + 1) * od];
949                    for c in 0..3 {
950                        for a in 0..pt {
951                            for b in 0..ps {
952                                for d in 0..ps {
953                                    px[((c * ot + ti * pt + a) * oh + hi * ps + b) * ow
954                                        + wi * ps
955                                        + d] = src[((c * pt + a) * ps + b) * ps + d];
956                                }
957                            }
958                        }
959                    }
960                }
961            }
962        }
963        vprof(7, t_px);
964        px
965    }
966
967    /// Every tile of a clip, all of one shape, in order. On a backend with
968    /// a resident chain the tiles go through the decoder's blocks in
969    /// batches — each tile an independent sequence of one launch — and
970    /// one at a time through the per-op path otherwise.
971    /// `CMF_VAE3D_CHAIN=0` keeps the per-op path.
972    fn decode_tiles(&self, subs: &[Vec<f32>], t: usize, h: usize, w: usize) -> Vec<Vec<f32>> {
973        if let Some(out) = self.decode_tiles_chain(subs, t, h, w) {
974            return out;
975        }
976        subs.iter().map(|z| self.decode_tile(z, t, h, w)).collect()
977    }
978
979    fn decode_tiles_chain(
980        &self,
981        subs: &[Vec<f32>],
982        t: usize,
983        h: usize,
984        w: usize,
985    ) -> Option<Vec<Vec<f32>>> {
986        if std::env::var("CMF_VAE3D_CHAIN").as_deref() == Ok("0")
987            || !crate::gpu::enabled_here()
988            || !crate::gpu::h3_chain_available()
989        {
990            return None;
991        }
992        let dim = self.dim;
993        let n = t * h * w + 1 + self.n_reg;
994        let mapped = |p: &Proj| match p {
995            Proj::Q(q) => q.mapped_device_gemm().map(|(m, i)| (m.clone(), i)),
996            Proj::F32 { .. } => None,
997        };
998        let model = mapped(&self.blocks.first()?.qkv)?.0;
999        let chain: Vec<crate::gpu::H3ChainBlock> = self
1000            .blocks
1001            .iter()
1002            .map(|b| {
1003                Some(crate::gpu::H3ChainBlock {
1004                    qkv: mapped(&b.qkv)?.1,
1005                    out: mapped(&b.out)?.1,
1006                    fc1: mapped(&b.w1)?.1,
1007                    fc2: mapped(&b.w2)?.1,
1008                    norm1: &b.norm1,
1009                    norm2: &b.norm2,
1010                    q_norm: &[],
1011                    k_norm: &[],
1012                    mods: &[],
1013                    qkv_bias: &b.qkv_b,
1014                    out_bias: &b.out_b,
1015                    fc1_bias: &b.w1_b,
1016                    fc2_bias: &b.w2_b,
1017                    scale1: &b.scale1,
1018                    scale2: &b.scale2,
1019                })
1020            })
1021            .collect::<Option<Vec<_>>>()?;
1022        let angles = self.rope_angles(t, h, w, 1 + self.n_reg);
1023        // Tiles a batch: the score rows are tiles · heads · n², six bytes
1024        // an entry on the card.
1025        let g_max = ((256usize << 20) / (self.heads * n * n * 6)).clamp(1, 64);
1026        let mut out = Vec::with_capacity(subs.len());
1027        for batch in subs.chunks(g_max) {
1028            let t_blk = std::time::Instant::now();
1029            let mut x = Vec::with_capacity(batch.len() * n * dim);
1030            for z in batch {
1031                x.extend_from_slice(&self.embed_tile(z, t, h, w));
1032            }
1033            let job = crate::gpu::H3Chain {
1034                model: &model,
1035                blocks: &chain,
1036                n,
1037                hidden: dim,
1038                heads: self.heads,
1039                head_dim: self.head_dim,
1040                ffn: 4 * dim,
1041                eps: self.eps as f32,
1042                qk_eps: self.eps as f32,
1043                angles: &angles,
1044                rows: &[],
1045                groups: batch.len(),
1046                qkv_interleaved: true,
1047            };
1048            if !crate::gpu::h3_chain(&job, &mut x) {
1049                return None;
1050            }
1051            vprof(3, t_blk);
1052            for k in 0..batch.len() {
1053                out.push(self.head_tile(&x[k * n * dim..(k + 1) * n * dim], t, h, w));
1054            }
1055        }
1056        Some(out)
1057    }
1058
1059    /// Spatially tiled decode of one temporal clip. `z` is
1060    /// `[z_channels, t, zh, zw]`; the result is `[3, t·4, zh·16, zw·16]`.
1061    fn decode_clip(&self, z: &[f32], t: usize, zh: usize, zw: usize) -> Vec<f32> {
1062        let r = self.patch;
1063        let (height, width) = (zh * r, zw * r);
1064        let (ys, yl, yo) = split_tiles(height, r);
1065        let (xs, xl, xo) = split_tiles(width, r);
1066        let frames = t * self.patch_t;
1067        let mut canvas = vec![0f32; 3 * frames * height * width];
1068
1069        // Every tile's latents first: the tiles are independent, so the
1070        // decoder may take them in batches; the blend below is sequential.
1071        let mut subs: Vec<Vec<f32>> = Vec::with_capacity(ys.len() * xs.len());
1072        for (&ip, &il) in ys.iter().zip(&yl) {
1073            let (zi, zl) = (ip / r, il / r);
1074            for (&jp, &jl) in xs.iter().zip(&xl) {
1075                let (zj, zw_t) = (jp / r, jl / r);
1076                let mut sub = vec![0f32; self.z_channels * t * zl * zw_t];
1077                for c in 0..self.z_channels {
1078                    for ti in 0..t {
1079                        for hh in 0..zl {
1080                            let s = ((c * t + ti) * zh + zi + hh) * zw + zj;
1081                            let d = ((c * t + ti) * zl + hh) * zw_t;
1082                            sub[d..d + zw_t].copy_from_slice(&z[s..s + zw_t]);
1083                        }
1084                    }
1085                }
1086                subs.push(sub);
1087            }
1088        }
1089        let mut tiles = self.decode_tiles(&subs, t, yl[0] / r, xl[0] / r);
1090        drop(subs);
1091
1092        // The reference blends each tile into its predecessor's tail and
1093        // then trims the overlap off, so a pixel is written once.
1094        let mut row_tails: Vec<Vec<f32>> = Vec::new();
1095        let mut out_y = 0usize;
1096        for (i, &il) in yl.iter().enumerate() {
1097            let zl = il / r;
1098            let mut new_tails: Vec<Vec<f32>> = Vec::new();
1099            let mut left_tail: Option<Vec<f32>> = None;
1100            let mut out_x = 0usize;
1101            let mut row_h = 0usize;
1102            for (j, &jl) in xl.iter().enumerate() {
1103                let zw_t = jl / r;
1104                let mut tile = std::mem::take(&mut tiles[i * xs.len() + j]);
1105                let (mut th, mut tw) = (zl * r, zw_t * r);
1106                if i + 1 < ys.len() {
1107                    new_tails.push(crop(&tile, frames, th, tw, th - yo[i], th, 0, tw));
1108                }
1109                let next_left = if j + 1 < xs.len() {
1110                    Some(crop(&tile, frames, th, tw, 0, th, tw - xo[j], tw))
1111                } else {
1112                    None
1113                };
1114                if i > 0 {
1115                    tile = blend(&row_tails[j], &tile, frames, th, tw, yo[i - 1], 2);
1116                }
1117                if j > 0 {
1118                    let lt = left_tail.as_ref().unwrap();
1119                    tile = blend(lt, &tile, frames, th, tw, xo[j - 1], 3);
1120                }
1121                left_tail = next_left;
1122                if i + 1 < ys.len() {
1123                    tile = crop(&tile, frames, th, tw, 0, th - yo[i], 0, tw);
1124                    th -= yo[i];
1125                }
1126                if j + 1 < xs.len() {
1127                    tile = crop(&tile, frames, th, tw, 0, th, 0, tw - xo[j]);
1128                    tw -= xo[j];
1129                }
1130                for c in 0..3 {
1131                    for f in 0..frames {
1132                        for hh in 0..th {
1133                            let dst = ((c * frames + f) * height + out_y + hh) * width + out_x;
1134                            let src = ((c * frames + f) * th + hh) * tw;
1135                            canvas[dst..dst + tw].copy_from_slice(&tile[src..src + tw]);
1136                        }
1137                    }
1138                }
1139                out_x += tw;
1140                row_h = th;
1141            }
1142            row_tails = new_tails;
1143            out_y += row_h;
1144        }
1145        canvas
1146    }
1147
1148    /// Normalized latents `[z_channels, t_lat, zh, zw]` → RGB in [0, 1],
1149    /// `[3, frames, zh·16, zw·16]`.
1150    pub fn decode(&self, z: &[f32], t_lat: usize, zh: usize, zw: usize) -> (Vec<f32>, usize) {
1151        let zc = self.z_channels;
1152        let np = t_lat * zh * zw;
1153        let mut zz = vec![0f32; zc * np];
1154        for c in 0..zc {
1155            let (m, s) = (self.latents_mean[c], self.latents_std[c]);
1156            for i in 0..np {
1157                zz[c * np + i] = z[c * np + i] * s + m;
1158            }
1159        }
1160
1161        let ratio_t = self.patch_t;
1162        let chunk_tokens = CLIP_LENGTH.div_ceil(ratio_t); // 5
1163        let token_overlap = (chunk_tokens - TOKEN_DROP % chunk_tokens) % chunk_tokens; // 2
1164        let frame_pre_pad = (ratio_t - CLIP_LENGTH % ratio_t) % ratio_t; // 3
1165        let frame_overlap = (token_overlap * ratio_t).saturating_sub(frame_pre_pad); // 5
1166        let chunk_dec = chunk_tokens * ratio_t; // 20
1167
1168        // The encoder dropped TOKEN_DROP tokens off the tail, so the
1169        // decoder plans against a longer pseudo-sequence and pads the
1170        // real one out with a repeat of its last token.
1171        let mut pseudo = t_lat + TOKEN_DROP;
1172        let mut pad = 0usize;
1173        if pseudo % chunk_tokens != 0 {
1174            pad = chunk_tokens - pseudo % chunk_tokens;
1175            pseudo += pad;
1176        }
1177        let mut chunks = pseudo / chunk_tokens - usize::from(TOKEN_DROP > 0);
1178        if chunks < 1 {
1179            pad += chunk_tokens;
1180            chunks += 1;
1181        }
1182        let t_pad = t_lat + pad;
1183        if pad > 0 {
1184            let mut grown = vec![0f32; zc * t_pad * zh * zw];
1185            for c in 0..zc {
1186                for ti in 0..t_pad {
1187                    let src = ti.min(t_lat - 1);
1188                    let a = (c * t_lat + src) * zh * zw;
1189                    let b = (c * t_pad + ti) * zh * zw;
1190                    grown[b..b + zh * zw].copy_from_slice(&zz[a..a + zh * zw]);
1191                }
1192            }
1193            zz = grown;
1194        }
1195
1196        let (h, w) = (zh * self.patch, zw * self.patch);
1197        let mut out: Vec<f32> = Vec::new();
1198        let mut carry: Option<(Vec<f32>, usize)> = None;
1199        for i in 0..chunks {
1200            let a = i * chunk_tokens;
1201            let b = (a + chunk_tokens + token_overlap).min(t_pad);
1202            let n = b.saturating_sub(a.min(t_pad));
1203            if n == 0 {
1204                continue;
1205            }
1206            let mut sub = vec![0f32; zc * n * zh * zw];
1207            for c in 0..zc {
1208                let src = (c * t_pad + a) * zh * zw;
1209                let dst = c * n * zh * zw;
1210                sub[dst..dst + n * zh * zw].copy_from_slice(&zz[src..src + n * zh * zw]);
1211            }
1212            let dec = self.decode_clip(&sub, n, zh, zw);
1213            let dec_frames = n * ratio_t;
1214            for j in 0..2 {
1215                let fa = j * chunk_dec;
1216                let fb = (fa + chunk_dec).min(dec_frames);
1217                if fb <= fa + frame_pre_pad {
1218                    continue;
1219                }
1220                let mut part = frames_of(&dec, dec_frames, h, w, fa + frame_pre_pad, fb);
1221                let mut pn = fb - fa - frame_pre_pad;
1222                if j == 0 {
1223                    if let Some((tail, tn)) = carry.take() {
1224                        part = blend_frames(&tail, tn, &part, pn, h, w, frame_overlap);
1225                        pn = part.len() / (3 * h * w);
1226                    }
1227                    append_frames(&mut out, &part, pn, h, w);
1228                } else {
1229                    carry = Some((part, pn));
1230                }
1231            }
1232            if i + 1 == chunks {
1233                if let Some((tail, tn)) = carry.take() {
1234                    append_frames(&mut out, &tail, tn, h, w);
1235                }
1236            }
1237        }
1238
1239        // Undo the ImageNet pixel normalization and clamp; the reference
1240        // then maps to [-1, 1] and every caller maps straight back, so
1241        // stop at [0, 1].
1242        let frames = out.len() / (3 * h * w);
1243        let want = t_lat * ratio_t - pad_frames(t_lat, pad, chunk_tokens, ratio_t);
1244        for c in 0..3 {
1245            let base = c * frames * h * w;
1246            for v in out[base..base + frames * h * w].iter_mut() {
1247                *v = (*v * IMAGENET_STD[c] + IMAGENET_MEAN[c]).clamp(0.0, 1.0);
1248            }
1249        }
1250        let keep = want.min(frames);
1251        if keep < frames {
1252            let mut trimmed = vec![0f32; 3 * keep * h * w];
1253            for c in 0..3 {
1254                let src = c * frames * h * w;
1255                let dst = c * keep * h * w;
1256                trimmed[dst..dst + keep * h * w].copy_from_slice(&out[src..src + keep * h * w]);
1257            }
1258            out = trimmed;
1259        }
1260        (out, keep)
1261    }
1262
1263    /// MiniMax-H3-Image: one normalized latent frame `[z_channels, zh, zw]`
1264    /// → RGB in [0, 1], `[3, zh·16, zw·16]`. The decoder takes the latent
1265    /// as it is (no clip of copies around it), tiled the way the video
1266    /// one is.
1267    pub fn decode_image(&self, z: &[f32], zh: usize, zw: usize) -> Vec<f32> {
1268        debug_assert_eq!(self.patch_t, 1, "decode_image on a video decoder");
1269        let zc = self.z_channels;
1270        let np = zh * zw;
1271        let mut zz = vec![0f32; zc * np];
1272        for c in 0..zc {
1273            let (m, s) = (self.latents_mean[c], self.latents_std[c]);
1274            for i in 0..np {
1275                zz[c * np + i] = z[c * np + i] * s + m;
1276            }
1277        }
1278        let mut out = self.decode_clip(&zz, 1, zh, zw);
1279        let hw = zh * self.patch * zw * self.patch;
1280        for c in 0..3 {
1281            for v in out[c * hw..(c + 1) * hw].iter_mut() {
1282                *v = (*v * IMAGENET_STD[c] + IMAGENET_MEAN[c]).clamp(0.0, 1.0);
1283            }
1284        }
1285        out
1286    }
1287
1288    pub fn spatial_ratio(&self) -> usize {
1289        self.patch
1290    }
1291    pub fn temporal_ratio(&self) -> usize {
1292        self.patch_t
1293    }
1294}
1295
1296/// Frames the padding tokens manufactured, which the reference trims.
1297fn pad_frames(t_lat: usize, pad: usize, chunk_tokens: usize, ratio_t: usize) -> usize {
1298    if pad == 0 {
1299        return 0;
1300    }
1301    let intra = CLIP_LENGTH % ratio_t;
1302    if intra == 0 {
1303        return pad * ratio_t;
1304    }
1305    (0..pad)
1306        .map(|k| {
1307            if (t_lat + k) % chunk_tokens == 0 {
1308                intra
1309            } else {
1310                ratio_t
1311            }
1312        })
1313        .sum()
1314}
1315
1316/// `[3, f, h, w]` sub-rectangle.
1317#[allow(clippy::too_many_arguments)]
1318fn crop(
1319    x: &[f32],
1320    f: usize,
1321    h: usize,
1322    w: usize,
1323    y0: usize,
1324    y1: usize,
1325    x0: usize,
1326    x1: usize,
1327) -> Vec<f32> {
1328    let (nh, nw) = (y1 - y0, x1 - x0);
1329    let mut out = vec![0f32; 3 * f * nh * nw];
1330    for c in 0..3 {
1331        for fi in 0..f {
1332            for yy in 0..nh {
1333                let s = ((c * f + fi) * h + y0 + yy) * w + x0;
1334                let d = ((c * f + fi) * nh + yy) * nw;
1335                out[d..d + nw].copy_from_slice(&x[s..s + nw]);
1336            }
1337        }
1338    }
1339    out
1340}
1341
1342/// Linear cross-fade of `a`'s tail into `b`'s head along `dim` (2 = y,
1343/// 3 = x); the result is `b` with its first `extent` rows/columns
1344/// replaced by the blend.
1345#[allow(clippy::too_many_arguments)]
1346fn blend(
1347    a: &[f32],
1348    b: &[f32],
1349    f: usize,
1350    h: usize,
1351    w: usize,
1352    extent: usize,
1353    dim: usize,
1354) -> Vec<f32> {
1355    let ah = a.len() / (3 * f * w);
1356    let aw = a.len() / (3 * f * h);
1357    let mut out = b.to_vec();
1358    let e = if dim == 2 {
1359        extent.min(ah).min(h)
1360    } else {
1361        extent.min(aw).min(w)
1362    };
1363    for c in 0..3 {
1364        for fi in 0..f {
1365            for k in 0..e {
1366                let wb = k as f32 / e as f32;
1367                let wa = 1.0 - wb;
1368                if dim == 2 {
1369                    let sa = ((c * f + fi) * ah + ah - e + k) * w;
1370                    let sb = ((c * f + fi) * h + k) * w;
1371                    for x in 0..w {
1372                        out[sb + x] = a[sa + x] * wa + b[sb + x] * wb;
1373                    }
1374                } else {
1375                    for y in 0..h {
1376                        let sa = ((c * f + fi) * h + y) * aw + aw - e + k;
1377                        let sb = ((c * f + fi) * h + y) * w + k;
1378                        out[sb] = a[sa] * wa + b[sb] * wb;
1379                    }
1380                }
1381            }
1382        }
1383    }
1384    out
1385}
1386
1387/// Frames `[a, b)` of a `[3, f, h, w]` clip.
1388fn frames_of(x: &[f32], f: usize, h: usize, w: usize, a: usize, b: usize) -> Vec<f32> {
1389    let n = b - a;
1390    let mut out = vec![0f32; 3 * n * h * w];
1391    for c in 0..3 {
1392        let s = (c * f + a) * h * w;
1393        let d = c * n * h * w;
1394        out[d..d + n * h * w].copy_from_slice(&x[s..s + n * h * w]);
1395    }
1396    out
1397}
1398
1399/// Cross-fade `tail` into the head of `part` along the frame axis.
1400#[allow(clippy::too_many_arguments)]
1401fn blend_frames(
1402    tail: &[f32],
1403    tn: usize,
1404    part: &[f32],
1405    pn: usize,
1406    h: usize,
1407    w: usize,
1408    extent: usize,
1409) -> Vec<f32> {
1410    let e = extent.min(tn).min(pn);
1411    let mut out = part.to_vec();
1412    for c in 0..3 {
1413        for k in 0..e {
1414            let wb = k as f32 / e as f32;
1415            let wa = 1.0 - wb;
1416            let s = ((c * tn) + tn - e + k) * h * w;
1417            let d = ((c * pn) + k) * h * w;
1418            for i in 0..h * w {
1419                out[d + i] = tail[s + i] * wa + part[d + i] * wb;
1420            }
1421        }
1422    }
1423    out
1424}
1425
1426/// Append a `[3, n, h, w]` clip to a growing `[3, ?, h, w]` buffer.
1427fn append_frames(out: &mut Vec<f32>, part: &[f32], n: usize, h: usize, w: usize) {
1428    let old = out.len() / (3 * h * w);
1429    let total = old + n;
1430    let mut grown = vec![0f32; 3 * total * h * w];
1431    for c in 0..3 {
1432        if old > 0 {
1433            let s = c * old * h * w;
1434            let d = c * total * h * w;
1435            grown[d..d + old * h * w].copy_from_slice(&out[s..s + old * h * w]);
1436        }
1437        let s = c * n * h * w;
1438        let d = (c * total + old) * h * w;
1439        grown[d..d + n * h * w].copy_from_slice(&part[s..s + n * h * w]);
1440    }
1441    *out = grown;
1442}
1443
1444// ── the encoder, for keyframes only ─────────────────────────────────
1445//
1446// `fl2va` conditions on a first and/or last frame, and a frame is ONE
1447// frame. That collapses the whole 3-D causal encoder to a 2-D one:
1448// causal padding fills the front with zeros, and the reference's own
1449// `autopad="causal_zero"` therefore trims the kernel to
1450// `weight[:, :, -T:]` — at T = 1, the last temporal tap and nothing
1451// else. So the packer stores that tap alone (a third of the bytes) and
1452// this runs plain 2-D convolutions over it. Encoding real video would
1453// need the other two taps back; keyframes never reach them.
1454
1455/// A 2-D convolution with the reference's padding policy: reflect on
1456/// H and W, applied by hand because the checkpoint's convolutions
1457/// carry no padding of their own.
1458struct EncConv {
1459    w: Vec<f32>, // [out, in, kh, kw]
1460    b: Vec<f32>,
1461    out_ch: usize,
1462    in_ch: usize,
1463    k: usize,
1464    stride: usize,
1465    pad: usize,
1466}
1467
1468impl EncConv {
1469    fn load(model: &Arc<CmfModel>, name: &str, stride: usize, pad: usize) -> Result<Self, String> {
1470        let e = model
1471            .tensor(&format!("{name}.weight"))
1472            .ok_or_else(|| format!("missing {name}.weight"))?;
1473        // [out, in, kh, kw] — the packer already dropped the temporal axis.
1474        let (out_ch, in_ch, k) = (e.shape[0], e.shape[1], e.shape[2]);
1475        Ok(Self {
1476            w: crate::dit::cmf_f32(model, &format!("{name}.weight"))?,
1477            b: crate::dit::cmf_f32(model, &format!("{name}.bias"))?,
1478            out_ch,
1479            in_ch,
1480            k,
1481            stride,
1482            pad,
1483        })
1484    }
1485
1486    /// `x` is `[in_ch, h, w]`; reflect-padded by `self.pad` on each side.
1487    fn apply(
1488        &self,
1489        x: &[f32],
1490        h: usize,
1491        w: usize,
1492        pool: Option<&Pool>,
1493    ) -> (Vec<f32>, usize, usize) {
1494        let (ph, pw) = (h + 2 * self.pad, w + 2 * self.pad);
1495        let oh = (ph - self.k) / self.stride + 1;
1496        let ow = (pw - self.k) / self.stride + 1;
1497        // Reflect padding: index |p| mirrored about the edges, which for
1498        // pad 1 on a plane of at least 2 is just the neighbour row.
1499        let refl = |i: isize, n: usize| -> usize {
1500            let n = n as isize;
1501            let mut i = i;
1502            while i < 0 || i >= n {
1503                if i < 0 {
1504                    i = -i;
1505                }
1506                if i >= n {
1507                    i = 2 * (n - 1) - i;
1508                }
1509            }
1510            i as usize
1511        };
1512        let mut out = vec![0f32; self.out_ch * oh * ow];
1513        // A convolution of any size is an im2col panel and one GEMM: the
1514        // panel rows are output pixels, its columns the kernel's (channel,
1515        // ky, kx) taps — the weight's own layout — and the GEMM is the
1516        // pooled host one, or the device's where a device is on. The loop
1517        // below did the same sums one output channel a worker and was
1518        // minutes a tile on a reference picture.
1519        let kk = self.in_ch * self.k * self.k;
1520        if kk * self.out_ch >= (1 << 12) && oh * ow >= 64 {
1521            let n = oh * ow;
1522            let chunk = ((64usize << 20) / (kk * 4)).clamp(1, n);
1523            let mut cols = vec![0f32; chunk * kk];
1524            let mut y = vec![0f32; chunk * self.out_ch];
1525            let (k, stride, pad) = (self.k, self.stride, self.pad as isize);
1526            for p0 in (0..n).step_by(chunk) {
1527                let np = chunk.min(n - p0);
1528                {
1529                    let cp = SendPtr(cols.as_mut_ptr());
1530                    rows_par(pool, np, &|lo, hi| {
1531                        for p in lo..hi {
1532                            let (oy, ox) = ((p0 + p) / ow, (p0 + p) % ow);
1533                            // SAFETY: workers own disjoint panel rows.
1534                            let row = unsafe { cp.row(p * kk, kk) };
1535                            for ky in 0..k {
1536                                let sy = refl((oy * stride + ky) as isize - pad, h);
1537                                for kx in 0..k {
1538                                    let sx = refl((ox * stride + kx) as isize - pad, w);
1539                                    let at = sy * w + sx;
1540                                    for i in 0..self.in_ch {
1541                                        row[(i * k + ky) * k + kx] = x[i * h * w + at];
1542                                    }
1543                                }
1544                            }
1545                        }
1546                    });
1547                }
1548                crate::fcd_ops::gemm_nt(
1549                    &cols[..np * kk],
1550                    &self.w,
1551                    &mut y[..np * self.out_ch],
1552                    np,
1553                    kk,
1554                    self.out_ch,
1555                    pool,
1556                );
1557                for o in 0..self.out_ch {
1558                    let dst = &mut out[o * n + p0..o * n + p0 + np];
1559                    for (p, d) in dst.iter_mut().enumerate() {
1560                        *d = y[p * self.out_ch + o] + self.b[o];
1561                    }
1562                }
1563            }
1564            return (out, oh, ow);
1565        }
1566        let ptr = SendPtr(out.as_mut_ptr());
1567        let work = |lo: usize, hi: usize| {
1568            for o in lo..hi {
1569                // SAFETY: workers own disjoint output channels.
1570                let dst = unsafe { ptr.row(o * oh * ow, oh * ow) };
1571                dst.fill(self.b[o]);
1572                for i in 0..self.in_ch {
1573                    let ker = &self.w[(o * self.in_ch + i) * self.k * self.k
1574                        ..(o * self.in_ch + i + 1) * self.k * self.k];
1575                    let src = &x[i * h * w..(i + 1) * h * w];
1576                    for oy in 0..oh {
1577                        for ox in 0..ow {
1578                            let mut acc = 0f32;
1579                            for ky in 0..self.k {
1580                                let sy = (oy * self.stride + ky) as isize - self.pad as isize;
1581                                let sy = refl(sy, h);
1582                                for kx in 0..self.k {
1583                                    let sx = (ox * self.stride + kx) as isize - self.pad as isize;
1584                                    acc += ker[ky * self.k + kx] * src[sy * w + refl(sx, w)];
1585                                }
1586                            }
1587                            dst[oy * ow + ox] += acc;
1588                        }
1589                    }
1590                }
1591            }
1592        };
1593        match pool {
1594            Some(p) => p.run_rows(self.out_ch, &work),
1595            None => work(0, self.out_ch),
1596        }
1597        (out, oh, ow)
1598    }
1599}
1600
1601/// GroupNorm(32) with affine parameters, over `[ch, h, w]`.
1602struct GroupNorm {
1603    w: Vec<f32>,
1604    b: Vec<f32>,
1605}
1606
1607impl GroupNorm {
1608    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
1609        Ok(Self {
1610            w: crate::dit::cmf_f32(model, &format!("{name}.weight"))?,
1611            b: crate::dit::cmf_f32(model, &format!("{name}.bias"))?,
1612        })
1613    }
1614
1615    fn apply(&self, x: &mut [f32], ch: usize, hw: usize) {
1616        let groups = 32;
1617        let per = ch / groups;
1618        for g in 0..groups {
1619            let seg = &mut x[g * per * hw..(g + 1) * per * hw];
1620            let n = seg.len() as f64;
1621            let mean = seg.iter().map(|&v| v as f64).sum::<f64>() / n;
1622            let var = seg.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / n;
1623            let inv = 1.0 / (var + 1e-6).sqrt();
1624            for (i, v) in seg.iter_mut().enumerate() {
1625                let c = g * per + i / hw;
1626                *v = ((*v as f64 - mean) * inv) as f32 * self.w[c] + self.b[c];
1627            }
1628        }
1629    }
1630}
1631
1632struct ResBlock {
1633    norm1: GroupNorm,
1634    norm2: GroupNorm,
1635    conv1: EncConv,
1636    conv2: EncConv,
1637    shortcut: Option<EncConv>,
1638}
1639
1640/// The encoder half: `[3, h, w]` in [-1, 1] → normalized latents
1641/// `[24, h/16, w/16]`.
1642pub struct VideoVaeEncoder {
1643    conv_in: EncConv,
1644    levels: Vec<(Vec<ResBlock>, Option<EncConv>)>,
1645    norm_out: GroupNorm,
1646    conv_out: EncConv,
1647    quant: Vec<f32>, // [48, 48] 1x1x1
1648    quant_b: Vec<f32>,
1649    latents_mean: Vec<f32>,
1650    latents_std: Vec<f32>,
1651    pool: Option<Arc<Pool>>,
1652    z_channels: usize,
1653    ratio: usize,
1654}
1655
1656impl VideoVaeEncoder {
1657    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
1658        let cfg: serde_json::Value = serde_json::from_slice(
1659            model
1660                .tensor_bytes("vvae.config_json")
1661                .map_err(|e| e.to_string())?,
1662        )
1663        .map_err(|e| format!("vvae.config_json: {e}"))?;
1664        let space_down: Vec<usize> = cfg["space_down"]
1665            .as_array()
1666            .map(|a| a.iter().map(|v| v.as_u64().unwrap_or(1) as usize).collect())
1667            .unwrap_or_else(|| vec![2, 2, 2, 2, 1, 1]);
1668        let n_res = cfg["num_res_blocks"].as_u64().unwrap_or(2) as usize;
1669        let mut levels = Vec::new();
1670        for (i, &sd) in space_down.iter().enumerate() {
1671            let mut blocks = Vec::new();
1672            for j in 0..n_res {
1673                let p = format!("vvae.enc.down.{i}.block.{j}");
1674                let shortcut = model
1675                    .tensor(&format!("{p}.nin_shortcut.weight"))
1676                    .map(|_| EncConv::load(model, &format!("{p}.nin_shortcut"), 1, 0))
1677                    .transpose()?;
1678                blocks.push(ResBlock {
1679                    norm1: GroupNorm::load(model, &format!("{p}.norm1"))?,
1680                    norm2: GroupNorm::load(model, &format!("{p}.norm2"))?,
1681                    conv1: EncConv::load(model, &format!("{p}.conv1"), 1, 1)?,
1682                    conv2: EncConv::load(model, &format!("{p}.conv2"), 1, 1)?,
1683                    shortcut,
1684                });
1685            }
1686            // The downsample's own convolution pads nothing: the caller
1687            // reflect-pads one row and column on the far side instead.
1688            let down = if sd > 1 {
1689                Some(EncConv::load(
1690                    model,
1691                    &format!("vvae.enc.down.{i}.downsample.conv"),
1692                    sd,
1693                    0,
1694                )?)
1695            } else {
1696                None
1697            };
1698            levels.push((blocks, down));
1699        }
1700        Ok(Self {
1701            conv_in: EncConv::load(model, "vvae.enc.conv_in", 1, 1)?,
1702            levels,
1703            norm_out: GroupNorm::load(model, "vvae.enc.norm_out")?,
1704            conv_out: EncConv::load(model, "vvae.enc.conv_out", 1, 1)?,
1705            quant: crate::dit::cmf_f32(model, "vvae.quant_conv.weight")?,
1706            quant_b: crate::dit::cmf_f32(model, "vvae.quant_conv.bias")?,
1707            latents_mean: crate::dit::cmf_f32(model, "vvae.latents_mean")?,
1708            latents_std: crate::dit::cmf_f32(model, "vvae.latents_std")?,
1709            pool: Pool::from_env(),
1710            z_channels: cfg["z_channels"].as_u64().unwrap_or(24) as usize,
1711            ratio: cfg["patch_size"].as_u64().unwrap_or(16) as usize,
1712        })
1713    }
1714
1715    /// The encoder's stages as one chain for a device that runs it whole:
1716    /// the tile goes up once and the pre-quant moments come back.
1717    fn encode_tile_chain(&self, x: &[f32], h: usize, w: usize) -> Option<(Vec<f32>, usize, usize)> {
1718        use crate::gpu::ConvStep;
1719        if std::env::var("CMF_VAE3D_CHAIN").as_deref() == Ok("0") || !crate::gpu::enabled_here() {
1720            return None;
1721        }
1722        fn conv(c: &EncConv, far: bool) -> ConvStep<'_> {
1723            ConvStep::Conv {
1724                w: &c.w,
1725                b: &c.b,
1726                out_ch: c.out_ch,
1727                in_ch: c.in_ch,
1728                k: c.k,
1729                stride: c.stride,
1730                pad: c.pad,
1731                far,
1732            }
1733        }
1734        let mut steps = vec![conv(&self.conv_in, false)];
1735        for (blocks, down) in &self.levels {
1736            for b in blocks {
1737                steps.push(ConvStep::Save);
1738                steps.push(ConvStep::GnSilu { w: &b.norm1.w, b: &b.norm1.b });
1739                steps.push(conv(&b.conv1, false));
1740                steps.push(ConvStep::GnSilu { w: &b.norm2.w, b: &b.norm2.b });
1741                steps.push(conv(&b.conv2, false));
1742                steps.push(ConvStep::AddSaved {
1743                    via: b.shortcut.as_ref().map(|s| (&s.w[..], &s.b[..])),
1744                });
1745            }
1746            if let Some(d) = down {
1747                // one reflected row and column on the far side, then the
1748                // unpadded stride-2 kernel
1749                steps.push(conv(d, true));
1750            }
1751        }
1752        steps.push(ConvStep::GnSilu { w: &self.norm_out.w, b: &self.norm_out.b });
1753        steps.push(conv(&self.conv_out, false));
1754        let (moments, _, mh, mw) = crate::gpu::conv_chain(&steps, x, 3, h, w)?;
1755        Some((moments, mh, mw))
1756    }
1757
1758    /// The 1×1×1 quant conv over the encoder's moments: a per-position
1759    /// linear.
1760    fn quant_moments(&self, moments: &[f32], hw: usize) -> Vec<f32> {
1761        let n = 2 * self.z_channels;
1762        let mut out = vec![0f32; n * hw];
1763        for o in 0..n {
1764            for p in 0..hw {
1765                let mut acc = self.quant_b[o];
1766                for i in 0..n {
1767                    acc += self.quant[o * n + i] * moments[i * hw + p];
1768                }
1769                out[o * hw + p] = acc;
1770            }
1771        }
1772        out
1773    }
1774
1775    /// One tile, unpadded: `[3, h, w]` → moments `[2·z, h/16, w/16]`.
1776    fn encode_tile(&self, x: &[f32], h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1777        if let Some((moments, mh, mw)) = self.encode_tile_chain(x, h, w) {
1778            return (self.quant_moments(&moments, mh * mw), mh, mw);
1779        }
1780        let pool = self.pool.as_deref();
1781        let (mut cur, mut ch, mut cw) = self.conv_in.apply(x, h, w, pool);
1782        let mut c = self.conv_in.out_ch;
1783        for (blocks, down) in &self.levels {
1784            for b in blocks {
1785                let mut hh = cur.clone();
1786                self.norm_out_like(&b.norm1, &mut hh, c, ch * cw);
1787                for v in hh.iter_mut() {
1788                    *v = silu(*v);
1789                }
1790                let (mut t, th, tw) = b.conv1.apply(&hh, ch, cw, pool);
1791                let oc = b.conv1.out_ch;
1792                self.norm_out_like(&b.norm2, &mut t, oc, th * tw);
1793                for v in t.iter_mut() {
1794                    *v = silu(*v);
1795                }
1796                let (t2, th2, tw2) = b.conv2.apply(&t, th, tw, pool);
1797                let skip = match &b.shortcut {
1798                    Some(s) => s.apply(&cur, ch, cw, pool).0,
1799                    None => cur.clone(),
1800                };
1801                cur = t2.iter().zip(&skip).map(|(&a, &b)| a + b).collect();
1802                (ch, cw, c) = (th2, tw2, oc);
1803            }
1804            if let Some(d) = down {
1805                // reflect one row and column on the far side, as the
1806                // reference does before its unpadded stride-2 kernel
1807                let (padded, ph, pw) = reflect_pad_far(&cur, c, ch, cw);
1808                let (t, th, tw) = d.apply(&padded, ph, pw, pool);
1809                cur = t;
1810                (ch, cw, c) = (th, tw, d.out_ch);
1811            }
1812        }
1813        self.norm_out_like(&self.norm_out, &mut cur, c, ch * cw);
1814        for v in cur.iter_mut() {
1815            *v = silu(*v);
1816        }
1817        let (moments, mh, mw) = self.conv_out.apply(&cur, ch, cw, pool);
1818        (self.quant_moments(&moments, mh * mw), mh, mw)
1819    }
1820
1821    fn norm_out_like(&self, g: &GroupNorm, x: &mut [f32], ch: usize, hw: usize) {
1822        g.apply(x, ch, hw);
1823    }
1824
1825    /// A single frame in [-1, 1], `[3, h, w]` → normalized latents
1826    /// `[z, h/16, w/16]`, spatially tiled exactly as the reference does:
1827    /// the posterior MEAN, no sampling (what a video keyframe conditions
1828    /// on).
1829    pub fn encode_frame(&self, rgb: &[f32], h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1830        let (canvas, zh, zw) = self.moments(rgb, h, w);
1831        let mut z = vec![0f32; self.z_channels * zh * zw];
1832        for c in 0..self.z_channels {
1833            let (m, s) = (self.latents_mean[c], self.latents_std[c]);
1834            for p in 0..zh * zw {
1835                z[c * zh * zw + p] = (canvas[c * zh * zw + p] - m) / s;
1836            }
1837        }
1838        (z, zh, zw)
1839    }
1840
1841    /// The same frame as a SAMPLE of the posterior — mean + σ·ε with
1842    /// `logvar` clamped to [-30, 20] — which is what MiniMax-H3-Image's
1843    /// reference pictures condition on. The reference draws ε from a
1844    /// torch generator at a fixed seed; ours is its own stream of the
1845    /// same law, fixed by `seed`.
1846    pub fn encode_frame_sampled(
1847        &self,
1848        rgb: &[f32],
1849        h: usize,
1850        w: usize,
1851        seed: u64,
1852    ) -> (Vec<f32>, usize, usize) {
1853        let (canvas, zh, zw) = self.moments(rgb, h, w);
1854        let zc = self.z_channels;
1855        let noise = crate::videogen::gauss_pub(zc * zh * zw, seed);
1856        let mut z = vec![0f32; zc * zh * zw];
1857        for c in 0..zc {
1858            let (m, s) = (self.latents_mean[c], self.latents_std[c]);
1859            for p in 0..zh * zw {
1860                let mean = canvas[c * zh * zw + p];
1861                let logvar = canvas[(zc + c) * zh * zw + p].clamp(-30.0, 20.0);
1862                let v = mean + (0.5 * logvar).exp() * noise[c * zh * zw + p];
1863                z[c * zh * zw + p] = (v - m) / s;
1864            }
1865        }
1866        (z, zh, zw)
1867    }
1868
1869    /// The posterior moments of one frame: `[2·z, h/16, w/16]`, the mean's
1870    /// channels then the log-variance's, tiles blended as the reference
1871    /// blends them.
1872    fn moments(&self, rgb: &[f32], h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1873        // [-1, 1] → [0, 1] → ImageNet-normalized, the reference's order.
1874        let mut x = vec![0f32; 3 * h * w];
1875        for c in 0..3 {
1876            for p in 0..h * w {
1877                let v = (rgb[c * h * w + p] + 1.0) * 0.5;
1878                x[c * h * w + p] = (v - IMAGENET_MEAN[c]) / IMAGENET_STD[c];
1879            }
1880        }
1881        let r = self.ratio;
1882        let (ys, yl, yo) = split_tiles(h, r);
1883        let (xs, xl, xo) = split_tiles(w, r);
1884        let (zh, zw) = (h / r, w / r);
1885        let zc = 2 * self.z_channels;
1886        let mut rows: Vec<Vec<Vec<f32>>> = Vec::new();
1887        let mut dims: Vec<Vec<(usize, usize)>> = Vec::new();
1888        for (&ip, &il) in ys.iter().zip(&yl) {
1889            let mut row = Vec::new();
1890            let mut rd = Vec::new();
1891            for (&jp, &jl) in xs.iter().zip(&xl) {
1892                let mut sub = vec![0f32; 3 * il * jl];
1893                for c in 0..3 {
1894                    for yy in 0..il {
1895                        let s = (c * h + ip + yy) * w + jp;
1896                        let d = (c * il + yy) * jl;
1897                        sub[d..d + jl].copy_from_slice(&x[s..s + jl]);
1898                    }
1899                }
1900                let (t, th, tw) = self.encode_tile(&sub, il, jl);
1901                row.push(t);
1902                rd.push((th, tw));
1903            }
1904            rows.push(row);
1905            dims.push(rd);
1906        }
1907        // Latent-space blend then trim, mirroring `tiled_encode`.
1908        let mut canvas = vec![0f32; zc * zh * zw];
1909        let mut out_y = 0usize;
1910        for i in 0..rows.len() {
1911            let mut out_x = 0usize;
1912            let mut row_h = 0usize;
1913            for j in 0..rows[i].len() {
1914                let (th, tw) = dims[i][j];
1915                let mut tile = rows[i][j].clone();
1916                let (mut ch_, mut cw_) = (th, tw);
1917                if i > 0 {
1918                    tile = blend_plane(
1919                        &rows[i - 1][j],
1920                        &tile,
1921                        zc,
1922                        dims[i - 1][j],
1923                        (ch_, cw_),
1924                        yo[i - 1] / r,
1925                        0,
1926                    );
1927                }
1928                if j > 0 {
1929                    tile = blend_plane(
1930                        &rows[i][j - 1],
1931                        &tile,
1932                        zc,
1933                        dims[i][j - 1],
1934                        (ch_, cw_),
1935                        xo[j - 1] / r,
1936                        1,
1937                    );
1938                }
1939                if i + 1 < rows.len() {
1940                    tile = crop_plane(&tile, zc, ch_, cw_, 0, ch_ - yo[i] / r, 0, cw_);
1941                    ch_ -= yo[i] / r;
1942                }
1943                if j + 1 < rows[i].len() {
1944                    tile = crop_plane(&tile, zc, ch_, cw_, 0, ch_, 0, cw_ - xo[j] / r);
1945                    cw_ -= xo[j] / r;
1946                }
1947                for c in 0..zc {
1948                    for yy in 0..ch_ {
1949                        let d = (c * zh + out_y + yy) * zw + out_x;
1950                        let s = (c * ch_ + yy) * cw_;
1951                        canvas[d..d + cw_].copy_from_slice(&tile[s..s + cw_]);
1952                    }
1953                }
1954                out_x += cw_;
1955                row_h = ch_;
1956            }
1957            out_y += row_h;
1958        }
1959        (canvas, zh, zw)
1960    }
1961}
1962
1963/// Reflect one row and one column onto the far edge — `F.pad(x, (0,1,0,1))`.
1964fn reflect_pad_far(x: &[f32], c: usize, h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1965    let (ph, pw) = (h + 1, w + 1);
1966    let mut out = vec![0f32; c * ph * pw];
1967    for ci in 0..c {
1968        for y in 0..ph {
1969            let sy = if y < h { y } else { h - 2 };
1970            for x2 in 0..pw {
1971                let sx = if x2 < w { x2 } else { w - 2 };
1972                out[(ci * ph + y) * pw + x2] = x[(ci * h + sy) * w + sx];
1973            }
1974        }
1975    }
1976    (out, ph, pw)
1977}
1978
1979/// Cross-fade `a`'s tail into `b`'s head over `extent`, `dim` 0 = y, 1 = x.
1980fn blend_plane(
1981    a: &[f32],
1982    b: &[f32],
1983    c: usize,
1984    ad: (usize, usize),
1985    bd: (usize, usize),
1986    extent: usize,
1987    dim: usize,
1988) -> Vec<f32> {
1989    let (ah, aw) = ad;
1990    let (bh, bw) = bd;
1991    let mut out = b.to_vec();
1992    let e = if dim == 0 {
1993        extent.min(ah).min(bh)
1994    } else {
1995        extent.min(aw).min(bw)
1996    };
1997    if e == 0 {
1998        return out;
1999    }
2000    for ci in 0..c {
2001        for k in 0..e {
2002            let wb = k as f32 / e as f32;
2003            let wa = 1.0 - wb;
2004            if dim == 0 {
2005                for x in 0..bw.min(aw) {
2006                    let sa = (ci * ah + ah - e + k) * aw + x;
2007                    let sb = (ci * bh + k) * bw + x;
2008                    out[sb] = a[sa] * wa + b[sb] * wb;
2009                }
2010            } else {
2011                for y in 0..bh.min(ah) {
2012                    let sa = (ci * ah + y) * aw + aw - e + k;
2013                    let sb = (ci * bh + y) * bw + k;
2014                    out[sb] = a[sa] * wa + b[sb] * wb;
2015                }
2016            }
2017        }
2018    }
2019    out
2020}
2021
2022/// `[c, h, w]` sub-rectangle.
2023#[allow(clippy::too_many_arguments)]
2024fn crop_plane(
2025    x: &[f32],
2026    c: usize,
2027    h: usize,
2028    w: usize,
2029    y0: usize,
2030    y1: usize,
2031    x0: usize,
2032    x1: usize,
2033) -> Vec<f32> {
2034    let (nh, nw) = (y1 - y0, x1 - x0);
2035    let mut out = vec![0f32; c * nh * nw];
2036    for ci in 0..c {
2037        for y in 0..nh {
2038            let s = (ci * h + y0 + y) * w + x0;
2039            let d = (ci * nh + y) * nw;
2040            out[d..d + nw].copy_from_slice(&x[s..s + nw]);
2041        }
2042    }
2043    out
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048    use super::*;
2049
2050    #[test]
2051    fn tile_schedule_matches_the_reference() {
2052        // 288 tall: two 256 tiles, the overlap grown to swallow the slack.
2053        let (s, l, o) = split_tiles(288, 16);
2054        assert_eq!(s, vec![0, 32]);
2055        assert_eq!(l, vec![256, 256]);
2056        assert_eq!(o, vec![224]);
2057        // 512 wide does not fit in two tiles at the minimum overlap.
2058        let (s, l, o) = split_tiles(512, 16);
2059        assert_eq!(s, vec![0, 128, 256]);
2060        assert_eq!(l, vec![256; 3]);
2061        assert_eq!(o, vec![128, 128]);
2062        // Anything at or under one tile is one tile.
2063        assert_eq!(split_tiles(256, 16).0, vec![0]);
2064        assert_eq!(split_tiles(128, 16).1, vec![128]);
2065    }
2066
2067    #[test]
2068    fn temporal_constants_come_out_as_the_reference_computes_them() {
2069        let ratio_t = 4usize;
2070        let chunk = CLIP_LENGTH.div_ceil(ratio_t);
2071        assert_eq!(chunk, 5);
2072        assert_eq!((chunk - TOKEN_DROP % chunk) % chunk, 2);
2073        assert_eq!((ratio_t - CLIP_LENGTH % ratio_t) % ratio_t, 3);
2074    }
2075}