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(
95 path: &Path,
96) -> Result<HashMap<String, (Vec<usize>, Vec<f32>)>, String> {
97 crate::ltxlora::read_safetensors(path)
98}
99
100#[derive(Clone)]
102pub struct Vol {
103 pub c: usize,
104 pub t: usize,
105 pub h: usize,
106 pub w: usize,
107 pub data: Vec<f32>,
108}
109
110impl Vol {
111 fn zeros(c: usize, t: usize, h: usize, w: usize) -> Vol {
112 Vol { c, t, h, w, data: vec![0f32; c * t * h * w] }
113 }
114 #[inline]
115 fn at(&self, c: usize, t: usize, h: usize, w: usize) -> f32 {
116 self.data[((c * self.t + t) * self.h + h) * self.w + w]
117 }
118}
119
120struct Conv3 {
122 w: Vec<f32>, b: Vec<f32>,
124 c_out: usize,
125 c_in: usize,
126 k: usize, }
128
129impl Conv3 {
130 fn apply(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
134 let (t, h, w) = (x.t, x.h, x.w);
135 let mut out = Vol::zeros(self.c_out, t, h, w);
136 let k = self.k;
137 let pad = k / 2;
138 let patch = self.c_in * k * k * k;
139 let n = h * w;
140 let mut col = vec![0f32; n * patch];
141 let mut acc = vec![0f32; n * self.c_out];
142 for ti in 0..t {
143 col.iter_mut().for_each(|v| *v = 0.0);
145 for hi in 0..h {
146 for wi in 0..w {
147 let row = (hi * w + wi) * patch;
148 for ci in 0..self.c_in {
149 for kt in 0..k {
150 let tt = ti as isize + kt as isize - pad as isize;
151 if tt < 0 || tt >= t as isize {
152 continue;
153 }
154 for kh in 0..k {
155 let hh = hi as isize + kh as isize - pad as isize;
156 if hh < 0 || hh >= h as isize {
157 continue;
158 }
159 for kw in 0..k {
160 let ww = wi as isize + kw as isize - pad as isize;
161 if ww < 0 || ww >= w as isize {
162 continue;
163 }
164 let idx = ((ci * k + kt) * k + kh) * k + kw;
165 col[row + idx] =
166 x.at(ci, tt as usize, hh as usize, ww as usize);
167 }
168 }
169 }
170 }
171 }
172 }
173 crate::gpu::cpu_scope(|| {
174 crate::fcd_ops::gemm_nt(&col, &self.w, &mut acc, n, patch, self.c_out, pool);
175 });
176 for p in 0..n {
177 let (hi, wi) = (p / w, p % w);
178 for co in 0..self.c_out {
179 out.data[((co * t + ti) * h + hi) * w + wi] = acc[p * self.c_out + co] + self.b[co];
180 }
181 }
182 }
183 out
184 }
185}
186
187struct DepthwiseT {
189 w: Vec<f32>, b: Vec<f32>,
191 k: usize,
192}
193
194impl DepthwiseT {
195 fn apply(&self, x: &Vol) -> Vol {
196 let mut out = Vol::zeros(x.c, x.t, x.h, x.w);
197 let pad = self.k / 2;
198 for c in 0..x.c {
199 for ti in 0..x.t {
200 for hi in 0..x.h {
201 for wi in 0..x.w {
202 let mut acc = self.b[c];
203 for kt in 0..self.k {
204 let tt = ti as isize + kt as isize - pad as isize;
205 if tt < 0 || tt >= x.t as isize {
206 continue;
207 }
208 acc += self.w[c * self.k + kt] * x.at(c, tt as usize, hi, wi);
209 }
210 out.data[((c * x.t + ti) * x.h + hi) * x.w + wi] = acc;
211 }
212 }
213 }
214 }
215 out
216 }
217}
218
219struct GroupNorm {
221 w: Vec<f32>,
222 b: Vec<f32>,
223 groups: usize,
224}
225
226impl GroupNorm {
227 fn apply(&self, x: &Vol) -> Vol {
228 let mut out = x.clone();
229 let per = x.c / self.groups;
230 let n = x.t * x.h * x.w;
231 for g in 0..self.groups {
232 let (lo, hi) = (g * per, (g + 1) * per);
233 let mut sum = 0f64;
234 let mut sq = 0f64;
235 for c in lo..hi {
236 for i in 0..n {
237 let v = x.data[c * n + i] as f64;
238 sum += v;
239 sq += v * v;
240 }
241 }
242 let cnt = (per * n) as f64;
243 let mean = sum / cnt;
244 let var = (sq / cnt - mean * mean).max(0.0);
245 let inv = 1.0 / (var + 1e-5).sqrt();
246 for c in lo..hi {
247 for i in 0..n {
248 let v = (x.data[c * n + i] as f64 - mean) * inv;
249 out.data[c * n + i] = v as f32 * self.w[c] + self.b[c];
250 }
251 }
252 }
253 out
254 }
255}
256
257fn silu_in_place(v: &mut [f32]) {
258 for x in v.iter_mut() {
259 *x /= 1.0 + (-*x).exp();
260 }
261}
262
263struct ResBlock {
264 in_norm: GroupNorm,
265 in_conv: Conv3,
266 emb: (Vec<f32>, Vec<f32>), out_norm: GroupNorm,
268 out_conv: Conv3,
269 skip: Option<Conv3>,
270}
271
272impl ResBlock {
273 fn apply(&self, x: &Vol, emb: &[f32], pool: Option<&Pool>) -> Vol {
274 let mut h = self.in_norm.apply(x);
275 silu_in_place(&mut h.data);
276 let h = self.in_conv.apply(&h, pool);
277
278 let e_in = emb.len();
280 let two_c = self.emb.1.len();
281 let mut es = emb.to_vec();
282 silu_in_place(&mut es);
283 let mut mod_v = vec![0f32; two_c];
284 for (o, m) in mod_v.iter_mut().enumerate() {
285 let row = &self.emb.0[o * e_in..(o + 1) * e_in];
286 *m = self.emb.1[o] + row.iter().zip(&es).map(|(a, b)| a * b).sum::<f32>();
287 }
288
289 let mut h2 = self.out_norm.apply(&h);
290 let n = h2.t * h2.h * h2.w;
291 let c = h2.c;
292 for ci in 0..c {
293 let (s, sh) = (mod_v[ci], mod_v[c + ci]);
294 for i in 0..n {
295 h2.data[ci * n + i] = h2.data[ci * n + i] * (1.0 + s) + sh;
296 }
297 }
298 silu_in_place(&mut h2.data);
299 let h2 = self.out_conv.apply(&h2, pool);
300
301 let mut out = match &self.skip {
302 Some(cv) => cv.apply(x, pool),
303 None => x.clone(),
304 };
305 for (o, v) in out.data.iter_mut().zip(&h2.data) {
306 *o += *v;
307 }
308 out
309 }
310}
311
312struct TemporalConv {
313 norm: GroupNorm,
314 dw: DepthwiseT,
315 pw: Conv3, }
317
318impl TemporalConv {
319 fn apply(&self, x: &Vol, pool: Option<&Pool>) -> Vol {
320 let mut h = self.norm.apply(x);
321 silu_in_place(&mut h.data);
322 let h = self.dw.apply(&h);
323 let h = self.pw.apply(&h, pool);
324 let mut out = x.clone();
325 for (o, v) in out.data.iter_mut().zip(&h.data) {
326 *o += *v;
327 }
328 out
329 }
330}
331
332enum Block {
333 Res(ResBlock),
334 Temporal(TemporalConv),
335}
336
337pub struct LatentUpscaler {
338 conv_in: Conv3,
339 embed: ((Vec<f32>, Vec<f32>), (Vec<f32>, Vec<f32>)),
340 in_blocks: Vec<Block>,
341 out_blocks: Vec<Block>,
342 norm_out: GroupNorm,
343 conv_out: Conv3,
344 pub channels: usize,
345}
346
347type St = HashMap<String, (Vec<usize>, Vec<f32>)>;
348
349fn take(st: &St, name: &str) -> Result<(Vec<usize>, Vec<f32>), String> {
350 st.get(name)
351 .cloned()
352 .ok_or_else(|| format!("upscaler: missing {name}"))
353}
354
355fn conv(st: &St, prefix: &str) -> Result<Conv3, String> {
356 let (shape, w) = take(st, &format!("{prefix}.weight"))?;
357 let (_, b) = take(st, &format!("{prefix}.bias"))?;
358 if shape.len() != 5 || shape[2] != shape[3] || shape[3] != shape[4] {
359 return Err(format!("{prefix}: expected a cubic 3-D kernel, got {shape:?}"));
360 }
361 Ok(Conv3 { w, b, c_out: shape[0], c_in: shape[1], k: shape[2] })
362}
363
364fn gnorm(st: &St, prefix: &str) -> Result<GroupNorm, String> {
365 let (_, w) = take(st, &format!("{prefix}.weight"))?;
366 let (_, b) = take(st, &format!("{prefix}.bias"))?;
367 Ok(GroupNorm { w, b, groups: 32 })
368}
369
370impl LatentUpscaler {
371 pub fn load(path: &Path) -> Result<LatentUpscaler, String> {
375 let st = crate::ltxlora::read_safetensors(path)?;
376 let conv_in = conv(&st, "conv_in")?;
377 let channels = conv_in.c_out;
378 let e0 = (take(&st, "embed.0.weight")?.1, take(&st, "embed.0.bias")?.1);
379 let e2 = (take(&st, "embed.2.weight")?.1, take(&st, "embed.2.bias")?.1);
380
381 let load_blocks = |side: &str| -> Result<Vec<Block>, String> {
382 let mut out = Vec::new();
383 for i in 0.. {
384 let p = format!("{side}.{i}");
385 if st.contains_key(&format!("{p}.dwconv.weight")) {
386 let (shape, w) = take(&st, &format!("{p}.dwconv.weight"))?;
387 let (_, b) = take(&st, &format!("{p}.dwconv.bias"))?;
388 out.push(Block::Temporal(TemporalConv {
389 norm: gnorm(&st, &format!("{p}.norm"))?,
390 dw: DepthwiseT { w, b, k: shape[2] },
391 pw: conv(&st, &format!("{p}.pwconv"))?,
392 }));
393 } else if st.contains_key(&format!("{p}.in_layers.0.weight")) {
394 let (eshape, ew) = take(&st, &format!("{p}.emb_layers.1.weight"))?;
395 let (_, eb) = take(&st, &format!("{p}.emb_layers.1.bias"))?;
396 let _ = eshape;
397 out.push(Block::Res(ResBlock {
398 in_norm: gnorm(&st, &format!("{p}.in_layers.0"))?,
399 in_conv: conv(&st, &format!("{p}.in_layers.2"))?,
400 emb: (ew, eb),
401 out_norm: gnorm(&st, &format!("{p}.out_norm"))?,
402 out_conv: conv(&st, &format!("{p}.out_layers.2"))?,
403 skip: match st.contains_key(&format!("{p}.skip.weight")) {
404 true => Some(conv(&st, &format!("{p}.skip"))?),
405 false => None,
406 },
407 }));
408 } else {
409 break;
410 }
411 }
412 if out.is_empty() {
413 return Err(format!("upscaler: no {side} found"));
414 }
415 Ok(out)
416 };
417
418 Ok(LatentUpscaler {
419 conv_in,
420 embed: (e0, e2),
421 in_blocks: load_blocks("in_blocks")?,
422 out_blocks: load_blocks("out_blocks")?,
423 norm_out: gnorm(&st, "norm_out")?,
424 conv_out: conv(&st, "conv_out")?,
425 channels,
426 })
427 }
428
429 fn embedding(&self, scale: f32) -> Vec<f32> {
431 let ((w0, b0), (w2, b2)) = &self.embed;
432 let mut h: Vec<f32> = b0.iter().zip(w0).map(|(b, w)| b + w * (scale - 1.0)).collect();
433 silu_in_place(&mut h);
434 let e = b2.len();
435 let mut out = vec![0f32; e];
436 for (o, v) in out.iter_mut().enumerate() {
437 let row = &w2[o * h.len()..(o + 1) * h.len()];
438 *v = b2[o] + row.iter().zip(&h).map(|(a, b)| a * b).sum::<f32>();
439 }
440 out
441 }
442
443 fn resize(x: &Vol, t: usize, h: usize, w: usize) -> Vol {
446 let mut out = Vol::zeros(x.c, t, h, w);
447 let map = |o: usize, n_out: usize, n_in: usize| -> (usize, usize, f32) {
448 if n_out == n_in {
449 return (o, o, 0.0);
450 }
451 let s = ((o as f32 + 0.5) * n_in as f32 / n_out as f32 - 0.5).max(0.0);
452 let i0 = s.floor() as usize;
453 let i1 = (i0 + 1).min(n_in - 1);
454 (i0, i1, s - i0 as f32)
455 };
456 for c in 0..x.c {
457 for ti in 0..t {
458 let (t0, t1, ft) = map(ti, t, x.t);
459 for hi in 0..h {
460 let (h0, h1, fh) = map(hi, h, x.h);
461 for wi in 0..w {
462 let (w0, w1, fw) = map(wi, w, x.w);
463 let p = |tt: usize, hh: usize, ww: usize| x.at(c, tt, hh, ww);
464 let lerp = |a: f32, b: f32, f: f32| a + (b - a) * f;
465 let v00 = lerp(p(t0, h0, w0), p(t0, h0, w1), fw);
466 let v01 = lerp(p(t0, h1, w0), p(t0, h1, w1), fw);
467 let v10 = lerp(p(t1, h0, w0), p(t1, h0, w1), fw);
468 let v11 = lerp(p(t1, h1, w0), p(t1, h1, w1), fw);
469 let v0 = lerp(v00, v01, fh);
470 let v1 = lerp(v10, v11, fh);
471 out.data[((c * t + ti) * h + hi) * w + wi] = lerp(v0, v1, ft);
472 }
473 }
474 }
475 }
476 out
477 }
478
479 pub fn upscale(&self, z: &Vol, h_out: usize, w_out: usize, pool: Option<&Pool>) -> Vol {
484 assert_eq!(z.c, LATENT_MEAN.len(), "upscaler expects a 24-channel latent");
485 let n = z.t * z.h * z.w;
486 let mut x = z.clone();
487 for c in 0..z.c {
488 for i in 0..n {
489 x.data[c * n + i] = (x.data[c * n + i] - LATENT_MEAN[c]) / LATENT_STD[c];
490 }
491 }
492 let scale = h_out as f32 / z.h as f32;
494 let emb = self.embedding(scale);
495
496 let mut v = self.conv_in.apply(&x, pool);
497 for b in &self.in_blocks {
498 v = match b {
499 Block::Res(r) => r.apply(&v, &emb, pool),
500 Block::Temporal(t) => t.apply(&v, pool),
501 };
502 }
503 v = Self::resize(&v, v.t, h_out, w_out);
504 for b in &self.out_blocks {
505 v = match b {
506 Block::Res(r) => r.apply(&v, &emb, pool),
507 Block::Temporal(t) => t.apply(&v, pool),
508 };
509 }
510 let mut v = self.norm_out.apply(&v);
511 silu_in_place(&mut v.data);
512 let mut out = self.conv_out.apply(&v, pool);
513 let n = out.t * out.h * out.w;
514 for c in 0..out.c {
515 for i in 0..n {
516 out.data[c * n + i] = out.data[c * n + i] * LATENT_STD[c] + LATENT_MEAN[c];
517 }
518 }
519 out
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
531 fn resize_matches_torch_convention() {
532 let x = Vol { c: 1, t: 1, h: 1, w: 2, data: vec![0.0, 1.0] };
533 let up = LatentUpscaler::resize(&x, 1, 1, 4);
534 let want = [0.0f32, 0.25, 0.75, 1.0];
537 for (g, w) in up.data.iter().zip(&want) {
538 assert!((g - w).abs() < 1e-6, "{:?} vs {want:?}", up.data);
539 }
540 }
541
542 #[test]
544 fn resize_identity() {
545 let x = Vol { c: 1, t: 2, h: 2, w: 2, data: (0..8).map(|i| i as f32).collect() };
546 let up = LatentUpscaler::resize(&x, 2, 2, 2);
547 assert_eq!(up.data, x.data);
548 }
549}