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