Skip to main content

cortiq_engine/
dit.rs

1//! Lumina2 Next-DiT forward (the Lumina-Image 2.0 denoiser):
2//! noisy latent + caption features + timestep → velocity prediction.
3//!
4//! Third increment of the image-generation runtime
5//! (docs/GENERATIVE.ru.md). Standalone f32 forward loaded from a
6//! diffusers `transformer/` directory, mirroring
7//! Lumina2Transformer2DModel exactly: plain-w RMSNorm (not Gemma's
8//! 1+w), per-head qk-norm, 3-axis complex-interleaved RoPE θ=10000
9//! (caption tokens advance axis 0, image tokens sit at axis0=cap_len
10//! with row/col on axes 1/2), AdaLN modulation with tanh gates from
11//! the 1024-d timestep embedding, sandwich RMSNorms, SwiGLU FFN, and
12//! a final LayerNorm(eps 1e-6, no affine) scaled by (1+scale) before
13//! the patch projection. Attention is full/bidirectional.
14//!
15//! Parity: `python/nextdit_ref.py` + `tests/dit_parity.rs` on the
16//! real Lumina transformer weights.
17
18use crate::pool::Pool;
19use crate::qtensor::QTensor;
20use crate::vae::{StTensor, read_safetensors};
21use cortiq_core::CmfModel;
22use std::collections::HashMap;
23use std::path::Path;
24use std::sync::Arc;
25
26/// A projection weight: exact f32 (diffusers load — Accelerate GEMM)
27/// or a CMF-quantized tensor on the engine's batched dot kernels.
28pub(crate) enum Proj {
29    F32 {
30        w: Vec<f32>,
31        rows: usize,
32        cols: usize,
33    },
34    Q(QTensor),
35}
36
37impl Proj {
38    /// f32 weight `[?, cols]`; rows derived from the data length.
39    pub(crate) fn f32(w: Vec<f32>, cols: usize) -> Self {
40        let rows = w.len() / cols;
41        debug_assert_eq!(w.len(), rows * cols);
42        Proj::F32 { w, rows, cols }
43    }
44
45    /// Load from a CMF directory entry (mmap-resident when quantized;
46    /// an F32 entry dequantizes into the exact-GEMM arm).
47    pub(crate) fn from_model(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
48        Ok(match QTensor::from_model(model, name)? {
49            QTensor::F32 { data, rows, cols } => Proj::F32 {
50                w: data,
51                rows,
52                cols,
53            },
54            q => Proj::Q(q),
55        })
56    }
57
58    /// The container-mapped q4tp weight behind this projection, when the
59    /// codec put one there — a batched device path needs the tensor index,
60    /// not the bytes.
61    pub(crate) fn q4tp_mapped(&self) -> Option<(&Arc<CmfModel>, usize)> {
62        match self {
63            Proj::Q(q) => q.q4tp_mapped(),
64            Proj::F32 { .. } => None,
65        }
66    }
67
68    pub(crate) fn rows(&self) -> usize {
69        match self {
70            Proj::F32 { rows, .. } => *rows,
71            Proj::Q(q) => q.rows(),
72        }
73    }
74
75    /// The token-graph descriptor for this weight, when it is a
76    /// quantized mapped tensor the graph can read in place.
77    pub(crate) fn graph_w(&self) -> Option<(&Arc<CmfModel>, crate::gpu::GraphW<'_>)> {
78        match self {
79            Proj::Q(q) => q.graph_weight().map(|(m, idx, kind, rs)| {
80                (
81                    m,
82                    crate::gpu::GraphW {
83                        idx,
84                        kind,
85                        row_scale: rs,
86                        data: &[],
87                        prism: crate::gpu::GraphPrismOp::None,
88                        affine: false,
89                    },
90                )
91            }),
92            Proj::F32 { .. } => None,
93        }
94    }
95
96    pub(crate) fn cols(&self) -> usize {
97        match self {
98            Proj::F32 { cols, .. } => *cols,
99            Proj::Q(q) => q.cols(),
100        }
101    }
102
103    /// The same product as `matmat`, but as `b` separate matvecs.
104    ///
105    /// For a NARROW batch that is not a pessimisation, it is the point:
106    /// the batched device kernel tiles for 32 columns and at b=2 — an
107    /// autoregressive decode with its classifier-free pair — throws away
108    /// fifteen sixteenths of every tile, which is why simply lowering the
109    /// batch floor measured SLOWER than the host. The matvec kernel is
110    /// written for this shape and carries its own probe and threshold.
111    pub(crate) fn matvec_rows(&self, xs: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
112        let (rows, cols) = (self.rows(), self.cols());
113        match self {
114            Proj::F32 { w, .. } => crate::fcd_ops::gemm_nt(xs, w, out, b, cols, rows, pool),
115            Proj::Q(q) => {
116                for i in 0..b {
117                    q.matvec(
118                        &xs[i * cols..(i + 1) * cols],
119                        &mut out[i * rows..(i + 1) * rows],
120                        pool,
121                    );
122                }
123            }
124        }
125    }
126
127    /// y[b, rows] = x[b, cols] · Wᵀ.
128    pub(crate) fn matmat(&self, xs: &[f32], b: usize, out: &mut [f32], pool: Option<&Pool>) {
129        match self {
130            Proj::F32 { w, rows, cols } => {
131                crate::fcd_ops::gemm_nt(xs, w, out, b, *cols, *rows, pool)
132            }
133            Proj::Q(q) => q.matmat(xs, b, out, pool),
134        }
135    }
136}
137
138struct Block {
139    /// AdaLN: (linear [4·hidden, 1024], bias [4·hidden]); None = plain norm1.
140    modulation: Option<(Proj, Vec<f32>)>,
141    norm1: Vec<f32>,
142    q: Proj, // [nh·hd, hidden]
143    k: Proj, // [nkv·hd, hidden]
144    v: Proj,
145    o: Proj,          // [hidden, nh·hd]
146    norm_q: Vec<f32>, // [hd]
147    norm_k: Vec<f32>,
148    norm2: Vec<f32>,
149    ffn_norm1: Vec<f32>,
150    w1: Proj, // gate [inter, hidden]
151    w3: Proj, // up
152    w2: Proj, // down [hidden, inter]
153    ffn_norm2: Vec<f32>,
154}
155
156/// Next-DiT: exact f32 from a diffusers directory, or CMF-quantized
157/// (mmap-resident) from a packaged file.
158pub struct NextDit {
159    x_emb: Proj, // [hidden, p·p·c]
160    x_emb_b: Vec<f32>,
161    t_lin1_w: Vec<f32>, // [temb, 256]
162    t_lin1_b: Vec<f32>,
163    t_lin2_w: Vec<f32>, // [temb, temb]
164    t_lin2_b: Vec<f32>,
165    cap_norm: Vec<f32>, // [cap_feat]
166    cap_w: Proj,        // [hidden, cap_feat]
167    cap_b: Vec<f32>,
168    context_refiner: Vec<Block>,
169    noise_refiner: Vec<Block>,
170    layers: Vec<Block>,
171    out_lin1_w: Vec<f32>, // [hidden, temb]
172    out_lin1_b: Vec<f32>,
173    out_lin2: Proj, // [p·p·c, hidden]
174    out_lin2_b: Vec<f32>,
175    pool: Option<Arc<Pool>>,
176    pub hidden: usize,
177    pub in_channels: usize,
178    pub patch: usize,
179    nh: usize,
180    nkv: usize,
181    hd: usize,
182    axes_dim: Vec<usize>,
183    eps: f64,
184}
185
186/// `CMF_DIT_PROF=1`: wall-time totals per forward stage, accumulated
187/// across every block of every step and dumped when the model drops.
188/// Same spirit as `CMF_VAE_PROF` — the knife for "where do the
189/// seconds go" before touching any kernel.
190mod prof {
191    use std::sync::OnceLock;
192    use std::sync::atomic::{AtomicU64, Ordering};
193
194    pub const MODNORM: usize = 0;
195    pub const QKV: usize = 1;
196    pub const ROPE: usize = 2;
197    pub const APACK: usize = 3;
198    pub const AQK: usize = 4;
199    pub const SOFTMAX: usize = 5;
200    pub const APV: usize = 6;
201    pub const OPROJ: usize = 7;
202    pub const FFN: usize = 8;
203    pub const FFNEL: usize = 9;
204    pub const HEADTAIL: usize = 10;
205    pub const GPUBLK: usize = 11;
206    const NAMES: [&str; 12] = [
207        "mod+norms",
208        "qkv-proj",
209        "qknorm+rope",
210        "attn-pack",
211        "attn-qk",
212        "softmax",
213        "attn-pv",
214        "o-proj",
215        "ffn-mm",
216        "ffn-silu",
217        "head+tail",
218        "gpu-block",
219    ];
220    static NS: [AtomicU64; 12] = [const { AtomicU64::new(0) }; 12];
221
222    pub fn on() -> bool {
223        static ON: OnceLock<bool> = OnceLock::new();
224        *ON.get_or_init(|| std::env::var("CMF_DIT_PROF").is_ok_and(|v| v != "0"))
225    }
226
227    /// RAII span: charges its category on drop. Free when prof is off.
228    pub struct Span(Option<(std::time::Instant, usize)>);
229    pub fn span(cat: usize) -> Span {
230        Span(on().then(|| (std::time::Instant::now(), cat)))
231    }
232    impl Drop for Span {
233        fn drop(&mut self) {
234            if let Some((t0, c)) = self.0 {
235                NS[c].fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
236            }
237        }
238    }
239
240    pub fn dump() {
241        if !on() {
242            return;
243        }
244        let total: u64 = NS.iter().map(|a| a.load(Ordering::Relaxed)).sum();
245        if total == 0 {
246            return;
247        }
248        eprintln!("dit prof ({:.1} s total in blocks):", total as f64 / 1e9);
249        for (name, a) in NAMES.iter().zip(&NS) {
250            let ns = a.load(Ordering::Relaxed);
251            eprintln!(
252                "  {name:<12} {:>7.2} s  {:>4.1}%",
253                ns as f64 / 1e9,
254                ns as f64 * 100.0 / total as f64
255            );
256        }
257    }
258}
259
260/// diffusers RMSNorm: x·rsqrt(mean x²+eps) · w (plain w).
261fn rms_norm(x: &[f32], w: &[f32], eps: f64) -> Vec<f32> {
262    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
263    let inv = 1.0 / (ss + eps).sqrt();
264    x.iter()
265        .zip(w)
266        .map(|(&v, &g)| (v as f64 * inv) as f32 * g)
267        .collect()
268}
269
270/// `rms_norm` into a caller buffer — same math, no per-row alloc.
271fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
272    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
273    let inv = 1.0 / (ss + eps).sqrt();
274    for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
275        *d = (v as f64 * inv) as f32 * g;
276    }
277}
278
279/// `rms_norm` in place — same math, no per-row alloc.
280fn rms_norm_inplace(v: &mut [f32], w: &[f32], eps: f64) {
281    let ss = v.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / v.len() as f64;
282    let inv = 1.0 / (ss + eps).sqrt();
283    for (x, &g) in v.iter_mut().zip(w) {
284        *x = (*x as f64 * inv) as f32 * g;
285    }
286}
287
288/// Rows of `n` items split across pool workers (serial without a pool).
289fn pool_rows(pool: Option<&Pool>, n: usize, f: &(dyn Fn(usize, usize) + Sync)) {
290    match pool {
291        Some(p) => p.run_rows(n, f),
292        None => f(0, n),
293    }
294}
295
296fn silu(v: f32) -> f32 {
297    v / (1.0 + (-v).exp())
298}
299
300/// y = x·Wᵀ + b for a single row.
301fn linear(x: &[f32], w: &[f32], b: &[f32]) -> Vec<f32> {
302    let k = x.len();
303    b.iter()
304        .enumerate()
305        .map(|(o, &bias)| {
306            let row = &w[o * k..(o + 1) * k];
307            bias + row.iter().zip(x).map(|(&a, &c)| a * c).sum::<f32>()
308        })
309        .collect()
310}
311
312/// Row handout for pool workers over one flat buffer (Rust-2021
313/// closures capture the raw pointer field, not the wrapper — hence
314/// the accessor method).
315struct SendRows(*mut f32);
316unsafe impl Send for SendRows {}
317unsafe impl Sync for SendRows {}
318impl SendRows {
319    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
320    #[allow(clippy::mut_from_ref)] // the disjoint-rows contract IS the point
321    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
322        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
323    }
324
325    /// Single scattered element (transposed stores). SAFETY: as `row`.
326    unsafe fn set(&self, off: usize, v: f32) {
327        unsafe { *self.0.add(off) = v }
328    }
329}
330
331/// Numerically stable in-place softmax of one full row (NEON exp on
332/// aarch64, scalar elsewhere).
333fn softmax_inplace(row: &mut [f32]) {
334    #[cfg(target_arch = "aarch64")]
335    {
336        crate::attention::softmax_row(row);
337    }
338    #[cfg(not(target_arch = "aarch64"))]
339    {
340        let mx = row.iter().cloned().fold(f32::MIN, f32::max);
341        let mut den = 0f32;
342        for r in row.iter_mut() {
343            *r = (*r - mx).exp();
344            den += *r;
345        }
346        if den > 0.0 {
347            let inv = 1.0 / den;
348            for r in row.iter_mut() {
349                *r *= inv;
350            }
351        }
352    }
353}
354
355/// Any CMF directory entry → f32 (norms, biases, f16 conv kernels).
356pub(crate) fn cmf_f32(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
357    let entry = model
358        .tensor(name)
359        .ok_or_else(|| format!("missing tensor {name}"))?;
360    let bytes = model.entry_bytes(entry);
361    let mut out = vec![0f32; entry.shape.iter().product()];
362    cortiq_core::quant::dequant_tensor(entry, bytes, &mut out)?;
363    Ok(out)
364}
365
366/// Per-token RoPE table: interleaved-pair rotation angles, f64.
367/// `ids` is [n, 3]; each axis contributes dim/2 frequencies.
368fn rope_table(ids: &[[u32; 3]], axes_dim: &[usize]) -> (Vec<f64>, Vec<f64>) {
369    let pairs: usize = axes_dim.iter().sum::<usize>() / 2;
370    let mut cos = Vec::with_capacity(ids.len() * pairs);
371    let mut sin = Vec::with_capacity(ids.len() * pairs);
372    for id in ids {
373        for (a, &d) in axes_dim.iter().enumerate() {
374            for j in 0..d / 2 {
375                let freq = 1.0 / 10000f64.powf(2.0 * j as f64 / d as f64);
376                let ang = id[a] as f64 * freq;
377                cos.push(ang.cos());
378                sin.push(ang.sin());
379            }
380        }
381    }
382    (cos, sin)
383}
384
385impl Drop for NextDit {
386    fn drop(&mut self) {
387        prof::dump();
388    }
389}
390
391impl NextDit {
392    pub fn load_dir(dir: &Path) -> Result<Self, String> {
393        let cfg: serde_json::Value = serde_json::from_slice(
394            &std::fs::read(dir.join("config.json")).map_err(|e| format!("config.json: {e}"))?,
395        )
396        .map_err(|e| format!("config.json: {e}"))?;
397        let idx: serde_json::Value = serde_json::from_slice(
398            &std::fs::read(dir.join("diffusion_pytorch_model.safetensors.index.json"))
399                .map_err(|e| format!("index: {e}"))?,
400        )
401        .map_err(|e| format!("index: {e}"))?;
402        let mut shards: Vec<String> = idx["weight_map"]
403            .as_object()
404            .ok_or("weight_map")?
405            .values()
406            .filter_map(|v| v.as_str().map(String::from))
407            .collect();
408        shards.sort();
409        shards.dedup();
410        let mut t: HashMap<String, StTensor> = HashMap::new();
411        for sh in &shards {
412            t.extend(read_safetensors(&dir.join(sh))?);
413        }
414        let mut take = |n: String| -> Result<Vec<f32>, String> {
415            t.remove(&n)
416                .map(|v| v.data)
417                .ok_or_else(|| format!("missing tensor {n}"))
418        };
419        let hidden = cfg["hidden_size"].as_u64().ok_or("hidden")? as usize;
420        let mut blocks = |pfx: &str, count: usize, modulated: bool| -> Result<Vec<Block>, String> {
421            (0..count)
422                .map(|l| {
423                    let p = format!("{pfx}.{l}");
424                    let w1 = take(format!("{p}.feed_forward.linear_1.weight"))?;
425                    let inter = w1.len() / hidden;
426                    Ok(Block {
427                        modulation: if modulated {
428                            let mw = take(format!("{p}.norm1.linear.weight"))?;
429                            let cols = mw.len() / (4 * hidden);
430                            Some((Proj::f32(mw, cols), take(format!("{p}.norm1.linear.bias"))?))
431                        } else {
432                            None
433                        },
434                        norm1: if modulated {
435                            take(format!("{p}.norm1.norm.weight"))?
436                        } else {
437                            take(format!("{p}.norm1.weight"))?
438                        },
439                        q: Proj::f32(take(format!("{p}.attn.to_q.weight"))?, hidden),
440                        k: Proj::f32(take(format!("{p}.attn.to_k.weight"))?, hidden),
441                        v: Proj::f32(take(format!("{p}.attn.to_v.weight"))?, hidden),
442                        o: {
443                            let o = take(format!("{p}.attn.to_out.0.weight"))?;
444                            let cols = o.len() / hidden;
445                            Proj::f32(o, cols)
446                        },
447                        norm_q: take(format!("{p}.attn.norm_q.weight"))?,
448                        norm_k: take(format!("{p}.attn.norm_k.weight"))?,
449                        norm2: take(format!("{p}.norm2.weight"))?,
450                        ffn_norm1: take(format!("{p}.ffn_norm1.weight"))?,
451                        w1: Proj::f32(w1, hidden),
452                        w3: Proj::f32(take(format!("{p}.feed_forward.linear_3.weight"))?, hidden),
453                        w2: Proj::f32(take(format!("{p}.feed_forward.linear_2.weight"))?, inter),
454                        ffn_norm2: take(format!("{p}.ffn_norm2.weight"))?,
455                    })
456                })
457                .collect()
458        };
459        let nl = cfg["num_layers"].as_u64().ok_or("num_layers")? as usize;
460        let nr = cfg["num_refiner_layers"].as_u64().unwrap_or(2) as usize;
461        let context_refiner = blocks("context_refiner", nr, false)?;
462        let noise_refiner = blocks("noise_refiner", nr, true)?;
463        let layers = blocks("layers", nl, true)?;
464        let nh = cfg["num_attention_heads"].as_u64().ok_or("nh")? as usize;
465        let in_channels = cfg["in_channels"].as_u64().ok_or("in_channels")? as usize;
466        let patch = cfg["patch_size"].as_u64().unwrap_or(2) as usize;
467        let axes_dim: Vec<usize> = cfg["axes_dim_rope"]
468            .as_array()
469            .ok_or("axes_dim_rope")?
470            .iter()
471            .map(|v| v.as_u64().unwrap_or(0) as usize)
472            .collect();
473        let cap_norm = take("time_caption_embed.caption_embedder.0.weight".into())?;
474        let cap_feat = cap_norm.len();
475        Ok(Self {
476            x_emb: Proj::f32(
477                take("x_embedder.weight".into())?,
478                patch * patch * in_channels,
479            ),
480            x_emb_b: take("x_embedder.bias".into())?,
481            t_lin1_w: take("time_caption_embed.timestep_embedder.linear_1.weight".into())?,
482            t_lin1_b: take("time_caption_embed.timestep_embedder.linear_1.bias".into())?,
483            t_lin2_w: take("time_caption_embed.timestep_embedder.linear_2.weight".into())?,
484            t_lin2_b: take("time_caption_embed.timestep_embedder.linear_2.bias".into())?,
485            cap_norm,
486            cap_w: Proj::f32(
487                take("time_caption_embed.caption_embedder.1.weight".into())?,
488                cap_feat,
489            ),
490            cap_b: take("time_caption_embed.caption_embedder.1.bias".into())?,
491            context_refiner,
492            noise_refiner,
493            layers,
494            out_lin1_w: take("norm_out.linear_1.weight".into())?,
495            out_lin1_b: take("norm_out.linear_1.bias".into())?,
496            out_lin2: Proj::f32(take("norm_out.linear_2.weight".into())?, hidden),
497            out_lin2_b: take("norm_out.linear_2.bias".into())?,
498            pool: Pool::from_env(),
499            hidden,
500            in_channels,
501            patch,
502            nh,
503            nkv: cfg["num_kv_heads"].as_u64().unwrap_or(nh as u64) as usize,
504            hd: hidden / nh,
505            axes_dim,
506            eps: cfg["norm_eps"].as_f64().unwrap_or(1e-5),
507        })
508    }
509
510    /// Load from a packaged imagegen .cmf (`dit.*` tensors +
511    /// `dit.config_json`). Quantized projections stay mmap-resident.
512    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
513        let cfg: serde_json::Value = serde_json::from_slice(
514            model
515                .tensor_bytes("dit.config_json")
516                .map_err(|e| e.to_string())?,
517        )
518        .map_err(|e| format!("dit.config_json: {e}"))?;
519        let f32v = |n: &str| -> Result<Vec<f32>, String> { cmf_f32(model, n) };
520        let hidden = cfg["hidden_size"].as_u64().ok_or("hidden")? as usize;
521        let blocks = |pfx: &str, count: usize, modulated: bool| -> Result<Vec<Block>, String> {
522            (0..count)
523                .map(|l| {
524                    let p = format!("dit.{pfx}.{l}");
525                    Ok(Block {
526                        modulation: if modulated {
527                            Some((
528                                Proj::from_model(model, &format!("{p}.norm1.linear.weight"))?,
529                                f32v(&format!("{p}.norm1.linear.bias"))?,
530                            ))
531                        } else {
532                            None
533                        },
534                        norm1: if modulated {
535                            f32v(&format!("{p}.norm1.norm.weight"))?
536                        } else {
537                            f32v(&format!("{p}.norm1.weight"))?
538                        },
539                        q: Proj::from_model(model, &format!("{p}.attn.to_q.weight"))?,
540                        k: Proj::from_model(model, &format!("{p}.attn.to_k.weight"))?,
541                        v: Proj::from_model(model, &format!("{p}.attn.to_v.weight"))?,
542                        o: Proj::from_model(model, &format!("{p}.attn.to_out.0.weight"))?,
543                        norm_q: f32v(&format!("{p}.attn.norm_q.weight"))?,
544                        norm_k: f32v(&format!("{p}.attn.norm_k.weight"))?,
545                        norm2: f32v(&format!("{p}.norm2.weight"))?,
546                        ffn_norm1: f32v(&format!("{p}.ffn_norm1.weight"))?,
547                        w1: Proj::from_model(model, &format!("{p}.feed_forward.linear_1.weight"))?,
548                        w3: Proj::from_model(model, &format!("{p}.feed_forward.linear_3.weight"))?,
549                        w2: Proj::from_model(model, &format!("{p}.feed_forward.linear_2.weight"))?,
550                        ffn_norm2: f32v(&format!("{p}.ffn_norm2.weight"))?,
551                    })
552                })
553                .collect()
554        };
555        let nl = cfg["num_layers"].as_u64().ok_or("num_layers")? as usize;
556        let nr = cfg["num_refiner_layers"].as_u64().unwrap_or(2) as usize;
557        let nh = cfg["num_attention_heads"].as_u64().ok_or("nh")? as usize;
558        let axes_dim: Vec<usize> = cfg["axes_dim_rope"]
559            .as_array()
560            .ok_or("axes_dim_rope")?
561            .iter()
562            .map(|v| v.as_u64().unwrap_or(0) as usize)
563            .collect();
564        Ok(Self {
565            x_emb: Proj::from_model(model, "dit.x_embedder.weight")?,
566            x_emb_b: f32v("dit.x_embedder.bias")?,
567            t_lin1_w: f32v("dit.time_caption_embed.timestep_embedder.linear_1.weight")?,
568            t_lin1_b: f32v("dit.time_caption_embed.timestep_embedder.linear_1.bias")?,
569            t_lin2_w: f32v("dit.time_caption_embed.timestep_embedder.linear_2.weight")?,
570            t_lin2_b: f32v("dit.time_caption_embed.timestep_embedder.linear_2.bias")?,
571            cap_norm: f32v("dit.time_caption_embed.caption_embedder.0.weight")?,
572            cap_w: Proj::from_model(model, "dit.time_caption_embed.caption_embedder.1.weight")?,
573            cap_b: f32v("dit.time_caption_embed.caption_embedder.1.bias")?,
574            context_refiner: blocks("context_refiner", nr, false)?,
575            noise_refiner: blocks("noise_refiner", nr, true)?,
576            layers: blocks("layers", nl, true)?,
577            out_lin1_w: f32v("dit.norm_out.linear_1.weight")?,
578            out_lin1_b: f32v("dit.norm_out.linear_1.bias")?,
579            out_lin2: Proj::from_model(model, "dit.norm_out.linear_2.weight")?,
580            out_lin2_b: f32v("dit.norm_out.linear_2.bias")?,
581            pool: Pool::from_env(),
582            hidden,
583            in_channels: cfg["in_channels"].as_u64().ok_or("in_channels")? as usize,
584            patch: cfg["patch_size"].as_u64().unwrap_or(2) as usize,
585            nh,
586            nkv: cfg["num_kv_heads"].as_u64().unwrap_or(nh as u64) as usize,
587            hd: hidden / nh,
588            axes_dim,
589            eps: cfg["norm_eps"].as_f64().unwrap_or(1e-5),
590        })
591    }
592
593    /// Sinusoidal(256, cos-first) → 2-layer MLP → temb [1024].
594    fn time_embed(&self, t: f32) -> Vec<f32> {
595        const HALF: usize = 128;
596        let mut freq = [0f32; 2 * HALF];
597        for i in 0..HALF {
598            let ang = t as f64 * (-(10000f64.ln()) * i as f64 / HALF as f64).exp();
599            freq[i] = ang.cos() as f32;
600            freq[HALF + i] = ang.sin() as f32;
601        }
602        let mut h = linear(&freq, &self.t_lin1_w, &self.t_lin1_b);
603        for v in h.iter_mut() {
604            *v = silu(*v);
605        }
606        linear(&h, &self.t_lin2_w, &self.t_lin2_b)
607    }
608
609    /// Fused on-device SwiGLU FFN (Metal). Taken only once the wide
610    /// GEMM probe has settled on the GPU arm: during probing the
611    /// per-op path feeds the samples, after a CPU verdict (or a
612    /// contention kill) the CPU path is the right one anyway. Carries
613    /// the same work-proportional contention tripwire as the per-op
614    /// route (cold ops exempt).
615    fn gpu_ffn(&self, blk: &Block, xn: &[f32], n: usize, out: &mut [f32]) -> bool {
616        use crate::gpu;
617        if n < 128 || !gpu::enabled_here() || gpu::mm_killed() {
618            return false;
619        }
620        // A fused block is not a wide matmat: see `fused_block_trusted`.
621        if !gpu::fused_block_trusted()
622            && (gpu::probe_deciding(gpu::OpClass::MatmatWide)
623                || !matches!(gpu::probe_arm(gpu::OpClass::MatmatWide), gpu::ProbeArm::Gpu))
624        {
625            return false;
626        }
627        let (Proj::Q(q1), Proj::Q(q3), Proj::Q(q2)) = (&blk.w1, &blk.w3, &blk.w2) else {
628            return false;
629        };
630        // q4t or q4tp — the fused chain exists for both, and picking by dtype
631        // here is what keeps a q4tp image model off the unfused path (which
632        // ships the [b, inter] intermediates across the CPU boundary twice per
633        // layer: 28 s against 14 s on Lumina at 256px).
634        let tp = q1.mapped_q4tp().is_some();
635        let (Some((m, i1)), Some((_, i3)), Some((_, i2))) = (if tp {
636            (q1.mapped_q4tp(), q3.mapped_q4tp(), q2.mapped_q4tp())
637        } else {
638            (q1.mapped_q4t(), q3.mapped_q4t(), q2.mapped_q4t())
639        }) else {
640            return false;
641        };
642        let inter = q1.rows();
643        let t0 = std::time::Instant::now();
644        let ok = if tp {
645            gpu::q4tp_ffn(m, i1, i3, i2, xn, n, self.hidden, inter, out)
646        } else {
647            gpu::q4t_ffn(m, i1, i3, i2, xn, n, self.hidden, inter, out)
648        };
649        if !ok {
650            return false;
651        }
652        let flops = 6.0 * n as f64 * self.hidden as f64 * inter as f64;
653        let budget = std::time::Duration::from_secs_f64(flops / 1.5e12 * 8.0 + 0.020);
654        let el = t0.elapsed();
655        gpu::mm_budget_check("ffn", el, budget, gpu::probe_was_cold());
656        true
657    }
658
659    /// All-heads attention on the device (same probe/kill gating as
660    /// the fused FFN). Packs q/k/v head-major (pool-parallel), runs
661    /// scores→softmax→P·V→unstack in one command buffer, and writes
662    /// straight into the [n][nh·hd] attn layout the O-projection
663    /// consumes. Returns false → caller runs the CPU per-head loop.
664    fn gpu_attention(
665        &self,
666        q_all: &[f32],
667        k_all: &[f32],
668        v_all: &[f32],
669        n: usize,
670        scale: f32,
671        attn: &mut [f32],
672    ) -> bool {
673        use crate::gpu;
674        let (nh, nkv, hd) = (self.nh, self.nkv, self.hd);
675        if n < 128 || !gpu::enabled_here() || gpu::mm_killed() {
676            return false;
677        }
678        // A fused block is not a wide matmat: see `fused_block_trusted`.
679        if !gpu::fused_block_trusted()
680            && (gpu::probe_deciding(gpu::OpClass::MatmatWide)
681                || !matches!(gpu::probe_arm(gpu::OpClass::MatmatWide), gpu::ProbeArm::Gpu))
682        {
683            return false;
684        }
685        let pool = self.pool.as_deref();
686        let mut qh = vec![0f32; nh * n * hd];
687        let mut kh = vec![0f32; nkv * n * hd];
688        let mut vh = vec![0f32; nkv * n * hd];
689        {
690            let _s = prof::span(prof::APACK);
691            let (sq, sk, sv) = (
692                SendRows(qh.as_mut_ptr()),
693                SendRows(kh.as_mut_ptr()),
694                SendRows(vh.as_mut_ptr()),
695            );
696            pool_rows(pool, n, &|start, end| {
697                for p in start..end {
698                    for h in 0..nh {
699                        // SAFETY: workers cover disjoint token ranges.
700                        unsafe { sq.row((h * n + p) * hd, hd) }
701                            .copy_from_slice(&q_all[(p * nh + h) * hd..(p * nh + h + 1) * hd]);
702                    }
703                    for h in 0..nkv {
704                        unsafe { sk.row((h * n + p) * hd, hd) }
705                            .copy_from_slice(&k_all[(p * nkv + h) * hd..(p * nkv + h + 1) * hd]);
706                        unsafe { sv.row((h * n + p) * hd, hd) }
707                            .copy_from_slice(&v_all[(p * nkv + h) * hd..(p * nkv + h + 1) * hd]);
708                    }
709                }
710            });
711        }
712        let _s = prof::span(prof::AQK);
713        let t0 = std::time::Instant::now();
714        if !gpu::dit_attention(&qh, &kh, &vh, nh, nkv, n, hd, scale, attn) {
715            return false;
716        }
717        let flops = 4.0 * nh as f64 * (n as f64) * (n as f64) * hd as f64;
718        let budget = std::time::Duration::from_secs_f64(flops / 1.5e12 * 8.0 + 0.020);
719        let el = t0.elapsed();
720        gpu::mm_budget_check("attention", el, budget, gpu::probe_was_cold());
721        true
722    }
723
724    /// One whole block on the device (norms → qkv → RoPE → attention
725    /// → O → residual → FFN → residual, single command buffer). Same
726    /// gating and contention tripwire as the per-stage GPU arms.
727    fn gpu_block(
728        &self,
729        blk: &Block,
730        x: &mut [f32],
731        n: usize,
732        rope32: &(Vec<f32>, Vec<f32>),
733        m: &[f32],
734    ) -> bool {
735        self.gpu_block_seg(blk, x, n, rope32, m, &[n], (false, false))
736    }
737
738    #[allow(clippy::too_many_arguments)]
739    fn gpu_block_seg(
740        &self,
741        blk: &Block,
742        x: &mut [f32],
743        n: usize,
744        rope32: &(Vec<f32>, Vec<f32>),
745        m: &[f32],
746        segs: &[usize],
747        resident: (bool, bool),
748    ) -> bool {
749        use crate::gpu;
750        let (hs, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
751        if n < 128 || !gpu::enabled_here() || gpu::mm_killed() {
752            return false;
753        }
754        // A fused block is not a wide matmat: see `fused_block_trusted`.
755        if !gpu::fused_block_trusted()
756            && (gpu::probe_deciding(gpu::OpClass::MatmatWide)
757                || !matches!(gpu::probe_arm(gpu::OpClass::MatmatWide), gpu::ProbeArm::Gpu))
758        {
759            return false;
760        }
761        // The pack kernel assumes the rope table covers the full head
762        // dim (axes_dim sums to hd — true for Lumina; bail otherwise).
763        if rope32.0.len() != n * hd / 2 {
764            return false;
765        }
766        // Either 4-bit tiled layout: the ladder (q4tp) is what the
767        // published file uses, and taking only the older one is how the
768        // fused path came to be dead for it on every backend.
769        fn q(p: &Proj) -> Option<(&Arc<CmfModel>, usize)> {
770            match p {
771                Proj::Q(q) => q.mapped_q4t().or_else(|| q.mapped_q4tp()),
772                Proj::F32 { .. } => None,
773            }
774        }
775        let is_q4tp = matches!(&blk.q, Proj::Q(q) if q.mapped_q4tp().is_some());
776        let (
777            Some((model, wq)),
778            Some((_, wk)),
779            Some((_, wv)),
780            Some((_, wo)),
781            Some((_, w1)),
782            Some((_, w3)),
783            Some((_, w2)),
784        ) = (
785            q(&blk.q),
786            q(&blk.k),
787            q(&blk.v),
788            q(&blk.o),
789            q(&blk.w1),
790            q(&blk.w3),
791            q(&blk.w2),
792        )
793        else {
794            return false;
795        };
796        let inter = blk.w1.rows();
797        let gate_msa: Vec<f32> = m[hs..2 * hs].iter().map(|&v| v.tanh()).collect();
798        let gate_mlp: Vec<f32> = m[3 * hs..].iter().map(|&v| v.tanh()).collect();
799        let args = gpu::DitBlockArgs {
800            q4tp: is_q4tp,
801            resident_in: resident.0,
802            resident_out: resident.1,
803            n,
804            hidden: hs,
805            inter,
806            nh,
807            nkv,
808            hd,
809            eps: self.eps as f32,
810            rope_cos: &rope32.0,
811            rope_sin: &rope32.1,
812            norm1: &blk.norm1,
813            norm2: &blk.norm2,
814            ffn_norm1: &blk.ffn_norm1,
815            ffn_norm2: &blk.ffn_norm2,
816            norm_q: &blk.norm_q,
817            norm_k: &blk.norm_k,
818            s_msa: &m[..hs],
819            gate_msa: &gate_msa,
820            s_mlp: &m[2 * hs..3 * hs],
821            gate_mlp: &gate_mlp,
822            wq,
823            wk,
824            wv,
825            wo,
826            w1,
827            w3,
828            w2,
829        };
830        let t0 = std::time::Instant::now();
831        if !gpu::dit_block_seg(model, &args, segs, x) {
832            return false;
833        }
834        let flops = 2.0 * n as f64 * hs as f64 * ((nh + 2 * nkv) * hd) as f64
835            + 4.0 * nh as f64 * (n as f64) * (n as f64) * hd as f64
836            + 2.0 * n as f64 * hs as f64 * (nh * hd) as f64
837            + 6.0 * n as f64 * hs as f64 * inter as f64;
838        let budget = std::time::Duration::from_secs_f64(flops / 1.5e12 * 8.0 + 0.030);
839        let el = t0.elapsed();
840        gpu::mm_budget_check("dit block", el, budget, gpu::probe_was_cold());
841        true
842    }
843
844    /// Full bidirectional attention over ONE sequence: per head, pack
845    /// q/k/v, scores as a GEMM, row softmax, P·V, scatter back. Lifted
846    /// out of the block so a batched call can run it per segment
847    /// without the segments ever meeting in a score matrix.
848    fn attention_seq(
849        &self,
850        q_all: &[f32],
851        k_all: &[f32],
852        v_all: &[f32],
853        n: usize,
854        scale: f32,
855        attn: &mut [f32],
856    ) {
857        let (nh, nkv, hd) = (self.nh, self.nkv, self.hd);
858        let hpk = nh / nkv;
859        let pool = self.pool.as_deref();
860        let mut qh = vec![0f32; n * hd];
861        let mut kh = vec![0f32; n * hd];
862        let mut vt = vec![0f32; hd * n]; // V transposed: gemm_nt's W layout
863        let mut scores = vec![0f32; n * n];
864        let mut oh = vec![0f32; n * hd];
865        for hh in 0..nh {
866            let kv = hh / hpk;
867            {
868                let _s = prof::span(prof::APACK);
869                let (sq, sk, sv) = (
870                    SendRows(qh.as_mut_ptr()),
871                    SendRows(kh.as_mut_ptr()),
872                    SendRows(vt.as_mut_ptr()),
873                );
874                pool_rows(pool, n, &|start, end| {
875                    for p in start..end {
876                        let qsrc = &q_all[(p * nh + hh) * hd..(p * nh + hh + 1) * hd];
877                        // SAFETY: workers cover disjoint token ranges
878                        // (`vt` columns are indexed by token too).
879                        let qd = unsafe { sq.row(p * hd, hd) };
880                        for (d, &v) in qsrc.iter().enumerate() {
881                            qd[d] = v * scale;
882                        }
883                        unsafe { sk.row(p * hd, hd) }
884                            .copy_from_slice(&k_all[(p * nkv + kv) * hd..(p * nkv + kv + 1) * hd]);
885                        let vv = &v_all[(p * nkv + kv) * hd..(p * nkv + kv + 1) * hd];
886                        for (d, &val) in vv.iter().enumerate() {
887                            unsafe { sv.set(d * n + p, val) };
888                        }
889                    }
890                });
891            }
892            {
893                let _s = prof::span(prof::AQK);
894                crate::fcd_ops::gemm_nt(&qh, &kh, &mut scores, n, hd, n, pool);
895            }
896            {
897                let _s = prof::span(prof::SOFTMAX);
898                let sp = SendRows(scores.as_mut_ptr());
899                let soft = |start: usize, end: usize| {
900                    for r in start..end {
901                        // SAFETY: workers cover disjoint row ranges.
902                        softmax_inplace(unsafe { sp.row(r * n, n) });
903                    }
904                };
905                match pool {
906                    Some(p) => p.run_rows(n, &soft),
907                    None => soft(0, n),
908                }
909            }
910            {
911                let _s = prof::span(prof::APV);
912                crate::fcd_ops::gemm_nt(&scores, &vt, &mut oh, n, n, hd, pool);
913            }
914            let _s = prof::span(prof::APACK);
915            let sa = SendRows(attn.as_mut_ptr());
916            pool_rows(pool, n, &|start, end| {
917                for p in start..end {
918                    // SAFETY: workers cover disjoint token ranges.
919                    unsafe { sa.row((p * nh + hh) * hd, hd) }
920                        .copy_from_slice(&oh[p * hd..(p + 1) * hd]);
921                }
922            });
923        }
924    }
925
926    fn block_forward(
927        &self,
928        blk: &Block,
929        x: &mut [f32],
930        rope: &(Vec<f64>, Vec<f64>),
931        rope32: Option<&(Vec<f32>, Vec<f32>)>,
932        temb: Option<&[f32]>,
933    ) {
934        let n_all = x.len() / self.hidden;
935        let _ = self.block_forward_seg(blk, x, rope, rope32, temb, &[n_all], (false, false));
936    }
937
938    /// `block_forward` over a CONCATENATION of independent sequences.
939    /// Everything position-wise (norms, projections, FFN) sees one tall
940    /// batch — the weights are read once for all of them, which is the
941    /// whole point on a CPU or a phone — while attention runs per
942    /// segment, so no sample ever attends to another's tokens. `segs`
943    /// are the token counts in order; a single-element slice is the
944    /// plain path, bit for bit.
945    fn block_forward_seg(
946        &self,
947        blk: &Block,
948        x: &mut [f32],
949        rope: &(Vec<f64>, Vec<f64>),
950        rope32: Option<&(Vec<f32>, Vec<f32>)>,
951        temb: Option<&[f32]>,
952        segs: &[usize],
953        resident: (bool, bool),
954    ) -> bool {
955        let (hs, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
956        let pool = self.pool.as_deref();
957        let n = x.len() / hs;
958        let modv = {
959            let _s = prof::span(prof::MODNORM);
960            blk.modulation.as_ref().zip(temb).map(|((w, b), t)| {
961                let s: Vec<f32> = t.iter().map(|&v| silu(v)).collect();
962                let mut m = vec![0f32; w.rows()];
963                w.matmat(&s, 1, &mut m, pool);
964                for (v, &bias) in m.iter_mut().zip(b) {
965                    *v += bias;
966                }
967                m
968            })
969        };
970        if let (Some(m), Some(r32)) = (&modv, rope32) {
971            let _s = prof::span(prof::GPUBLK);
972            if self.gpu_block_seg(blk, x, n, r32, m, segs, resident) {
973                return true;
974            }
975        }
976        // Falling back to the host after a chained block: the device holds
977        // the state and `x` is stale. Recover it before reading `x`.
978        if resident.0 {
979            crate::gpu::dit_state_fetch(&mut x[..n * hs]);
980        }
981        let modnorm = prof::span(prof::MODNORM);
982        let (s_msa, g_msa, s_mlp, g_mlp) = match &modv {
983            Some(m) => (
984                Some(&m[..hs]),
985                Some(&m[hs..2 * hs]),
986                Some(&m[2 * hs..3 * hs]),
987                Some(&m[3 * hs..]),
988            ),
989            None => (None, None, None, None),
990        };
991        // Gates: tanh once per block — every row shares the same gate
992        // vector, and the naive per-element tanh in the residual loop
993        // was billions of repeated evaluations per render.
994        let gate_msa: Option<Vec<f32>> = g_msa.map(|g| g.iter().map(|&v| v.tanh()).collect());
995        let gate_mlp: Option<Vec<f32>> = g_mlp.map(|g| g.iter().map(|&v| v.tanh()).collect());
996        // Pool-parallel row helpers: dst = rms(src)·w · (1+s)  and
997        // x += gate ⊙ rms(src)·w. Same math and per-row summation
998        // order as the serial loops — rows are independent, so the
999        // parallel split is bit-exact.
1000        let norm_scaled = |src: &[f32], w: &[f32], s: Option<&[f32]>, dst: &mut [f32]| {
1001            let sr = SendRows(dst.as_mut_ptr());
1002            pool_rows(pool, n, &|start, end| {
1003                for p in start..end {
1004                    // SAFETY: workers cover disjoint row ranges.
1005                    let row = unsafe { sr.row(p * hs, hs) };
1006                    rms_norm_into(&src[p * hs..(p + 1) * hs], w, self.eps, row);
1007                    if let Some(s) = s {
1008                        for (r, &sc) in row.iter_mut().zip(s) {
1009                            *r *= 1.0 + sc;
1010                        }
1011                    }
1012                }
1013            });
1014        };
1015        let residual = |src: &[f32], w: &[f32], gate: Option<&[f32]>, x: &mut [f32]| {
1016            let sr = SendRows(x.as_mut_ptr());
1017            pool_rows(pool, n, &|start, end| {
1018                let mut tmp = vec![0f32; hs];
1019                for p in start..end {
1020                    rms_norm_into(&src[p * hs..(p + 1) * hs], w, self.eps, &mut tmp);
1021                    // SAFETY: workers cover disjoint row ranges.
1022                    let dst = unsafe { sr.row(p * hs, hs) };
1023                    match gate {
1024                        Some(g) => {
1025                            for ((d, &v), &gt) in dst.iter_mut().zip(&tmp).zip(g) {
1026                                *d += gt * v;
1027                            }
1028                        }
1029                        None => {
1030                            for (d, &v) in dst.iter_mut().zip(&tmp) {
1031                                *d += v;
1032                            }
1033                        }
1034                    }
1035                }
1036            });
1037        };
1038        // ── attention ──
1039        let mut xn = vec![0f32; n * hs];
1040        norm_scaled(x, &blk.norm1, s_msa, &mut xn);
1041        drop(modnorm);
1042        let mut q_all = vec![0f32; n * nh * hd];
1043        let mut k_all = vec![0f32; n * nkv * hd];
1044        let mut v_all = vec![0f32; n * nkv * hd];
1045        {
1046            let _s = prof::span(prof::QKV);
1047            // One submission for the three projections when the device
1048            // offers it: they share `xn`, so three uploads and three
1049            // waits a block were ceremony.
1050            let fused = match (&blk.q, &blk.k, &blk.v) {
1051                (Proj::Q(q), Proj::Q(k), Proj::Q(v))
1052                    if n >= 128 && crate::gpu::enabled_here() && !crate::gpu::mm_killed() =>
1053                {
1054                    match (q.model_arc(), q.model_idx(), k.model_idx(), v.model_idx()) {
1055                        (Some(m), Some(iq), Some(ik), Some(iv)) => crate::gpu::dit_qkv(
1056                            &m,
1057                            iq,
1058                            ik,
1059                            iv,
1060                            &xn,
1061                            n,
1062                            hs,
1063                            nh * hd,
1064                            nkv * hd,
1065                            &mut q_all,
1066                            &mut k_all,
1067                            &mut v_all,
1068                        ),
1069                        _ => false,
1070                    }
1071                }
1072                _ => false,
1073            };
1074            if !fused {
1075                blk.q.matmat(&xn, n, &mut q_all, pool);
1076                blk.k.matmat(&xn, n, &mut k_all, pool);
1077                blk.v.matmat(&xn, n, &mut v_all, pool);
1078            }
1079        }
1080        let rope_span = prof::span(prof::ROPE);
1081        // per-head qk-norm, then interleaved-pair RoPE
1082        let (cos, sin) = rope;
1083        let pairs = hd / 2;
1084        for (all, heads, w) in [
1085            (&mut q_all, nh, &blk.norm_q),
1086            (&mut k_all, nkv, &blk.norm_k),
1087        ] {
1088            let sr = SendRows(all.as_mut_ptr());
1089            pool_rows(pool, n, &|start, end| {
1090                for p in start..end {
1091                    for hh in 0..heads {
1092                        // SAFETY: workers cover disjoint token ranges.
1093                        let v = unsafe { sr.row((p * heads + hh) * hd, hd) };
1094                        rms_norm_inplace(v, w, 1e-5);
1095                        for j in 0..pairs {
1096                            let (c, s) = (cos[p * pairs + j], sin[p * pairs + j]);
1097                            let (a, b) = (v[2 * j] as f64, v[2 * j + 1] as f64);
1098                            v[2 * j] = (a * c - b * s) as f32;
1099                            v[2 * j + 1] = (a * s + b * c) as f32;
1100                        }
1101                    }
1102                }
1103            });
1104        }
1105        drop(rope_span);
1106        // full (bidirectional) softmax attention, GQA — per head:
1107        // scores = (Q·s)·Kᵀ and P·V as GEMMs (Accelerate/blocked),
1108        // pool-parallel row softmax between them. The naive
1109        // per-position loop was the depth wall: at 512px (1064
1110        // tokens) attention alone cost hundreds of serial GFLOP.
1111        let scale = 1.0 / (hd as f32).sqrt();
1112        let _hpk = nh / nkv;
1113        let mut attn = vec![0f32; n * nh * hd];
1114        if segs.len() > 1 {
1115            // Each sequence attends within itself. The slices are row
1116            // ranges of the same buffers, so the per-head math below is
1117            // the one the single-sequence path runs.
1118            let mut off = 0usize;
1119            for &ns in segs {
1120                let (qs, ks, vs) = (
1121                    &q_all[off * nh * hd..(off + ns) * nh * hd],
1122                    &k_all[off * nkv * hd..(off + ns) * nkv * hd],
1123                    &v_all[off * nkv * hd..(off + ns) * nkv * hd],
1124                );
1125                let dst = &mut attn[off * nh * hd..(off + ns) * nh * hd];
1126                if !self.gpu_attention(qs, ks, vs, ns, scale, dst) {
1127                    self.attention_seq(qs, ks, vs, ns, scale, dst);
1128                }
1129                off += ns;
1130            }
1131        } else if !self.gpu_attention(&q_all, &k_all, &v_all, n, scale, &mut attn) {
1132            self.attention_seq(&q_all, &k_all, &v_all, n, scale, &mut attn);
1133        }
1134        let mut proj = vec![0f32; n * hs];
1135        {
1136            let _s = prof::span(prof::OPROJ);
1137            blk.o.matmat(&attn, n, &mut proj, pool);
1138        }
1139        let modnorm = prof::span(prof::MODNORM);
1140        residual(&proj, &blk.norm2, gate_msa.as_deref(), x);
1141        // ── SwiGLU FFN ──
1142        norm_scaled(x, &blk.ffn_norm1, s_mlp, &mut xn);
1143        drop(modnorm);
1144        let mut d_all = vec![0f32; n * hs];
1145        let fused = {
1146            let _s = prof::span(prof::FFN);
1147            self.gpu_ffn(blk, &xn, n, &mut d_all)
1148        };
1149        if !fused {
1150            let inter = blk.w1.rows();
1151            let mut g_all = vec![0f32; n * inter];
1152            let mut u_all = vec![0f32; n * inter];
1153            {
1154                let _s = prof::span(prof::FFN);
1155                blk.w1.matmat(&xn, n, &mut g_all, pool);
1156                blk.w3.matmat(&xn, n, &mut u_all, pool);
1157            }
1158            {
1159                let _s = prof::span(prof::FFNEL);
1160                let sg = SendRows(g_all.as_mut_ptr());
1161                pool_rows(pool, n, &|start, end| {
1162                    for p in start..end {
1163                        // SAFETY: workers cover disjoint token ranges.
1164                        let g = unsafe { sg.row(p * inter, inter) };
1165                        for (gv, &uv) in g.iter_mut().zip(&u_all[p * inter..(p + 1) * inter]) {
1166                            *gv = silu(*gv) * uv;
1167                        }
1168                    }
1169                });
1170            }
1171            {
1172                let _s = prof::span(prof::FFN);
1173                blk.w2.matmat(&g_all, n, &mut d_all, pool);
1174            }
1175        }
1176        let _modnorm = prof::span(prof::MODNORM);
1177        residual(&d_all, &blk.ffn_norm2, gate_mlp.as_deref(), x);
1178        false
1179    }
1180
1181    /// One denoising forward: latent `[c, h, w]` (NCHW), caption
1182    /// features `[cap_n, cap_feat]`, timestep `t` ∈ [0,1] (the
1183    /// pipeline's `1 − σ`). Returns the velocity prediction `[c, h, w]`.
1184    pub fn forward(
1185        &self,
1186        latent: &[f32],
1187        h: usize,
1188        w: usize,
1189        cap: &[f32],
1190        cap_n: usize,
1191        t: f32,
1192    ) -> Vec<f32> {
1193        self.forward_with_cap(latent, h, w, &self.refine_caption(cap, cap_n), cap_n, t)
1194    }
1195
1196    /// Caption features → hidden, through the context refiner. Depends on
1197    /// NOTHING that moves during denoising — not the timestep, not the
1198    /// latents — so the whole thing is a constant of the prompt. The
1199    /// denoise loop hoists it out and hands the result to
1200    /// `forward_with_cap`; it used to be recomputed on every model call,
1201    /// which for 30 steps under CFG meant 60 evaluations of a value with
1202    /// two distinct instances.
1203    pub fn refine_caption(&self, cap: &[f32], cap_n: usize) -> Vec<f32> {
1204        let hs = self.hidden;
1205        let cap_feat = self.cap_norm.len();
1206        let mut cap_n_all = vec![0f32; cap_n * cap_feat];
1207        for i in 0..cap_n {
1208            cap_n_all[i * cap_feat..(i + 1) * cap_feat].copy_from_slice(&rms_norm(
1209                &cap[i * cap_feat..(i + 1) * cap_feat],
1210                &self.cap_norm,
1211                self.eps,
1212            ));
1213        }
1214        let mut cap_e = vec![0f32; cap_n * hs];
1215        self.cap_w
1216            .matmat(&cap_n_all, cap_n, &mut cap_e, self.pool.as_deref());
1217        for i in 0..cap_n {
1218            for (v, &b) in cap_e[i * hs..(i + 1) * hs].iter_mut().zip(&self.cap_b) {
1219                *v += b;
1220            }
1221        }
1222        let cap_ids: Vec<[u32; 3]> = (0..cap_n).map(|i| [i as u32, 0, 0]).collect();
1223        let cap_rope = rope_table(&cap_ids, &self.axes_dim);
1224        for blk in &self.context_refiner {
1225            self.block_forward(blk, &mut cap_e, &cap_rope, None, None);
1226        }
1227        cap_e
1228    }
1229
1230    /// The rest of the forward, from an already-refined caption.
1231    /// Classifier-free guidance in ONE pass: the conditional and the
1232    /// unconditional sequence go through the joint stack as a single
1233    /// batch. Every weight is read once for both — which is the whole
1234    /// cost on a CPU or a phone — and the image branch (patchify,
1235    /// x_embedder, the noise refiners) is computed once instead of
1236    /// twice, because it does not depend on the caption at all.
1237    /// Attention stays per sequence, so the two never mix and each
1238    /// prediction equals what the single-sequence path returns.
1239    #[allow(clippy::too_many_arguments)]
1240    pub fn forward_cfg_pair(
1241        &self,
1242        latent: &[f32],
1243        h: usize,
1244        w: usize,
1245        cap_c: &[f32],
1246        cap_c_n: usize,
1247        cap_u: &[f32],
1248        cap_u_n: usize,
1249        t: f32,
1250    ) -> (Vec<f32>, Vec<f32>) {
1251        let (c, p, hs) = (self.in_channels, self.patch, self.hidden);
1252        let (hp, wp) = (h / p, w / p);
1253        let n_img = hp * wp;
1254        let head = prof::span(prof::HEADTAIL);
1255        let temb = self.time_embed(t);
1256        let pv = p * p * c;
1257        let mut tok = vec![0f32; n_img * pv];
1258        for ph in 0..hp {
1259            for pw in 0..wp {
1260                let dst = &mut tok[(ph * wp + pw) * pv..(ph * wp + pw + 1) * pv];
1261                for dy in 0..p {
1262                    for dx in 0..p {
1263                        for ch in 0..c {
1264                            dst[(dy * p + dx) * c + ch] =
1265                                latent[ch * h * w + (ph * p + dy) * w + pw * p + dx];
1266                        }
1267                    }
1268                }
1269            }
1270        }
1271        let mut img = vec![0f32; n_img * hs];
1272        self.x_emb
1273            .matmat(&tok, n_img, &mut img, self.pool.as_deref());
1274        for i in 0..n_img {
1275            for (v, &b) in img[i * hs..(i + 1) * hs].iter_mut().zip(&self.x_emb_b) {
1276                *v += b;
1277            }
1278        }
1279        let img_ids: Vec<[u32; 3]> = (0..n_img)
1280            .map(|i| [0, (i / wp) as u32, (i % wp) as u32])
1281            .collect();
1282        let to32 = |r: &(Vec<f64>, Vec<f64>)| {
1283            (
1284                r.0.iter().map(|&v| v as f32).collect::<Vec<f32>>(),
1285                r.1.iter().map(|&v| v as f32).collect::<Vec<f32>>(),
1286            )
1287        };
1288        // The refiners see the image alone — same for both branches.
1289        let img_rope_r = rope_table(
1290            &(0..n_img)
1291                .map(|i| [0u32, (i / wp) as u32, (i % wp) as u32])
1292                .collect::<Vec<_>>(),
1293            &self.axes_dim,
1294        );
1295        let img_rope32_r = to32(&img_rope_r);
1296        drop(head);
1297        for blk in &self.noise_refiner {
1298            self.block_forward(blk, &mut img, &img_rope_r, Some(&img_rope32_r), Some(&temb));
1299        }
1300        let _ = img_ids;
1301        // Joint sequences, concatenated: [cap_c | img] then [cap_u | img].
1302        let n_c = cap_c_n + n_img;
1303        let n_u = cap_u_n + n_img;
1304        let mut x = Vec::with_capacity((n_c + n_u) * hs);
1305        x.extend_from_slice(&cap_c[..cap_c_n * hs]);
1306        x.extend_from_slice(&img);
1307        x.extend_from_slice(&cap_u[..cap_u_n * hs]);
1308        x.extend_from_slice(&img);
1309        let rope_for = |cap_n: usize| -> (Vec<f64>, Vec<f64>) {
1310            let cap_ids: Vec<[u32; 3]> = (0..cap_n).map(|i| [i as u32, 0, 0]).collect();
1311            let im_ids: Vec<[u32; 3]> = (0..n_img)
1312                .map(|i| [cap_n as u32, (i / wp) as u32, (i % wp) as u32])
1313                .collect();
1314            let a = rope_table(&cap_ids, &self.axes_dim);
1315            let b = rope_table(&im_ids, &self.axes_dim);
1316            ([a.0, b.0].concat(), [a.1, b.1].concat())
1317        };
1318        let (rc, ru) = (rope_for(cap_c_n), rope_for(cap_u_n));
1319        let joint_rope = ([rc.0, ru.0].concat(), [rc.1, ru.1].concat());
1320        let joint_rope32 = to32(&joint_rope);
1321        let segs = [n_c, n_u];
1322        // The state stays on the device for the whole stack: nothing here
1323        // reads `x` between blocks, so uploading and reading back 19 MB
1324        // around each of 28 blocks was pure round trip. Only the first
1325        // block uploads and only the last reads back.
1326        let chain = crate::gpu::dit_chain_supported();
1327        let last = self.layers.len().saturating_sub(1);
1328        let mut resident = false;
1329        for (i, blk) in self.layers.iter().enumerate() {
1330            let want = if chain {
1331                (resident, i != last)
1332            } else {
1333                (false, false)
1334            };
1335            // A block the device declines runs on the host and leaves the
1336            // state there, so the next one must upload again.
1337            let on_gpu = self.block_forward_seg(
1338                blk,
1339                &mut x,
1340                &joint_rope,
1341                Some(&joint_rope32),
1342                Some(&temb),
1343                &segs,
1344                want,
1345            );
1346            resident = on_gpu && want.1;
1347        }
1348        let _tail = prof::span(prof::HEADTAIL);
1349        let n = n_c + n_u;
1350        let silu_t: Vec<f32> = temb.iter().map(|&v| silu(v)).collect();
1351        let scale = linear(&silu_t, &self.out_lin1_w, &self.out_lin1_b);
1352        for row in x.chunks_exact_mut(hs) {
1353            let mean = row.iter().map(|&v| v as f64).sum::<f64>() / hs as f64;
1354            let var = row
1355                .iter()
1356                .map(|&v| (v as f64 - mean) * (v as f64 - mean))
1357                .sum::<f64>()
1358                / hs as f64;
1359            let inv = 1.0 / (var + 1e-6).sqrt();
1360            for (v, &s) in row.iter_mut().zip(&scale) {
1361                *v = ((*v as f64 - mean) * inv) as f32 * (1.0 + s);
1362            }
1363        }
1364        let mut out = vec![0f32; n * pv];
1365        self.out_lin2.matmat(&x, n, &mut out, self.pool.as_deref());
1366        for i in 0..n {
1367            for (v, &b) in out[i * pv..(i + 1) * pv].iter_mut().zip(&self.out_lin2_b) {
1368                *v += b;
1369            }
1370        }
1371        let unpatch = |base: usize| -> Vec<f32> {
1372            let mut pred = vec![0f32; c * h * w];
1373            for ph in 0..hp {
1374                for pw in 0..wp {
1375                    let src = &out[(base + ph * wp + pw) * pv..(base + ph * wp + pw + 1) * pv];
1376                    for dy in 0..p {
1377                        for dx in 0..p {
1378                            for ch in 0..c {
1379                                pred[ch * h * w + (ph * p + dy) * w + pw * p + dx] =
1380                                    src[(dy * p + dx) * c + ch];
1381                            }
1382                        }
1383                    }
1384                }
1385            }
1386            pred
1387        };
1388        (unpatch(cap_c_n), unpatch(n_c + cap_u_n))
1389    }
1390
1391    pub fn forward_with_cap(
1392        &self,
1393        latent: &[f32],
1394        h: usize,
1395        w: usize,
1396        cap_e_in: &[f32],
1397        cap_n: usize,
1398        t: f32,
1399    ) -> Vec<f32> {
1400        let (c, p, hs) = (self.in_channels, self.patch, self.hidden);
1401        assert_eq!(latent.len(), c * h * w);
1402        let (hp, wp) = (h / p, w / p);
1403        let n_img = hp * wp;
1404        let head = prof::span(prof::HEADTAIL);
1405        let temb = self.time_embed(t);
1406        let cap_e = cap_e_in.to_vec();
1407
1408        // patchify (dy, dx, ch inner order) + x_embedder
1409        let pv = p * p * c;
1410        let mut tok = vec![0f32; n_img * pv];
1411        for ph in 0..hp {
1412            for pw in 0..wp {
1413                let dst = &mut tok[(ph * wp + pw) * pv..(ph * wp + pw + 1) * pv];
1414                for dy in 0..p {
1415                    for dx in 0..p {
1416                        for ch in 0..c {
1417                            dst[(dy * p + dx) * c + ch] =
1418                                latent[ch * h * w + (ph * p + dy) * w + pw * p + dx];
1419                        }
1420                    }
1421                }
1422            }
1423        }
1424        let mut img = vec![0f32; n_img * hs];
1425        self.x_emb
1426            .matmat(&tok, n_img, &mut img, self.pool.as_deref());
1427        for i in 0..n_img {
1428            for (v, &b) in img[i * hs..(i + 1) * hs].iter_mut().zip(&self.x_emb_b) {
1429                *v += b;
1430            }
1431        }
1432
1433        // 3-axis position ids: caption (i,0,0), image (cap_n, row, col)
1434        let cap_ids: Vec<[u32; 3]> = (0..cap_n).map(|i| [i as u32, 0, 0]).collect();
1435        let img_ids: Vec<[u32; 3]> = (0..n_img)
1436            .map(|i| [cap_n as u32, (i / wp) as u32, (i % wp) as u32])
1437            .collect();
1438        let cap_rope = rope_table(&cap_ids, &self.axes_dim);
1439        let img_rope = rope_table(&img_ids, &self.axes_dim);
1440        // f32 twins for the on-device block (values stay f64-derived).
1441        let to32 = |r: &(Vec<f64>, Vec<f64>)| {
1442            (
1443                r.0.iter().map(|&v| v as f32).collect::<Vec<f32>>(),
1444                r.1.iter().map(|&v| v as f32).collect::<Vec<f32>>(),
1445            )
1446        };
1447        let img_rope32 = to32(&img_rope);
1448        drop(head);
1449
1450        // The context refiner already ran in `refine_caption` — it is a
1451        // constant of the prompt, not of this call.
1452        for blk in &self.noise_refiner {
1453            self.block_forward(blk, &mut img, &img_rope, Some(&img_rope32), Some(&temb));
1454        }
1455
1456        // joint sequence: caption first
1457        let n = cap_n + n_img;
1458        let mut x = cap_e;
1459        x.extend_from_slice(&img);
1460        let joint_rope = (
1461            [cap_rope.0, img_rope.0].concat(),
1462            [cap_rope.1, img_rope.1].concat(),
1463        );
1464        let joint_rope32 = to32(&joint_rope);
1465        for blk in &self.layers {
1466            self.block_forward(blk, &mut x, &joint_rope, Some(&joint_rope32), Some(&temb));
1467        }
1468
1469        // norm_out: LayerNorm(eps 1e-6, no affine) · (1+scale), project
1470        let _tail = prof::span(prof::HEADTAIL);
1471        let silu_t: Vec<f32> = temb.iter().map(|&v| silu(v)).collect();
1472        let scale = linear(&silu_t, &self.out_lin1_w, &self.out_lin1_b);
1473        for row in x.chunks_exact_mut(hs) {
1474            let mean = row.iter().map(|&v| v as f64).sum::<f64>() / hs as f64;
1475            let var = row
1476                .iter()
1477                .map(|&v| (v as f64 - mean) * (v as f64 - mean))
1478                .sum::<f64>()
1479                / hs as f64;
1480            let inv = 1.0 / (var + 1e-6).sqrt();
1481            for (v, &s) in row.iter_mut().zip(&scale) {
1482                *v = ((*v as f64 - mean) * inv) as f32 * (1.0 + s);
1483            }
1484        }
1485        let mut out = vec![0f32; n * pv];
1486        self.out_lin2.matmat(&x, n, &mut out, self.pool.as_deref());
1487        for i in 0..n {
1488            for (v, &b) in out[i * pv..(i + 1) * pv].iter_mut().zip(&self.out_lin2_b) {
1489                *v += b;
1490            }
1491        }
1492
1493        // unpatchify image tokens → [c, h, w]
1494        let mut pred = vec![0f32; c * h * w];
1495        for ph in 0..hp {
1496            for pw in 0..wp {
1497                let src = &out[(cap_n + ph * wp + pw) * pv..(cap_n + ph * wp + pw + 1) * pv];
1498                for dy in 0..p {
1499                    for dx in 0..p {
1500                        for ch in 0..c {
1501                            pred[ch * h * w + (ph * p + dy) * w + pw * p + dx] =
1502                                src[(dy * p + dx) * c + ch];
1503                        }
1504                    }
1505                }
1506            }
1507        }
1508        pred
1509    }
1510}