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.
82///
83/// In f32 and, where it matters, across the pool. A step puts half a billion
84/// values through this — 672 tokens × 16384 wide × 48 blocks — so computing
85/// it in f64 on one thread was worth several seconds of every step on its
86/// own. The f32 form agrees with the f64 one to ~1e-7 relative, which is far
87/// inside what the 4-bit weights around it carry.
88#[inline]
89pub(crate) fn gelu_tanh(v: f32) -> f32 {
90    const K: f32 = 0.797_884_56; // sqrt(2/pi)
91    0.5 * v * (1.0 + (K * (v + 0.044715 * v * v * v)).tanh())
92}
93
94/// The same activation over a whole buffer, split across the pool.
95pub(crate) fn gelu_tanh_rows(x: &mut [f32], pool: Option<&Pool>) {
96    let dst = Shared(x.as_mut_ptr());
97    let n = x.len();
98    let grain = 4096usize;
99    let chunks = n.div_ceil(grain);
100    rows(pool, chunks, &|s, e| {
101        let (lo, hi) = (s * grain, (e * grain).min(n));
102        let r = unsafe { dst.at(lo, hi - lo) };
103        for v in r.iter_mut() {
104            *v = gelu_tanh(*v);
105        }
106    });
107}
108
109/// One attention row, normalized. A step softmaxes on the order of a
110/// billion scores — forty-eight blocks × thirty-two heads × every query
111/// against every key — so the scalar `exp()` here was not a detail: it was
112/// the arithmetic. The NEON path evaluates four at a time.
113pub(crate) fn softmax(row: &mut [f32]) {
114    #[cfg(target_arch = "aarch64")]
115    {
116        crate::attention::softmax_row(row);
117    }
118    #[cfg(not(target_arch = "aarch64"))]
119    {
120        let mx = row.iter().cloned().fold(f32::MIN, f32::max);
121        let mut den = 0f32;
122        for r in row.iter_mut() {
123            *r = (*r - mx).exp();
124            den += *r;
125        }
126        if den > 0.0 {
127            let inv = 1.0 / den;
128            for r in row.iter_mut() {
129                *r *= inv;
130            }
131        }
132    }
133}
134
135/// LayerNorm with no affine — the output head's only normalization.
136fn layer_norm(x: &[f32], dst: &mut [f32]) {
137    let n = x.len() as f64;
138    let mean = x.iter().map(|&v| v as f64).sum::<f64>() / n;
139    let var = x
140        .iter()
141        .map(|&v| (v as f64 - mean) * (v as f64 - mean))
142        .sum::<f64>()
143        / n;
144    let inv = 1.0 / (var + EPS).sqrt();
145    for (d, &v) in dst.iter_mut().zip(x) {
146        *d = ((v as f64 - mean) * inv) as f32;
147    }
148}
149
150// ---------------------------------------------------------------- linear
151
152/// `y = x·Wᵀ + b`, the weight read in place when the container quantized it.
153pub(crate) struct Lin {
154    w: Proj,
155    b: Option<Vec<f32>>,
156    /// A runtime low-rank branch, when an adapter is loaded. The container's
157    /// q4tp weights cannot absorb one, so it is evaluated beside them.
158    lora: Option<crate::ltxlora::LoraBranch>,
159}
160
161impl Lin {
162    pub(crate) fn load(model: &Arc<CmfModel>, name: &str, bias: bool) -> Result<Lin, String> {
163        Lin::load_lora(model, name, bias, None)
164    }
165
166    /// The same, consulting an adapter for a low-rank branch on this
167    /// projection. A bank that carries nothing for `name` leaves the
168    /// projection exactly as `load` would.
169    pub(crate) fn load_lora(
170        model: &Arc<CmfModel>,
171        name: &str,
172        bias: bool,
173        bank: Option<&crate::ltxlora::LoraBank>,
174    ) -> Result<Lin, String> {
175        let w = Proj::from_model(model, &format!("{name}.weight"))?;
176        let b = if bias {
177            Some(cmf_f32(model, &format!("{name}.bias"))?)
178        } else {
179            None
180        };
181        // Shape-checked: a branch that does not match this projection is an
182        // adapter for another model, and the branch writes through a raw
183        // pointer sized by ITS out dimension.
184        let lora = match bank {
185            Some(k) => k.branch_for(name, w.rows(), w.cols())?,
186            None => None,
187        };
188        Ok(Lin { w, b, lora })
189    }
190
191    /// The adapter half of `apply`, for the fused paths that got the base
192    /// product elsewhere. Runs before the bias, which is where the reference
193    /// implementations put it — the branch sees the same activations the
194    /// base projection did.
195    pub(crate) fn has_lora(&self) -> bool {
196        self.lora.as_ref().is_some_and(|l| l.live())
197    }
198
199    pub(crate) fn add_lora(&self, out: &mut [f32], x: &[f32], n: usize, pool: Option<&Pool>) {
200        if let Some(l) = &self.lora {
201            l.add(x, n, out, pool);
202        }
203    }
204
205    /// The container-mapped q4tp weight behind this projection, when there
206    /// is one: (model, tensor index, rows, cols).
207    #[cfg(target_os = "macos")]
208    fn mapped(&self) -> Option<(&Arc<CmfModel>, usize, usize, usize)> {
209        let (model, idx) = self.w.q4tp_mapped()?;
210        Some((model, idx, self.w.rows(), self.w.cols()))
211    }
212
213    /// This projection as the resident chain reads it: its container matrix
214    /// (or its dense one) and its bias. `None` with an adapter bound, or for
215    /// a weight the chain cannot read in place.
216    fn chain(&self) -> Option<crate::gpu::LtxLin<'_>> {
217        if self.has_lora() {
218            return None;
219        }
220        let (w, dense): (Option<usize>, &[f32]) = match &self.w {
221            Proj::F32 { w, .. } => (None, w),
222            Proj::Q(q) => (Some(q.mapped_device_gemm()?.1), &[]),
223        };
224        Some(crate::gpu::LtxLin {
225            w,
226            dense,
227            rows: self.w.rows(),
228            cols: self.w.cols(),
229            bias: self.b.as_deref().unwrap_or(&[]),
230        })
231    }
232
233    /// The bias half of `apply`, for callers that got the product elsewhere.
234    pub(crate) fn add_bias(&self, out: &mut [f32], n: usize, pool: Option<&Pool>) {
235        let Some(b) = &self.b else { return };
236        let m = self.w.rows();
237        let dst = Shared(out.as_mut_ptr());
238        rows(pool, n, &|s, e| {
239            let r = unsafe { dst.at(s * m, (e - s) * m) };
240            for row in r.chunks_exact_mut(m) {
241                for (v, &bb) in row.iter_mut().zip(b) {
242                    *v += bb;
243                }
244            }
245        });
246    }
247
248    pub(crate) fn apply(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
249        let m = self.w.rows();
250        let cols = self.w.cols();
251        let t_alloc = std::time::Instant::now();
252        let mut out = vec![0f32; n * m];
253        attn_prof::ALLOC.fetch_add(
254            t_alloc.elapsed().as_micros() as u64,
255            std::sync::atomic::Ordering::Relaxed,
256        );
257        let t_mm = std::time::Instant::now();
258        // With an adapter on this projection, the base GEMM and the branch go
259        // to the device in ONE submission: the branch reads the activation the
260        // base GEMM already uploaded and accumulates into the output it just
261        // wrote, so the adapter costs no transfer. Falls through to the split
262        // path when the device declines the shape.
263        #[cfg(target_os = "macos")]
264        if let (Some(l), Some((model, idx, r, cl))) = (&self.lora, self.mapped()) {
265            // The router measures a branch by comparing it against the base
266            // panel, and this path never separates the two. With routing on,
267            // take the split path so the measurement can happen at all.
268            if l.live()
269                && crate::ltxlora::route_threshold().is_none()
270                && n >= 32
271                && n * r * cl >= 128_000_000
272                && cl % 32 == 0
273                && !crate::gpu::mm_killed()
274                && crate::gpu::enabled_here()
275                && crate::gpu_metal::q4tp_matmat_lora(model, idx, x, n, r, cl, &mut out, &l.side())
276            {
277                attn_prof::MATMAT.fetch_add(
278                    t_mm.elapsed().as_micros() as u64,
279                    std::sync::atomic::Ordering::Relaxed,
280                );
281                self.add_bias(&mut out, n, pool);
282                return out;
283            }
284        }
285        // A GPU binding is capped near 2 GiB, and the prompt encoder's
286        // aggregate projection reads 188160 numbers per token — 1024 of them
287        // at once is past the cap. Chunk the batch so no single dispatch
288        // binds more than a quarter of a gigabyte of activations.
289        let per_row = cols * 4;
290        let chunk = (0x1000_0000usize / per_row.max(1)).max(1);
291        let mut done = 0usize;
292        while done < n {
293            let take = chunk.min(n - done);
294            self.w.matmat(
295                &x[done * cols..(done + take) * cols],
296                take,
297                &mut out[done * m..(done + take) * m],
298                pool,
299            );
300            done += take;
301        }
302        attn_prof::MATMAT.fetch_add(
303            t_mm.elapsed().as_micros() as u64,
304            std::sync::atomic::Ordering::Relaxed,
305        );
306        if let Some(l) = &self.lora {
307            l.add(x, n, &mut out, pool);
308        }
309        if let Some(b) = &self.b {
310            let dst = Shared(out.as_mut_ptr());
311            rows(pool, n, &|s, e| {
312                let r = unsafe { dst.at(s * m, (e - s) * m) };
313                for row in r.chunks_exact_mut(m) {
314                    for (v, &bb) in row.iter_mut().zip(b) {
315                        *v += bb;
316                    }
317                }
318            });
319        }
320        out
321    }
322}
323
324// ------------------------------------------------------------------ rope
325
326/// Split-RoPE tables: `cos`/`sin` laid out as `[tokens, heads·dh/2]`.
327pub struct Rope {
328    cos: Vec<f32>,
329    sin: Vec<f32>,
330    heads: usize,
331    half: usize,
332}
333
334impl Rope {
335    /// `positions[t][d]` are patch midpoints and `max_pos[d]` the axis
336    /// extent they are divided by. `dim` is the *inner* dimension
337    /// (heads·dh) the frequency ladder is sized from.
338    pub fn build(
339        positions: &[Vec<f64>],
340        max_pos: &[f64],
341        dim: usize,
342        heads: usize,
343        theta: f64,
344    ) -> Rope {
345        let ndim = max_pos.len();
346        let count = dim / (2 * ndim);
347        // indices = theta^linspace(0, 1, count) · π/2, in f64
348        let idx: Vec<f64> = (0..count)
349            .map(|j| {
350                let e = if count > 1 {
351                    j as f64 / (count - 1) as f64
352                } else {
353                    0.0
354                };
355                theta.powf(e) * std::f64::consts::PI / 2.0
356            })
357            .collect();
358        let n = positions.len();
359        let half = dim / 2;
360        let pad = half - count * ndim;
361        let mut cos = vec![0f32; n * half];
362        let mut sin = vec![0f32; n * half];
363        for (t, p) in positions.iter().enumerate() {
364            let base = t * half;
365            for i in 0..pad {
366                cos[base + i] = 1.0;
367            }
368            for (j, &ind) in idx.iter().enumerate() {
369                for (d, &mp) in max_pos.iter().enumerate() {
370                    let f = ind * (p[d] / mp * 2.0 - 1.0);
371                    let o = base + pad + j * ndim + d;
372                    cos[o] = f.cos() as f32;
373                    sin[o] = f.sin() as f32;
374                }
375            }
376        }
377        Rope {
378            cos,
379            sin,
380            heads,
381            half: half / heads,
382        }
383    }
384
385    /// In-place split rotation of one token's `[heads·dh]` projection.
386    fn apply_row(&self, t: usize, row: &mut [f32]) {
387        let dh = self.half * 2;
388        let stride = self.heads * self.half;
389        for h in 0..self.heads {
390            let off = t * stride + h * self.half;
391            let (c, s) = (
392                &self.cos[off..off + self.half],
393                &self.sin[off..off + self.half],
394            );
395            let v = &mut row[h * dh..(h + 1) * dh];
396            for i in 0..self.half {
397                let (a, b) = (v[i], v[i + self.half]);
398                v[i] = a * c[i] - b * s[i];
399                v[i + self.half] = b * c[i] + a * s[i];
400            }
401        }
402    }
403}
404
405/// Sub-phase microseconds inside attention, summed across every call in a
406/// step. `CMF_LTX_PROF=1` prints them: the phase timers above say *which*
407/// attention is slow, these say *what part of it* is.
408pub(crate) mod attn_prof {
409    use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
410    pub static PROJ: AtomicU64 = AtomicU64::new(0);
411    pub static NORM: AtomicU64 = AtomicU64::new(0);
412    pub static GATHER: AtomicU64 = AtomicU64::new(0);
413    pub static SCORE: AtomicU64 = AtomicU64::new(0);
414    pub static SOFT: AtomicU64 = AtomicU64::new(0);
415    pub static VALUE: AtomicU64 = AtomicU64::new(0);
416    pub static OUT: AtomicU64 = AtomicU64::new(0);
417    pub static ALLOC: AtomicU64 = AtomicU64::new(0);
418    pub static MATMAT: AtomicU64 = AtomicU64::new(0);
419
420    pub fn add(c: &AtomicU64, t: std::time::Instant) -> std::time::Instant {
421        c.fetch_add(t.elapsed().as_micros() as u64, Relaxed);
422        std::time::Instant::now()
423    }
424
425    pub fn report() -> String {
426        let s = |c: &AtomicU64| c.swap(0, Relaxed) as f64 / 1e6;
427        format!(
428            "proj {:.2}s  qk-norm+rope {:.2}s  gather {:.2}s  scores {:.2}s  softmax {:.2}s  values {:.2}s  out {:.2}s  [linear: alloc {:.2}s  matmat {:.2}s]",
429            s(&PROJ),
430            s(&NORM),
431            s(&GATHER),
432            s(&SCORE),
433            s(&SOFT),
434            s(&VALUE),
435            s(&OUT),
436            s(&ALLOC),
437            s(&MATMAT)
438        )
439    }
440}
441
442// ------------------------------------------------------------- attention
443
444pub(crate) struct Attn {
445    q: Lin,
446    k: Lin,
447    v: Lin,
448    o: Lin,
449    q_norm: Vec<f32>,
450    k_norm: Vec<f32>,
451    gate: Option<Lin>,
452    heads: usize,
453    dh: usize,
454}
455
456impl Attn {
457    pub(crate) fn load(
458        model: &Arc<CmfModel>,
459        p: &str,
460        heads: usize,
461        dh: usize,
462    ) -> Result<Attn, String> {
463        Attn::load_lora(model, p, heads, dh, None)
464    }
465
466    pub(crate) fn load_lora(
467        model: &Arc<CmfModel>,
468        p: &str,
469        heads: usize,
470        dh: usize,
471        bank: Option<&crate::ltxlora::LoraBank>,
472    ) -> Result<Attn, String> {
473        Ok(Attn {
474            q: Lin::load_lora(model, &format!("{p}.to_q"), true, bank)?,
475            k: Lin::load_lora(model, &format!("{p}.to_k"), true, bank)?,
476            v: Lin::load_lora(model, &format!("{p}.to_v"), true, bank)?,
477            o: Lin::load_lora(model, &format!("{p}.to_out.0"), true, bank)?,
478            q_norm: cmf_f32(model, &format!("{p}.q_norm.weight"))?,
479            k_norm: cmf_f32(model, &format!("{p}.k_norm.weight"))?,
480            gate: match model.tensor(&format!("{p}.to_gate_logits.weight")) {
481                Some(_) => Some(Lin::load(model, &format!("{p}.to_gate_logits"), true)?),
482                None => None,
483            },
484            heads,
485            dh,
486        })
487    }
488
489    /// This attention as the resident chain reads it (see `Lin::chain`).
490    fn chain(&self) -> Option<crate::gpu::LtxAttn<'_>> {
491        Some(crate::gpu::LtxAttn {
492            q: self.q.chain()?,
493            k: self.k.chain()?,
494            v: self.v.chain()?,
495            o: self.o.chain()?,
496            q_norm: &self.q_norm,
497            k_norm: &self.k_norm,
498            gate: match &self.gate {
499                Some(g) => Some(g.chain()?),
500                None => None,
501            },
502            heads: self.heads,
503            dh: self.dh,
504        })
505    }
506
507    /// The three projections in one submission, when the platform has a
508    /// batched entry and all three read the same activation. Returns `None`
509    /// when they do not — cross-attention's query comes from the tokens and
510    /// its keys from the prompt — and the caller falls back to three calls.
511    #[cfg(target_os = "macos")]
512    fn fused_qkv(
513        &self,
514        x: &[f32],
515        n: usize,
516        ctx: &[f32],
517        m: usize,
518        pool: Option<&Pool>,
519    ) -> Option<(Vec<f32>, Vec<f32>, Vec<f32>)> {
520        if !std::ptr::eq(x.as_ptr(), ctx.as_ptr()) || n != m {
521            return None;
522        }
523        if !crate::gpu::enabled_here() || crate::gpu::mm_killed() {
524            return None;
525        }
526        // The batched entry carries one weight per job and no branch. When an
527        // adapter is loaded, decline: `apply` then takes the per-projection
528        // path that fuses the base GEMM and the branch into one submission,
529        // which is the cheaper of the two.
530        if self.q.has_lora() || self.k.has_lora() || self.v.has_lora() {
531            return None;
532        }
533        let (qw, kw, vw) = (self.q.mapped()?, self.k.mapped()?, self.v.mapped()?);
534        if !Arc::ptr_eq(qw.0, kw.0) || !Arc::ptr_eq(qw.0, vw.0) {
535            return None;
536        }
537        let jobs = [
538            crate::gpu_metal::MmJob {
539                idx: qw.1,
540                rows: qw.2,
541                cols: qw.3,
542            },
543            crate::gpu_metal::MmJob {
544                idx: kw.1,
545                rows: kw.2,
546                cols: kw.3,
547            },
548            crate::gpu_metal::MmJob {
549                idx: vw.1,
550                rows: vw.2,
551                cols: vw.3,
552            },
553        ];
554        if n * jobs[0].rows * jobs[0].cols < 128_000_000 || n < 32 {
555            return None;
556        }
557        let mut oq = vec![0f32; n * jobs[0].rows];
558        let mut ok = vec![0f32; n * jobs[1].rows];
559        let mut ov = vec![0f32; n * jobs[2].rows];
560        let done = {
561            let mut outs: [&mut [f32]; 3] = [&mut oq, &mut ok, &mut ov];
562            crate::gpu_metal::q4tp_matmat_many(qw.0, &jobs, x, n, &mut outs)
563        };
564        if !done {
565            return None;
566        }
567        self.q.add_lora(&mut oq, x, n, pool);
568        self.k.add_lora(&mut ok, x, n, pool);
569        self.v.add_lora(&mut ov, x, n, pool);
570        self.q.add_bias(&mut oq, n, pool);
571        self.k.add_bias(&mut ok, n, pool);
572        self.v.add_bias(&mut ov, n, pool);
573        Some((oq, ok, ov))
574    }
575
576    /// The key and value projections in one submission — they always read
577    /// the same buffer, whatever the query does.
578    #[cfg(target_os = "macos")]
579    fn fused_kv(&self, ctx: &[f32], m: usize) -> Option<(Vec<f32>, Vec<f32>)> {
580        if !crate::gpu::enabled_here() || crate::gpu::mm_killed() {
581            return None;
582        }
583        if self.k.has_lora() || self.v.has_lora() {
584            return None;
585        }
586        let (kw, vw) = (self.k.mapped()?, self.v.mapped()?);
587        if !Arc::ptr_eq(kw.0, vw.0) {
588            return None;
589        }
590        let jobs = [
591            crate::gpu_metal::MmJob {
592                idx: kw.1,
593                rows: kw.2,
594                cols: kw.3,
595            },
596            crate::gpu_metal::MmJob {
597                idx: vw.1,
598                rows: vw.2,
599                cols: vw.3,
600            },
601        ];
602        if m < 32 || m * jobs[0].rows * jobs[0].cols < 128_000_000 {
603            return None;
604        }
605        let mut ok = vec![0f32; m * jobs[0].rows];
606        let mut ov = vec![0f32; m * jobs[1].rows];
607        let done = {
608            let mut outs: [&mut [f32]; 2] = [&mut ok, &mut ov];
609            crate::gpu_metal::q4tp_matmat_many(kw.0, &jobs, ctx, m, &mut outs)
610        };
611        if !done {
612            return None;
613        }
614        self.k.add_lora(&mut ok, ctx, m, None);
615        self.v.add_lora(&mut ov, ctx, m, None);
616        self.k.add_bias(&mut ok, m, None);
617        self.v.add_bias(&mut ov, m, None);
618        Some((ok, ov))
619    }
620
621    #[cfg(not(target_os = "macos"))]
622    fn fused_kv(&self, _ctx: &[f32], _m: usize) -> Option<(Vec<f32>, Vec<f32>)> {
623        None
624    }
625
626    #[cfg(not(target_os = "macos"))]
627    fn fused_qkv(
628        &self,
629        _x: &[f32],
630        _n: usize,
631        _ctx: &[f32],
632        _m: usize,
633        _pool: Option<&Pool>,
634    ) -> Option<(Vec<f32>, Vec<f32>, Vec<f32>)> {
635        None
636    }
637
638    /// `x` is `[n, query_dim]`, `ctx` is `[m, context_dim]` (self-attention
639    /// passes the same buffer twice). `mask` is an additive per-key bias.
640    #[allow(clippy::too_many_arguments)]
641    pub(crate) fn forward(
642        &self,
643        x: &[f32],
644        n: usize,
645        ctx: &[f32],
646        m: usize,
647        pe_q: Option<&Rope>,
648        pe_k: Option<&Rope>,
649        mask: Option<&[f32]>,
650        pool: Option<&Pool>,
651    ) -> Vec<f32> {
652        let inner = self.heads * self.dh;
653        let prof = std::env::var("CMF_LTX_PROF").is_ok();
654        let mut t = std::time::Instant::now();
655        // q, k and v do not depend on each other. When they also read the
656        // same buffer — self-attention, and the A↔V pair on the context
657        // side — they go to the device as one command buffer instead of
658        // three, which is three times less of the ~1.3 ms a completion
659        // costs whatever it contains.
660        let (mut q, mut k, v) = match self.fused_qkv(x, n, ctx, m, pool) {
661            Some(t) => t,
662            // Cross-attention's query reads the tokens and its keys read the
663            // prompt, so those two cannot share a submission — but the keys
664            // and the values still can.
665            None => match self.fused_kv(ctx, m) {
666                Some((k, v)) => (self.q.apply(x, n, pool), k, v),
667                None => (
668                    self.q.apply(x, n, pool),
669                    self.k.apply(ctx, m, pool),
670                    self.v.apply(ctx, m, pool),
671                ),
672            },
673        };
674        if prof {
675            t = attn_prof::add(&attn_prof::PROJ, t);
676        }
677
678        let qn = Shared(q.as_mut_ptr());
679        rows(pool, n, &|s, e| {
680            let r = unsafe { qn.at(s * inner, (e - s) * inner) };
681            for (i, row) in r.chunks_exact_mut(inner).enumerate() {
682                rms_w(row, &self.q_norm);
683                if let Some(pe) = pe_q {
684                    pe.apply_row(s + i, row);
685                }
686            }
687        });
688        let kn = Shared(k.as_mut_ptr());
689        rows(pool, m, &|s, e| {
690            let r = unsafe { kn.at(s * inner, (e - s) * inner) };
691            for (i, row) in r.chunks_exact_mut(inner).enumerate() {
692                rms_w(row, &self.k_norm);
693                if let Some(pe) = pe_k {
694                    pe.apply_row(s + i, row);
695                }
696            }
697        });
698
699        // Per head, both halves of attention are GEMMs: scores are
700        // q·kᵀ and the value product is p·v. Gathering each head into a
701        // contiguous `[tokens, dh]` block costs one copy and buys the
702        // engine's blocked/BLAS/GPU kernels instead of a scalar loop —
703        // this is most of a step's arithmetic.
704        if prof {
705            t = attn_prof::add(&attn_prof::NORM, t);
706        }
707        let mut out = vec![0f32; n * inner];
708        let scale = 1.0 / (self.dh as f32).sqrt();
709        let dh = self.dh;
710        let mut qh = vec![0f32; n * dh];
711        let mut kh = vec![0f32; m * dh];
712        let mut vh = vec![0f32; m * dh];
713        let mut sc = vec![0f32; n * m];
714        let mut oh = vec![0f32; n * dh];
715        // PV and QK cost the same flops, and PV takes twice the clock:
716        // measured on this stand at 512x512x49, per denoise step, scores
717        // 6.03 s against values 12.13 s (three steps, 6.03-6.54 and
718        // 12.13-12.71). The reason is which cooperative kernel each lands
719        // on -- `gemm_nt` picks `gemm_nt_coop`, whose right operand is read
720        // along rows, while `gemm_dx` picks `gemm_nn_coop` and reads v down
721        // columns of its [tokens, dh] block. Both are on the matrix units;
722        // the difference is the access pattern, not the acceleration.
723        //
724        // The image DiT already solved exactly this (`gpu_wgpu.rs:23664`:
725        // "transpose it once per block and PV becomes the NT product QK
726        // already is", 4.76 -> 2.91 s a step, frames within 0.1%). Here the
727        // transpose is free: v is gathered per head anyway, so gathering it
728        // as [dh, tokens] is the same count of writes to different offsets.
729        //
730        // OFF by default until measured on THIS path. The image DiT's
731        // number is its own; quoting it as the expected win here would be
732        // the same mistake as taking a benchmark out of a docstring.
733        // `CMF_LTX_PV_NT=1` turns it on.
734        let pv_nt = std::env::var("CMF_LTX_PV_NT").as_deref() == Ok("1");
735        for h in 0..self.heads {
736            for i in 0..n {
737                qh[i * dh..(i + 1) * dh].copy_from_slice(&q[i * inner + h * dh..][..dh]);
738            }
739            for j in 0..m {
740                kh[j * dh..(j + 1) * dh].copy_from_slice(&k[j * inner + h * dh..][..dh]);
741            }
742            if pv_nt {
743                // vh holds vᵀ: [dh, tokens], so PV is an NT product.
744                for j in 0..m {
745                    let src = &v[j * inner + h * dh..][..dh];
746                    for (d, s) in src.iter().enumerate() {
747                        vh[d * m + j] = *s;
748                    }
749                }
750            } else {
751                for j in 0..m {
752                    vh[j * dh..(j + 1) * dh].copy_from_slice(&v[j * inner + h * dh..][..dh]);
753                }
754            }
755            if prof {
756                t = attn_prof::add(&attn_prof::GATHER, t);
757            }
758            crate::fcd_ops::gemm_nt(&qh, &kh, &mut sc, n, dh, m, pool);
759            if prof {
760                t = attn_prof::add(&attn_prof::SCORE, t);
761            }
762            let sp = Shared(sc.as_mut_ptr());
763            rows(pool, n, &|s, e| {
764                let r = unsafe { sp.at(s * m, (e - s) * m) };
765                for row in r.chunks_exact_mut(m) {
766                    for (x, j) in row.iter_mut().zip(0..m) {
767                        *x = *x * scale + mask.map_or(0.0, |mk| mk[j]);
768                    }
769                    softmax(row);
770                }
771            });
772            if prof {
773                t = attn_prof::add(&attn_prof::SOFT, t);
774            }
775            oh.iter_mut().for_each(|x| *x = 0.0);
776            if pv_nt {
777                // oh[n, dh] = sc[n, m] · (vᵀ[dh, m])ᵀ  -- gemm_nt's (n, k, m)
778                // is (tokens_out, reduction, width), so k = m and m = dh here.
779                crate::fcd_ops::gemm_nt(&sc, &vh, &mut oh, n, m, dh, pool);
780            } else {
781                crate::fcd_ops::gemm_dx(&sc, &vh, &mut oh, n, dh, m, pool);
782            }
783            if prof {
784                t = attn_prof::add(&attn_prof::VALUE, t);
785            }
786            for i in 0..n {
787                out[i * inner + h * dh..i * inner + (h + 1) * dh]
788                    .copy_from_slice(&oh[i * dh..(i + 1) * dh]);
789            }
790            if prof {
791                t = attn_prof::add(&attn_prof::GATHER, t);
792            }
793        }
794
795        if let Some(g) = &self.gate {
796            let logits = g.apply(x, n, pool);
797            let h = self.heads;
798            let dst = Shared(out.as_mut_ptr());
799            rows(pool, n, &|s, e| {
800                let r = unsafe { dst.at(s * inner, (e - s) * inner) };
801                for (i, row) in r.chunks_exact_mut(inner).enumerate() {
802                    for hh in 0..h {
803                        let gate = 2.0 / (1.0 + (-logits[(s + i) * h + hh]).exp());
804                        for d in row[hh * self.dh..(hh + 1) * self.dh].iter_mut() {
805                            *d *= gate;
806                        }
807                    }
808                }
809            });
810        }
811        let r = self.o.apply(&out, n, pool);
812        if prof {
813            attn_prof::add(&attn_prof::OUT, t);
814        }
815        r
816    }
817}
818
819// ---------------------------------------------------------- adaLN single
820
821/// `AdaLayerNormSingle`: sinusoidal timestep → SiLU MLP → `coeff·dim`
822/// modulation values, plus the embedding the output head reuses.
823struct AdaLn {
824    l1: Lin,
825    l2: Lin,
826    lin: Lin,
827    dim: usize,
828}
829
830impl AdaLn {
831    fn load(model: &Arc<CmfModel>, p: &str, dim: usize) -> Result<AdaLn, String> {
832        Ok(AdaLn {
833            l1: Lin::load(model, &format!("{p}.emb.timestep_embedder.linear_1"), true)?,
834            l2: Lin::load(model, &format!("{p}.emb.timestep_embedder.linear_2"), true)?,
835            lin: Lin::load(model, &format!("{p}.linear"), true)?,
836            dim,
837        })
838    }
839
840    /// `(values [n, coeff·dim], embedded [n, dim])` for `n` timesteps.
841    fn forward(&self, t: &[f32], pool: Option<&Pool>) -> (Vec<f32>, Vec<f32>) {
842        let n = t.len();
843        // get_timestep_embedding(256, flip_sin_to_cos=True, shift=0): the
844        // flip puts cosine first, so the halves are [cos, sin].
845        let half = 128usize;
846        let mut proj = vec![0f32; n * 256];
847        let ws: Vec<f64> = (0..half)
848            .map(|j| (-(10000f64).ln() * j as f64 / half as f64).exp())
849            .collect();
850        for (i, &tv) in t.iter().enumerate() {
851            for (j, &w) in ws.iter().enumerate() {
852                let a = tv as f64 * w;
853                proj[i * 256 + j] = a.cos() as f32;
854                proj[i * 256 + half + j] = a.sin() as f32;
855            }
856        }
857        let mut h = self.l1.apply(&proj, n, pool);
858        for v in h.iter_mut() {
859            *v = silu(*v);
860        }
861        let embedded = self.l2.apply(&h, n, pool);
862        let mut act = embedded.clone();
863        for v in act.iter_mut() {
864            *v = silu(*v);
865        }
866        (self.lin.apply(&act, n, pool), embedded)
867    }
868}
869
870/// adaLN values for the *distinct* timesteps of a stream, plus each token's
871/// index into them. Per-token timesteps take only a handful of values here
872/// (a conditioning token sits at 0 while the rest sit at the current
873/// sigma), so the `[36864, 4096]` projection runs a few times, not `T`.
874struct TsTable {
875    vals: Vec<f32>,
876    emb: Vec<f32>,
877    idx: Vec<usize>,
878    width: usize,
879    edim: usize,
880}
881
882impl TsTable {
883    fn build(a: &AdaLn, ts: &[f32], scale: f64, pool: Option<&Pool>) -> TsTable {
884        let mut vals: Vec<f32> = Vec::new();
885        let mut idx = Vec::with_capacity(ts.len());
886        for &t in ts {
887            match vals.iter().position(|&v| v.to_bits() == t.to_bits()) {
888                Some(i) => idx.push(i),
889                None => {
890                    vals.push(t);
891                    idx.push(vals.len() - 1);
892                }
893            }
894        }
895        let scaled: Vec<f32> = vals.iter().map(|&v| (v as f64 * scale) as f32).collect();
896        let (v, e) = a.forward(&scaled, pool);
897        let width = v.len() / scaled.len().max(1);
898        TsTable {
899            vals: v,
900            emb: e,
901            idx,
902            width,
903            edim: a.dim,
904        }
905    }
906
907    fn distinct(&self) -> usize {
908        self.vals.len() / self.width.max(1)
909    }
910
911    fn row(&self, r: usize) -> &[f32] {
912        &self.vals[r * self.width..(r + 1) * self.width]
913    }
914
915    fn emb_row(&self, r: usize) -> &[f32] {
916        &self.emb[r * self.edim..(r + 1) * self.edim]
917    }
918
919    /// `(shift, scale, gate)` per distinct timestep for the adaLN triple at
920    /// table row `off`: the block's static table plus the timestep's own
921    /// contribution, summed once instead of once per token.
922    fn triples(&self, table: &[f32], dim: usize, off: usize) -> Vec<[Vec<f32>; 3]> {
923        (0..self.distinct())
924            .map(|r| {
925                let v = self.row(r);
926                std::array::from_fn(|j| {
927                    let o = (off + j) * dim;
928                    (0..dim).map(|d| table[o + d] + v[o + d]).collect()
929                })
930            })
931            .collect()
932    }
933
934    /// `(scale, shift)` per distinct timestep for an A↔V table, which — unlike
935    /// the self-attention rows — comes out scale-first.
936    fn pairs(&self, table: &[f32], dim: usize, off: usize) -> Vec<[Vec<f32>; 2]> {
937        (0..self.distinct())
938            .map(|r| {
939                let v = self.row(r);
940                std::array::from_fn(|j| {
941                    let o = (off + j) * dim;
942                    (0..dim).map(|d| table[o + d] + v[o + d]).collect()
943                })
944            })
945            .collect()
946    }
947}
948
949/// `rms_norm(x) · (1 + scale) + shift` over every token, in parallel.
950/// `mods[r]` is the `(shift, scale, gate)` triple of the r-th distinct
951/// timestep and `idx[t]` says which one token `t` uses.
952fn ada_zero_rows(
953    x: &[f32],
954    out: &mut [f32],
955    n: usize,
956    dim: usize,
957    mods: &[[Vec<f32>; 3]],
958    idx: &[usize],
959    pool: Option<&Pool>,
960) {
961    let dst = Shared(out.as_mut_ptr());
962    rows(pool, n, &|s, e| {
963        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
964        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
965            let md = &mods[idx[i]];
966            rms_plain(&x[i * dim..(i + 1) * dim], row);
967            for d in 0..dim {
968                row[d] = row[d] * (1.0 + md[1][d]) + md[0][d];
969            }
970        }
971    });
972}
973
974/// `x += y · gate`, the residual every sub-layer writes back through.
975fn add_gated(
976    x: &mut [f32],
977    y: &[f32],
978    n: usize,
979    dim: usize,
980    mods: &[[Vec<f32>; 3]],
981    idx: &[usize],
982    pool: Option<&Pool>,
983) {
984    let dst = Shared(x.as_mut_ptr());
985    rows(pool, n, &|s, e| {
986        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
987        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
988            let g = &mods[idx[i]][2];
989            for d in 0..dim {
990                row[d] += y[i * dim + d] * g[d];
991            }
992        }
993    });
994}
995
996/// The post-SA pair: fold the gated update into the residual and hand the
997/// re-normalized result to cross-attention, in one pass over the tokens.
998fn post_sa_rows(
999    x: &mut [f32],
1000    y: &[f32],
1001    normed: &mut [f32],
1002    n: usize,
1003    dim: usize,
1004    mods: &[[Vec<f32>; 3]],
1005    idx: &[usize],
1006    pool: Option<&Pool>,
1007) {
1008    let a = Shared(x.as_mut_ptr());
1009    let b = Shared(normed.as_mut_ptr());
1010    rows(pool, n, &|s, e| {
1011        let xr = unsafe { a.at(s * dim, (e - s) * dim) };
1012        let nr = unsafe { b.at(s * dim, (e - s) * dim) };
1013        for ((row, nrow), i) in xr
1014            .chunks_exact_mut(dim)
1015            .zip(nr.chunks_exact_mut(dim))
1016            .zip(s..e)
1017        {
1018            let g = &mods[idx[i]][2];
1019            for d in 0..dim {
1020                row[d] += y[i * dim + d] * g[d];
1021            }
1022            rms_plain(row, nrow);
1023        }
1024    });
1025}
1026
1027/// `x += y · g` with one shared per-channel gate (the A↔V fusion).
1028fn add_scaled(x: &mut [f32], y: &[f32], n: usize, dim: usize, g: &[f32], pool: Option<&Pool>) {
1029    let dst = Shared(x.as_mut_ptr());
1030    rows(pool, n, &|s, e| {
1031        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1032        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1033            for d in 0..dim {
1034                row[d] += y[i * dim + d] * g[d];
1035            }
1036        }
1037    });
1038}
1039
1040/// The cross-attention query: an affine on an already-normalized row.
1041fn affine_rows(
1042    x: &[f32],
1043    out: &mut [f32],
1044    n: usize,
1045    dim: usize,
1046    mods: &[[Vec<f32>; 3]],
1047    idx: &[usize],
1048    pool: Option<&Pool>,
1049) {
1050    let dst = Shared(out.as_mut_ptr());
1051    rows(pool, n, &|s, e| {
1052        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1053        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1054            let md = &mods[idx[i]];
1055            for d in 0..dim {
1056                row[d] = x[i * dim + d] * (1.0 + md[1][d]) + md[0][d];
1057            }
1058        }
1059    });
1060}
1061
1062/// Where a denoising step actually goes. `CMF_LTX_PROF=1` prints the split
1063/// once per forward: guessing at this is how ports stay slow.
1064#[derive(Default)]
1065struct Prof {
1066    on: bool,
1067    t: [f64; 6],
1068}
1069
1070const P_ADALN: usize = 0;
1071const P_SELF: usize = 1;
1072const P_CROSS: usize = 2;
1073const P_FUSE: usize = 3;
1074const P_FF: usize = 4;
1075const P_MOD: usize = 5;
1076
1077impl Prof {
1078    fn new() -> Prof {
1079        Prof {
1080            on: std::env::var("CMF_LTX_PROF").is_ok(),
1081            t: [0.0; 6],
1082        }
1083    }
1084    #[inline]
1085    fn tick(&mut self, slot: usize, at: std::time::Instant) -> std::time::Instant {
1086        if self.on {
1087            self.t[slot] += at.elapsed().as_secs_f64();
1088            return std::time::Instant::now();
1089        }
1090        at
1091    }
1092    fn report(&self) {
1093        if !self.on {
1094            return;
1095        }
1096        // The q4tp GEMM's own split, when Metal is keeping it: a copy-bound
1097        // step wants fused blocks, a kernel-bound one wants a better kernel.
1098        #[cfg(target_os = "macos")]
1099        {
1100            use std::sync::atomic::Ordering::Relaxed;
1101            let n = crate::gpu_metal::MM_N.swap(0, Relaxed);
1102            if n > 0 {
1103                let us = |a: &std::sync::atomic::AtomicU64| a.swap(0, Relaxed) as f64 / 1e6;
1104                println!(
1105                    "  q4tp on device: {n} calls, upload {:.2}s  kernel {:.2}s  download {:.2}s",
1106                    us(&crate::gpu_metal::MM_UP),
1107                    us(&crate::gpu_metal::MM_GPU),
1108                    us(&crate::gpu_metal::MM_DN),
1109                );
1110            }
1111        }
1112        println!("  attention: {}", attn_prof::report());
1113        let names = [
1114            "adaln",
1115            "self-attn",
1116            "cross-attn",
1117            "a<->v",
1118            "ffn",
1119            "modulate",
1120        ];
1121        let total: f64 = self.t.iter().sum();
1122        let parts: Vec<String> = names
1123            .iter()
1124            .zip(&self.t)
1125            .map(|(n, v)| format!("{n} {v:.1}s ({:.0}%)", 100.0 * v / total.max(1e-9)))
1126            .collect();
1127        println!("  profile: {}", parts.join("  "));
1128    }
1129}
1130
1131// ----------------------------------------------------------------- block
1132
1133struct Stream {
1134    attn1: Attn,
1135    attn2: Attn,
1136    ff_in: Lin,
1137    ff_out: Lin,
1138    sst: Vec<f32>,        // [9, dim]
1139    prompt_sst: Vec<f32>, // [2, dim]
1140}
1141
1142impl Stream {
1143    fn load(
1144        model: &Arc<CmfModel>,
1145        p: &str,
1146        prefix: &str,
1147        heads: usize,
1148        dh: usize,
1149        ff_bias: bool,
1150        bank: Option<&crate::ltxlora::LoraBank>,
1151    ) -> Result<Stream, String> {
1152        let a = |n: &str| format!("{p}.{prefix}{n}");
1153        Ok(Stream {
1154            attn1: Attn::load_lora(model, &a("attn1"), heads, dh, bank)?,
1155            attn2: Attn::load_lora(model, &a("attn2"), heads, dh, bank)?,
1156            ff_in: Lin::load_lora(model, &a("ff.net.0.proj"), ff_bias, bank)?,
1157            ff_out: Lin::load_lora(model, &a("ff.net.2"), ff_bias, bank)?,
1158            sst: cmf_f32(model, &a("scale_shift_table"))?,
1159            prompt_sst: cmf_f32(model, &a("prompt_scale_shift_table"))?,
1160        })
1161    }
1162
1163    fn ff(&self, x: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
1164        let mut h = self.ff_in.apply(x, n, pool);
1165        gelu_tanh_rows(&mut h, pool);
1166        self.ff_out.apply(&h, n, pool)
1167    }
1168}
1169
1170struct Block {
1171    video: Stream,
1172    audio: Stream,
1173    a2v: Attn,
1174    v2a: Attn,
1175    sst_a2v_video: Vec<f32>, // [5, 4096]
1176    sst_a2v_audio: Vec<f32>, // [5, 2048]
1177}
1178
1179// ----------------------------------------------------------------- model
1180
1181/// One modality's per-step conditioning: everything the blocks read that is
1182/// not a weight.
1183pub struct StreamInput {
1184    /// Patchified latent, `[tokens, in_channels]`.
1185    pub latent: Vec<f32>,
1186    pub tokens: usize,
1187    /// Per-token timestep (the sigma·denoising-mask product).
1188    pub timesteps: Vec<f32>,
1189    /// Per-token patch midpoints, one entry per RoPE axis.
1190    pub positions: Vec<Vec<f64>>,
1191    /// Prompt embeddings out of the connector, `[ctx_len, cross_dim]`.
1192    pub context: Vec<f32>,
1193    pub ctx_len: usize,
1194    /// Additive per-key prompt mask, or empty for "attend to all".
1195    pub context_mask: Vec<f32>,
1196    /// Non-zero for tokens holding a standalone pixel frame (video only).
1197    pub keyframes: Vec<f32>,
1198    /// This stream's sigma — the *other* stream's fusion gate reads it.
1199    pub sigma: f32,
1200}
1201
1202pub struct LtxDit {
1203    model: Arc<CmfModel>,
1204    blocks: Vec<Block>,
1205    patchify: Lin,
1206    a_patchify: Lin,
1207    keyframes_emb: Option<Vec<f32>>,
1208    adaln: AdaLn,
1209    a_adaln: AdaLn,
1210    prompt_adaln: AdaLn,
1211    a_prompt_adaln: AdaLn,
1212    av_v_ss: AdaLn,
1213    av_a_ss: AdaLn,
1214    av_a2v_gate: AdaLn,
1215    av_v2a_gate: AdaLn,
1216    proj_out: Lin,
1217    a_proj_out: Lin,
1218    sst_out: Vec<f32>,
1219    a_sst_out: Vec<f32>,
1220    pub heads: usize,
1221    pub dh: usize,
1222    pub a_heads: usize,
1223    pub a_dh: usize,
1224    pub max_pos: Vec<f64>,
1225    pub a_max_pos: Vec<f64>,
1226    pub cross_max_pos: f64,
1227    pub theta: f64,
1228    pub t_scale: f64,
1229    pub av_t_scale: f64,
1230    pub audio_cross_dim: usize,
1231}
1232
1233impl LtxDit {
1234    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<LtxDit, String> {
1235        LtxDit::from_cmf_lora(model, None)
1236    }
1237
1238    /// The same, with a runtime adapter bound to whichever projections it
1239    /// names. Reported once at load so a file that matched nothing says so
1240    /// instead of rendering as if it had.
1241    pub fn from_cmf_lora(
1242        model: &Arc<CmfModel>,
1243        bank: Option<&crate::ltxlora::LoraBank>,
1244    ) -> Result<LtxDit, String> {
1245        let cfg_bytes = ["ltx.config_json", "dit.config_json"]
1246            .iter()
1247            .find_map(|n| model.tensor(n).map(|e| model.entry_bytes(e)))
1248            .ok_or("container carries no ltx.config_json")?;
1249        let cfg: serde_json::Value =
1250            serde_json::from_slice(cfg_bytes).map_err(|e| format!("ltx.config_json: {e}"))?;
1251        let t = cfg.get("transformer").unwrap_or(&cfg).clone();
1252        let g = |k: &str, d: f64| t.get(k).and_then(|v| v.as_f64()).unwrap_or(d);
1253        let heads = g("num_attention_heads", 32.0) as usize;
1254        let dh = g("attention_head_dim", 128.0) as usize;
1255        let a_heads = g("audio_num_attention_heads", 32.0) as usize;
1256        let a_dh = g("audio_attention_head_dim", 64.0) as usize;
1257        let n_layers = g("num_layers", 48.0) as usize;
1258        let ff_bias = t.get("ff_bias").and_then(|v| v.as_bool()).unwrap_or(true);
1259        let a_ff_bias = t
1260            .get("audio_ff_bias")
1261            .and_then(|v| v.as_bool())
1262            .unwrap_or(true);
1263        let arr = |k: &str, d: Vec<f64>| -> Vec<f64> {
1264            t.get(k)
1265                .and_then(|v| v.as_array())
1266                .map(|a| a.iter().filter_map(|x| x.as_f64()).collect())
1267                .unwrap_or(d)
1268        };
1269        let max_pos = arr("positional_embedding_max_pos", vec![20.0, 2048.0, 2048.0]);
1270        let a_max_pos = arr("audio_positional_embedding_max_pos", vec![20.0]);
1271        let cross_max_pos = max_pos[0].max(a_max_pos[0]);
1272        let dim = heads * dh;
1273        let a_dim = a_heads * a_dh;
1274
1275        let mut blocks = Vec::with_capacity(n_layers);
1276        for i in 0..n_layers {
1277            let p = format!("dit.transformer_blocks.{i}");
1278            blocks.push(Block {
1279                video: Stream::load(model, &p, "", heads, dh, ff_bias, bank)?,
1280                audio: Stream::load(model, &p, "audio_", a_heads, a_dh, a_ff_bias, bank)?,
1281                a2v: Attn::load(model, &format!("{p}.audio_to_video_attn"), a_heads, a_dh)?,
1282                v2a: Attn::load(model, &format!("{p}.video_to_audio_attn"), a_heads, a_dh)?,
1283                sst_a2v_video: cmf_f32(model, &format!("{p}.scale_shift_table_a2v_ca_video"))?,
1284                sst_a2v_audio: cmf_f32(model, &format!("{p}.scale_shift_table_a2v_ca_audio"))?,
1285            });
1286        }
1287        let _ = (dim, a_dim);
1288        Ok(LtxDit {
1289            blocks,
1290            patchify: Lin::load(model, "dit.patchify_proj", true)?,
1291            a_patchify: Lin::load(model, "dit.audio_patchify_proj", true)?,
1292            keyframes_emb: match model.tensor("dit.keyframes_abs_pos_embedding") {
1293                Some(_) => Some(cmf_f32(model, "dit.keyframes_abs_pos_embedding")?),
1294                None => None,
1295            },
1296            adaln: AdaLn::load(model, "dit.adaln_single", dim)?,
1297            a_adaln: AdaLn::load(model, "dit.audio_adaln_single", a_dim)?,
1298            prompt_adaln: AdaLn::load(model, "dit.prompt_adaln_single", dim)?,
1299            a_prompt_adaln: AdaLn::load(model, "dit.audio_prompt_adaln_single", a_dim)?,
1300            av_v_ss: AdaLn::load(model, "dit.av_ca_video_scale_shift_adaln_single", dim)?,
1301            av_a_ss: AdaLn::load(model, "dit.av_ca_audio_scale_shift_adaln_single", a_dim)?,
1302            av_a2v_gate: AdaLn::load(model, "dit.av_ca_a2v_gate_adaln_single", dim)?,
1303            av_v2a_gate: AdaLn::load(model, "dit.av_ca_v2a_gate_adaln_single", a_dim)?,
1304            proj_out: Lin::load(model, "dit.proj_out", true)?,
1305            a_proj_out: Lin::load(model, "dit.audio_proj_out", true)?,
1306            sst_out: cmf_f32(model, "dit.scale_shift_table")?,
1307            a_sst_out: cmf_f32(model, "dit.audio_scale_shift_table")?,
1308            heads,
1309            dh,
1310            a_heads,
1311            a_dh,
1312            max_pos,
1313            a_max_pos,
1314            cross_max_pos,
1315            theta: g("positional_embedding_theta", 10000.0),
1316            t_scale: g("timestep_scale_multiplier", 1000.0),
1317            av_t_scale: g("av_ca_timestep_scale_multiplier", 1.0),
1318            audio_cross_dim: g("audio_cross_attention_dim", 2048.0) as usize,
1319            model: model.clone(),
1320        })
1321    }
1322
1323    pub fn blocks(&self) -> usize {
1324        self.blocks.len()
1325    }
1326
1327    pub fn container(&self) -> &Arc<CmfModel> {
1328        &self.model
1329    }
1330
1331    /// One denoising step: `(video velocity [T, C], audio velocity [T, C])`.
1332    /// The blocks run as one resident chain where the backend has one
1333    /// (`CMF_LTX_CHAIN=0` keeps the per-operation path).
1334    pub fn forward(
1335        &self,
1336        video: &StreamInput,
1337        audio: &StreamInput,
1338        pool: Option<&Pool>,
1339    ) -> (Vec<f32>, Vec<f32>) {
1340        self.forward_impl(video, audio, pool, &mut |_, _| {}, true)
1341    }
1342
1343    /// The blocks over both streams as one resident chain on the card, when
1344    /// the backend has one and every projection can ride it. `false`:
1345    /// nothing was written.
1346    fn run_chain(
1347        &self,
1348        video: &StreamInput,
1349        audio: &StreamInput,
1350        vx: &mut [f32],
1351        ax: &mut [f32],
1352        ts: [&TsTable; 8],
1353        pes: [&Rope; 4],
1354    ) -> bool {
1355        use crate::gpu::{LtxBlock, LtxChain, LtxSide, LtxStream};
1356        let [vt, at, vpt, apt, vxs, axs, vgt, agt] = ts;
1357        let [v_pe, a_pe, v_xpe, a_xpe] = pes;
1358        let (dim, a_dim) = (self.heads * self.dh, self.a_heads * self.a_dh);
1359        // A block's table plus each distinct timestep's row, over `len`
1360        // values; `None` when either is shorter than that.
1361        let sum = |t: &TsTable, table: &[f32], len: usize, all: bool| -> Option<Vec<f32>> {
1362            if table.len() < len || t.width < len || t.distinct() == 0 {
1363                return None;
1364            }
1365            let rows = if all { t.distinct() } else { 1 };
1366            let mut o = Vec::with_capacity(rows * len);
1367            for r in 0..rows {
1368                o.extend(table[..len].iter().zip(&t.row(r)[..len]).map(|(a, b)| a + b));
1369            }
1370            Some(o)
1371        };
1372        struct Tabs {
1373            v: Vec<f32>,
1374            a: Vec<f32>,
1375            vc: Vec<f32>,
1376            ac: Vec<f32>,
1377            xv: Vec<f32>,
1378            xa: Vec<f32>,
1379            ga: Vec<f32>,
1380            gv: Vec<f32>,
1381        }
1382        let mut tabs: Vec<Tabs> = Vec::with_capacity(self.blocks.len());
1383        for b in &self.blocks {
1384            if b.sst_a2v_video.len() < 5 * dim || b.sst_a2v_audio.len() < 5 * a_dim {
1385                return false;
1386            }
1387            let (Some(v), Some(a), Some(vc), Some(ac), Some(xv), Some(xa), Some(ga), Some(gv)) = (
1388                sum(vt, &b.video.sst, 9 * dim, true),
1389                sum(at, &b.audio.sst, 9 * a_dim, true),
1390                sum(vpt, &b.video.prompt_sst, 2 * dim, false),
1391                sum(apt, &b.audio.prompt_sst, 2 * a_dim, false),
1392                sum(vxs, &b.sst_a2v_video, 4 * dim, true),
1393                sum(axs, &b.sst_a2v_audio, 4 * a_dim, true),
1394                sum(vgt, &b.sst_a2v_video[4 * dim..], dim, false),
1395                sum(agt, &b.sst_a2v_audio[4 * a_dim..], a_dim, false),
1396            ) else {
1397                return false;
1398            };
1399            tabs.push(Tabs { v, a, vc, ac, xv, xa, ga, gv });
1400        }
1401        fn stream<'a>(s: &'a Stream, mods: &'a [f32], ctx_mod: &'a [f32]) -> Option<LtxStream<'a>> {
1402            Some(LtxStream {
1403                attn1: s.attn1.chain()?,
1404                attn2: s.attn2.chain()?,
1405                ff_in: s.ff_in.chain()?,
1406                ff_out: s.ff_out.chain()?,
1407                mods,
1408                ctx_mod,
1409            })
1410        }
1411        let mut blocks = Vec::with_capacity(self.blocks.len());
1412        for (b, t) in self.blocks.iter().zip(&tabs) {
1413            let (Some(v), Some(a), Some(a2v), Some(v2a)) =
1414                (stream(&b.video, &t.v, &t.vc), stream(&b.audio, &t.a, &t.ac), b.a2v.chain(), b.v2a.chain())
1415            else {
1416                return false;
1417            };
1418            blocks.push(LtxBlock {
1419                video: v,
1420                audio: a,
1421                a2v,
1422                v2a,
1423                xmods_v: &t.xv,
1424                xmods_a: &t.xa,
1425                gate_a2v: &t.ga,
1426                gate_v2a: &t.gv,
1427            });
1428        }
1429        let u32s = |i: &[usize]| i.iter().map(|&v| v as u32).collect::<Vec<u32>>();
1430        let (vr, vxr, ar, axr) = (u32s(&vt.idx), u32s(&vxs.idx), u32s(&at.idx), u32s(&axs.idx));
1431        fn side<'a>(
1432            s: &'a StreamInput,
1433            dim: usize,
1434            rows: &'a [u32],
1435            xrows: &'a [u32],
1436            pe: &'a Rope,
1437            xpe: &'a Rope,
1438        ) -> LtxSide<'a> {
1439            LtxSide {
1440                n: s.tokens,
1441                dim,
1442                rows,
1443                xrows,
1444                pe: (&pe.cos, &pe.sin, pe.half),
1445                xpe: (&xpe.cos, &xpe.sin, xpe.half),
1446                ctx: &s.context,
1447                ctx_len: s.ctx_len,
1448                mask: &s.context_mask,
1449            }
1450        }
1451        let job = LtxChain {
1452            model: &self.model,
1453            blocks: &blocks,
1454            video: side(video, dim, &vr, &vxr, v_pe, v_xpe),
1455            audio: side(audio, a_dim, &ar, &axr, a_pe, a_xpe),
1456        };
1457        crate::gpu::ltx_chain(&job, vx, ax)
1458    }
1459
1460    pub fn forward_traced(
1461        &self,
1462        video: &StreamInput,
1463        audio: &StreamInput,
1464        pool: Option<&Pool>,
1465        trace: &mut dyn FnMut(&str, &[f32]),
1466    ) -> (Vec<f32>, Vec<f32>) {
1467        // a trace reads the streams between blocks: the per-operation path
1468        self.forward_impl(video, audio, pool, trace, false)
1469    }
1470
1471    fn forward_impl(
1472        &self,
1473        video: &StreamInput,
1474        audio: &StreamInput,
1475        pool: Option<&Pool>,
1476        trace: &mut dyn FnMut(&str, &[f32]),
1477        chain_ok: bool,
1478    ) -> (Vec<f32>, Vec<f32>) {
1479        // A denoising step is the opposite of what the per-op probe is built
1480        // for: forty-eight identical blocks, the same shapes every time, the
1481        // device warm throughout. Take the probe out of it.
1482        let _trust = crate::gpu::trust_gpu();
1483        let dim = self.heads * self.dh;
1484        let a_dim = self.a_heads * self.a_dh;
1485        let (n, m) = (video.tokens, audio.tokens);
1486
1487        // --- patchify -----------------------------------------------------
1488        let mut vx = self.patchify.apply(&video.latent, n, pool);
1489        if let Some(emb) = &self.keyframes_emb {
1490            for i in 0..n {
1491                if video.keyframes.get(i).copied().unwrap_or(0.0) > 0.0 {
1492                    for (d, &e) in vx[i * dim..(i + 1) * dim].iter_mut().zip(emb) {
1493                        *d += e;
1494                    }
1495                }
1496            }
1497        }
1498        let mut ax = self.a_patchify.apply(&audio.latent, m, pool);
1499        trace("v.args.x", &vx);
1500        trace("a.args.x", &ax);
1501
1502        // --- adaLN tables, one row per distinct timestep -------------------
1503        // Where the blocks will run as a resident chain, the tables are a few
1504        // rows each and their exact weights (a gigabyte in f32) have no
1505        // business on the card beside the chain's: the host builds them.
1506        let to_chain = chain_ok
1507            && std::env::var("CMF_LTX_CHAIN").as_deref() != Ok("0")
1508            && crate::gpu::enabled_here()
1509            && crate::gpu::ltx_chain_available();
1510        let build = |a: &AdaLn, ts: &[f32], scale: f64| {
1511            if to_chain {
1512                crate::gpu::cpu_scope(|| TsTable::build(a, ts, scale, pool))
1513            } else {
1514                TsTable::build(a, ts, scale, pool)
1515            }
1516        };
1517        let vt = build(&self.adaln, &video.timesteps, self.t_scale);
1518        let at = build(&self.a_adaln, &audio.timesteps, self.t_scale);
1519        let vpt = build(&self.prompt_adaln, &[video.sigma], self.t_scale);
1520        let apt = build(&self.a_prompt_adaln, &[audio.sigma], self.t_scale);
1521        if std::env::var("CMF_LTX_PROMPTADALN").is_ok() {
1522            let r = vpt.row(0);
1523            let mx = r.iter().fold(0f32, |m, &v| m.max(v.abs()));
1524            let sum: f32 = r.iter().sum();
1525            eprintln!(
1526                "prompt-adaln sigma={:.6} max|row|={mx:.6e} sum={sum:.6e}",
1527                video.sigma
1528            );
1529        }
1530        let vxs = build(&self.av_v_ss, &video.timesteps, self.t_scale);
1531        let axs = build(&self.av_a_ss, &audio.timesteps, self.t_scale);
1532        // The fusion gate reads the *other* stream's sigma — the noise level
1533        // it is being asked to trust — at the A-V multiplier.
1534        let vgt = build(&self.av_a2v_gate, &[audio.sigma], self.av_t_scale);
1535        let agt = build(&self.av_v2a_gate, &[video.sigma], self.av_t_scale);
1536
1537        // --- RoPE ---------------------------------------------------------
1538        let v_pe = Rope::build(&video.positions, &self.max_pos, dim, self.heads, self.theta);
1539        let a_pe = Rope::build(
1540            &audio.positions,
1541            &self.a_max_pos,
1542            a_dim,
1543            self.a_heads,
1544            self.theta,
1545        );
1546        let time_only = |p: &[Vec<f64>]| p.iter().map(|r| vec![r[0]]).collect::<Vec<_>>();
1547        let v_xpe = Rope::build(
1548            &time_only(&video.positions),
1549            &[self.cross_max_pos],
1550            self.audio_cross_dim,
1551            self.heads,
1552            self.theta,
1553        );
1554        let a_xpe = Rope::build(
1555            &time_only(&audio.positions),
1556            &[self.cross_max_pos],
1557            self.audio_cross_dim,
1558            self.a_heads,
1559            self.theta,
1560        );
1561
1562        let vmask = (!video.context_mask.is_empty()).then_some(&video.context_mask[..]);
1563        let amask = (!audio.context_mask.is_empty()).then_some(&audio.context_mask[..]);
1564
1565        let mut prof = Prof::new();
1566        let chained = to_chain
1567            && self.run_chain(
1568                video,
1569                audio,
1570                &mut vx,
1571                &mut ax,
1572                [&vt, &at, &vpt, &apt, &vxs, &axs, &vgt, &agt],
1573                [&v_pe, &a_pe, &v_xpe, &a_xpe],
1574            );
1575        let blocks: &[Block] = if chained { &[] } else { &self.blocks };
1576        for (bi, blk) in blocks.iter().enumerate() {
1577            let mut pt = std::time::Instant::now();
1578            let v_msa = vt.triples(&blk.video.sst, dim, 0);
1579            let v_ca = vt.triples(&blk.video.sst, dim, 6);
1580            let v_mlp = vt.triples(&blk.video.sst, dim, 3);
1581            let a_msa = at.triples(&blk.audio.sst, a_dim, 0);
1582            let a_ca = at.triples(&blk.audio.sst, a_dim, 6);
1583            let a_mlp = at.triples(&blk.audio.sst, a_dim, 3);
1584
1585            // ---- video: self-attention, then prompt cross-attention ----
1586            pt = prof.tick(P_ADALN, pt);
1587            let mut vnorm = vec![0f32; n * dim];
1588            ada_zero_rows(&vx, &mut vnorm, n, dim, &v_msa, &vt.idx, pool);
1589            pt = prof.tick(P_MOD, pt);
1590            if bi == 0 {
1591                trace("v.b0.sa.in", &vnorm);
1592            }
1593            let vsa =
1594                blk.video
1595                    .attn1
1596                    .forward(&vnorm, n, &vnorm, n, Some(&v_pe), Some(&v_pe), None, pool);
1597            if bi == 0 {
1598                trace("v.b0.sa.out", &vsa);
1599            }
1600            pt = prof.tick(P_SELF, pt);
1601            let mut vnormed = vec![0f32; n * dim];
1602            post_sa_rows(&mut vx, &vsa, &mut vnormed, n, dim, &v_msa, &vt.idx, pool);
1603            let mut vq = vec![0f32; n * dim];
1604            affine_rows(&vnormed, &mut vq, n, dim, &v_ca, &vt.idx, pool);
1605            let vctx = modulate_kv(
1606                &video.context,
1607                video.ctx_len,
1608                dim,
1609                &blk.video.prompt_sst,
1610                vpt.row(0),
1611                pool,
1612            );
1613            let vca =
1614                blk.video
1615                    .attn2
1616                    .forward(&vq, n, &vctx, video.ctx_len, None, None, vmask, pool);
1617            if bi == 0 {
1618                trace("v.b0.ca.in", &vq);
1619                trace("v.b0.ca.ctx", &vctx);
1620                trace("v.b0.ca.out", &vca);
1621            }
1622            add_gated(&mut vx, &vca, n, dim, &v_ca, &vt.idx, pool);
1623            pt = prof.tick(P_CROSS, pt);
1624
1625            // ---- audio: the same two steps ----
1626            let mut anorm = vec![0f32; m * a_dim];
1627            ada_zero_rows(&ax, &mut anorm, m, a_dim, &a_msa, &at.idx, pool);
1628            if bi == 0 {
1629                trace("a.b0.sa.in", &anorm);
1630            }
1631            let asa =
1632                blk.audio
1633                    .attn1
1634                    .forward(&anorm, m, &anorm, m, Some(&a_pe), Some(&a_pe), None, pool);
1635            if bi == 0 {
1636                trace("a.b0.sa.out", &asa);
1637            }
1638            let mut anormed = vec![0f32; m * a_dim];
1639            post_sa_rows(&mut ax, &asa, &mut anormed, m, a_dim, &a_msa, &at.idx, pool);
1640            let mut aq = vec![0f32; m * a_dim];
1641            affine_rows(&anormed, &mut aq, m, a_dim, &a_ca, &at.idx, pool);
1642            let actx = modulate_kv(
1643                &audio.context,
1644                audio.ctx_len,
1645                a_dim,
1646                &blk.audio.prompt_sst,
1647                apt.row(0),
1648                pool,
1649            );
1650            let aca =
1651                blk.audio
1652                    .attn2
1653                    .forward(&aq, m, &actx, audio.ctx_len, None, None, amask, pool);
1654            if bi == 0 {
1655                trace("a.b0.ca.in", &aq);
1656                trace("a.b0.ca.ctx", &actx);
1657                trace("a.b0.ca.out", &aca);
1658            }
1659            add_gated(&mut ax, &aca, m, a_dim, &a_ca, &at.idx, pool);
1660            pt = prof.tick(P_CROSS, pt);
1661
1662            // ---- audio ↔ video, both directions off the pre-fusion state ----
1663            let vx_pre = vx.clone();
1664            let ax_pre = ax.clone();
1665            let a2v_vp = vxs.pairs(&blk.sst_a2v_video, dim, 0);
1666            let a2v_ap = axs.pairs(&blk.sst_a2v_audio, a_dim, 0);
1667            let a2v_v = ada_pair(&vx_pre, n, dim, &a2v_vp, &vxs.idx, pool);
1668            let a2v_a = ada_pair(&ax_pre, m, a_dim, &a2v_ap, &axs.idx, pool);
1669            let a2v = blk
1670                .a2v
1671                .forward(&a2v_v, n, &a2v_a, m, Some(&v_xpe), Some(&a_xpe), None, pool);
1672            if bi == 0 {
1673                trace("v.b0.a2v.in", &a2v_v);
1674                trace("v.b0.a2v.ctx", &a2v_a);
1675                trace("v.b0.a2v.out", &a2v);
1676            }
1677            let gate_a2v = gate_row(&blk.sst_a2v_video, dim, vgt.row(0));
1678            add_scaled(&mut vx, &a2v, n, dim, &gate_a2v, pool);
1679            let v2a_ap = axs.pairs(&blk.sst_a2v_audio, a_dim, 2);
1680            let v2a_vp = vxs.pairs(&blk.sst_a2v_video, dim, 2);
1681            let v2a_a = ada_pair(&ax_pre, m, a_dim, &v2a_ap, &axs.idx, pool);
1682            let v2a_v = ada_pair(&vx_pre, n, dim, &v2a_vp, &vxs.idx, pool);
1683            let v2a = blk
1684                .v2a
1685                .forward(&v2a_a, m, &v2a_v, n, Some(&a_xpe), Some(&v_xpe), None, pool);
1686            if bi == 0 {
1687                trace("a.b0.v2a.in", &v2a_a);
1688                trace("a.b0.v2a.ctx", &v2a_v);
1689                trace("a.b0.v2a.out", &v2a);
1690            }
1691            let gate_v2a = gate_row(&blk.sst_a2v_audio, a_dim, agt.row(0));
1692            add_scaled(&mut ax, &v2a, m, a_dim, &gate_v2a, pool);
1693            pt = prof.tick(P_FUSE, pt);
1694
1695            // ---- feed-forward ----
1696            let mut vsc = vec![0f32; n * dim];
1697            ada_zero_rows(&vx, &mut vsc, n, dim, &v_mlp, &vt.idx, pool);
1698            let vff = blk.video.ff(&vsc, n, pool);
1699            if bi == 0 {
1700                trace("v.b0.ff.in", &vsc);
1701                trace("v.b0.ff.out", &vff);
1702            }
1703            add_gated(&mut vx, &vff, n, dim, &v_mlp, &vt.idx, pool);
1704            let mut asc = vec![0f32; m * a_dim];
1705            ada_zero_rows(&ax, &mut asc, m, a_dim, &a_mlp, &at.idx, pool);
1706            let aff = blk.audio.ff(&asc, m, pool);
1707            if bi == 0 {
1708                trace("a.b0.ff.in", &asc);
1709                trace("a.b0.ff.out", &aff);
1710            }
1711            add_gated(&mut ax, &aff, m, a_dim, &a_mlp, &at.idx, pool);
1712            pt = prof.tick(P_FF, pt);
1713            trace(&format!("v.block{bi}"), &vx);
1714            trace(&format!("a.block{bi}"), &ax);
1715        }
1716
1717        prof.report();
1718
1719        // --- output head: LayerNorm (no affine), adaLN, projection --------
1720        let vout = head(&vx, n, dim, &self.sst_out, &vt, &self.proj_out, pool);
1721        let aout = head(&ax, m, a_dim, &self.a_sst_out, &at, &self.a_proj_out, pool);
1722        trace("v.out", &vout);
1723        trace("a.out", &aout);
1724        (vout, aout)
1725    }
1726}
1727
1728/// `prompt_scale_shift_table` plus the prompt adaLN row, modulating the
1729/// cross-attention K/V — the same modulation for every context token.
1730fn modulate_kv(
1731    ctx: &[f32],
1732    len: usize,
1733    dim: usize,
1734    table: &[f32],
1735    extra: &[f32],
1736    pool: Option<&Pool>,
1737) -> Vec<f32> {
1738    let mut out = vec![0f32; len * dim];
1739    let shift: Vec<f32> = (0..dim).map(|d| table[d] + extra[d]).collect();
1740    let scale: Vec<f32> = (0..dim).map(|d| table[dim + d] + extra[dim + d]).collect();
1741    // A thousand prompt tokens by four thousand channels, rebuilt in every
1742    // one of forty-eight blocks: three hundred million writes a step, which
1743    // is not something to do on one thread.
1744    let dst = Shared(out.as_mut_ptr());
1745    rows(pool, len, &|s, e| {
1746        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1747        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1748            for d in 0..dim {
1749                row[d] = ctx[i * dim + d] * (1.0 + scale[d]) + shift[d];
1750            }
1751        }
1752    });
1753    out
1754}
1755
1756/// `ada_zero` with an A↔V `(scale, shift)` pair per distinct timestep.
1757fn ada_pair(
1758    x: &[f32],
1759    n: usize,
1760    dim: usize,
1761    pairs: &[[Vec<f32>; 2]],
1762    idx: &[usize],
1763    pool: Option<&Pool>,
1764) -> Vec<f32> {
1765    let mut out = vec![0f32; n * dim];
1766    let dst = Shared(out.as_mut_ptr());
1767    rows(pool, n, &|s, e| {
1768        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1769        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1770            let p = &pairs[idx[i]];
1771            rms_plain(&x[i * dim..(i + 1) * dim], row);
1772            for d in 0..dim {
1773                row[d] = row[d] * (1.0 + p[0][d]) + p[1][d];
1774            }
1775        }
1776    });
1777    out
1778}
1779
1780/// The single gate row of an A↔V table — row 4 of `[5, dim]`, plus the
1781/// gate adaLN's own output.
1782fn gate_row(table: &[f32], dim: usize, extra: &[f32]) -> Vec<f32> {
1783    (0..dim).map(|d| table[4 * dim + d] + extra[d]).collect()
1784}
1785
1786/// The output head: LayerNorm without affine, the final scale/shift pair
1787/// (both offset by the same embedded timestep), then the projection.
1788fn head(
1789    x: &[f32],
1790    n: usize,
1791    dim: usize,
1792    sst: &[f32],
1793    ts: &TsTable,
1794    proj: &Lin,
1795    pool: Option<&Pool>,
1796) -> Vec<f32> {
1797    let mut y = vec![0f32; n * dim];
1798    let dst = Shared(y.as_mut_ptr());
1799    rows(pool, n, &|s, e| {
1800        let r = unsafe { dst.at(s * dim, (e - s) * dim) };
1801        let mut ln = vec![0f32; dim];
1802        for (row, i) in r.chunks_exact_mut(dim).zip(s..e) {
1803            let emb = ts.emb_row(ts.idx[i.min(ts.idx.len() - 1)]);
1804            layer_norm(&x[i * dim..(i + 1) * dim], &mut ln);
1805            for d in 0..dim {
1806                row[d] = ln[d] * (1.0 + sst[dim + d] + emb[d]) + sst[d] + emb[d];
1807            }
1808        }
1809    });
1810    proj.apply(&y, n, pool)
1811}