1use crate::pool::Pool;
35use std::collections::HashMap;
36use std::path::Path;
37
38pub const LATENT_MEAN: [f32; 24] = [
41 0.858_090_34,
42 -0.960_659_15,
43 1.066_164,
44 -0.509_032_55,
45 -0.272_758_19,
46 -1.367_541_4,
47 -0.255_325_5,
48 -0.269_075_54,
49 -0.537_684_08,
50 -0.046_409_73,
51 0.665_737_03,
52 0.196_901_28,
53 -0.546_060_8,
54 -0.403_534_2,
55 -0.236_830_25,
56 0.259_284_53,
57 -0.301_339_45,
58 0.211_341_99,
59 -1.120_684_9,
60 0.358_193_34,
61 -0.042_251_44,
62 0.260_483,
63 0.228_640_93,
64 0.705_603_2,
65];
66pub const LATENT_STD: [f32; 24] = [
67 1.222_377_4,
68 1.276_726_4,
69 1.683_177_5,
70 1.754_945_5,
71 1.563_621_6,
72 2.194_143_5,
73 0.965_313_8,
74 1.056_988_6,
75 0.841_948_9,
76 0.772_995_3,
77 1.895_593_8,
78 0.946_841_8,
79 0.799_680_95,
80 0.449_889,
81 0.719_739_97,
82 0.693_629_3,
83 2.961_095_1,
84 2.769_419_9,
85 3.049_618_5,
86 2.108_805_4,
87 3.276_226_3,
88 3.162_735_7,
89 2.281_681_3,
90 2.612_784_4,
91];
92
93pub fn read_oracle(path: &Path) -> Result<HashMap<String, (Vec<usize>, Vec<f32>)>, String> {
95 crate::ltxlora::read_safetensors(path)
96}
97
98#[derive(Clone)]
100pub struct Vol {
101 pub c: usize,
102 pub t: usize,
103 pub h: usize,
104 pub w: usize,
105 pub data: Vec<f32>,
106}
107
108impl Vol {
109 fn zeros(c: usize, t: usize, h: usize, w: usize) -> Vol {
110 Vol {
111 c,
112 t,
113 h,
114 w,
115 data: vec![0f32; c * t * h * w],
116 }
117 }
118 #[inline]
119 fn at(&self, c: usize, t: usize, h: usize, w: usize) -> f32 {
120 self.data[((c * self.t + t) * self.h + h) * self.w + w]
121 }
122}
123
124struct Conv3 {
126 w: Vec<f32>, b: Vec<f32>,
128 c_out: usize,
129 c_in: usize,
130 k: usize, }
132
133impl Conv3 {
134 fn apply(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
138 let (t, h, w) = (x.t, x.h, x.w);
139 let mut out = Vol::zeros(self.c_out, t, h, w);
140 let k = self.k;
141 let pad = k / 2;
142 let patch = self.c_in * k * k * k;
143 let n = h * w;
144 let mut col = vec![0f32; n * patch];
145 let mut acc = vec![0f32; n * self.c_out];
146 for ti in 0..t {
147 col.iter_mut().for_each(|v| *v = 0.0);
149 for hi in 0..h {
150 for wi in 0..w {
151 let row = (hi * w + wi) * patch;
152 for ci in 0..self.c_in {
153 for kt in 0..k {
154 let tt = ti as isize + kt as isize - pad as isize;
155 if tt < 0 || tt >= t as isize {
156 continue;
157 }
158 for kh in 0..k {
159 let hh = hi as isize + kh as isize - pad as isize;
160 if hh < 0 || hh >= h as isize {
161 continue;
162 }
163 for kw in 0..k {
164 let ww = wi as isize + kw as isize - pad as isize;
165 if ww < 0 || ww >= w as isize {
166 continue;
167 }
168 let idx = ((ci * k + kt) * k + kh) * k + kw;
169 col[row + idx] =
170 x.at(ci, tt as usize, hh as usize, ww as usize);
171 }
172 }
173 }
174 }
175 }
176 }
177 crate::gpu::cpu_scope(|| {
178 crate::fcd_ops::gemm_nt(&col, &self.w, &mut acc, n, patch, self.c_out, pool);
179 });
180 for p in 0..n {
181 let (hi, wi) = (p / w, p % w);
182 for co in 0..self.c_out {
183 out.data[((co * t + ti) * h + hi) * w + wi] =
184 acc[p * self.c_out + co] + self.b[co];
185 }
186 }
187 }
188 out
189 }
190}
191
192struct DepthwiseT {
194 w: Vec<f32>, b: Vec<f32>,
196 k: usize,
197}
198
199impl DepthwiseT {
200 fn apply(&self, x: &Vol) -> Vol {
201 let mut out = Vol::zeros(x.c, x.t, x.h, x.w);
202 let pad = self.k / 2;
203 for c in 0..x.c {
204 for ti in 0..x.t {
205 for hi in 0..x.h {
206 for wi in 0..x.w {
207 let mut acc = self.b[c];
208 for kt in 0..self.k {
209 let tt = ti as isize + kt as isize - pad as isize;
210 if tt < 0 || tt >= x.t as isize {
211 continue;
212 }
213 acc += self.w[c * self.k + kt] * x.at(c, tt as usize, hi, wi);
214 }
215 out.data[((c * x.t + ti) * x.h + hi) * x.w + wi] = acc;
216 }
217 }
218 }
219 }
220 out
221 }
222}
223
224struct GroupNorm {
226 w: Vec<f32>,
227 b: Vec<f32>,
228 groups: usize,
229}
230
231impl GroupNorm {
232 fn apply(&self, x: &Vol) -> Vol {
233 let mut out = x.clone();
234 let per = x.c / self.groups;
235 let n = x.t * x.h * x.w;
236 for g in 0..self.groups {
237 let (lo, hi) = (g * per, (g + 1) * per);
238 let mut sum = 0f64;
239 let mut sq = 0f64;
240 for c in lo..hi {
241 for i in 0..n {
242 let v = x.data[c * n + i] as f64;
243 sum += v;
244 sq += v * v;
245 }
246 }
247 let cnt = (per * n) as f64;
248 let mean = sum / cnt;
249 let var = (sq / cnt - mean * mean).max(0.0);
250 let inv = 1.0 / (var + 1e-5).sqrt();
251 for c in lo..hi {
252 for i in 0..n {
253 let v = (x.data[c * n + i] as f64 - mean) * inv;
254 out.data[c * n + i] = v as f32 * self.w[c] + self.b[c];
255 }
256 }
257 }
258 out
259 }
260}
261
262fn silu_in_place(v: &mut [f32]) {
263 for x in v.iter_mut() {
264 *x /= 1.0 + (-*x).exp();
265 }
266}
267
268struct ResBlock {
269 in_norm: GroupNorm,
270 in_conv: Conv3,
271 emb: (Vec<f32>, Vec<f32>), out_norm: GroupNorm,
273 out_conv: Conv3,
274 skip: Option<Conv3>,
275}
276
277impl ResBlock {
278 fn apply(&self, x: &Vol, emb: &[f32], pool: Option<&Pool>) -> Vol {
279 let mut h = self.in_norm.apply(x);
280 silu_in_place(&mut h.data);
281 let h = self.in_conv.apply(&h, pool);
282
283 let e_in = emb.len();
285 let two_c = self.emb.1.len();
286 let mut es = emb.to_vec();
287 silu_in_place(&mut es);
288 let mut mod_v = vec![0f32; two_c];
289 for (o, m) in mod_v.iter_mut().enumerate() {
290 let row = &self.emb.0[o * e_in..(o + 1) * e_in];
291 *m = self.emb.1[o] + row.iter().zip(&es).map(|(a, b)| a * b).sum::<f32>();
292 }
293
294 let mut h2 = self.out_norm.apply(&h);
295 let n = h2.t * h2.h * h2.w;
296 let c = h2.c;
297 for ci in 0..c {
298 let (s, sh) = (mod_v[ci], mod_v[c + ci]);
299 for i in 0..n {
300 h2.data[ci * n + i] = h2.data[ci * n + i] * (1.0 + s) + sh;
301 }
302 }
303 silu_in_place(&mut h2.data);
304 let h2 = self.out_conv.apply(&h2, pool);
305
306 let mut out = match &self.skip {
307 Some(cv) => cv.apply(x, pool),
308 None => x.clone(),
309 };
310 for (o, v) in out.data.iter_mut().zip(&h2.data) {
311 *o += *v;
312 }
313 out
314 }
315}
316
317struct TemporalConv {
318 norm: GroupNorm,
319 dw: DepthwiseT,
320 pw: Conv3, }
322
323impl TemporalConv {
324 fn apply(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
325 let mut h = self.norm.apply(x);
326 silu_in_place(&mut h.data);
327 let h = self.dw.apply(&h);
328 let h = self.pw.apply(&h, pool);
329 let mut out = x.clone();
330 for (o, v) in out.data.iter_mut().zip(&h.data) {
331 *o += *v;
332 }
333 out
334 }
335}
336
337enum Block {
338 Res(ResBlock),
339 Temporal(TemporalConv),
340}
341
342pub struct LatentUpscaler {
343 conv_in: Conv3,
344 embed: ((Vec<f32>, Vec<f32>), (Vec<f32>, Vec<f32>)),
345 in_blocks: Vec<Block>,
346 out_blocks: Vec<Block>,
347 norm_out: GroupNorm,
348 conv_out: Conv3,
349 pub channels: usize,
350}
351
352type St = HashMap<String, (Vec<usize>, Vec<f32>)>;
353
354fn take(st: &St, name: &str) -> Result<(Vec<usize>, Vec<f32>), String> {
355 st.get(name)
356 .cloned()
357 .ok_or_else(|| format!("upscaler: missing {name}"))
358}
359
360fn conv(st: &St, prefix: &str) -> Result<Conv3, String> {
361 let (shape, w) = take(st, &format!("{prefix}.weight"))?;
362 let (_, b) = take(st, &format!("{prefix}.bias"))?;
363 if shape.len() != 5 || shape[2] != shape[3] || shape[3] != shape[4] {
364 return Err(format!(
365 "{prefix}: expected a cubic 3-D kernel, got {shape:?}"
366 ));
367 }
368 Ok(Conv3 {
369 w,
370 b,
371 c_out: shape[0],
372 c_in: shape[1],
373 k: shape[2],
374 })
375}
376
377fn gnorm(st: &St, prefix: &str) -> Result<GroupNorm, String> {
378 let (_, w) = take(st, &format!("{prefix}.weight"))?;
379 let (_, b) = take(st, &format!("{prefix}.bias"))?;
380 Ok(GroupNorm { w, b, groups: 32 })
381}
382
383impl LatentUpscaler {
384 pub fn load(path: &Path) -> Result<LatentUpscaler, String> {
388 let st = crate::ltxlora::read_safetensors(path)?;
389 let conv_in = conv(&st, "conv_in")?;
390 let channels = conv_in.c_out;
391 let e0 = (take(&st, "embed.0.weight")?.1, take(&st, "embed.0.bias")?.1);
392 let e2 = (take(&st, "embed.2.weight")?.1, take(&st, "embed.2.bias")?.1);
393
394 let load_blocks = |side: &str| -> Result<Vec<Block>, String> {
395 let mut out = Vec::new();
396 for i in 0.. {
397 let p = format!("{side}.{i}");
398 if st.contains_key(&format!("{p}.dwconv.weight")) {
399 let (shape, w) = take(&st, &format!("{p}.dwconv.weight"))?;
400 let (_, b) = take(&st, &format!("{p}.dwconv.bias"))?;
401 out.push(Block::Temporal(TemporalConv {
402 norm: gnorm(&st, &format!("{p}.norm"))?,
403 dw: DepthwiseT { w, b, k: shape[2] },
404 pw: conv(&st, &format!("{p}.pwconv"))?,
405 }));
406 } else if st.contains_key(&format!("{p}.in_layers.0.weight")) {
407 let (eshape, ew) = take(&st, &format!("{p}.emb_layers.1.weight"))?;
408 let (_, eb) = take(&st, &format!("{p}.emb_layers.1.bias"))?;
409 let _ = eshape;
410 out.push(Block::Res(ResBlock {
411 in_norm: gnorm(&st, &format!("{p}.in_layers.0"))?,
412 in_conv: conv(&st, &format!("{p}.in_layers.2"))?,
413 emb: (ew, eb),
414 out_norm: gnorm(&st, &format!("{p}.out_norm"))?,
415 out_conv: conv(&st, &format!("{p}.out_layers.2"))?,
416 skip: match st.contains_key(&format!("{p}.skip.weight")) {
417 true => Some(conv(&st, &format!("{p}.skip"))?),
418 false => None,
419 },
420 }));
421 } else {
422 break;
423 }
424 }
425 if out.is_empty() {
426 return Err(format!("upscaler: no {side} found"));
427 }
428 Ok(out)
429 };
430
431 Ok(LatentUpscaler {
432 conv_in,
433 embed: (e0, e2),
434 in_blocks: load_blocks("in_blocks")?,
435 out_blocks: load_blocks("out_blocks")?,
436 norm_out: gnorm(&st, "norm_out")?,
437 conv_out: conv(&st, "conv_out")?,
438 channels,
439 })
440 }
441
442 fn embedding(&self, scale: f32) -> Vec<f32> {
444 let ((w0, b0), (w2, b2)) = &self.embed;
445 let mut h: Vec<f32> = b0
446 .iter()
447 .zip(w0)
448 .map(|(b, w)| b + w * (scale - 1.0))
449 .collect();
450 silu_in_place(&mut h);
451 let e = b2.len();
452 let mut out = vec![0f32; e];
453 for (o, v) in out.iter_mut().enumerate() {
454 let row = &w2[o * h.len()..(o + 1) * h.len()];
455 *v = b2[o] + row.iter().zip(&h).map(|(a, b)| a * b).sum::<f32>();
456 }
457 out
458 }
459
460 fn resize(x: &Vol, t: usize, h: usize, w: usize) -> Vol {
463 let mut out = Vol::zeros(x.c, t, h, w);
464 let map = |o: usize, n_out: usize, n_in: usize| -> (usize, usize, f32) {
465 if n_out == n_in {
466 return (o, o, 0.0);
467 }
468 let s = ((o as f32 + 0.5) * n_in as f32 / n_out as f32 - 0.5).max(0.0);
469 let i0 = s.floor() as usize;
470 let i1 = (i0 + 1).min(n_in - 1);
471 (i0, i1, s - i0 as f32)
472 };
473 for c in 0..x.c {
474 for ti in 0..t {
475 let (t0, t1, ft) = map(ti, t, x.t);
476 for hi in 0..h {
477 let (h0, h1, fh) = map(hi, h, x.h);
478 for wi in 0..w {
479 let (w0, w1, fw) = map(wi, w, x.w);
480 let p = |tt: usize, hh: usize, ww: usize| x.at(c, tt, hh, ww);
481 let lerp = |a: f32, b: f32, f: f32| a + (b - a) * f;
482 let v00 = lerp(p(t0, h0, w0), p(t0, h0, w1), fw);
483 let v01 = lerp(p(t0, h1, w0), p(t0, h1, w1), fw);
484 let v10 = lerp(p(t1, h0, w0), p(t1, h0, w1), fw);
485 let v11 = lerp(p(t1, h1, w0), p(t1, h1, w1), fw);
486 let v0 = lerp(v00, v01, fh);
487 let v1 = lerp(v10, v11, fh);
488 out.data[((c * t + ti) * h + hi) * w + wi] = lerp(v0, v1, ft);
489 }
490 }
491 }
492 }
493 out
494 }
495
496 pub fn upscale(&self, z: &Vol, h_out: usize, w_out: usize, pool: Option<&Pool>) -> Vol {
501 assert_eq!(
502 z.c,
503 LATENT_MEAN.len(),
504 "upscaler expects a 24-channel latent"
505 );
506 let n = z.t * z.h * z.w;
507 let mut x = z.clone();
508 for c in 0..z.c {
509 for i in 0..n {
510 x.data[c * n + i] = (x.data[c * n + i] - LATENT_MEAN[c]) / LATENT_STD[c];
511 }
512 }
513 let scale = h_out as f32 / z.h as f32;
515 let emb = self.embedding(scale);
516
517 let mut v = self.conv_in.apply(&x, pool);
518 for b in &self.in_blocks {
519 v = match b {
520 Block::Res(r) => r.apply(&v, &emb, pool),
521 Block::Temporal(t) => t.apply(&v, pool),
522 };
523 }
524 v = Self::resize(&v, v.t, h_out, w_out);
525 for b in &self.out_blocks {
526 v = match b {
527 Block::Res(r) => r.apply(&v, &emb, pool),
528 Block::Temporal(t) => t.apply(&v, pool),
529 };
530 }
531 let mut v = self.norm_out.apply(&v);
532 silu_in_place(&mut v.data);
533 let mut out = self.conv_out.apply(&v, pool);
534 let n = out.t * out.h * out.w;
535 for c in 0..out.c {
536 for i in 0..n {
537 out.data[c * n + i] = out.data[c * n + i] * LATENT_STD[c] + LATENT_MEAN[c];
538 }
539 }
540 out
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
552 fn resize_matches_torch_convention() {
553 let x = Vol {
554 c: 1,
555 t: 1,
556 h: 1,
557 w: 2,
558 data: vec![0.0, 1.0],
559 };
560 let up = LatentUpscaler::resize(&x, 1, 1, 4);
561 let want = [0.0f32, 0.25, 0.75, 1.0];
564 for (g, w) in up.data.iter().zip(&want) {
565 assert!((g - w).abs() < 1e-6, "{:?} vs {want:?}", up.data);
566 }
567 }
568
569 #[test]
571 fn resize_identity() {
572 let x = Vol {
573 c: 1,
574 t: 2,
575 h: 2,
576 w: 2,
577 data: (0..8).map(|i| i as f32).collect(),
578 };
579 let up = LatentUpscaler::resize(&x, 2, 2, 2);
580 assert_eq!(up.data, x.data);
581 }
582}