Skip to main content

cortiq_engine/
ltxdit.rs

1//! LTX-2.5 `AVTransformer3DModel` — the audio-video diffusion transformer,
2//! read straight from an `ltx-2.5-av` CMF container.
3//!
4//! One forward is one denoising step for both streams at once: 48 blocks
5//! that each run video self-attention, video↔prompt cross-attention,
6//! audio self-attention, audio↔prompt cross-attention and the two
7//! directions of audio↔video cross-attention, every one of them modulated
8//! by adaLN values that come from the timestep — per token, not per sample.
9//!
10//! What the reference does and this reproduces:
11//!
12//! * **adaLN-single**: a sinusoidal timestep embedding (256) → SiLU MLP →
13//!   one `[9·dim]` vector per distinct timestep, added to the block's own
14//!   `scale_shift_table`. Rows 0..3 modulate self-attention, 3..6 the
15//!   feed-forward, 6..9 the prompt cross-attention.
16//! * **ada-zero**: `rms_norm(x) · (1 + scale) + shift`, no learned weight.
17//! * **post-SA**: `x + y·gate`, then a second `rms_norm` whose output is
18//!   what cross-attention reads — the block never normalizes twice.
19//! * **Split RoPE** over three video axes (frame, row, column) and one
20//!   audio axis, evaluated at the *middle* of each patch's `[start, end)`
21//!   bounds, with the frequency ladder built in f64 (the checkpoint's
22//!   `frequencies_precision: float64`).
23//! * **Gated attention**: `2·sigmoid(to_gate_logits(x))`, per head.
24//! * **q/k RMS-norm across the whole inner dimension**, not per head.
25//! * The A↔V pair reads the *pre-fusion* state of both streams, so the
26//!   order the two directions run in cannot bias the result.
27//!
28//! Gated against reference forward-hook dumps by `cortiq ltx-dit`.
29
30use crate::dit::{Proj, cmf_f32};
31use crate::pool::Pool;
32use cortiq_core::CmfModel;
33use std::sync::Arc;
34
35const EPS: f64 = 1e-6;
36
37// ---------------------------------------------------------------- helpers
38
39/// Rows of `n` items split across pool workers (serial without a pool).
40pub(crate) fn rows(pool: Option<&Pool>, n: usize, f: &(dyn Fn(usize, usize) + Sync)) {
41    match pool {
42        Some(p) => p.run_rows(n, f),
43        None => f(0, n),
44    }
45}
46
47/// Row handout for pool workers over one flat buffer.
48pub(crate) struct Shared(pub(crate) *mut f32);
49unsafe impl Send for Shared {}
50unsafe impl Sync for Shared {}
51impl Shared {
52    /// SAFETY: callers take disjoint `[off, off+len)` ranges.
53    #[allow(clippy::mut_from_ref)]
54    pub(crate) unsafe fn at(&self, off: usize, len: usize) -> &mut [f32] {
55        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
56    }
57}
58
59/// RMS normalization with no learned weight (`ada_zero`, `post_sa`).
60pub(crate) fn rms_plain(x: &[f32], dst: &mut [f32]) {
61    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
62    let inv = 1.0 / (ss + EPS).sqrt();
63    for (d, &v) in dst.iter_mut().zip(x) {
64        *d = (v as f64 * inv) as f32;
65    }
66}
67
68/// RMS normalization with a learned weight (q/k-norm).
69fn rms_w(x: &mut [f32], w: &[f32]) {
70    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
71    let inv = 1.0 / (ss + EPS).sqrt();
72    for (v, &g) in x.iter_mut().zip(w) {
73        *v = (*v as f64 * inv) as f32 * g;
74    }
75}
76
77fn silu(v: f32) -> f32 {
78    v / (1.0 + (-v).exp())
79}
80
81/// `gelu(x, approximate="tanh")`, the feed-forward's projection activation.
82pub(crate) fn gelu_tanh(v: f32) -> f32 {
83    let x = v as f64;
84    let inner = (2.0f64 / std::f64::consts::PI).sqrt() * (x + 0.044715 * x * x * x);
85    (0.5 * x * (1.0 + inner.tanh())) as f32
86}
87
88pub(crate) fn softmax(row: &mut [f32]) {
89    let mx = row.iter().cloned().fold(f32::MIN, f32::max);
90    let mut den = 0f32;
91    for r in row.iter_mut() {
92        *r = (*r - mx).exp();
93        den += *r;
94    }
95    if den > 0.0 {
96        let inv = 1.0 / den;
97        for r in row.iter_mut() {
98            *r *= inv;
99        }
100    }
101}
102
103/// LayerNorm with no affine — the output head's only normalization.
104fn layer_norm(x: &[f32], dst: &mut [f32]) {
105    let n = x.len() as f64;
106    let mean = x.iter().map(|&v| v as f64).sum::<f64>() / n;
107    let var = x.iter().map(|&v| (v as f64 - mean) * (v as f64 - mean)).sum::<f64>() / n;
108    let inv = 1.0 / (var + EPS).sqrt();
109    for (d, &v) in dst.iter_mut().zip(x) {
110        *d = ((v as f64 - mean) * inv) as f32;
111    }
112}
113
114// ---------------------------------------------------------------- linear
115
116/// `y = x·Wᵀ + b`, the weight read in place when the container quantized it.
117pub(crate) struct Lin {
118    w: Proj,
119    b: Option<Vec<f32>>,
120}
121
122impl Lin {
123    pub(crate) fn load(model: &Arc<CmfModel>, name: &str, bias: bool) -> Result<Lin, String> {
124        let w = Proj::from_model(model, &format!("{name}.weight"))?;
125        let b = if bias {
126            Some(cmf_f32(model, &format!("{name}.bias"))?)
127        } else {
128            None
129        };
130        Ok(Lin { w, b })
131    }
132
133    pub(crate) fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
134        let m = self.w.rows();
135        let cols = self.w.cols();
136        let mut out = vec![0f32; n * m];
137        // A GPU binding is capped near 2 GiB, and the prompt encoder's
138        // aggregate projection reads 188160 numbers per token — 1024 of them
139        // at once is past the cap. Chunk the batch so no single dispatch
140        // binds more than a quarter of a gigabyte of activations.
141        let per_row = cols * 4;
142        let chunk = (0x1000_0000usize / per_row.max(1)).max(1);
143        let mut done = 0usize;
144        while done < n {
145            let take = chunk.min(n - done);
146            self.w.matmat(
147                &x[done * cols..(done + take) * cols],
148                take,
149                &mut out[done * m..(done + take) * m],
150                pool,
151            );
152            done += take;
153        }
154        if let Some(b) = &self.b {
155            let dst = Shared(out.as_mut_ptr());
156            rows(pool, n, &|s, e| {
157                let r = unsafe { dst.at(s * m, (e - s) * m) };
158                for row in r.chunks_exact_mut(m) {
159                    for (v, &bb) in row.iter_mut().zip(b) {
160                        *v += bb;
161                    }
162                }
163            });
164        }
165        out
166    }
167}
168
169// ------------------------------------------------------------------ rope
170
171/// Split-RoPE tables: `cos`/`sin` laid out as `[tokens, heads·dh/2]`.
172pub struct Rope {
173    cos: Vec<f32>,
174    sin: Vec<f32>,
175    heads: usize,
176    half: usize,
177}
178
179impl Rope {
180    /// `positions[t][d]` are patch midpoints and `max_pos[d]` the axis
181    /// extent they are divided by. `dim` is the *inner* dimension
182    /// (heads·dh) the frequency ladder is sized from.
183    pub fn build(positions: &[Vec<f64>], max_pos: &[f64], dim: usize, heads: usize, theta: f64) -> Rope {
184        let ndim = max_pos.len();
185        let count = dim / (2 * ndim);
186        // indices = theta^linspace(0, 1, count) · π/2, in f64
187        let idx: Vec<f64> = (0..count)
188            .map(|j| {
189                let e = if count > 1 { j as f64 / (count - 1) as f64 } else { 0.0 };
190                theta.powf(e) * std::f64::consts::PI / 2.0
191            })
192            .collect();
193        let n = positions.len();
194        let half = dim / 2;
195        let pad = half - count * ndim;
196        let mut cos = vec![0f32; n * half];
197        let mut sin = vec![0f32; n * half];
198        for (t, p) in positions.iter().enumerate() {
199            let base = t * half;
200            for i in 0..pad {
201                cos[base + i] = 1.0;
202            }
203            for (j, &ind) in idx.iter().enumerate() {
204                for (d, &mp) in max_pos.iter().enumerate() {
205                    let f = ind * (p[d] / mp * 2.0 - 1.0);
206                    let o = base + pad + j * ndim + d;
207                    cos[o] = f.cos() as f32;
208                    sin[o] = f.sin() as f32;
209                }
210            }
211        }
212        Rope { cos, sin, heads, half: half / heads }
213    }
214
215    /// In-place split rotation of one token's `[heads·dh]` projection.
216    fn apply_row(&self, t: usize, row: &mut [f32]) {
217        let dh = self.half * 2;
218        let stride = self.heads * self.half;
219        for h in 0..self.heads {
220            let off = t * stride + h * self.half;
221            let (c, s) = (&self.cos[off..off + self.half], &self.sin[off..off + self.half]);
222            let v = &mut row[h * dh..(h + 1) * dh];
223            for i in 0..self.half {
224                let (a, b) = (v[i], v[i + self.half]);
225                v[i] = a * c[i] - b * s[i];
226                v[i + self.half] = b * c[i] + a * s[i];
227            }
228        }
229    }
230}
231
232// ------------------------------------------------------------- attention
233
234pub(crate) struct Attn {
235    q: Lin,
236    k: Lin,
237    v: Lin,
238    o: Lin,
239    q_norm: Vec<f32>,
240    k_norm: Vec<f32>,
241    gate: Option<Lin>,
242    heads: usize,
243    dh: usize,
244}
245
246impl Attn {
247    pub(crate) fn load(model: &Arc<CmfModel>, p: &str, heads: usize, dh: usize) -> Result<Attn, String> {
248        Ok(Attn {
249            q: Lin::load(model, &format!("{p}.to_q"), true)?,
250            k: Lin::load(model, &format!("{p}.to_k"), true)?,
251            v: Lin::load(model, &format!("{p}.to_v"), true)?,
252            o: Lin::load(model, &format!("{p}.to_out.0"), true)?,
253            q_norm: cmf_f32(model, &format!("{p}.q_norm.weight"))?,
254            k_norm: cmf_f32(model, &format!("{p}.k_norm.weight"))?,
255            gate: match model.tensor(&format!("{p}.to_gate_logits.weight")) {
256                Some(_) => Some(Lin::load(model, &format!("{p}.to_gate_logits"), true)?),
257                None => None,
258            },
259            heads,
260            dh,
261        })
262    }
263
264    /// `x` is `[n, query_dim]`, `ctx` is `[m, context_dim]` (self-attention
265    /// passes the same buffer twice). `mask` is an additive per-key bias.
266    #[allow(clippy::too_many_arguments)]
267    pub(crate) fn forward(
268        &self,
269        x: &[f32],
270        n: usize,
271        ctx: &[f32],
272        m: usize,
273        pe_q: Option<&Rope>,
274        pe_k: Option<&Rope>,
275        mask: Option<&[f32]>,
276        pool: Option<&Pool>,
277    ) -> Vec<f32> {
278        let inner = self.heads * self.dh;
279        let mut q = self.q.apply(x, n, pool);
280        let mut k = self.k.apply(ctx, m, pool);
281        let v = self.v.apply(ctx, m, pool);
282
283        let qn = Shared(q.as_mut_ptr());
284        rows(pool, n, &|s, e| {
285            let r = unsafe { qn.at(s * inner, (e - s) * inner) };
286            for (i, row) in r.chunks_exact_mut(inner).enumerate() {
287                rms_w(row, &self.q_norm);
288                if let Some(pe) = pe_q {
289                    pe.apply_row(s + i, row);
290                }
291            }
292        });
293        let kn = Shared(k.as_mut_ptr());
294        rows(pool, m, &|s, e| {
295            let r = unsafe { kn.at(s * inner, (e - s) * inner) };
296            for (i, row) in r.chunks_exact_mut(inner).enumerate() {
297                rms_w(row, &self.k_norm);
298                if let Some(pe) = pe_k {
299                    pe.apply_row(s + i, row);
300                }
301            }
302        });
303
304        // Per head, both halves of attention are GEMMs: scores are
305        // q·kᵀ and the value product is p·v. Gathering each head into a
306        // contiguous `[tokens, dh]` block costs one copy and buys the
307        // engine's blocked/BLAS/GPU kernels instead of a scalar loop —
308        // this is most of a step's arithmetic.
309        let mut out = vec![0f32; n * inner];
310        let scale = 1.0 / (self.dh as f32).sqrt();
311        let dh = self.dh;
312        let mut qh = vec![0f32; n * dh];
313        let mut kh = vec![0f32; m * dh];
314        let mut vh = vec![0f32; m * dh];
315        let mut sc = vec![0f32; n * m];
316        let mut oh = vec![0f32; n * dh];
317        for h in 0..self.heads {
318            for i in 0..n {
319                qh[i * dh..(i + 1) * dh].copy_from_slice(&q[i * inner + h * dh..][..dh]);
320            }
321            for j in 0..m {
322                kh[j * dh..(j + 1) * dh].copy_from_slice(&k[j * inner + h * dh..][..dh]);
323                vh[j * dh..(j + 1) * dh].copy_from_slice(&v[j * inner + h * dh..][..dh]);
324            }
325            crate::fcd_ops::gemm_nt(&qh, &kh, &mut sc, n, dh, m, pool);
326            let sp = Shared(sc.as_mut_ptr());
327            rows(pool, n, &|s, e| {
328                let r = unsafe { sp.at(s * m, (e - s) * m) };
329                for row in r.chunks_exact_mut(m) {
330                    for (x, j) in row.iter_mut().zip(0..m) {
331                        *x = *x * scale + mask.map_or(0.0, |mk| mk[j]);
332                    }
333                    softmax(row);
334                }
335            });
336            oh.iter_mut().for_each(|x| *x = 0.0);
337            crate::fcd_ops::gemm_dx(&sc, &vh, &mut oh, n, dh, m, pool);
338            for i in 0..n {
339                out[i * inner + h * dh..i * inner + (h + 1) * dh]
340                    .copy_from_slice(&oh[i * dh..(i + 1) * dh]);
341            }
342        }
343
344        if let Some(g) = &self.gate {
345            let logits = g.apply(x, n, pool);
346            let h = self.heads;
347            let dst = Shared(out.as_mut_ptr());
348            rows(pool, n, &|s, e| {
349                let r = unsafe { dst.at(s * inner, (e - s) * inner) };
350                for (i, row) in r.chunks_exact_mut(inner).enumerate() {
351                    for hh in 0..h {
352                        let gate = 2.0 / (1.0 + (-logits[(s + i) * h + hh]).exp());
353                        for d in row[hh * self.dh..(hh + 1) * self.dh].iter_mut() {
354                            *d *= gate;
355                        }
356                    }
357                }
358            });
359        }
360        self.o.apply(&out, n, pool)
361    }
362}
363
364// ---------------------------------------------------------- adaLN single
365
366/// `AdaLayerNormSingle`: sinusoidal timestep → SiLU MLP → `coeff·dim`
367/// modulation values, plus the embedding the output head reuses.
368struct AdaLn {
369    l1: Lin,
370    l2: Lin,
371    lin: Lin,
372    dim: usize,
373}
374
375impl AdaLn {
376    fn load(model: &Arc<CmfModel>, p: &str, dim: usize) -> Result<AdaLn, String> {
377        Ok(AdaLn {
378            l1: Lin::load(model, &format!("{p}.emb.timestep_embedder.linear_1"), true)?,
379            l2: Lin::load(model, &format!("{p}.emb.timestep_embedder.linear_2"), true)?,
380            lin: Lin::load(model, &format!("{p}.linear"), true)?,
381            dim,
382        })
383    }
384
385    /// `(values [n, coeff·dim], embedded [n, dim])` for `n` timesteps.
386    fn forward(&self, t: &[f32], pool: Option<&Pool>) -> (Vec<f32>, Vec<f32>) {
387        let n = t.len();
388        // get_timestep_embedding(256, flip_sin_to_cos=True, shift=0): the
389        // flip puts cosine first, so the halves are [cos, sin].
390        let half = 128usize;
391        let mut proj = vec![0f32; n * 256];
392        let ws: Vec<f64> = (0..half)
393            .map(|j| (-(10000f64).ln() * j as f64 / half as f64).exp())
394            .collect();
395        for (i, &tv) in t.iter().enumerate() {
396            for (j, &w) in ws.iter().enumerate() {
397                let a = tv as f64 * w;
398                proj[i * 256 + j] = a.cos() as f32;
399                proj[i * 256 + half + j] = a.sin() as f32;
400            }
401        }
402        let mut h = self.l1.apply(&proj, n, pool);
403        for v in h.iter_mut() {
404            *v = silu(*v);
405        }
406        let embedded = self.l2.apply(&h, n, pool);
407        let mut act = embedded.clone();
408        for v in act.iter_mut() {
409            *v = silu(*v);
410        }
411        (self.lin.apply(&act, n, pool), embedded)
412    }
413}
414
415/// adaLN values for the *distinct* timesteps of a stream, plus each token's
416/// index into them. Per-token timesteps take only a handful of values here
417/// (a conditioning token sits at 0 while the rest sit at the current
418/// sigma), so the `[36864, 4096]` projection runs a few times, not `T`.
419struct TsTable {
420    vals: Vec<f32>,
421    emb: Vec<f32>,
422    idx: Vec<usize>,
423    width: usize,
424    edim: usize,
425}
426
427impl TsTable {
428    fn build(a: &AdaLn, ts: &[f32], scale: f64, pool: Option<&Pool>) -> TsTable {
429        let mut vals: Vec<f32> = Vec::new();
430        let mut idx = Vec::with_capacity(ts.len());
431        for &t in ts {
432            match vals.iter().position(|&v| v.to_bits() == t.to_bits()) {
433                Some(i) => idx.push(i),
434                None => {
435                    vals.push(t);
436                    idx.push(vals.len() - 1);
437                }
438            }
439        }
440        let scaled: Vec<f32> = vals.iter().map(|&v| (v as f64 * scale) as f32).collect();
441        let (v, e) = a.forward(&scaled, pool);
442        let width = v.len() / scaled.len().max(1);
443        TsTable { vals: v, emb: e, idx, width, edim: a.dim }
444    }
445
446    fn distinct(&self) -> usize {
447        self.vals.len() / self.width.max(1)
448    }
449
450    fn row(&self, r: usize) -> &[f32] {
451        &self.vals[r * self.width..(r + 1) * self.width]
452    }
453
454    fn emb_row(&self, r: usize) -> &[f32] {
455        &self.emb[r * self.edim..(r + 1) * self.edim]
456    }
457
458    /// `(shift, scale, gate)` per distinct timestep for the adaLN triple at
459    /// table row `off`: the block's static table plus the timestep's own
460    /// contribution, summed once instead of once per token.
461    fn triples(&self, table: &[f32], dim: usize, off: usize) -> Vec<[Vec<f32>; 3]> {
462        (0..self.distinct())
463            .map(|r| {
464                let v = self.row(r);
465                std::array::from_fn(|j| {
466                    let o = (off + j) * dim;
467                    (0..dim).map(|d| table[o + d] + v[o + d]).collect()
468                })
469            })
470            .collect()
471    }
472
473    /// `(scale, shift)` per distinct timestep for an A↔V table, which — unlike
474    /// the self-attention rows — comes out scale-first.
475    fn pairs(&self, table: &[f32], dim: usize, off: usize) -> Vec<[Vec<f32>; 2]> {
476        (0..self.distinct())
477            .map(|r| {
478                let v = self.row(r);
479                std::array::from_fn(|j| {
480                    let o = (off + j) * dim;
481                    (0..dim).map(|d| table[o + d] + v[o + d]).collect()
482                })
483            })
484            .collect()
485    }
486}
487
488
489/// `rms_norm(x) · (1 + scale) + shift` over every token, in parallel.
490/// `mods[r]` is the `(shift, scale, gate)` triple of the r-th distinct
491/// timestep and `idx[t]` says which one token `t` uses.
492fn ada_zero_rows(
493    x: &[f32],
494    out: &mut [f32],
495    n: usize,
496    dim: usize,
497    mods: &[[Vec<f32>; 3]],
498    idx: &[usize],
499    pool: Option<&Pool>,
500) {
501    let dst = Shared(out.as_mut_ptr());
502    rows(pool, n, &|s, e| {
503        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
504        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
505            let md = &mods[idx[i]];
506            rms_plain(&x[i * dim..(i + 1) * dim], row);
507            for d in 0..dim {
508                row[d] = row[d] * (1.0 + md[1][d]) + md[0][d];
509            }
510        }
511    });
512}
513
514/// `x += y · gate`, the residual every sub-layer writes back through.
515fn add_gated(
516    x: &mut [f32],
517    y: &[f32],
518    n: usize,
519    dim: usize,
520    mods: &[[Vec<f32>; 3]],
521    idx: &[usize],
522    pool: Option<&Pool>,
523) {
524    let dst = Shared(x.as_mut_ptr());
525    rows(pool, n, &|s, e| {
526        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
527        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
528            let g = &mods[idx[i]][2];
529            for d in 0..dim {
530                row[d] += y[i * dim + d] * g[d];
531            }
532        }
533    });
534}
535
536/// The post-SA pair: fold the gated update into the residual and hand the
537/// re-normalized result to cross-attention, in one pass over the tokens.
538fn post_sa_rows(
539    x: &mut [f32],
540    y: &[f32],
541    normed: &mut [f32],
542    n: usize,
543    dim: usize,
544    mods: &[[Vec<f32>; 3]],
545    idx: &[usize],
546    pool: Option<&Pool>,
547) {
548    let a = Shared(x.as_mut_ptr());
549    let b = Shared(normed.as_mut_ptr());
550    rows(pool, n, &|s, e| {
551        let xr = unsafe { a.at(s * dim, (e - s) * dim) };
552        let nr = unsafe { b.at(s * dim, (e - s) * dim) };
553        for ((row, nrow), i) in xr.chunks_exact_mut(dim).zip(nr.chunks_exact_mut(dim)).zip(s..e) {
554            let g = &mods[idx[i]][2];
555            for d in 0..dim {
556                row[d] += y[i * dim + d] * g[d];
557            }
558            rms_plain(row, nrow);
559        }
560    });
561}
562
563/// `x += y · g` with one shared per-channel gate (the A↔V fusion).
564fn add_scaled(x: &mut [f32], y: &[f32], n: usize, dim: usize, g: &[f32], pool: Option<&Pool>) {
565    let dst = Shared(x.as_mut_ptr());
566    rows(pool, n, &|s, e| {
567        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
568        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
569            for d in 0..dim {
570                row[d] += y[i * dim + d] * g[d];
571            }
572        }
573    });
574}
575
576/// The cross-attention query: an affine on an already-normalized row.
577fn affine_rows(
578    x: &[f32],
579    out: &mut [f32],
580    n: usize,
581    dim: usize,
582    mods: &[[Vec<f32>; 3]],
583    idx: &[usize],
584    pool: Option<&Pool>,
585) {
586    let dst = Shared(out.as_mut_ptr());
587    rows(pool, n, &|s, e| {
588        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
589        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
590            let md = &mods[idx[i]];
591            for d in 0..dim {
592                row[d] = x[i * dim + d] * (1.0 + md[1][d]) + md[0][d];
593            }
594        }
595    });
596}
597
598
599/// Where a denoising step actually goes. `CMF_LTX_PROF=1` prints the split
600/// once per forward: guessing at this is how ports stay slow.
601#[derive(Default)]
602struct Prof {
603    on: bool,
604    t: [f64; 6],
605}
606
607const P_ADALN: usize = 0;
608const P_SELF: usize = 1;
609const P_CROSS: usize = 2;
610const P_FUSE: usize = 3;
611const P_FF: usize = 4;
612const P_MOD: usize = 5;
613
614impl Prof {
615    fn new() -> Prof {
616        Prof { on: std::env::var("CMF_LTX_PROF").is_ok(), t: [0.0; 6] }
617    }
618    #[inline]
619    fn tick(&mut self, slot: usize, at: std::time::Instant) -> std::time::Instant {
620        if self.on {
621            self.t[slot] += at.elapsed().as_secs_f64();
622            return std::time::Instant::now();
623        }
624        at
625    }
626    fn report(&self) {
627        if !self.on {
628            return;
629        }
630        let names = ["adaln", "self-attn", "cross-attn", "a<->v", "ffn", "modulate"];
631        let total: f64 = self.t.iter().sum();
632        let parts: Vec<String> = names
633            .iter()
634            .zip(&self.t)
635            .map(|(n, v)| format!("{n} {v:.1}s ({:.0}%)", 100.0 * v / total.max(1e-9)))
636            .collect();
637        println!("  profile: {}", parts.join("  "));
638    }
639}
640
641// ----------------------------------------------------------------- block
642
643struct Stream {
644    attn1: Attn,
645    attn2: Attn,
646    ff_in: Lin,
647    ff_out: Lin,
648    sst: Vec<f32>,        // [9, dim]
649    prompt_sst: Vec<f32>, // [2, dim]
650}
651
652impl Stream {
653    fn load(
654        model: &Arc<CmfModel>,
655        p: &str,
656        prefix: &str,
657        heads: usize,
658        dh: usize,
659        ff_bias: bool,
660    ) -> Result<Stream, String> {
661        let a = |n: &str| format!("{p}.{prefix}{n}");
662        Ok(Stream {
663            attn1: Attn::load(model, &a("attn1"), heads, dh)?,
664            attn2: Attn::load(model, &a("attn2"), heads, dh)?,
665            ff_in: Lin::load(model, &a("ff.net.0.proj"), ff_bias)?,
666            ff_out: Lin::load(model, &a("ff.net.2"), ff_bias)?,
667            sst: cmf_f32(model, &a("scale_shift_table"))?,
668            prompt_sst: cmf_f32(model, &a("prompt_scale_shift_table"))?,
669        })
670    }
671
672    fn ff(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
673        let mut h = self.ff_in.apply(x, n, pool);
674        for v in h.iter_mut() {
675            *v = gelu_tanh(*v);
676        }
677        self.ff_out.apply(&h, n, pool)
678    }
679}
680
681struct Block {
682    video: Stream,
683    audio: Stream,
684    a2v: Attn,
685    v2a: Attn,
686    sst_a2v_video: Vec<f32>, // [5, 4096]
687    sst_a2v_audio: Vec<f32>, // [5, 2048]
688}
689
690// ----------------------------------------------------------------- model
691
692/// One modality's per-step conditioning: everything the blocks read that is
693/// not a weight.
694pub struct StreamInput {
695    /// Patchified latent, `[tokens, in_channels]`.
696    pub latent: Vec<f32>,
697    pub tokens: usize,
698    /// Per-token timestep (the sigma·denoising-mask product).
699    pub timesteps: Vec<f32>,
700    /// Per-token patch midpoints, one entry per RoPE axis.
701    pub positions: Vec<Vec<f64>>,
702    /// Prompt embeddings out of the connector, `[ctx_len, cross_dim]`.
703    pub context: Vec<f32>,
704    pub ctx_len: usize,
705    /// Additive per-key prompt mask, or empty for "attend to all".
706    pub context_mask: Vec<f32>,
707    /// Non-zero for tokens holding a standalone pixel frame (video only).
708    pub keyframes: Vec<f32>,
709    /// This stream's sigma — the *other* stream's fusion gate reads it.
710    pub sigma: f32,
711}
712
713pub struct LtxDit {
714    model: Arc<CmfModel>,
715    blocks: Vec<Block>,
716    patchify: Lin,
717    a_patchify: Lin,
718    keyframes_emb: Option<Vec<f32>>,
719    adaln: AdaLn,
720    a_adaln: AdaLn,
721    prompt_adaln: AdaLn,
722    a_prompt_adaln: AdaLn,
723    av_v_ss: AdaLn,
724    av_a_ss: AdaLn,
725    av_a2v_gate: AdaLn,
726    av_v2a_gate: AdaLn,
727    proj_out: Lin,
728    a_proj_out: Lin,
729    sst_out: Vec<f32>,
730    a_sst_out: Vec<f32>,
731    pub heads: usize,
732    pub dh: usize,
733    pub a_heads: usize,
734    pub a_dh: usize,
735    pub max_pos: Vec<f64>,
736    pub a_max_pos: Vec<f64>,
737    pub cross_max_pos: f64,
738    pub theta: f64,
739    pub t_scale: f64,
740    pub av_t_scale: f64,
741    pub audio_cross_dim: usize,
742}
743
744impl LtxDit {
745    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<LtxDit, String> {
746        let cfg_bytes = ["ltx.config_json", "dit.config_json"]
747            .iter()
748            .find_map(|n| model.tensor(n).map(|e| model.entry_bytes(e)))
749            .ok_or("container carries no ltx.config_json")?;
750        let cfg: serde_json::Value =
751            serde_json::from_slice(cfg_bytes).map_err(|e| format!("ltx.config_json: {e}"))?;
752        let t = cfg.get("transformer").unwrap_or(&cfg).clone();
753        let g = |k: &str, d: f64| t.get(k).and_then(|v| v.as_f64()).unwrap_or(d);
754        let heads = g("num_attention_heads", 32.0) as usize;
755        let dh = g("attention_head_dim", 128.0) as usize;
756        let a_heads = g("audio_num_attention_heads", 32.0) as usize;
757        let a_dh = g("audio_attention_head_dim", 64.0) as usize;
758        let n_layers = g("num_layers", 48.0) as usize;
759        let ff_bias = t.get("ff_bias").and_then(|v| v.as_bool()).unwrap_or(true);
760        let a_ff_bias = t.get("audio_ff_bias").and_then(|v| v.as_bool()).unwrap_or(true);
761        let arr = |k: &str, d: Vec<f64>| -> Vec<f64> {
762            t.get(k)
763                .and_then(|v| v.as_array())
764                .map(|a| a.iter().filter_map(|x| x.as_f64()).collect())
765                .unwrap_or(d)
766        };
767        let max_pos = arr("positional_embedding_max_pos", vec![20.0, 2048.0, 2048.0]);
768        let a_max_pos = arr("audio_positional_embedding_max_pos", vec![20.0]);
769        let cross_max_pos = max_pos[0].max(a_max_pos[0]);
770        let dim = heads * dh;
771        let a_dim = a_heads * a_dh;
772
773        let mut blocks = Vec::with_capacity(n_layers);
774        for i in 0..n_layers {
775            let p = format!("dit.transformer_blocks.{i}");
776            blocks.push(Block {
777                video: Stream::load(model, &p, "", heads, dh, ff_bias)?,
778                audio: Stream::load(model, &p, "audio_", a_heads, a_dh, a_ff_bias)?,
779                a2v: Attn::load(model, &format!("{p}.audio_to_video_attn"), a_heads, a_dh)?,
780                v2a: Attn::load(model, &format!("{p}.video_to_audio_attn"), a_heads, a_dh)?,
781                sst_a2v_video: cmf_f32(model, &format!("{p}.scale_shift_table_a2v_ca_video"))?,
782                sst_a2v_audio: cmf_f32(model, &format!("{p}.scale_shift_table_a2v_ca_audio"))?,
783            });
784        }
785        let _ = (dim, a_dim);
786        Ok(LtxDit {
787            blocks,
788            patchify: Lin::load(model, "dit.patchify_proj", true)?,
789            a_patchify: Lin::load(model, "dit.audio_patchify_proj", true)?,
790            keyframes_emb: match model.tensor("dit.keyframes_abs_pos_embedding") {
791                Some(_) => Some(cmf_f32(model, "dit.keyframes_abs_pos_embedding")?),
792                None => None,
793            },
794            adaln: AdaLn::load(model, "dit.adaln_single", dim)?,
795            a_adaln: AdaLn::load(model, "dit.audio_adaln_single", a_dim)?,
796            prompt_adaln: AdaLn::load(model, "dit.prompt_adaln_single", dim)?,
797            a_prompt_adaln: AdaLn::load(model, "dit.audio_prompt_adaln_single", a_dim)?,
798            av_v_ss: AdaLn::load(model, "dit.av_ca_video_scale_shift_adaln_single", dim)?,
799            av_a_ss: AdaLn::load(model, "dit.av_ca_audio_scale_shift_adaln_single", a_dim)?,
800            av_a2v_gate: AdaLn::load(model, "dit.av_ca_a2v_gate_adaln_single", dim)?,
801            av_v2a_gate: AdaLn::load(model, "dit.av_ca_v2a_gate_adaln_single", a_dim)?,
802            proj_out: Lin::load(model, "dit.proj_out", true)?,
803            a_proj_out: Lin::load(model, "dit.audio_proj_out", true)?,
804            sst_out: cmf_f32(model, "dit.scale_shift_table")?,
805            a_sst_out: cmf_f32(model, "dit.audio_scale_shift_table")?,
806            heads,
807            dh,
808            a_heads,
809            a_dh,
810            max_pos,
811            a_max_pos,
812            cross_max_pos,
813            theta: g("positional_embedding_theta", 10000.0),
814            t_scale: g("timestep_scale_multiplier", 1000.0),
815            av_t_scale: g("av_ca_timestep_scale_multiplier", 1.0),
816            audio_cross_dim: g("audio_cross_attention_dim", 2048.0) as usize,
817            model: model.clone(),
818        })
819    }
820
821    pub fn blocks(&self) -> usize {
822        self.blocks.len()
823    }
824
825    pub fn container(&self) -> &Arc<CmfModel> {
826        &self.model
827    }
828
829    /// One denoising step: `(video velocity [T, C], audio velocity [T, C])`.
830    pub fn forward(
831        &self,
832        video: &StreamInput,
833        audio: &StreamInput,
834        pool: Option<&Pool>,
835    ) -> (Vec<f32>, Vec<f32>) {
836        self.forward_traced(video, audio, pool, &mut |_, _| {})
837    }
838
839    pub fn forward_traced(
840        &self,
841        video: &StreamInput,
842        audio: &StreamInput,
843        pool: Option<&Pool>,
844        trace: &mut dyn FnMut(&str, &[f32]),
845    ) -> (Vec<f32>, Vec<f32>) {
846        // A denoising step is the opposite of what the per-op probe is built
847        // for: forty-eight identical blocks, the same shapes every time, the
848        // device warm throughout. Take the probe out of it.
849        let _trust = crate::gpu::trust_gpu();
850        let dim = self.heads * self.dh;
851        let a_dim = self.a_heads * self.a_dh;
852        let (n, m) = (video.tokens, audio.tokens);
853
854        // --- patchify -----------------------------------------------------
855        let mut vx = self.patchify.apply(&video.latent, n, pool);
856        if let Some(emb) = &self.keyframes_emb {
857            for i in 0..n {
858                if video.keyframes.get(i).copied().unwrap_or(0.0) > 0.0 {
859                    for (d, &e) in vx[i * dim..(i + 1) * dim].iter_mut().zip(emb) {
860                        *d += e;
861                    }
862                }
863            }
864        }
865        let mut ax = self.a_patchify.apply(&audio.latent, m, pool);
866        trace("v.args.x", &vx);
867        trace("a.args.x", &ax);
868
869        // --- adaLN tables, one row per distinct timestep -------------------
870        let vt = TsTable::build(&self.adaln, &video.timesteps, self.t_scale, pool);
871        let at = TsTable::build(&self.a_adaln, &audio.timesteps, self.t_scale, pool);
872        let vpt = TsTable::build(&self.prompt_adaln, &[video.sigma], self.t_scale, pool);
873        let apt = TsTable::build(&self.a_prompt_adaln, &[audio.sigma], self.t_scale, pool);
874        let vxs = TsTable::build(&self.av_v_ss, &video.timesteps, self.t_scale, pool);
875        let axs = TsTable::build(&self.av_a_ss, &audio.timesteps, self.t_scale, pool);
876        // The fusion gate reads the *other* stream's sigma — the noise level
877        // it is being asked to trust — at the A-V multiplier.
878        let vgt = TsTable::build(&self.av_a2v_gate, &[audio.sigma], self.av_t_scale, pool);
879        let agt = TsTable::build(&self.av_v2a_gate, &[video.sigma], self.av_t_scale, pool);
880
881        // --- RoPE ---------------------------------------------------------
882        let v_pe = Rope::build(&video.positions, &self.max_pos, dim, self.heads, self.theta);
883        let a_pe = Rope::build(&audio.positions, &self.a_max_pos, a_dim, self.a_heads, self.theta);
884        let time_only = |p: &[Vec<f64>]| p.iter().map(|r| vec![r[0]]).collect::<Vec<_>>();
885        let v_xpe = Rope::build(
886            &time_only(&video.positions),
887            &[self.cross_max_pos],
888            self.audio_cross_dim,
889            self.heads,
890            self.theta,
891        );
892        let a_xpe = Rope::build(
893            &time_only(&audio.positions),
894            &[self.cross_max_pos],
895            self.audio_cross_dim,
896            self.a_heads,
897            self.theta,
898        );
899
900        let vmask = (!video.context_mask.is_empty()).then_some(&video.context_mask[..]);
901        let amask = (!audio.context_mask.is_empty()).then_some(&audio.context_mask[..]);
902
903        let mut prof = Prof::new();
904        for (bi, blk) in self.blocks.iter().enumerate() {
905            let mut pt = std::time::Instant::now();
906            let v_msa = vt.triples(&blk.video.sst, dim, 0);
907            let v_ca = vt.triples(&blk.video.sst, dim, 6);
908            let v_mlp = vt.triples(&blk.video.sst, dim, 3);
909            let a_msa = at.triples(&blk.audio.sst, a_dim, 0);
910            let a_ca = at.triples(&blk.audio.sst, a_dim, 6);
911            let a_mlp = at.triples(&blk.audio.sst, a_dim, 3);
912
913            // ---- video: self-attention, then prompt cross-attention ----
914            pt = prof.tick(P_ADALN, pt);
915            let mut vnorm = vec![0f32; n * dim];
916            ada_zero_rows(&vx, &mut vnorm, n, dim, &v_msa, &vt.idx, pool);
917            pt = prof.tick(P_MOD, pt);
918            if bi == 0 {
919                trace("v.b0.sa.in", &vnorm);
920            }
921            let vsa = blk
922                .video
923                .attn1
924                .forward(&vnorm, n, &vnorm, n, Some(&v_pe), Some(&v_pe), None, pool);
925            if bi == 0 {
926                trace("v.b0.sa.out", &vsa);
927            }
928            pt = prof.tick(P_SELF, pt);
929            let mut vnormed = vec![0f32; n * dim];
930            post_sa_rows(&mut vx, &vsa, &mut vnormed, n, dim, &v_msa, &vt.idx, pool);
931            let mut vq = vec![0f32; n * dim];
932            affine_rows(&vnormed, &mut vq, n, dim, &v_ca, &vt.idx, pool);
933            let vctx = modulate_kv(&video.context, video.ctx_len, dim, &blk.video.prompt_sst, vpt.row(0));
934            let vca = blk.video.attn2.forward(&vq, n, &vctx, video.ctx_len, None, None, vmask, pool);
935            if bi == 0 {
936                trace("v.b0.ca.in", &vq);
937                trace("v.b0.ca.ctx", &vctx);
938                trace("v.b0.ca.out", &vca);
939            }
940            add_gated(&mut vx, &vca, n, dim, &v_ca, &vt.idx, pool);
941            pt = prof.tick(P_CROSS, pt);
942
943            // ---- audio: the same two steps ----
944            let mut anorm = vec![0f32; m * a_dim];
945            ada_zero_rows(&ax, &mut anorm, m, a_dim, &a_msa, &at.idx, pool);
946            if bi == 0 {
947                trace("a.b0.sa.in", &anorm);
948            }
949            let asa = blk
950                .audio
951                .attn1
952                .forward(&anorm, m, &anorm, m, Some(&a_pe), Some(&a_pe), None, pool);
953            if bi == 0 {
954                trace("a.b0.sa.out", &asa);
955            }
956            let mut anormed = vec![0f32; m * a_dim];
957            post_sa_rows(&mut ax, &asa, &mut anormed, m, a_dim, &a_msa, &at.idx, pool);
958            let mut aq = vec![0f32; m * a_dim];
959            affine_rows(&anormed, &mut aq, m, a_dim, &a_ca, &at.idx, pool);
960            let actx = modulate_kv(&audio.context, audio.ctx_len, a_dim, &blk.audio.prompt_sst, apt.row(0));
961            let aca = blk.audio.attn2.forward(&aq, m, &actx, audio.ctx_len, None, None, amask, pool);
962            if bi == 0 {
963                trace("a.b0.ca.in", &aq);
964                trace("a.b0.ca.ctx", &actx);
965                trace("a.b0.ca.out", &aca);
966            }
967            add_gated(&mut ax, &aca, m, a_dim, &a_ca, &at.idx, pool);
968            pt = prof.tick(P_CROSS, pt);
969
970            // ---- audio ↔ video, both directions off the pre-fusion state ----
971            let vx_pre = vx.clone();
972            let ax_pre = ax.clone();
973            let a2v_vp = vxs.pairs(&blk.sst_a2v_video, dim, 0);
974            let a2v_ap = axs.pairs(&blk.sst_a2v_audio, a_dim, 0);
975            let a2v_v = ada_pair(&vx_pre, n, dim, &a2v_vp, &vxs.idx, pool);
976            let a2v_a = ada_pair(&ax_pre, m, a_dim, &a2v_ap, &axs.idx, pool);
977            let a2v = blk
978                .a2v
979                .forward(&a2v_v, n, &a2v_a, m, Some(&v_xpe), Some(&a_xpe), None, pool);
980            if bi == 0 {
981                trace("v.b0.a2v.in", &a2v_v);
982                trace("v.b0.a2v.ctx", &a2v_a);
983                trace("v.b0.a2v.out", &a2v);
984            }
985            let gate_a2v = gate_row(&blk.sst_a2v_video, dim, vgt.row(0));
986            add_scaled(&mut vx, &a2v, n, dim, &gate_a2v, pool);
987            let v2a_ap = axs.pairs(&blk.sst_a2v_audio, a_dim, 2);
988            let v2a_vp = vxs.pairs(&blk.sst_a2v_video, dim, 2);
989            let v2a_a = ada_pair(&ax_pre, m, a_dim, &v2a_ap, &axs.idx, pool);
990            let v2a_v = ada_pair(&vx_pre, n, dim, &v2a_vp, &vxs.idx, pool);
991            let v2a = blk
992                .v2a
993                .forward(&v2a_a, m, &v2a_v, n, Some(&a_xpe), Some(&v_xpe), None, pool);
994            if bi == 0 {
995                trace("a.b0.v2a.in", &v2a_a);
996                trace("a.b0.v2a.ctx", &v2a_v);
997                trace("a.b0.v2a.out", &v2a);
998            }
999            let gate_v2a = gate_row(&blk.sst_a2v_audio, a_dim, agt.row(0));
1000            add_scaled(&mut ax, &v2a, m, a_dim, &gate_v2a, pool);
1001            pt = prof.tick(P_FUSE, pt);
1002
1003            // ---- feed-forward ----
1004            let mut vsc = vec![0f32; n * dim];
1005            ada_zero_rows(&vx, &mut vsc, n, dim, &v_mlp, &vt.idx, pool);
1006            let vff = blk.video.ff(&vsc, n, pool);
1007            if bi == 0 {
1008                trace("v.b0.ff.in", &vsc);
1009                trace("v.b0.ff.out", &vff);
1010            }
1011            add_gated(&mut vx, &vff, n, dim, &v_mlp, &vt.idx, pool);
1012            let mut asc = vec![0f32; m * a_dim];
1013            ada_zero_rows(&ax, &mut asc, m, a_dim, &a_mlp, &at.idx, pool);
1014            let aff = blk.audio.ff(&asc, m, pool);
1015            if bi == 0 {
1016                trace("a.b0.ff.in", &asc);
1017                trace("a.b0.ff.out", &aff);
1018            }
1019            add_gated(&mut ax, &aff, m, a_dim, &a_mlp, &at.idx, pool);
1020            pt = prof.tick(P_FF, pt);
1021            trace(&format!("v.block{bi}"), &vx);
1022            trace(&format!("a.block{bi}"), &ax);
1023        }
1024
1025        prof.report();
1026
1027        // --- output head: LayerNorm (no affine), adaLN, projection --------
1028        let vout = head(&vx, n, dim, &self.sst_out, &vt, &self.proj_out, pool);
1029        let aout = head(&ax, m, a_dim, &self.a_sst_out, &at, &self.a_proj_out, pool);
1030        trace("v.out", &vout);
1031        trace("a.out", &aout);
1032        (vout, aout)
1033    }
1034}
1035
1036/// `prompt_scale_shift_table` plus the prompt adaLN row, modulating the
1037/// cross-attention K/V — the same modulation for every context token.
1038fn modulate_kv(ctx: &[f32], len: usize, dim: usize, table: &[f32], extra: &[f32]) -> Vec<f32> {
1039    let mut out = vec![0f32; len * dim];
1040    let shift: Vec<f32> = (0..dim).map(|d| table[d] + extra[d]).collect();
1041    let scale: Vec<f32> = (0..dim).map(|d| table[dim + d] + extra[dim + d]).collect();
1042    for i in 0..len {
1043        for d in 0..dim {
1044            out[i * dim + d] = ctx[i * dim + d] * (1.0 + scale[d]) + shift[d];
1045        }
1046    }
1047    out
1048}
1049
1050
1051/// `ada_zero` with an A↔V `(scale, shift)` pair per distinct timestep.
1052fn ada_pair(
1053    x: &[f32],
1054    n: usize,
1055    dim: usize,
1056    pairs: &[[Vec<f32>; 2]],
1057    idx: &[usize],
1058    pool: Option<&Pool>,
1059) -> Vec<f32> {
1060    let mut out = vec![0f32; n * dim];
1061    let dst = Shared(out.as_mut_ptr());
1062    rows(pool, n, &|s, e| {
1063        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1064        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1065            let p = &pairs[idx[i]];
1066            rms_plain(&x[i * dim..(i + 1) * dim], row);
1067            for d in 0..dim {
1068                row[d] = row[d] * (1.0 + p[0][d]) + p[1][d];
1069            }
1070        }
1071    });
1072    out
1073}
1074
1075/// The single gate row of an A↔V table — row 4 of `[5, dim]`, plus the
1076/// gate adaLN's own output.
1077fn gate_row(table: &[f32], dim: usize, extra: &[f32]) -> Vec<f32> {
1078    (0..dim).map(|d| table[4 * dim + d] + extra[d]).collect()
1079}
1080
1081/// The output head: LayerNorm without affine, the final scale/shift pair
1082/// (both offset by the same embedded timestep), then the projection.
1083fn head(
1084    x: &[f32],
1085    n: usize,
1086    dim: usize,
1087    sst: &[f32],
1088    ts: &TsTable,
1089    proj: &Lin,
1090    pool: Option<&Pool>,
1091) -> Vec<f32> {
1092    let mut y = vec![0f32; n * dim];
1093    let dst = Shared(y.as_mut_ptr());
1094    rows(pool, n, &|s, e| {
1095        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1096        let mut ln = vec![0f32; dim];
1097        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1098            let emb = ts.emb_row(ts.idx[i.min(ts.idx.len() - 1)]);
1099            layer_norm(&x[i * dim..(i + 1) * dim], &mut ln);
1100            for d in 0..dim {
1101                row[d] = ln[d] * (1.0 + sst[dim + d] + emb[d]) + sst[d] + emb[d];
1102            }
1103        }
1104    });
1105    proj.apply(&y, n, pool)
1106}