1use crate::pool::Pool;
29use cortiq_core::CmfModel;
30use std::sync::Arc;
31
32const CHUNK: usize = 8192;
34
35pub struct Conv3d {
38 w: Vec<f32>,
39 b: Vec<f32>,
40 c_out: usize,
41 c_in: usize,
42}
43
44#[derive(Clone)]
46pub struct Vol {
47 pub c: usize,
48 pub f: usize,
49 pub h: usize,
50 pub w: usize,
51 pub data: Vec<f32>,
52}
53
54impl Vol {
55 pub fn zeros(c: usize, f: usize, h: usize, w: usize) -> Vol {
56 Vol { c, f, h, w, data: vec![0.0; c * f * h * w] }
57 }
58 #[inline]
59 pub fn at(&self, c: usize, f: usize, h: usize, w: usize) -> f32 {
60 self.data[((c * self.f + f) * self.h + h) * self.w + w]
61 }
62 pub fn positions(&self) -> usize {
63 self.f * self.h * self.w
64 }
65}
66
67fn tensor_f32(model: &Arc<CmfModel>, name: &str) -> Result<(Vec<f32>, Vec<usize>), String> {
70 let e = model
71 .tensor(name)
72 .ok_or_else(|| format!("missing tensor {name}"))?;
73 let mut out = vec![0.0f32; e.n_elems()];
74 cortiq_core::quant::dequant_tensor(e, model.entry_bytes(e), &mut out)?;
75 Ok((out, e.shape.clone()))
76}
77
78impl Conv3d {
79 fn load(model: &Arc<CmfModel>, prefix: &str, _pool: Option<&Pool>) -> Result<Conv3d, String> {
80 let (w, shape) = tensor_f32(model, &format!("{prefix}.conv.weight"))?;
81 let (c_out, c_in) = (shape[0], shape[1]);
82 let b = match model.tensor(&format!("{prefix}.conv.bias")) {
83 Some(_) => tensor_f32(model, &format!("{prefix}.conv.bias"))?.0,
84 None => vec![0.0; c_out],
85 };
86 Ok(Conv3d { w, b, c_out, c_in })
87 }
88
89 pub fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
91 assert_eq!(x.c, self.c_in, "conv3d channels");
92 let (f, h, w) = (x.f, x.h, x.w);
93 let mut out = Vol::zeros(self.c_out, f, h, w);
94 let k = self.c_in * 27;
95 let npos = f * h * w;
96 let mut patches = vec![0.0f32; CHUNK.min(npos) * k];
97 let mut ybuf = vec![0.0f32; CHUNK.min(npos) * self.c_out];
98 let mut p0 = 0usize;
99 while p0 < npos {
100 let n = CHUNK.min(npos - p0);
101 patches[..n * k].fill(0.0);
106 for i in 0..n {
107 let p = p0 + i;
108 let (pw, rest) = (p % w, p / w);
109 let (ph, pf) = (rest % h, rest / h);
110 let row = &mut patches[i * k..(i + 1) * k];
111 for ci in 0..self.c_in {
112 for kf in 0..3usize {
113 let sf = (pf + kf).saturating_sub(1).min(f - 1);
115 for kh in 0..3usize {
116 let sh = ph + kh;
117 if sh == 0 || sh > h {
118 continue; }
120 let sh = sh - 1;
121 for kw in 0..3usize {
122 let sw = pw + kw;
123 if sw == 0 || sw > w {
124 continue;
125 }
126 let sw = sw - 1;
127 row[(ci * 3 + kf) * 9 + kh * 3 + kw] = x.at(ci, sf, sh, sw);
128 }
129 }
130 }
131 }
132 }
133 crate::fcd_ops::gemm_nt(&patches[..n * k], &self.w, &mut ybuf[..n * self.c_out], n, k, self.c_out, pool);
135 for i in 0..n {
136 let p = p0 + i;
137 for co in 0..self.c_out {
138 out.data[co * npos + p] = ybuf[i * self.c_out + co] + self.b[co];
139 }
140 }
141 p0 += n;
142 }
143 out
144 }
145}
146
147#[inline]
148fn silu(x: f32) -> f32 {
149 x / (1.0 + (-x).exp())
150}
151
152fn pixel_norm(x: &mut Vol, eps: f32) {
154 let npos = x.positions();
155 for p in 0..npos {
156 let mut s = 0.0f32;
157 for c in 0..x.c {
158 let v = x.data[c * npos + p];
159 s += v * v;
160 }
161 let inv = 1.0 / (s / x.c as f32 + eps).sqrt();
162 for c in 0..x.c {
163 x.data[c * npos + p] *= inv;
164 }
165 }
166}
167
168fn silu_inplace(x: &mut Vol) {
169 for v in x.data.iter_mut() {
170 *v = silu(*v);
171 }
172}
173
174pub struct ResBlock {
176 conv1: Conv3d,
177 conv2: Conv3d,
178}
179
180impl ResBlock {
181 fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
182 let mut h = x.clone();
183 pixel_norm(&mut h, 1e-8);
184 silu_inplace(&mut h);
185 let mut h = self.conv1.forward(&h, pool);
186 pixel_norm(&mut h, 1e-8);
187 silu_inplace(&mut h);
188 let h = self.conv2.forward(&h, pool);
189 let mut out = x.clone();
190 for (o, v) in out.data.iter_mut().zip(&h.data) {
191 *o += v;
192 }
193 out
194 }
195}
196
197pub struct DepthToSpaceUp {
200 conv: Conv3d,
201 stride: (usize, usize, usize),
202}
203
204impl DepthToSpaceUp {
205 fn forward(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
206 let y = self.conv.forward(x, pool);
207 let (p1, p2, p3) = self.stride;
208 let cout = y.c / (p1 * p2 * p3);
209 let (f2, h2, w2) = (y.f * p1, y.h * p2, y.w * p3);
210 let mut out = Vol::zeros(cout, f2, h2, w2);
211 for c in 0..cout {
212 for a in 0..p1 {
213 for b in 0..p2 {
214 for d in 0..p3 {
215 let src_c = ((c * p1 + a) * p2 + b) * p3 + d;
216 for f in 0..y.f {
217 for hh in 0..y.h {
218 for ww in 0..y.w {
219 let v = y.at(src_c, f, hh, ww);
220 let (of, oh, ow) = (f * p1 + a, hh * p2 + b, ww * p3 + d);
221 out.data[((c * f2 + of) * h2 + oh) * w2 + ow] = v;
222 }
223 }
224 }
225 }
226 }
227 }
228 }
229 if p1 == 2 {
230 let f3 = f2 - 1;
232 let mut trimmed = Vol::zeros(cout, f3, h2, w2);
233 for c in 0..cout {
234 for f in 0..f3 {
235 let src = ((c * f2 + f + 1) * h2) * w2;
236 let dst = ((c * f3 + f) * h2) * w2;
237 trimmed.data[dst..dst + h2 * w2].copy_from_slice(&out.data[src..src + h2 * w2]);
238 }
239 }
240 return trimmed;
241 }
242 out
243 }
244}
245
246enum Block {
247 Res(Vec<ResBlock>),
248 Up(DepthToSpaceUp),
249}
250
251pub struct ConvVaeDecoder {
253 conv_in: Conv3d,
254 blocks: Vec<Block>,
255 conv_out: Conv3d,
256 mean: Vec<f32>,
257 std: Vec<f32>,
258 patch: usize,
259}
260
261impl ConvVaeDecoder {
262 pub fn from_cmf(model: &Arc<CmfModel>, pool: Option<&Pool>) -> Result<ConvVaeDecoder, String> {
265 let cfg: serde_json::Value = ["vvae.config_json", "ltx.config_json"]
270 .iter()
271 .filter_map(|n| model.tensor(n).map(|e| model.entry_bytes(e)))
272 .filter_map(|b| serde_json::from_slice::<serde_json::Value>(b).ok())
273 .find(|c| c.get("vae").and_then(|v| v.get("decoder_blocks")).is_some())
274 .ok_or("no config in this container carries vae.decoder_blocks")?;
275 let vae = &cfg["vae"];
276 let patch = vae["patch_size"].as_u64().unwrap_or(4) as usize;
277 let blocks_cfg = vae["decoder_blocks"]
278 .as_array()
279 .ok_or("vae.decoder_blocks missing")?;
280 let conv_in = Conv3d::load(model, "vvae.decoder.conv_in", pool)?;
281 let conv_out = Conv3d::load(model, "vvae.decoder.conv_out", pool)?;
282 let mut blocks = Vec::new();
283 for (i, entry) in blocks_cfg.iter().rev().enumerate() {
286 let name = entry[0].as_str().unwrap_or("");
287 let params = &entry[1];
288 let prefix = format!("vvae.decoder.up_blocks.{i}");
289 match name {
290 "res_x" => {
291 let n = params["num_layers"].as_u64().unwrap_or(1) as usize;
292 let mut res = Vec::new();
293 for j in 0..n {
294 res.push(ResBlock {
295 conv1: Conv3d::load(model, &format!("{prefix}.res_blocks.{j}.conv1"), pool)?,
296 conv2: Conv3d::load(model, &format!("{prefix}.res_blocks.{j}.conv2"), pool)?,
297 });
298 }
299 blocks.push(Block::Res(res));
300 }
301 "compress_time" | "compress_space" | "compress_all" => {
302 let stride = match name {
303 "compress_time" => (2, 1, 1),
304 "compress_space" => (1, 2, 2),
305 _ => (2, 2, 2),
306 };
307 blocks.push(Block::Up(DepthToSpaceUp {
308 conv: Conv3d::load(model, &format!("{prefix}.conv"), pool)?,
309 stride,
310 }));
311 }
312 other => return Err(format!("unknown decoder block '{other}'")),
313 }
314 }
315 let mean = tensor_f32(model, "vvae.per_channel_statistics.mean-of-means")?.0;
316 let std = tensor_f32(model, "vvae.per_channel_statistics.std-of-means")?.0;
317 Ok(ConvVaeDecoder {
318 conv_in,
319 blocks,
320 conv_out,
321 mean,
322 std,
323 patch,
324 })
325 }
326
327 pub fn decode(&self, latent: &Vol, pool: Option<&Pool>) -> Vol {
329 self.decode_traced(latent, pool, &mut |_, _| {})
330 }
331
332 pub fn decode_traced(
335 &self,
336 latent: &Vol,
337 pool: Option<&Pool>,
338 trace: &mut dyn FnMut(&str, &Vol),
339 ) -> Vol {
340 let mut x = latent.clone();
341 let npos = x.positions();
343 for c in 0..x.c {
344 let (s, m) = (self.std[c], self.mean[c]);
345 for p in 0..npos {
346 x.data[c * npos + p] = x.data[c * npos + p] * s + m;
347 }
348 }
349 let mut h = self.conv_in.forward(&x, pool);
350 trace("after_conv_in", &h);
351 for (i, b) in self.blocks.iter().enumerate() {
352 h = match b {
353 Block::Res(res) => {
354 let mut cur = h;
355 for r in res {
356 cur = r.forward(&cur, pool);
357 }
358 cur
359 }
360 Block::Up(u) => u.forward(&h, pool),
361 };
362 trace(&format!("after_block_{i}"), &h);
363 }
364 pixel_norm(&mut h, 1e-8);
365 silu_inplace(&mut h);
366 let y = self.conv_out.forward(&h, pool);
367 trace("after_conv_out", &y);
368 let out = unpatchify(&y, self.patch);
369 trace("frames", &out);
370 out
371 }
372
373}
374
375pub fn unpatchify(x: &Vol, patch: usize) -> Vol {
377 if patch == 1 {
378 return x.clone();
379 }
380 let c = x.c / (patch * patch);
381 let (h2, w2) = (x.h * patch, x.w * patch);
382 let mut out = Vol::zeros(c, x.f, h2, w2);
383 for cc in 0..c {
384 for r in 0..patch {
385 for q in 0..patch {
386 let src_c = (cc * patch + r) * patch + q;
387 for f in 0..x.f {
388 for hh in 0..x.h {
389 for ww in 0..x.w {
390 let v = x.at(src_c, f, hh, ww);
391 out.data[((cc * x.f + f) * h2 + hh * patch + q) * w2 + ww * patch + r] = v;
392 }
393 }
394 }
395 }
396 }
397 }
398 out
399}