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