Skip to main content

cortiq_engine/
s2v_vae.rs

1//! HunyuanVideo VAE (`AutoencoderKLHunyuanVideo`): 3-D causal convolutions,
2//! 8× space and 4× time, 16 latent channels.
3//!
4//! What the reference does and this reproduces:
5//! - every conv replicates one frame's worth in space and pads two frames in
6//!   front (causal), never behind;
7//! - `GroupNorm` statistics run over a whole tile, every frame of it;
8//! - upsampling is nearest, with the first frame kept to one in time;
9//! - the mid attention is single-head over every (t, h, w) token, and a token
10//!   only sees tokens of its own frame or an earlier one;
11//! - decode ALWAYS tiles in time: 5 latent frames every 3, blending 4 pixel
12//!   frames across each seam. That is the default of the diffusers class and
13//!   not a memory strategy to drop: the output depends on it.
14//!
15//! The latents here are `[C, F, H, W]`, in [`crate::ltxvae::Vol`].
16
17use crate::dit::cmf_f32;
18use crate::ltxvae::Vol;
19use crate::pool::Pool;
20use crate::s2v::{Rows, pool_rows, silu};
21use crate::zimage::host_gemm;
22use cortiq_core::CmfModel;
23use std::sync::Arc;
24
25pub const SPATIAL: usize = 8;
26pub const TEMPORAL: usize = 4;
27pub const SCALING_FACTOR: f32 = 0.476986;
28const CHUNK: usize = 4096;
29const GN_EPS: f64 = 1e-6;
30
31pub(crate) struct Conv {
32    pub(crate) w: Vec<f32>,
33    pub(crate) b: Vec<f32>,
34    pub(crate) cin: usize,
35    pub(crate) cout: usize,
36    pub(crate) k: [usize; 3],
37    pub(crate) stride: [usize; 3],
38}
39
40impl Conv {
41    pub(crate) fn load(m: &Arc<CmfModel>, p: &str, stride: [usize; 3]) -> Result<Conv, String> {
42        let name = format!("{p}.weight");
43        let e = m.tensor(&name).ok_or_else(|| format!("missing tensor {name}"))?;
44        if e.shape.len() != 5 {
45            return Err(format!("{name}: not a 3-D conv"));
46        }
47        let (cout, cin, k) = (e.shape[0], e.shape[1], [e.shape[2], e.shape[3], e.shape[4]]);
48        Ok(Conv { w: cmf_f32(m, &name)?, b: cmf_f32(m, &format!("{p}.bias"))?, cin, cout, k, stride })
49    }
50
51    fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
52        assert_eq!(x.c, self.cin);
53        let [kt, kh, kw] = self.k;
54        let [st, sh, sw] = self.stride;
55        // Causal replicate padding; `k/2` in space, `k - 1` in front in time.
56        let (pt, ph, pw) = (kt - 1, kh / 2, kw / 2);
57        let out_f = (x.f + pt - (kt - 1) - 1) / st + 1;
58        let out_h = (x.h + 2 * ph - (kh - 1) - 1) / sh + 1;
59        let out_w = (x.w + 2 * pw - (kw - 1) - 1) / sw + 1;
60        let mut out = Vol::zeros(self.cout, out_f, out_h, out_w);
61        let npos = out_f * out_h * out_w;
62        let taps = kt * kh * kw;
63        let k = self.cin * taps;
64        let mut pat = vec![0f32; CHUNK.min(npos) * k];
65        let mut ybuf = vec![0f32; CHUNK.min(npos) * self.cout];
66        let mut p0 = 0;
67        while p0 < npos {
68            let n = CHUNK.min(npos - p0);
69            let pr = Rows(pat.as_mut_ptr());
70            pool_rows(pool, n, &|lo, hi| {
71                for i in lo..hi {
72                    let p = p0 + i;
73                    let (oc, rest) = (p % out_w, p / out_w);
74                    let (or, of) = (rest % out_h, rest / out_h);
75                    // SAFETY: disjoint rows.
76                    let row = unsafe { pr.at(i * k, k) };
77                    for c in 0..self.cin {
78                        let mut o = c * taps;
79                        for dt in 0..kt {
80                            let f = (of * st + dt).saturating_sub(pt).min(x.f - 1);
81                            for dh in 0..kh {
82                                let h = (or * sh + dh).saturating_sub(ph).min(x.h - 1);
83                                for dw in 0..kw {
84                                    let w = (oc * sw + dw).saturating_sub(pw).min(x.w - 1);
85                                    row[o] = x.at(c, f, h, w);
86                                    o += 1;
87                                }
88                            }
89                        }
90                    }
91                }
92            });
93            host_gemm::gemm_nt(&pat[..n * k], &self.w, &mut ybuf[..n * self.cout], n, k, self.cout, pool);
94            for co in 0..self.cout {
95                let bias = self.b[co];
96                let dst = &mut out.data[co * npos + p0..co * npos + p0 + n];
97                for (i, d) in dst.iter_mut().enumerate() {
98                    *d = ybuf[i * self.cout + co] + bias;
99                }
100            }
101            p0 += n;
102        }
103        out
104    }
105}
106
107pub(crate) struct GroupNorm {
108    pub(crate) w: Vec<f32>,
109    pub(crate) b: Vec<f32>,
110}
111
112impl GroupNorm {
113    pub(crate) fn load(m: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
114        Ok(Self { w: cmf_f32(m, &format!("{p}.weight"))?, b: cmf_f32(m, &format!("{p}.bias"))? })
115    }
116    /// 32 groups over `[C, F, H, W]`, in place.
117    fn apply(&self, x: &mut Vol, pool: Option<&Pool>) {
118        let npos = x.positions();
119        let per = x.c / 32;
120        let sr = Rows(x.data.as_mut_ptr());
121        pool_rows(pool, 32, &|lo, hi| {
122            for g in lo..hi {
123                // SAFETY: groups own disjoint channel ranges.
124                let d = unsafe { sr.at(g * per * npos, per * npos) };
125                let cnt = d.len() as f64;
126                let mean = d.iter().map(|&v| v as f64).sum::<f64>() / cnt;
127                let var = d.iter().map(|&v| (v as f64 - mean) * (v as f64 - mean)).sum::<f64>() / cnt;
128                let inv = 1.0 / (var + GN_EPS).sqrt();
129                for c in 0..per {
130                    let (w, b) = (self.w[g * per + c], self.b[g * per + c]);
131                    for v in &mut d[c * npos..(c + 1) * npos] {
132                        *v = ((*v as f64 - mean) * inv) as f32 * w + b;
133                    }
134                }
135            }
136        });
137    }
138}
139
140fn silu_all(x: &mut Vol, pool: Option<&Pool>) {
141    let n = x.data.len();
142    let sr = Rows(x.data.as_mut_ptr());
143    pool_rows(pool, n / 4096 + 1, &|lo, hi| {
144        for i in lo..hi {
145            let (a, b) = (i * 4096, ((i + 1) * 4096).min(n));
146            if a >= b {
147                continue;
148            }
149            // SAFETY: disjoint spans.
150            for v in unsafe { sr.at(a, b - a) } {
151                *v = silu(*v);
152            }
153        }
154    });
155}
156
157pub(crate) struct Res {
158    pub(crate) n1: GroupNorm,
159    pub(crate) c1: Conv,
160    pub(crate) n2: GroupNorm,
161    pub(crate) c2: Conv,
162    pub(crate) short: Option<Conv>,
163}
164
165impl Res {
166    pub(crate) fn load(m: &Arc<CmfModel>, p: &str) -> Result<Res, String> {
167        let s = format!("{p}.conv_shortcut.conv.weight");
168        Ok(Res {
169            n1: GroupNorm::load(m, &format!("{p}.norm1"))?,
170            c1: Conv::load(m, &format!("{p}.conv1.conv"), [1, 1, 1])?,
171            n2: GroupNorm::load(m, &format!("{p}.norm2"))?,
172            c2: Conv::load(m, &format!("{p}.conv2.conv"), [1, 1, 1])?,
173            short: if m.tensor(&s).is_some() {
174                Some(Conv::load(m, &format!("{p}.conv_shortcut.conv"), [1, 1, 1])?)
175            } else {
176                None
177            },
178        })
179    }
180    fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
181        let mut h = x.clone();
182        self.n1.apply(&mut h, pool);
183        silu_all(&mut h, pool);
184        let mut h = self.c1.forward(&h, pool);
185        self.n2.apply(&mut h, pool);
186        silu_all(&mut h, pool);
187        let mut h = self.c2.forward(&h, pool);
188        match &self.short {
189            Some(s) => {
190                let r = s.forward(x, pool);
191                for (a, &b) in h.data.iter_mut().zip(&r.data) {
192                    *a += b;
193                }
194            }
195            None => {
196                for (a, &b) in h.data.iter_mut().zip(&x.data) {
197                    *a += b;
198                }
199            }
200        }
201        h
202    }
203}
204
205pub(crate) struct Mid {
206    pub(crate) r0: Res,
207    pub(crate) gn: GroupNorm,
208    pub(crate) q: (Vec<f32>, Vec<f32>),
209    pub(crate) k: (Vec<f32>, Vec<f32>),
210    pub(crate) v: (Vec<f32>, Vec<f32>),
211    pub(crate) o: (Vec<f32>, Vec<f32>),
212    pub(crate) r1: Res,
213}
214
215impl Mid {
216    pub(crate) fn load(m: &Arc<CmfModel>, p: &str) -> Result<Mid, String> {
217        let a = format!("{p}.attentions.0");
218        let lin = |n: &str| -> Result<(Vec<f32>, Vec<f32>), String> {
219            Ok((cmf_f32(m, &format!("{a}.{n}.weight"))?, cmf_f32(m, &format!("{a}.{n}.bias"))?))
220        };
221        Ok(Mid {
222            r0: Res::load(m, &format!("{p}.resnets.0"))?,
223            gn: GroupNorm::load(m, &format!("{a}.group_norm"))?,
224            q: lin("to_q")?,
225            k: lin("to_k")?,
226            v: lin("to_v")?,
227            o: lin("to_out.0")?,
228            r1: Res::load(m, &format!("{p}.resnets.1"))?,
229        })
230    }
231
232    fn attention(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
233        let c = x.c;
234        let n = x.positions();
235        let per_frame = x.h * x.w;
236        let mut y = x.clone();
237        self.gn.apply(&mut y, pool);
238        // [C, n] → [n, C]
239        let mut rows = vec![0f32; n * c];
240        for ch in 0..c {
241            for p in 0..n {
242                rows[p * c + ch] = y.data[ch * n + p];
243            }
244        }
245        let lin = |l: &(Vec<f32>, Vec<f32>), s: &[f32], m: usize| -> Vec<f32> {
246            let mut out = vec![0f32; m * c];
247            host_gemm::gemm_nt(s, &l.0, &mut out, m, c, c, pool);
248            for r in out.chunks_exact_mut(c) {
249                for (v, &b) in r.iter_mut().zip(&l.1) {
250                    *v += b;
251                }
252            }
253            out
254        };
255        let q = lin(&self.q, &rows, n);
256        let k = lin(&self.k, &rows, n);
257        let v = lin(&self.v, &rows, n);
258        drop(rows);
259        let scale = 1.0 / (c as f32).sqrt();
260        // A query frame sees frames ≤ its own, so one frame of queries runs
261        // over a growing key prefix and the N² matrix never exists.
262        let mut att = vec![0f32; n * c];
263        for f in 0..x.f {
264            let live = (f + 1) * per_frame;
265            let mut s = vec![0f32; per_frame * live];
266            let qh: Vec<f32> = q[f * per_frame * c..(f + 1) * per_frame * c].iter().map(|&a| a * scale).collect();
267            host_gemm::gemm_nt(&qh, &k[..live * c], &mut s, per_frame, c, live, pool);
268            let sr = Rows(s.as_mut_ptr());
269            pool_rows(pool, per_frame, &|lo, hi| {
270                for r in lo..hi {
271                    // SAFETY: disjoint rows.
272                    let row = unsafe { sr.at(r * live, live) };
273                    let mx = row.iter().cloned().fold(f32::MIN, f32::max);
274                    let mut den = 0f64;
275                    for v in row.iter_mut() {
276                        *v = (*v - mx).exp();
277                        den += *v as f64;
278                    }
279                    let inv = (1.0 / den) as f32;
280                    for v in row.iter_mut() {
281                        *v *= inv;
282                    }
283                }
284            });
285            let mut vt = vec![0f32; c * live];
286            for d in 0..c {
287                for r in 0..live {
288                    vt[d * live + r] = v[r * c + d];
289                }
290            }
291            host_gemm::gemm_nt(&s, &vt, &mut att[f * per_frame * c..(f + 1) * per_frame * c], per_frame, live, c, pool);
292        }
293        let o = lin(&self.o, &att, n);
294        // Residual connection, back to [C, n].
295        let mut out = x.clone();
296        for ch in 0..c {
297            for p in 0..n {
298                out.data[ch * n + p] += o[p * c + ch];
299            }
300        }
301        out
302    }
303
304    fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
305        let h = self.r0.forward(x, pool);
306        let h = self.attention(&h, pool);
307        self.r1.forward(&h, pool)
308    }
309}
310
311pub(crate) struct Up {
312    pub(crate) res: Vec<Res>,
313    pub(crate) conv: Option<(Conv, [usize; 3])>,
314}
315
316/// Nearest upsample by `(ft, fs, fs)`; the first frame stays one frame.
317fn nearest_up(x: &Vol, ft: usize, fs: usize) -> Vol {
318    let f_out = if ft == 2 { 1 + (x.f - 1) * 2 } else { x.f };
319    let (h_out, w_out) = (x.h * fs, x.w * fs);
320    let mut out = Vol::zeros(x.c, f_out, h_out, w_out);
321    let (ni, no) = (x.positions(), f_out * h_out * w_out);
322    let src_f = |of: usize| if ft == 2 && of > 0 { 1 + (of - 1) / 2 } else { of };
323    for c in 0..x.c {
324        for of in 0..f_out {
325            let sf = src_f(of);
326            for oh in 0..h_out {
327                let sh = oh / fs;
328                for ow in 0..w_out {
329                    let sw = ow / fs;
330                    out.data[c * no + ((of * h_out + oh) * w_out + ow)] =
331                        x.data[c * ni + ((sf * x.h + sh) * x.w + sw)];
332                }
333            }
334        }
335    }
336    out
337}
338
339pub struct S2vVae {
340    pub(crate) post_quant: Conv,
341    pub(crate) conv_in: Conv,
342    pub(crate) mid: Mid,
343    pub(crate) ups: Vec<Up>,
344    pub(crate) norm_out: GroupNorm,
345    pub(crate) conv_out: Conv,
346    pub(crate) pool: Option<Arc<Pool>>,
347}
348
349pub struct S2vVaeEncoder {
350    pub(crate) quant: Conv,
351    pub(crate) conv_in: Conv,
352    pub(crate) downs: Vec<(Vec<Res>, Option<Conv>)>,
353    pub(crate) mid: Mid,
354    pub(crate) norm_out: GroupNorm,
355    pub(crate) conv_out: Conv,
356    pub(crate) pool: Option<Arc<Pool>>,
357}
358
359impl S2vVae {
360    pub fn from_cmf(m: &Arc<CmfModel>) -> Result<S2vVae, String> {
361        let mut ups = Vec::new();
362        for (i, factor) in [[1, 2, 2], [2, 2, 2], [2, 2, 2], [1, 1, 1]].into_iter().enumerate() {
363            let p = format!("vae.decoder.up_blocks.{i}");
364            let mut res = Vec::new();
365            for r in 0..3 {
366                res.push(Res::load(m, &format!("{p}.resnets.{r}"))?);
367            }
368            let conv = if i < 3 {
369                Some((Conv::load(m, &format!("{p}.upsamplers.0.conv.conv"), [1, 1, 1])?, factor))
370            } else {
371                None
372            };
373            ups.push(Up { res, conv });
374        }
375        Ok(S2vVae {
376            post_quant: Conv::load(m, "vae.post_quant_conv", [1, 1, 1])?,
377            conv_in: Conv::load(m, "vae.decoder.conv_in.conv", [1, 1, 1])?,
378            mid: Mid::load(m, "vae.decoder.mid_block")?,
379            ups,
380            norm_out: GroupNorm::load(m, "vae.decoder.conv_norm_out")?,
381            conv_out: Conv::load(m, "vae.decoder.conv_out.conv", [1, 1, 1])?,
382            pool: Pool::from_env(),
383        })
384    }
385
386    /// The post-quant conv alone, on the host (16 channels into a 1×1 conv).
387    pub(crate) fn post_quant_host(&self, z: &Vol) -> Vol {
388        self.post_quant.forward(z, self.pool.as_deref())
389    }
390
391    /// One temporal tile through the decoder (post-quant conv included).
392    fn decode_tile(&self, z: &Vol) -> Vol {
393        let pool = self.pool.as_deref();
394        let h = self.post_quant.forward(z, pool);
395        let h = self.conv_in.forward(&h, pool);
396        let mut h = self.mid.forward(&h, pool);
397        for up in &self.ups {
398            for r in &up.res {
399                h = r.forward(&h, pool);
400            }
401            if let Some((conv, f)) = &up.conv {
402                let u = nearest_up(&h, f[0], f[1]);
403                h = conv.forward(&u, pool);
404            }
405        }
406        self.norm_out.apply(&mut h, pool);
407        silu_all(&mut h, pool);
408        self.conv_out.forward(&h, pool)
409    }
410
411    /// `_temporal_tiled_decode` of the diffusers class, on latents already
412    /// divided by the scaling factor. `[16, F, H, W]` → `[3, 4(F-1)+1, 8H, 8W]`.
413    pub fn decode_with(&self, z: &Vol, tile: &mut dyn FnMut(&Vol) -> Vol) -> Vol {
414        tiled(z, tile)
415    }
416
417    pub fn decode(&self, z: &Vol) -> Vol {
418        tiled(z, &mut |t| self.decode_tile(t))
419    }
420}
421
422/// The schedule shared by every backend: 5 latent frames every 3.
423pub(crate) fn tiled(z: &Vol, tile: &mut dyn FnMut(&Vol) -> Vol) -> Vol {
424    const MIN_LAT: usize = 4;
425    const STRIDE_LAT: usize = 3;
426    const STRIDE_PX: usize = 12;
427    const BLEND: usize = 4;
428    let frames_out = (z.f - 1) * TEMPORAL + 1;
429    let mut row: Vec<Vol> = Vec::new();
430    let mut i = 0;
431    while i < z.f {
432        let take = (MIN_LAT + 1).min(z.f - i);
433        let mut t = Vol::zeros(z.c, take, z.h, z.w);
434        let per = z.h * z.w;
435        for c in 0..z.c {
436            let src = &z.data[c * z.positions() + i * per..c * z.positions() + (i + take) * per];
437            t.data[c * take * per..(c + 1) * take * per].copy_from_slice(src);
438        }
439        let mut d = tile(&t);
440        if i > 0 {
441            d = drop_first_frame(&d);
442        }
443        row.push(d);
444        i += STRIDE_LAT;
445    }
446    let first = &row[0];
447    let (h, w) = (first.h, first.w);
448    let per = h * w;
449    let mut out = Vol::zeros(first.c, frames_out, h, w);
450    let mut at = 0usize;
451    for ti in 0..row.len() {
452        let keep = if ti == 0 { STRIDE_PX + 1 } else { STRIDE_PX };
453        let tl = &row[ti];
454        let take = keep.min(tl.f).min(frames_out - at);
455        for fr in 0..take {
456            for c in 0..tl.c {
457                let cb = c * tl.f * per + fr * per;
458                let dst = &mut out.data[c * frames_out * per + (at + fr) * per..][..per];
459                if ti > 0 && fr < BLEND.min(row[ti - 1].f).min(tl.f) {
460                    // `blend_t` over the previous tile's already-blended frames
461                    // is what the reference writes back into `row`, but it reads
462                    // the previous tile from its tail only, which was never
463                    // blended unless the tile is very short.
464                    let prev = &row[ti - 1];
465                    let ext = BLEND.min(prev.f).min(tl.f);
466                    let pa = c * prev.f * per + (prev.f - ext + fr) * per;
467                    let wgt = fr as f32 / ext as f32;
468                    for j in 0..per {
469                        dst[j] = prev.data[pa + j] * (1.0 - wgt) + tl.data[cb + j] * wgt;
470                    }
471                } else {
472                    dst.copy_from_slice(&tl.data[cb..cb + per]);
473                }
474            }
475        }
476        at += take;
477    }
478    out
479}
480
481fn drop_first_frame(v: &Vol) -> Vol {
482    let per = v.h * v.w;
483    let mut o = Vol::zeros(v.c, v.f - 1, v.h, v.w);
484    for c in 0..v.c {
485        let s = c * v.f * per + per;
486        o.data[c * (v.f - 1) * per..(c + 1) * (v.f - 1) * per].copy_from_slice(&v.data[s..s + (v.f - 1) * per]);
487    }
488    o
489}
490
491impl S2vVaeEncoder {
492    pub fn from_cmf(m: &Arc<CmfModel>) -> Result<Self, String> {
493        let mut downs = Vec::new();
494        for (i, st) in [[1, 2, 2], [2, 2, 2], [2, 2, 2], [1, 1, 1]].into_iter().enumerate() {
495            let p = format!("vae.encoder.down_blocks.{i}");
496            let mut res = Vec::new();
497            for r in 0..2 {
498                res.push(Res::load(m, &format!("{p}.resnets.{r}"))?);
499            }
500            let d = if i < 3 {
501                Some(Conv::load(m, &format!("{p}.downsamplers.0.conv.conv"), st)?)
502            } else {
503                None
504            };
505            downs.push((res, d));
506        }
507        Ok(Self {
508            quant: Conv::load(m, "vae.quant_conv", [1, 1, 1])?,
509            conv_in: Conv::load(m, "vae.encoder.conv_in.conv", [1, 1, 1])?,
510            downs,
511            mid: Mid::load(m, "vae.encoder.mid_block")?,
512            norm_out: GroupNorm::load(m, "vae.encoder.conv_norm_out")?,
513            conv_out: Conv::load(m, "vae.encoder.conv_out.conv", [1, 1, 1])?,
514            pool: Pool::from_env(),
515        })
516    }
517
518    /// `[3, F, H, W]` in [-1, 1] → (mean, logvar), each `[16, F', H/8, W/8]`.
519    pub fn encode(&self, img: &Vol) -> (Vol, Vol) {
520        let pool = self.pool.as_deref();
521        let mut h = self.conv_in.forward(img, pool);
522        for (res, d) in &self.downs {
523            for r in res {
524                h = r.forward(&h, pool);
525            }
526            if let Some(d) = d {
527                h = d.forward(&h, pool);
528            }
529        }
530        let mut h = self.mid.forward(&h, pool);
531        self.norm_out.apply(&mut h, pool);
532        silu_all(&mut h, pool);
533        let h = self.conv_out.forward(&h, pool);
534        self.finish_host(&h)
535    }
536
537    /// The quant conv and the posterior's split, after `conv_out`.
538    pub(crate) fn finish_host(&self, h: &Vol) -> (Vol, Vol) {
539        let h = self.quant.forward(h, self.pool.as_deref());
540        let n = h.positions();
541        let mut mean = Vol::zeros(16, h.f, h.h, h.w);
542        let mut lv = Vol::zeros(16, h.f, h.h, h.w);
543        mean.data.copy_from_slice(&h.data[..16 * n]);
544        lv.data.copy_from_slice(&h.data[16 * n..32 * n]);
545        for v in &mut lv.data {
546            *v = v.clamp(-30.0, 20.0);
547        }
548        (mean, lv)
549    }
550}