Skip to main content

cortiq_engine/
ltxups.rs

1//! The LTX-2.5 spatial latent upscaler — the ×2 between the two denoising
2//! stages.
3//!
4//! Structurally simple and computationally not: 3-D convolutions at 1024
5//! channels with ordinary zero padding, GroupNorm(32) and SiLU, four
6//! residual blocks before a pixel-shuffle upsample and four after. It runs
7//! on the *un-normalized* latent — the video VAE's per-channel statistics
8//! are undone going in and reapplied coming out — because the upscaler was
9//! trained in the VAE's own units, not the diffusion model's.
10
11use crate::ltxvae::Vol;
12use crate::pool::Pool;
13use cortiq_core::CmfModel;
14use std::sync::Arc;
15
16fn tensor_f32(model: &Arc<CmfModel>, name: &str) -> Result<(Vec<f32>, Vec<usize>), String> {
17    let e = model.tensor(name).ok_or_else(|| format!("missing tensor {name}"))?;
18    let mut out = vec![0.0f32; e.n_elems()];
19    cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
20    Ok((out, e.shape.clone()))
21}
22
23fn silu(v: f32) -> f32 {
24    v / (1.0 + (-v).exp())
25}
26
27/// `[out, in, kf, kh, kw]` convolution with zero padding of one on every
28/// axis, as im2col into a single GEMM per position chunk.
29struct Conv {
30    w: Vec<f32>,
31    b: Vec<f32>,
32    c_out: usize,
33    c_in: usize,
34    kf: usize,
35    kh: usize,
36    kw: usize,
37}
38
39impl Conv {
40    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Conv, String> {
41        let (w, s) = tensor_f32(model, &format!("{name}.weight"))?;
42        let (b, _) = tensor_f32(model, &format!("{name}.bias"))?;
43        let (kf, kh, kw) = match s.len() {
44            5 => (s[2], s[3], s[4]),
45            4 => (1, s[2], s[3]),
46            _ => return Err(format!("{name}: rank {}", s.len())),
47        };
48        Ok(Conv { w, b, c_out: s[0], c_in: s[1], kf, kh, kw })
49    }
50
51    fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
52        let (f, h, w) = (x.f, x.h, x.w);
53        let npos = f * h * w;
54        let k = self.c_in * self.kf * self.kh * self.kw;
55        let mut out = Vol::zeros(self.c_out, f, h, w);
56        const CHUNK: usize = 4096;
57        let mut patches = vec![0f32; CHUNK * k];
58        let mut ys = vec![0f32; CHUNK * self.c_out];
59        let (pf, ph, pw) = (self.kf / 2, self.kh / 2, self.kw / 2);
60        let mut p0 = 0usize;
61        while p0 < npos {
62            let n = CHUNK.min(npos - p0);
63            // the buffer is reused across chunks — a stale row would become
64            // the padding value
65            patches[..n * k].fill(0.0);
66            for i in 0..n {
67                let p = p0 + i;
68                let (pw_, rest) = (p % w, p / w);
69                let (ph_, pf_) = (rest % h, rest / h);
70                for ci in 0..self.c_in {
71                    for a in 0..self.kf {
72                        let sf = pf_ as isize + a as isize - pf as isize;
73                        if sf < 0 || sf >= f as isize {
74                            continue;
75                        }
76                        for bb in 0..self.kh {
77                            let sh = ph_ as isize + bb as isize - ph as isize;
78                            if sh < 0 || sh >= h as isize {
79                                continue;
80                            }
81                            for c in 0..self.kw {
82                                let sw = pw_ as isize + c as isize - pw as isize;
83                                if sw < 0 || sw >= w as isize {
84                                    continue;
85                                }
86                                let v = x.data
87                                    [((ci * f + sf as usize) * h + sh as usize) * w + sw as usize];
88                                patches[i * k + ((ci * self.kf + a) * self.kh + bb) * self.kw + c] = v;
89                            }
90                        }
91                    }
92                }
93            }
94            crate::fcd_ops::gemm_nt(
95                &patches[..n * k],
96                &self.w,
97                &mut ys[..n * self.c_out],
98                n,
99                k,
100                self.c_out,
101                pool,
102            );
103            for i in 0..n {
104                let p = p0 + i;
105                for co in 0..self.c_out {
106                    out.data[co * npos + p] = ys[i * self.c_out + co] + self.b[co];
107                }
108            }
109            p0 += n;
110        }
111        out
112    }
113}
114
115/// GroupNorm(32) with affine, in place.
116fn group_norm(x: &mut Vol, w: &[f32], b: &[f32]) {
117    let groups = 32usize;
118    let per = x.c / groups;
119    let npos = x.positions();
120    for g in 0..groups {
121        let (lo, hi) = (g * per * npos, (g + 1) * per * npos);
122        let s = &x.data[lo..hi];
123        let n = s.len() as f64;
124        let mean = s.iter().map(|&v| v as f64).sum::<f64>() / n;
125        let var = s.iter().map(|&v| (v as f64 - mean) * (v as f64 - mean)).sum::<f64>() / n;
126        let inv = 1.0 / (var + 1e-5).sqrt();
127        for c in 0..per {
128            let ch = g * per + c;
129            let (gw, gb) = (w[ch], b[ch]);
130            for v in x.data[ch * npos..(ch + 1) * npos].iter_mut() {
131                *v = ((*v as f64 - mean) * inv) as f32 * gw + gb;
132            }
133        }
134    }
135}
136
137struct ResBlock {
138    conv1: Conv,
139    n1w: Vec<f32>,
140    n1b: Vec<f32>,
141    conv2: Conv,
142    n2w: Vec<f32>,
143    n2b: Vec<f32>,
144}
145
146impl ResBlock {
147    fn load(model: &Arc<CmfModel>, p: &str) -> Result<ResBlock, String> {
148        Ok(ResBlock {
149            conv1: Conv::load(model, &format!("{p}.conv1"))?,
150            n1w: tensor_f32(model, &format!("{p}.norm1.weight"))?.0,
151            n1b: tensor_f32(model, &format!("{p}.norm1.bias"))?.0,
152            conv2: Conv::load(model, &format!("{p}.conv2"))?,
153            n2w: tensor_f32(model, &format!("{p}.norm2.weight"))?.0,
154            n2b: tensor_f32(model, &format!("{p}.norm2.bias"))?.0,
155        })
156    }
157
158    fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
159        let mut h = self.conv1.forward(x, pool);
160        group_norm(&mut h, &self.n1w, &self.n1b);
161        for v in h.data.iter_mut() {
162            *v = silu(*v);
163        }
164        let mut h2 = self.conv2.forward(&h, pool);
165        group_norm(&mut h2, &self.n2w, &self.n2b);
166        // the activation comes *after* the residual add, not before
167        for (v, &r) in h2.data.iter_mut().zip(&x.data) {
168            *v = silu(*v + r);
169        }
170        h2
171    }
172}
173
174pub struct LatentUpscaler {
175    initial: Conv,
176    in_w: Vec<f32>,
177    in_b: Vec<f32>,
178    pre: Vec<ResBlock>,
179    up: Conv,
180    post: Vec<ResBlock>,
181    final_conv: Conv,
182    mean: Vec<f32>,
183    std: Vec<f32>,
184}
185
186impl LatentUpscaler {
187    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<LatentUpscaler, String> {
188        let load_blocks = |p: &str| -> Result<Vec<ResBlock>, String> {
189            let mut v = Vec::new();
190            let mut i = 0;
191            while model.tensor(&format!("{p}.{i}.conv1.weight")).is_some() {
192                v.push(ResBlock::load(model, &format!("{p}.{i}"))?);
193                i += 1;
194            }
195            Ok(v)
196        };
197        Ok(LatentUpscaler {
198            initial: Conv::load(model, "ups.initial_conv")?,
199            in_w: tensor_f32(model, "ups.initial_norm.weight")?.0,
200            in_b: tensor_f32(model, "ups.initial_norm.bias")?.0,
201            pre: load_blocks("ups.res_blocks")?,
202            up: Conv::load(model, "ups.upsampler.0")?,
203            post: load_blocks("ups.post_upsample_res_blocks")?,
204            final_conv: Conv::load(model, "ups.final_conv")?,
205            mean: tensor_f32(model, "vvae.per_channel_statistics.mean-of-means")?.0,
206            std: tensor_f32(model, "vvae.per_channel_statistics.std-of-means")?.0,
207        })
208    }
209
210    /// `[128, F, H, W]` → `[128, F, 2H, 2W]`, in the diffusion model's units
211    /// on both sides.
212    pub fn upscale(&self, latent: &Vol, pool: Option<&Pool>) -> Vol {
213        let npos = latent.positions();
214        let mut x = latent.clone();
215        // into the VAE's units
216        for c in 0..x.c {
217            for v in x.data[c * npos..(c + 1) * npos].iter_mut() {
218                *v = *v * self.std[c] + self.mean[c];
219            }
220        }
221        let mut h = self.initial.forward(&x, pool);
222        group_norm(&mut h, &self.in_w, &self.in_b);
223        for v in h.data.iter_mut() {
224            *v = silu(*v);
225        }
226        for b in &self.pre {
227            h = b.forward(&h, pool);
228        }
229        h = self.pixel_shuffle(&self.up.forward(&h, pool));
230        for b in &self.post {
231            h = b.forward(&h, pool);
232        }
233        let mut out = self.final_conv.forward(&h, pool);
234        let npos2 = out.positions();
235        for c in 0..out.c {
236            for v in out.data[c * npos2..(c + 1) * npos2].iter_mut() {
237                *v = (*v - self.mean[c]) / self.std[c];
238            }
239        }
240        out
241    }
242
243    /// `b (c p1 p2) f h w -> b c f (h p1) (w p2)`.
244    fn pixel_shuffle(&self, x: &Vol) -> Vol {
245        let c = x.c / 4;
246        let (h2, w2) = (x.h * 2, x.w * 2);
247        let mut out = Vol::zeros(c, x.f, h2, w2);
248        for ch in 0..c {
249            for p1 in 0..2 {
250                for p2 in 0..2 {
251                    let src = (ch * 2 + p1) * 2 + p2;
252                    for f in 0..x.f {
253                        for y in 0..x.h {
254                            for z in 0..x.w {
255                                let v = x.data[((src * x.f + f) * x.h + y) * x.w + z];
256                                out.data[((ch * x.f + f) * h2 + y * 2 + p1) * w2 + z * 2 + p2] = v;
257                            }
258                        }
259                    }
260                }
261            }
262        }
263        out
264    }
265}