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