Skip to main content

cortiq_engine/
zimage.rs

1//! Z-Image DiT (Tongyi-MAI Z-Image and Z-Image-Turbo: one 6.15B
2//! single-stream `ZImageTransformer2DModel`) — the CPU-exact reference
3//! path and the host glue around the `gpu::zimage_*` contract.
4//!
5//! New code, NOT a mode of Lumina (`dit.rs` stays bit-identical): helpers
6//! are copied, never shared. Sequence order is diffusers' [img, cap], so
7//! every intermediate lines up row-for-row with the oracle dumps
8//! (`python/zimage_oracle.py`).
9//!
10//! Semantics (diffusers 0.40 `transformer_z_image.py`): adaLN has NO SiLU
11//! before it (the final layer does); t is scaled by 1000; RoPE θ = 256,
12//! axes [32, 48, 48], complex-interleaved; caption ids (1+j, 0, 0) over
13//! the padded L_p, image ids (L_p+1, r, c), image pad ids (0, 0, 0); pad
14//! rows are real keys/queries (no mask at batch 1); caption pad rows :=
15//! cap_pad_token after the embedder, image pad rows := x_pad_token after
16//! the embed.
17//!
18//! Precision: every f32 op that diffusers runs in f32 on the host side
19//! (sinusoid args, σ schedule, RoPE angles) is reproduced in f32 in the
20//! same order; reductions (norms, the small linears) accumulate in f64.
21//!
22//! Per image the host precomputes everything latent-independent: caption
23//! embed + context refiner (per prompt), the modulation of all steps, the
24//! rope tables (per prompt and resolution). Per step the device (or
25//! `step_cpu`) runs exactly x_embed → 2 noise refiners → 30 layers → final.
26
27use crate::dit::Proj;
28use crate::gpu::{ZBlockRef, ZGeom};
29use crate::pool::Pool;
30use cortiq_core::CmfModel;
31use std::collections::HashMap;
32use std::path::Path;
33use std::sync::Arc;
34
35/// Sequence padding multiple (diffusers `SEQ_MULTI_OF`).
36pub const SEQ_MULTI_OF: usize = 32;
37/// Turbo scheduler shift (static, `use_dynamic_shifting: false`). The
38/// base model's scheduler uses 6.0; the container stores its own.
39pub const DEFAULT_SHIFT: f32 = 3.0;
40/// Default number of DiT forwards (Turbo).
41pub const DEFAULT_STEPS: usize = 8;
42
43/// The Turbo geometry — for random-weight kernel tests in WP2/WP3 that
44/// must not wait for a container.
45pub const ZGEOM_TURBO: ZGeom = ZGeom {
46    hidden: 3840,
47    nh: 30,
48    hd: 128,
49    inter: 10240,
50    eps: 1e-5,
51    final_eps: 1e-6,
52    patch_dim: 64,
53};
54
55/// `dit.config_json` / transformer `config.json` fields the runtime uses.
56#[derive(Clone, Debug)]
57pub struct ZConfig {
58    /// 3840
59    pub dim: usize,
60    /// 30 main layers
61    pub n_layers: usize,
62    /// 2 noise-refiner and 2 context-refiner blocks
63    pub n_refiner: usize,
64    /// 30 heads, 30 kv heads, head_dim 128
65    pub n_heads: usize,
66    pub n_kv_heads: usize,
67    pub head_dim: usize,
68    /// int(dim / 3 · 8) = 10240
69    pub ffn_dim: usize,
70    /// 2560 (Qwen3-4B hidden)
71    pub cap_feat_dim: usize,
72    /// 256 (`ADALN_EMBED_DIM`) and the t-embedder MLP width 1024
73    pub t_embed_dim: usize,
74    pub t_hidden: usize,
75    /// patch 2, latent channels 16 → patch vector 64
76    pub patch: usize,
77    pub in_channels: usize,
78    /// 1e-5 (all RMSNorms), 1e-6 (final LayerNorm)
79    pub norm_eps: f32,
80    pub final_eps: f32,
81    /// 256.0 and [32, 48, 48]
82    pub rope_theta: f64,
83    pub axes_dims: [usize; 3],
84    /// 1000.0
85    pub t_scale: f32,
86}
87
88impl ZConfig {
89    /// Parse the diffusers transformer `config.json` (the same JSON is
90    /// stored as `dit.config_json` in the container).
91    pub fn from_json(v: &serde_json::Value) -> Result<Self, String> {
92        let u = |k: &str| -> Result<usize, String> {
93            v[k].as_u64()
94                .map(|x| x as usize)
95                .ok_or_else(|| format!("dit config: missing {k}"))
96        };
97        let dim = u("dim")?;
98        let n_heads = u("n_heads")?;
99        let axes: Vec<usize> = v["axes_dims"]
100            .as_array()
101            .ok_or("dit config: axes_dims")?
102            .iter()
103            .map(|x| x.as_u64().unwrap_or(0) as usize)
104            .collect();
105        if axes.len() != 3 {
106            return Err("dit config: axes_dims must have 3 entries".into());
107        }
108        let patch = v["all_patch_size"]
109            .as_array()
110            .and_then(|a| a.first())
111            .and_then(|x| x.as_u64())
112            .unwrap_or(2) as usize;
113        let head_dim = dim / n_heads;
114        if axes.iter().sum::<usize>() != head_dim {
115            return Err(format!(
116                "dit config: axes_dims {axes:?} do not sum to head_dim {head_dim}"
117            ));
118        }
119        Ok(Self {
120            dim,
121            n_layers: u("n_layers")?,
122            n_refiner: v["n_refiner_layers"].as_u64().unwrap_or(2) as usize,
123            n_heads,
124            n_kv_heads: v["n_kv_heads"].as_u64().unwrap_or(n_heads as u64) as usize,
125            head_dim,
126            ffn_dim: (dim as f64 / 3.0 * 8.0) as usize,
127            cap_feat_dim: u("cap_feat_dim")?,
128            t_embed_dim: dim.min(256),
129            t_hidden: 1024,
130            patch,
131            in_channels: v["in_channels"].as_u64().unwrap_or(16) as usize,
132            norm_eps: v["norm_eps"].as_f64().unwrap_or(1e-5) as f32,
133            final_eps: 1e-6,
134            rope_theta: v["rope_theta"].as_f64().unwrap_or(256.0),
135            axes_dims: [axes[0], axes[1], axes[2]],
136            t_scale: v["t_scale"].as_f64().unwrap_or(1000.0) as f32,
137        })
138    }
139
140    pub fn geom(&self) -> ZGeom {
141        ZGeom {
142            hidden: self.dim,
143            nh: self.n_heads,
144            hd: self.head_dim,
145            inter: self.ffn_dim,
146            eps: self.norm_eps,
147            final_eps: self.final_eps,
148            patch_dim: self.patch * self.patch * self.in_channels,
149        }
150    }
151
152    /// Blocks carrying adaLN: the noise refiner, then the main layers.
153    pub fn n_mod_blocks(&self) -> usize {
154        self.n_refiner + self.n_layers
155    }
156}
157
158/// Token geometry of one (resolution, prompt length).
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct ZShape {
161    /// Latent size (H/8, W/8).
162    pub h_lat: usize,
163    pub w_lat: usize,
164    /// Patch grid (H/16, W/16).
165    pub grid: (usize, usize),
166    /// n_img = grid.0 · grid.1, n_img_p = ceil32(n_img).
167    pub n_img: usize,
168    pub n_img_p: usize,
169    /// Caption tokens L and ceil32(L).
170    pub l: usize,
171    pub l_p: usize,
172}
173
174impl ZShape {
175    /// From the image size in pixels (multiples of 16) and the caption length.
176    pub fn new(height: usize, width: usize, l: usize) -> Self {
177        let grid = (height / 16, width / 16);
178        let n_img = grid.0 * grid.1;
179        Self {
180            h_lat: height / 8,
181            w_lat: width / 8,
182            grid,
183            n_img,
184            n_img_p: ceil32(n_img),
185            l,
186            l_p: ceil32(l),
187        }
188    }
189
190    /// S = n_img_p + l_p.
191    pub fn seq(&self) -> usize {
192        self.n_img_p + self.l_p
193    }
194}
195
196/// RoPE tables, each `(cos, sin)` of [rows · hd/2] f32.
197pub struct ZRope {
198    /// Noise refiner, [n_img_p] rows (image ids).
199    pub img: (Vec<f32>, Vec<f32>),
200    /// Main layers, [n_img_p + l_p] rows ordered [img, cap].
201    pub joint: (Vec<f32>, Vec<f32>),
202    /// Context refiner, [l_p] rows (caption ids).
203    pub cap: (Vec<f32>, Vec<f32>),
204}
205
206/// Which block of the model.
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum ZBlockId {
209    NoiseRefiner(usize),
210    ContextRefiner(usize),
211    Layer(usize),
212}
213
214/// Device views of every block (for `ZPrepareArgs` and
215/// `gpu::zimage_refine_caption`). Borrowed from a `ZImageDit::from_cmf`.
216pub struct ZBlockRefs<'a> {
217    pub noise_refiner: Vec<ZBlockRef<'a>>,
218    pub context_refiner: Vec<ZBlockRef<'a>>,
219    pub layers: Vec<ZBlockRef<'a>>,
220}
221
222/// Per-(prompt, resolution) state: the refined caption and the rope
223/// tables, plus whether the device accepted `gpu::zimage_prepare`.
224pub struct ZPrepared {
225    pub key: u64,
226    pub shape: ZShape,
227    /// [l_p, dim], after cap_embedder, pad-token rows and the context refiner.
228    pub cap: Vec<f32>,
229    pub rope: ZRope,
230    pub device: bool,
231}
232
233/// One transformer block. `adaln` is None for the context refiner.
234struct ZBlock {
235    /// `adaLN_modulation.0`: weight [4·dim, 256] f32 (host precompute only),
236    /// bias [4·dim].
237    adaln: Option<(Vec<f32>, Vec<f32>)>,
238    norm1: Vec<f32>,
239    norm2: Vec<f32>,
240    ffn_norm1: Vec<f32>,
241    ffn_norm2: Vec<f32>,
242    norm_q: Vec<f32>,
243    norm_k: Vec<f32>,
244    q: Proj,
245    k: Proj,
246    v: Proj,
247    o: Proj,
248    w1: Proj,
249    w2: Proj,
250    w3: Proj,
251    /// Tensor indices [q, k, v, o, w1, w3, w2] in the container.
252    idx: Option<[usize; 7]>,
253}
254
255/// The Z-Image transformer. Tensor names are the ORIGINAL diffusers names
256/// under `dit.` (no rename map).
257pub struct ZImageDit {
258    pub cfg: ZConfig,
259    /// The container this was loaded from (None for `load_dir`) — device
260    /// paths need tensor indices into it.
261    pub model: Option<Arc<CmfModel>>,
262    x_emb_w: Vec<f32>, // [dim, 64]
263    x_emb_b: Vec<f32>,
264    x_pad: Vec<f32>,   // [dim]
265    cap_pad: Vec<f32>, // [dim]
266    t_w0: Vec<f32>,    // [1024, 256]
267    t_b0: Vec<f32>,
268    t_w2: Vec<f32>, // [256, 1024]
269    t_b2: Vec<f32>,
270    cap_norm: Vec<f32>, // [cap_feat]
271    cap_w: Proj,        // [dim, cap_feat]
272    cap_b: Vec<f32>,
273    final_mod_w: Vec<f32>, // [dim, 256]
274    final_mod_b: Vec<f32>,
275    final_w: Vec<f32>, // [64, dim]
276    final_b: Vec<f32>,
277    noise_refiner: Vec<ZBlock>,
278    context_refiner: Vec<ZBlock>,
279    layers: Vec<ZBlock>,
280    pool: Option<Arc<Pool>>,
281}
282
283// ───────────────────────────── small helpers (copied, not shared) ─────
284
285struct SendRows(*mut f32);
286unsafe impl Send for SendRows {}
287unsafe impl Sync for SendRows {}
288impl SendRows {
289    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
290    #[allow(clippy::mut_from_ref)]
291    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
292        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
293    }
294    unsafe fn set(&self, off: usize, v: f32) {
295        unsafe { *self.0.add(off) = v }
296    }
297}
298
299fn pool_rows(pool: Option<&Pool>, n: usize, f: &(dyn Fn(usize, usize) + Sync)) {
300    match pool {
301        Some(p) => p.run_rows(n, f),
302        None => f(0, n),
303    }
304}
305
306fn silu(v: f32) -> f32 {
307    v / (1.0 + (-v).exp())
308}
309
310/// diffusers RMSNorm (plain w), f64 accumulation, into `dst`.
311fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
312    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
313    let inv = 1.0 / (ss + eps).sqrt();
314    for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
315        *d = (v as f64 * inv) as f32 * g;
316    }
317}
318
319fn rms_norm_inplace(v: &mut [f32], w: &[f32], eps: f64) {
320    let ss = v.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>() / v.len() as f64;
321    let inv = 1.0 / (ss + eps).sqrt();
322    for (x, &g) in v.iter_mut().zip(w) {
323        *x = (*x as f64 * inv) as f32 * g;
324    }
325}
326
327/// y = x·Wᵀ + b for ONE row, f64 accumulation, pool-parallel over outputs.
328/// `linear_row` for several inputs at once (each weight row read once);
329/// bit-identical per input to `linear_row`.
330fn linear_rows_multi(xs: &[Vec<f32>], w: &[f32], b: &[f32], pool: Option<&Pool>) -> Vec<Vec<f32>> {
331    let rows = b.len();
332    let n = xs.len();
333    let mut out = vec![0f32; n * rows];
334    let op = SendRows(out.as_mut_ptr());
335    pool_rows(pool, rows, &|lo, hi| {
336        for o in lo..hi {
337            for (si, x) in xs.iter().enumerate() {
338                let k = x.len();
339                let row = &w[o * k..(o + 1) * k];
340                let s: f64 = row.iter().zip(x).map(|(&a, &c)| a as f64 * c as f64).sum();
341                // SAFETY: disjoint output indices per worker.
342                unsafe { op.set(si * rows + o, (s + b[o] as f64) as f32) };
343            }
344        }
345    });
346    out.chunks(rows.max(1)).map(|c| c.to_vec()).collect()
347}
348
349fn linear_row(x: &[f32], w: &[f32], b: &[f32], pool: Option<&Pool>) -> Vec<f32> {
350    let k = x.len();
351    let rows = b.len();
352    debug_assert_eq!(w.len(), rows * k);
353    let mut out = vec![0f32; rows];
354    let op = SendRows(out.as_mut_ptr());
355    pool_rows(pool, rows, &|lo, hi| {
356        for o in lo..hi {
357            let row = &w[o * k..(o + 1) * k];
358            let s: f64 = row
359                .iter()
360                .zip(x)
361                .map(|(&a, &c)| a as f64 * c as f64)
362                .sum();
363            // SAFETY: disjoint output indices per worker.
364            unsafe { op.set(o, (s + b[o] as f64) as f32) };
365        }
366    });
367    out
368}
369
370/// Row softmax, in place (f32, max-subtracted; f64 denominator).
371fn softmax_inplace(row: &mut [f32]) {
372    let mx = row.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
373    let mut den = 0f64;
374    for r in row.iter_mut() {
375        *r = (*r - mx).exp();
376        den += *r as f64;
377    }
378    let inv = (1.0 / den) as f32;
379    for r in row.iter_mut() {
380        *r *= inv;
381    }
382}
383
384/// The host GEMM of the reference path: y[n, m] = x[n, k] · w[m, k]ᵀ.
385///
386/// On x86-64 with AVX2+FMA a register-blocked kernel (3×3 micro-tiles of
387/// 8-wide FMAs, K blocked by 256, tiles pool-parallel); the engine's
388/// `fcd_ops::gemm_nt` only blocks under AVX-512, and on an AVX2 EPYC its
389/// per-output dot ran the 6B DiT at ~0.17 TFLOP/s on 14 threads. Elsewhere
390/// (aarch64: Accelerate/NEON) it defers to `fcd_ops::gemm_nt`. f32
391/// accumulation; the summation order differs from torch only by rounding.
392/// `CMF_ZIMAGE_HOST_GEMM=engine` forces `fcd_ops::gemm_nt` everywhere.
393pub(crate) mod host_gemm {
394    use crate::pool::Pool;
395
396    const KC: usize = 256;
397    const MB: usize = 48;
398    const NB: usize = 48;
399
400    fn native() -> bool {
401        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
402        *ON.get_or_init(|| {
403            if matches!(std::env::var("CMF_ZIMAGE_HOST_GEMM").as_deref(), Ok("engine")) {
404                return false;
405            }
406            #[cfg(target_arch = "x86_64")]
407            {
408                std::arch::is_x86_feature_detected!("avx2")
409                    && std::arch::is_x86_feature_detected!("fma")
410            }
411            #[cfg(not(target_arch = "x86_64"))]
412            {
413                false
414            }
415        })
416    }
417
418    struct YPtr(*mut f32);
419    unsafe impl Send for YPtr {}
420    unsafe impl Sync for YPtr {}
421
422    /// y[i·ldy + j] = Σ_k x[i·k + ..]·w[j·k + ..] for i < n, j < m.
423    /// `y` rows are `ldy` apart (≥ m): a column slice of a wider output.
424    pub(crate) fn gemm_nt_ld(
425        x: &[f32],
426        w: &[f32],
427        y: &mut [f32],
428        ldy: usize,
429        n: usize,
430        k: usize,
431        m: usize,
432        pool: Option<&Pool>,
433    ) {
434        debug_assert!(x.len() >= n * k && w.len() >= m * k);
435        debug_assert!(n == 0 || y.len() >= (n - 1) * ldy + m);
436        #[cfg(target_arch = "x86_64")]
437        if native() && k % 8 == 0 {
438            for i in 0..n {
439                y[i * ldy..i * ldy + m].fill(0.0);
440            }
441            let (ti, tj) = (n.div_ceil(MB), m.div_ceil(NB));
442            let yp = YPtr(y.as_mut_ptr());
443            let work = |lo: usize, hi: usize| {
444                let yp = &yp;
445                for t in lo..hi {
446                    let (bi, bj) = (t / tj, t % tj);
447                    let (i0, i1) = (bi * MB, ((bi + 1) * MB).min(n));
448                    let (j0, j1) = (bj * NB, ((bj + 1) * NB).min(m));
449                    // SAFETY: ISA checked; tiles write disjoint y blocks.
450                    unsafe { tile(x, w, yp.0, ldy, k, i0, i1, j0, j1) };
451                }
452            };
453            match pool {
454                Some(p) => p.run_rows(ti * tj, &work),
455                None => work(0, ti * tj),
456            }
457            return;
458        }
459        if ldy == m {
460            crate::fcd_ops::gemm_nt(&x[..n * k], &w[..m * k], &mut y[..n * m], n, k, m, pool);
461        } else {
462            let mut tmp = vec![0f32; n * m];
463            crate::fcd_ops::gemm_nt(&x[..n * k], &w[..m * k], &mut tmp, n, k, m, pool);
464            for i in 0..n {
465                y[i * ldy..i * ldy + m].copy_from_slice(&tmp[i * m..(i + 1) * m]);
466            }
467        }
468    }
469
470    pub(crate) fn gemm_nt(
471        x: &[f32],
472        w: &[f32],
473        y: &mut [f32],
474        n: usize,
475        k: usize,
476        m: usize,
477        pool: Option<&Pool>,
478    ) {
479        gemm_nt_ld(x, w, y, m, n, k, m, pool)
480    }
481
482    #[cfg(target_arch = "x86_64")]
483    #[target_feature(enable = "avx2,fma")]
484    unsafe fn hsum(v: std::arch::x86_64::__m256) -> f32 {
485        use std::arch::x86_64::*;
486        let lo = _mm256_castps256_ps128(v);
487        let hi = _mm256_extractf128_ps(v, 1);
488        let s = _mm_add_ps(lo, hi);
489        let s = _mm_add_ps(s, _mm_movehl_ps(s, s));
490        let s = _mm_add_ss(s, _mm_shuffle_ps(s, s, 1));
491        _mm_cvtss_f32(s)
492    }
493
494    #[cfg(target_arch = "x86_64")]
495    #[target_feature(enable = "avx2,fma")]
496    #[allow(clippy::too_many_arguments)]
497    unsafe fn tile(
498        x: &[f32],
499        w: &[f32],
500        y: *mut f32,
501        ldy: usize,
502        k: usize,
503        i0: usize,
504        i1: usize,
505        j0: usize,
506        j1: usize,
507    ) {
508        use std::arch::x86_64::*;
509        let xp = x.as_ptr();
510        let wp = w.as_ptr();
511        let mut kb = 0;
512        while kb < k {
513            let kl = (k - kb).min(KC);
514            let mut i = i0;
515            while i < i1 {
516                let ih = (i1 - i).min(3);
517                let mut j = j0;
518                while j < j1 {
519                    let jh = (j1 - j).min(3);
520                    if ih == 3 && jh == 3 {
521                        let (x0, x1, x2) = (
522                            xp.add(i * k + kb),
523                            xp.add((i + 1) * k + kb),
524                            xp.add((i + 2) * k + kb),
525                        );
526                        let (w0, w1, w2) = (
527                            wp.add(j * k + kb),
528                            wp.add((j + 1) * k + kb),
529                            wp.add((j + 2) * k + kb),
530                        );
531                        let mut a = [_mm256_setzero_ps(); 9];
532                        let mut kk = 0;
533                        while kk < kl {
534                            let b0 = _mm256_loadu_ps(w0.add(kk));
535                            let b1 = _mm256_loadu_ps(w1.add(kk));
536                            let b2 = _mm256_loadu_ps(w2.add(kk));
537                            let r0 = _mm256_loadu_ps(x0.add(kk));
538                            a[0] = _mm256_fmadd_ps(r0, b0, a[0]);
539                            a[1] = _mm256_fmadd_ps(r0, b1, a[1]);
540                            a[2] = _mm256_fmadd_ps(r0, b2, a[2]);
541                            let r1 = _mm256_loadu_ps(x1.add(kk));
542                            a[3] = _mm256_fmadd_ps(r1, b0, a[3]);
543                            a[4] = _mm256_fmadd_ps(r1, b1, a[4]);
544                            a[5] = _mm256_fmadd_ps(r1, b2, a[5]);
545                            let r2 = _mm256_loadu_ps(x2.add(kk));
546                            a[6] = _mm256_fmadd_ps(r2, b0, a[6]);
547                            a[7] = _mm256_fmadd_ps(r2, b1, a[7]);
548                            a[8] = _mm256_fmadd_ps(r2, b2, a[8]);
549                            kk += 8;
550                        }
551                        for r in 0..3 {
552                            for c in 0..3 {
553                                *y.add((i + r) * ldy + j + c) += hsum(a[r * 3 + c]);
554                            }
555                        }
556                    } else {
557                        for r in 0..ih {
558                            for c in 0..jh {
559                                let (xr, wr) = (xp.add((i + r) * k + kb), wp.add((j + c) * k + kb));
560                                let mut acc = _mm256_setzero_ps();
561                                let mut kk = 0;
562                                while kk < kl {
563                                    acc = _mm256_fmadd_ps(
564                                        _mm256_loadu_ps(xr.add(kk)),
565                                        _mm256_loadu_ps(wr.add(kk)),
566                                        acc,
567                                    );
568                                    kk += 8;
569                                }
570                                *y.add((i + r) * ldy + j + c) += hsum(acc);
571                            }
572                        }
573                    }
574                    j += jh;
575                }
576                i += ih;
577            }
578            kb += kl;
579        }
580    }
581}
582
583/// y[n, rows] = x[n, cols] · Wᵀ for a projection of any codec, on the host
584/// reference GEMM: f32 weights directly; a quantized weight is dequantized
585/// to f32 in row chunks (weight-only error — the activations stay f32, as
586/// on the device paths). `CMF_ZIMAGE_HOST_GEMM=engine` uses the engine's
587/// own `Proj::matmat` (its quantized CPU kernels and per-op device arms).
588fn lin(p: &Proj, x: &[f32], n: usize, y: &mut [f32], pool: Option<&Pool>) {
589    if matches!(std::env::var("CMF_ZIMAGE_HOST_GEMM").as_deref(), Ok("engine")) {
590        return p.matmat(x, n, y, pool);
591    }
592    let (rows, cols) = (p.rows(), p.cols());
593    match p {
594        Proj::F32 { w, .. } => host_gemm::gemm_nt(x, w, y, n, cols, rows, pool),
595        Proj::Q(q) => {
596            const CH: usize = 768;
597            let mut wbuf = vec![0f32; CH.min(rows) * cols];
598            let mut r0 = 0;
599            while r0 < rows {
600                let rc = CH.min(rows - r0);
601                {
602                    let wp = SendRows(wbuf.as_mut_ptr());
603                    pool_rows(pool, rc, &|lo, hi| {
604                        for r in lo..hi {
605                            // SAFETY: disjoint rows.
606                            q.row_f32(r0 + r, unsafe { wp.row(r * cols, cols) });
607                        }
608                    });
609                }
610                host_gemm::gemm_nt_ld(x, &wbuf[..rc * cols], &mut y[r0..], rows, n, cols, rc, pool);
611                r0 += rc;
612            }
613        }
614    }
615}
616
617/// Any CMF entry → f32.
618fn cmf_f32(model: &CmfModel, name: &str) -> Result<Vec<f32>, String> {
619    crate::dit::cmf_f32(model, name)
620}
621
622/// ceil to a multiple of `SEQ_MULTI_OF`.
623pub fn ceil32(n: usize) -> usize {
624    n.div_ceil(SEQ_MULTI_OF) * SEQ_MULTI_OF
625}
626
627/// torch.linspace(start, end, n) in f32, the CPU kernel's order: `start +
628/// step·i` for i < n/2, `end − step·(n−1−i)` otherwise.
629fn linspace_f32(start: f32, end: f32, n: usize) -> Vec<f32> {
630    if n == 1 {
631        return vec![start];
632    }
633    let step = (end - start) / (n - 1) as f32;
634    let half = n / 2;
635    (0..n)
636        .map(|i| {
637            if i < half {
638                start + step * i as f32
639            } else {
640                end - step * (n - 1 - i) as f32
641            }
642        })
643        .collect()
644}
645
646/// σ schedule in torch/numpy f32 order: linspace(1, 1/n, n), then
647/// shift·s/(1+(shift−1)·s), then a terminal 0. Length n + 1.
648/// N=4, shift 3 → [1, .9, .75, .5, 0].
649pub fn sigmas_torch_f32(n: usize, shift: f32) -> Vec<f32> {
650    // `1 / num_inference_steps` is a Python double rounded to f32 by torch.
651    let end = (1.0f64 / n as f64) as f32;
652    let mut s: Vec<f32> = linspace_f32(1.0, end, n)
653        .into_iter()
654        .map(|v| shift * v / (1.0 + (shift - 1.0) * v))
655        .collect();
656    s.push(0.0);
657    s
658}
659
660/// The pipeline's timestep: (1000 − σ·1000)/1000 in f32.
661pub fn t_model(sigma: f32) -> f32 {
662    let t = sigma * 1000.0;
663    (1000.0 - t) / 1000.0
664}
665
666/// Position ids and RoPE tables for a patch grid and a caption of `l`
667/// tokens (θ, axes from the config; f64 freqs, f32 angles, f32 cos/sin).
668pub fn ids_and_rope(grid: (usize, usize), l: usize, theta: f64, axes: [usize; 3]) -> ZRope {
669    let l_p = ceil32(l);
670    let n_img = grid.0 * grid.1;
671    let n_img_p = ceil32(n_img);
672    let mut img_ids: Vec<[usize; 3]> = Vec::with_capacity(n_img_p);
673    for r in 0..grid.0 {
674        for c in 0..grid.1 {
675            img_ids.push([l_p + 1, r, c]);
676        }
677    }
678    img_ids.resize(n_img_p, [0, 0, 0]);
679    let cap_ids: Vec<[usize; 3]> = (0..l_p).map(|j| [1 + j, 0, 0]).collect();
680    // freq tables per axis (f64, torch: 1 / theta ** (arange(0,d,2)/d))
681    let freqs: Vec<Vec<f64>> = axes
682        .iter()
683        .map(|&d| {
684            (0..d / 2)
685                .map(|j| 1.0 / theta.powf((2 * j) as f64 / d as f64))
686                .collect()
687        })
688        .collect();
689    let table = |ids: &[[usize; 3]]| -> (Vec<f32>, Vec<f32>) {
690        let pairs: usize = axes.iter().sum::<usize>() / 2;
691        let mut cos = Vec::with_capacity(ids.len() * pairs);
692        let mut sin = Vec::with_capacity(ids.len() * pairs);
693        for id in ids {
694            for (a, f) in freqs.iter().enumerate() {
695                for &fr in f {
696                    let ang = (id[a] as f64 * fr) as f32;
697                    cos.push(ang.cos());
698                    sin.push(ang.sin());
699                }
700            }
701        }
702        (cos, sin)
703    };
704    let img = table(&img_ids);
705    let cap = table(&cap_ids);
706    let joint = (
707        [img.0.as_slice(), cap.0.as_slice()].concat(),
708        [img.1.as_slice(), cap.1.as_slice()].concat(),
709    );
710    ZRope { img, joint, cap }
711}
712
713/// latent [c, h, w] → tokens [(h/2)·(w/2), 4c], feature (dy·2+dx)·c + ch.
714pub fn patchify(latent: &[f32], c: usize, h: usize, w: usize) -> Vec<f32> {
715    let (hp, wp) = (h / 2, w / 2);
716    let pd = 4 * c;
717    let mut out = vec![0f32; hp * wp * pd];
718    for r in 0..hp {
719        for q in 0..wp {
720            let t = r * wp + q;
721            for dy in 0..2 {
722                for dx in 0..2 {
723                    for ch in 0..c {
724                        out[t * pd + (dy * 2 + dx) * c + ch] =
725                            latent[(ch * h + 2 * r + dy) * w + 2 * q + dx];
726                    }
727                }
728            }
729        }
730    }
731    out
732}
733
734/// Exact inverse of `patchify`.
735pub fn unpatchify(tok: &[f32], c: usize, h: usize, w: usize) -> Vec<f32> {
736    let (hp, wp) = (h / 2, w / 2);
737    let pd = 4 * c;
738    let mut out = vec![0f32; c * h * w];
739    for r in 0..hp {
740        for q in 0..wp {
741            let t = r * wp + q;
742            for dy in 0..2 {
743                for dx in 0..2 {
744                    for ch in 0..c {
745                        out[(ch * h + 2 * r + dy) * w + 2 * q + dx] =
746                            tok[t * pd + (dy * 2 + dx) * c + ch];
747                    }
748                }
749            }
750        }
751    }
752    out
753}
754
755/// Pad `rows` rows of `width` to `rows_p` by repeating the last row.
756pub fn pad_rows_repeat_last(x: &[f32], rows: usize, rows_p: usize, width: usize) -> Vec<f32> {
757    let mut out = Vec::with_capacity(rows_p * width);
758    out.extend_from_slice(&x[..rows * width]);
759    for _ in rows..rows_p {
760        out.extend_from_within((rows - 1) * width..rows * width);
761    }
762    out
763}
764
765/// Weight source for the loaders: a container or a diffusers directory
766/// already read into f32.
767enum Src<'a> {
768    Cmf(&'a Arc<CmfModel>),
769    Dir(std::cell::RefCell<HashMap<String, crate::vae::StTensor>>),
770}
771
772impl Src<'_> {
773    fn f32(&self, name: &str) -> Result<Vec<f32>, String> {
774        match self {
775            Src::Cmf(m) => cmf_f32(m, &format!("dit.{name}")),
776            Src::Dir(t) => t
777                .borrow_mut()
778                .remove(name)
779                .map(|v| v.data)
780                .ok_or_else(|| format!("missing tensor {name}")),
781        }
782    }
783    fn proj(&self, name: &str) -> Result<(Proj, Option<usize>), String> {
784        match self {
785            Src::Cmf(m) => {
786                let full = format!("dit.{name}");
787                let idx = m.tensor_index(&full);
788                Ok((Proj::from_model(m, &full)?, idx))
789            }
790            Src::Dir(t) => {
791                let st = t
792                    .borrow_mut()
793                    .remove(name)
794                    .ok_or_else(|| format!("missing tensor {name}"))?;
795                if st.shape.len() != 2 {
796                    return Err(format!("{name}: expected 2-D"));
797                }
798                Ok((Proj::f32(st.data, st.shape[1]), None))
799            }
800        }
801    }
802}
803
804impl ZImageDit {
805    /// Load from a diffusers `transformer/` directory (bf16/fp32
806    /// safetensors, read whole and widened to f32 — a dev/parity path).
807    pub fn load_dir(dir: &Path) -> Result<Self, String> {
808        let cfg: serde_json::Value = serde_json::from_slice(
809            &std::fs::read(dir.join("config.json")).map_err(|e| format!("config.json: {e}"))?,
810        )
811        .map_err(|e| format!("config.json: {e}"))?;
812        let cfg = ZConfig::from_json(&cfg)?;
813        let idx: serde_json::Value = serde_json::from_slice(
814            &std::fs::read(dir.join("diffusion_pytorch_model.safetensors.index.json"))
815                .map_err(|e| format!("index: {e}"))?,
816        )
817        .map_err(|e| format!("index: {e}"))?;
818        let mut shards: Vec<String> = idx["weight_map"]
819            .as_object()
820            .ok_or("weight_map")?
821            .values()
822            .filter_map(|v| v.as_str().map(String::from))
823            .collect();
824        shards.sort();
825        shards.dedup();
826        let mut t = HashMap::new();
827        for sh in &shards {
828            t.extend(crate::vae::read_safetensors(&dir.join(sh))?);
829        }
830        Self::build(cfg, &Src::Dir(std::cell::RefCell::new(t)), None)
831    }
832
833    /// Load from a packaged `.cmf` (`dit.*` tensors + `dit.config_json`).
834    /// Quantized projections stay mmap-resident.
835    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
836        let cfg: serde_json::Value = serde_json::from_slice(
837            model
838                .tensor_bytes("dit.config_json")
839                .map_err(|e| e.to_string())?,
840        )
841        .map_err(|e| format!("dit.config_json: {e}"))?;
842        let cfg = ZConfig::from_json(&cfg)?;
843        Self::build(cfg, &Src::Cmf(model), Some(model.clone()))
844    }
845
846    fn build(cfg: ZConfig, src: &Src, model: Option<Arc<CmfModel>>) -> Result<Self, String> {
847        fn block(src: &Src, pfx: &str, modulated: bool) -> Result<ZBlock, String> {
848            let (q, iq) = src.proj(&format!("{pfx}.attention.to_q.weight"))?;
849            let (k, ik) = src.proj(&format!("{pfx}.attention.to_k.weight"))?;
850            let (v, iv) = src.proj(&format!("{pfx}.attention.to_v.weight"))?;
851            let (o, io) = src.proj(&format!("{pfx}.attention.to_out.0.weight"))?;
852            let (w1, i1) = src.proj(&format!("{pfx}.feed_forward.w1.weight"))?;
853            let (w2, i2) = src.proj(&format!("{pfx}.feed_forward.w2.weight"))?;
854            let (w3, i3) = src.proj(&format!("{pfx}.feed_forward.w3.weight"))?;
855            let idx = match (iq, ik, iv, io, i1, i3, i2) {
856                (Some(a), Some(b), Some(c), Some(d), Some(e), Some(f), Some(g)) => {
857                    Some([a, b, c, d, e, f, g])
858                }
859                _ => None,
860            };
861            Ok(ZBlock {
862                adaln: if modulated {
863                    Some((
864                        src.f32(&format!("{pfx}.adaLN_modulation.0.weight"))?,
865                        src.f32(&format!("{pfx}.adaLN_modulation.0.bias"))?,
866                    ))
867                } else {
868                    None
869                },
870                norm1: src.f32(&format!("{pfx}.attention_norm1.weight"))?,
871                norm2: src.f32(&format!("{pfx}.attention_norm2.weight"))?,
872                ffn_norm1: src.f32(&format!("{pfx}.ffn_norm1.weight"))?,
873                ffn_norm2: src.f32(&format!("{pfx}.ffn_norm2.weight"))?,
874                norm_q: src.f32(&format!("{pfx}.attention.norm_q.weight"))?,
875                norm_k: src.f32(&format!("{pfx}.attention.norm_k.weight"))?,
876                q,
877                k,
878                v,
879                o,
880                w1,
881                w2,
882                w3,
883                idx,
884            })
885        }
886        // A container's blocks load in parallel (B2: the adaLN widening of
887        // 34 blocks was a serial 0.5 s of every cold start).
888        let blocks = |names: Vec<(String, bool)>| -> Result<Vec<ZBlock>, String> {
889            match src {
890                Src::Cmf(m) => std::thread::scope(|sc| {
891                    let hs: Vec<_> = names
892                        .iter()
893                        .map(|(pfx, md)| sc.spawn(move || block(&Src::Cmf(m), pfx, *md)))
894                        .collect();
895                    hs.into_iter()
896                        .map(|h| h.join().map_err(|_| "block loader panicked".to_string())?)
897                        .collect()
898                }),
899                _ => names.iter().map(|(pfx, md)| block(src, pfx, *md)).collect(),
900            }
901        };
902        let noise_refiner = blocks((0..cfg.n_refiner).map(|i| (format!("noise_refiner.{i}"), true)).collect())?;
903        let context_refiner =
904            blocks((0..cfg.n_refiner).map(|i| (format!("context_refiner.{i}"), false)).collect())?;
905        let layers = blocks((0..cfg.n_layers).map(|i| (format!("layers.{i}"), true)).collect())?;
906        let pk = format!("{}-1", cfg.patch);
907        let (cap_w, _) = src.proj("cap_embedder.1.weight")?;
908        Ok(Self {
909            x_emb_w: src.f32(&format!("all_x_embedder.{pk}.weight"))?,
910            x_emb_b: src.f32(&format!("all_x_embedder.{pk}.bias"))?,
911            x_pad: src.f32("x_pad_token")?,
912            cap_pad: src.f32("cap_pad_token")?,
913            t_w0: src.f32("t_embedder.mlp.0.weight")?,
914            t_b0: src.f32("t_embedder.mlp.0.bias")?,
915            t_w2: src.f32("t_embedder.mlp.2.weight")?,
916            t_b2: src.f32("t_embedder.mlp.2.bias")?,
917            cap_norm: src.f32("cap_embedder.0.weight")?,
918            cap_w,
919            cap_b: src.f32("cap_embedder.1.bias")?,
920            final_mod_w: src.f32(&format!("all_final_layer.{pk}.adaLN_modulation.1.weight"))?,
921            final_mod_b: src.f32(&format!("all_final_layer.{pk}.adaLN_modulation.1.bias"))?,
922            final_w: src.f32(&format!("all_final_layer.{pk}.linear.weight"))?,
923            final_b: src.f32(&format!("all_final_layer.{pk}.linear.bias"))?,
924            noise_refiner,
925            context_refiner,
926            layers,
927            pool: Pool::from_env(),
928            cfg,
929            model,
930        })
931    }
932
933    pub fn geom(&self) -> ZGeom {
934        self.cfg.geom()
935    }
936
937    fn pool(&self) -> Option<&Pool> {
938        self.pool.as_deref()
939    }
940
941    fn blk(&self, id: ZBlockId) -> &ZBlock {
942        match id {
943            ZBlockId::NoiseRefiner(i) => &self.noise_refiner[i],
944            ZBlockId::ContextRefiner(i) => &self.context_refiner[i],
945            ZBlockId::Layer(i) => &self.layers[i],
946        }
947    }
948
949    /// Timestep embedding for `t_model` = (1000 − 1000σ)/1000: sinusoid of
950    /// t·1000 (cos first, f32 args) → mlp.0 → SiLU → mlp.2. Returns [256].
951    pub fn temb(&self, t_model: f32) -> Vec<f32> {
952        const HALF: usize = 128;
953        let t = t_model * self.cfg.t_scale;
954        // torch: exp((-ln(1e4) as f32) * arange_f32 / 128) in f32
955        let c = -(10000f64.ln()) as f32;
956        let mut freq = vec![0f32; 2 * HALF];
957        for i in 0..HALF {
958            let f = (c * i as f32 / HALF as f32).exp();
959            let arg = t * f;
960            freq[i] = arg.cos();
961            freq[HALF + i] = arg.sin();
962        }
963        let mut h = linear_row(&freq, &self.t_w0, &self.t_b0, None);
964        for v in h.iter_mut() {
965            *v = silu(*v);
966        }
967        linear_row(&h, &self.t_w2, &self.t_b2, None)
968    }
969
970    /// Raw `adaLN_modulation.0(temb)` for every step and block:
971    /// [steps][2 + n_layers][4 · dim], chunks [scale_msa, gate_msa,
972    /// scale_mlp, gate_mlp] (no +1, no tanh). f64 accumulation.
973    pub fn mods_for_steps(&self, t_models: &[f32]) -> Vec<f32> {
974        let per = self.cfg.n_mod_blocks() * 4 * self.cfg.dim;
975        let mut out = vec![0f32; t_models.len() * per];
976        // Every step's temb against each weight row while the row is in
977        // cache (B2: the per-step loop streamed the 0.5 GB of adaLN
978        // weights once per step — ~1 s for the base model's 28 steps).
979        // Same f64 dot per (row, step), so the values are unchanged.
980        let tembs: Vec<Vec<f32>> = t_models.iter().map(|&t| self.temb(t)).collect();
981        let mut off = 0;
982        for b in self.noise_refiner.iter().chain(&self.layers) {
983            let (w, bias) = b.adaln.as_ref().expect("modulated block");
984            let rows = bias.len();
985            let ys = linear_rows_multi(&tembs, w, bias, self.pool());
986            for (si, y) in ys.iter().enumerate() {
987                out[si * per + off..si * per + off + rows].copy_from_slice(y);
988            }
989            off += rows;
990        }
991        out
992    }
993
994    /// 1 + `all_final_layer.2-1.adaLN_modulation.1`(SiLU(temb)) per step:
995    /// [steps][dim].
996    pub fn final_scale_for_steps(&self, t_models: &[f32]) -> Vec<f32> {
997        let mut out = Vec::with_capacity(t_models.len() * self.cfg.dim);
998        for &t in t_models {
999            let te: Vec<f32> = self.temb(t).into_iter().map(silu).collect();
1000            let m = linear_row(&te, &self.final_mod_w, &self.final_mod_b, self.pool());
1001            out.extend(m.into_iter().map(|v| 1.0 + v));
1002        }
1003        out
1004    }
1005
1006    /// Caption features [l, cap_feat_dim] → [l_p, dim]: pad rows are copies
1007    /// of the last row, RMSNorm(w, 1e-5) → Linear + b, rows ≥ l :=
1008    /// cap_pad_token.
1009    pub fn embed_caption(&self, cap_feats: &[f32], l: usize) -> Vec<f32> {
1010        let (cf, dim) = (self.cfg.cap_feat_dim, self.cfg.dim);
1011        let l_p = ceil32(l);
1012        // Pad rows are replaced by cap_pad_token after the embedder, so
1013        // only the l real rows need the linear.
1014        let mut xn = vec![0f32; l * cf];
1015        for (o, src) in xn.chunks_exact_mut(cf).zip(cap_feats.chunks_exact(cf)) {
1016            rms_norm_into(src, &self.cap_norm, self.cfg.norm_eps as f64, o);
1017        }
1018        let mut out = vec![0f32; l_p * dim];
1019        lin(&self.cap_w, &xn, l, &mut out[..l * dim], self.pool());
1020        for row in out[..l * dim].chunks_exact_mut(dim) {
1021            for (v, &b) in row.iter_mut().zip(&self.cap_b) {
1022                *v += b;
1023            }
1024        }
1025        for row in out[l * dim..].chunks_exact_mut(dim) {
1026            row.copy_from_slice(&self.cap_pad);
1027        }
1028        out
1029    }
1030
1031    /// The two unmodulated context-refiner blocks on the host, in place on
1032    /// `cap` [l_p, dim].
1033    pub fn refine_caption_cpu(&self, cap: &mut [f32], rope_cap: (&[f32], &[f32])) {
1034        let n = cap.len() / self.cfg.dim;
1035        for i in 0..self.context_refiner.len() {
1036            self.block_cpu(ZBlockId::ContextRefiner(i), cap, n, rope_cap, None);
1037        }
1038    }
1039
1040    /// Per-head attention over [n, nh·hd] q/k/v (qk already normed and
1041    /// rotated), full bidirectional softmax at 1/√hd, into `attn`.
1042    fn attention(&self, q_all: &[f32], k_all: &[f32], v_all: &[f32], n: usize, attn: &mut [f32]) {
1043        let (nh, nkv, hd) = (self.cfg.n_heads, self.cfg.n_kv_heads, self.cfg.head_dim);
1044        let hpk = nh / nkv;
1045        let pool = self.pool();
1046        let scale = 1.0 / (hd as f32).sqrt();
1047        let mut qh = vec![0f32; n * hd];
1048        let mut kh = vec![0f32; n * hd];
1049        let mut vt = vec![0f32; hd * n];
1050        let mut scores = vec![0f32; n * n];
1051        let mut oh = vec![0f32; n * hd];
1052        for hh in 0..nh {
1053            let kv = hh / hpk;
1054            {
1055                let (sq, sk, sv) = (
1056                    SendRows(qh.as_mut_ptr()),
1057                    SendRows(kh.as_mut_ptr()),
1058                    SendRows(vt.as_mut_ptr()),
1059                );
1060                pool_rows(pool, n, &|lo, hi| {
1061                    for p in lo..hi {
1062                        let qsrc = &q_all[(p * nh + hh) * hd..(p * nh + hh + 1) * hd];
1063                        // SAFETY: workers cover disjoint token ranges.
1064                        let qd = unsafe { sq.row(p * hd, hd) };
1065                        for (d, &v) in qsrc.iter().enumerate() {
1066                            qd[d] = v * scale;
1067                        }
1068                        unsafe { sk.row(p * hd, hd) }.copy_from_slice(
1069                            &k_all[(p * nkv + kv) * hd..(p * nkv + kv + 1) * hd],
1070                        );
1071                        let vv = &v_all[(p * nkv + kv) * hd..(p * nkv + kv + 1) * hd];
1072                        for (d, &val) in vv.iter().enumerate() {
1073                            unsafe { sv.set(d * n + p, val) };
1074                        }
1075                    }
1076                });
1077            }
1078            host_gemm::gemm_nt(&qh, &kh, &mut scores, n, hd, n, pool);
1079            {
1080                let sp = SendRows(scores.as_mut_ptr());
1081                pool_rows(pool, n, &|lo, hi| {
1082                    for r in lo..hi {
1083                        // SAFETY: disjoint rows.
1084                        softmax_inplace(unsafe { sp.row(r * n, n) });
1085                    }
1086                });
1087            }
1088            host_gemm::gemm_nt(&scores, &vt, &mut oh, n, n, hd, pool);
1089            let sa = SendRows(attn.as_mut_ptr());
1090            pool_rows(pool, n, &|lo, hi| {
1091                for p in lo..hi {
1092                    // SAFETY: disjoint tokens.
1093                    unsafe { sa.row((p * nh + hh) * hd, hd) }
1094                        .copy_from_slice(&oh[p * hd..(p + 1) * hd]);
1095                }
1096            });
1097        }
1098    }
1099
1100    /// One block on the host, in place on `x` [n, dim]. `m` = the block's
1101    /// raw modulation [4 · dim] (None = unmodulated: s = 0, gate = 1).
1102    pub fn block_cpu(
1103        &self,
1104        blk: ZBlockId,
1105        x: &mut [f32],
1106        n: usize,
1107        rope: (&[f32], &[f32]),
1108        m: Option<&[f32]>,
1109    ) {
1110        let b = self.blk(blk);
1111        let (hs, nh, nkv, hd) = (
1112            self.cfg.dim,
1113            self.cfg.n_heads,
1114            self.cfg.n_kv_heads,
1115            self.cfg.head_dim,
1116        );
1117        let eps = self.cfg.norm_eps as f64;
1118        let pool = self.pool();
1119        let (s_msa, g_msa, s_mlp, g_mlp) = match m {
1120            Some(m) => (
1121                Some(&m[..hs]),
1122                Some(m[hs..2 * hs].iter().map(|v| v.tanh()).collect::<Vec<f32>>()),
1123                Some(&m[2 * hs..3 * hs]),
1124                Some(m[3 * hs..4 * hs].iter().map(|v| v.tanh()).collect::<Vec<f32>>()),
1125            ),
1126            None => (None, None, None, None),
1127        };
1128        // dst = rms(src)·w · (1+s)
1129        let norm_scaled = |src: &[f32], w: &[f32], s: Option<&[f32]>, dst: &mut [f32]| {
1130            let sr = SendRows(dst.as_mut_ptr());
1131            pool_rows(pool, n, &|lo, hi| {
1132                for p in lo..hi {
1133                    // SAFETY: disjoint rows.
1134                    let row = unsafe { sr.row(p * hs, hs) };
1135                    rms_norm_into(&src[p * hs..(p + 1) * hs], w, eps, row);
1136                    if let Some(s) = s {
1137                        for (r, &sc) in row.iter_mut().zip(s) {
1138                            *r *= 1.0 + sc;
1139                        }
1140                    }
1141                }
1142            });
1143        };
1144        // x += gate ⊙ rms(src)·w
1145        let residual = |src: &[f32], w: &[f32], gate: Option<&[f32]>, x: &mut [f32]| {
1146            let sr = SendRows(x.as_mut_ptr());
1147            pool_rows(pool, n, &|lo, hi| {
1148                let mut tmp = vec![0f32; hs];
1149                for p in lo..hi {
1150                    rms_norm_into(&src[p * hs..(p + 1) * hs], w, eps, &mut tmp);
1151                    // SAFETY: disjoint rows.
1152                    let dst = unsafe { sr.row(p * hs, hs) };
1153                    match gate {
1154                        Some(g) => {
1155                            for ((d, &v), &gt) in dst.iter_mut().zip(&tmp).zip(g) {
1156                                *d += gt * v;
1157                            }
1158                        }
1159                        None => {
1160                            for (d, &v) in dst.iter_mut().zip(&tmp) {
1161                                *d += v;
1162                            }
1163                        }
1164                    }
1165                }
1166            });
1167        };
1168        // ── attention ──
1169        let mut xn = vec![0f32; n * hs];
1170        norm_scaled(x, &b.norm1, s_msa, &mut xn);
1171        let mut q_all = vec![0f32; n * nh * hd];
1172        let mut k_all = vec![0f32; n * nkv * hd];
1173        let mut v_all = vec![0f32; n * nkv * hd];
1174        lin(&b.q, &xn, n, &mut q_all, pool);
1175        lin(&b.k, &xn, n, &mut k_all, pool);
1176        lin(&b.v, &xn, n, &mut v_all, pool);
1177        let (cos, sin) = rope;
1178        let pairs = hd / 2;
1179        for (all, heads, w) in [(&mut q_all, nh, &b.norm_q), (&mut k_all, nkv, &b.norm_k)] {
1180            let sr = SendRows(all.as_mut_ptr());
1181            pool_rows(pool, n, &|lo, hi| {
1182                for p in lo..hi {
1183                    for h in 0..heads {
1184                        // SAFETY: disjoint tokens.
1185                        let v = unsafe { sr.row((p * heads + h) * hd, hd) };
1186                        rms_norm_inplace(v, w, eps);
1187                        for j in 0..pairs {
1188                            let (c, s) = (cos[p * pairs + j], sin[p * pairs + j]);
1189                            let (a, bb) = (v[2 * j], v[2 * j + 1]);
1190                            v[2 * j] = a * c - bb * s;
1191                            v[2 * j + 1] = a * s + bb * c;
1192                        }
1193                    }
1194                }
1195            });
1196        }
1197        let mut attn = vec![0f32; n * nh * hd];
1198        self.attention(&q_all, &k_all, &v_all, n, &mut attn);
1199        drop((q_all, k_all, v_all));
1200        let mut proj = vec![0f32; n * hs];
1201        lin(&b.o, &attn, n, &mut proj, pool);
1202        drop(attn);
1203        residual(&proj, &b.norm2, g_msa.as_deref(), x);
1204        // ── SwiGLU FFN ──
1205        norm_scaled(x, &b.ffn_norm1, s_mlp, &mut xn);
1206        let inter = b.w1.rows();
1207        let mut g_all = vec![0f32; n * inter];
1208        let mut u_all = vec![0f32; n * inter];
1209        lin(&b.w1, &xn, n, &mut g_all, pool);
1210        lin(&b.w3, &xn, n, &mut u_all, pool);
1211        {
1212            let sg = SendRows(g_all.as_mut_ptr());
1213            pool_rows(pool, n, &|lo, hi| {
1214                for p in lo..hi {
1215                    // SAFETY: disjoint tokens.
1216                    let g = unsafe { sg.row(p * inter, inter) };
1217                    for (gv, &uv) in g.iter_mut().zip(&u_all[p * inter..(p + 1) * inter]) {
1218                        *gv = silu(*gv) * uv;
1219                    }
1220                }
1221            });
1222        }
1223        drop(u_all);
1224        lin(&b.w2, &g_all, n, &mut proj, pool);
1225        residual(&proj, &b.ffn_norm2, g_mlp.as_deref(), x);
1226    }
1227
1228    /// Device views of all blocks (requires `from_cmf`).
1229    pub fn block_refs(&self) -> Option<ZBlockRefs<'_>> {
1230        fn r(b: &ZBlock) -> Option<ZBlockRef<'_>> {
1231            let [wq, wk, wv, wo, w1, w3, w2] = b.idx?;
1232            Some(ZBlockRef {
1233                wq,
1234                wk,
1235                wv,
1236                wo,
1237                w1,
1238                w3,
1239                w2,
1240                norm1: &b.norm1,
1241                norm2: &b.norm2,
1242                ffn_norm1: &b.ffn_norm1,
1243                ffn_norm2: &b.ffn_norm2,
1244                norm_q: &b.norm_q,
1245                norm_k: &b.norm_k,
1246            })
1247        }
1248        self.model.as_ref()?;
1249        Some(ZBlockRefs {
1250            noise_refiner: self.noise_refiner.iter().map(r).collect::<Option<_>>()?,
1251            context_refiner: self.context_refiner.iter().map(r).collect::<Option<_>>()?,
1252            layers: self.layers.iter().map(r).collect::<Option<_>>()?,
1253        })
1254    }
1255
1256    /// Once per (prompt, resolution): caption embed → context refiner
1257    /// (`gpu::zimage_refine_caption`, else CPU) → rope tables →
1258    /// `gpu::zimage_prepare` (sets `ZPrepared::device`). `mods_all` =
1259    /// (mods_for_steps, final_scale_for_steps) forwarded to the backend.
1260    pub fn prepare(
1261        &self,
1262        cap_feats: &[f32],
1263        shape: ZShape,
1264        key: u64,
1265        mods_all: Option<(&[f32], &[f32])>,
1266    ) -> Result<ZPrepared, String> {
1267        self.prepare_with(cap_feats, shape, key, mods_all, gpu_allowed())
1268    }
1269
1270    /// `prepare` with the device use explicit: `device = false` builds a
1271    /// pure host state (CPU context refiner, no `gpu::zimage_prepare`) —
1272    /// the reference a device test diffs `step` against.
1273    pub fn prepare_with(
1274        &self,
1275        cap_feats: &[f32],
1276        shape: ZShape,
1277        key: u64,
1278        mods_all: Option<(&[f32], &[f32])>,
1279        device: bool,
1280    ) -> Result<ZPrepared, String> {
1281        let mut p = self.prepare_host(cap_feats, shape, key, device)?;
1282        if device {
1283            self.attach_device(&mut p, mods_all);
1284        }
1285        Ok(p)
1286    }
1287
1288    /// The host half of `prepare`: caption embed → context refiner
1289    /// (`gpu::zimage_refine_caption` when `device_refine`, else CPU) →
1290    /// rope tables. `device` is false until `attach_device`.
1291    pub fn prepare_host(
1292        &self,
1293        cap_feats: &[f32],
1294        shape: ZShape,
1295        key: u64,
1296        device_refine: bool,
1297    ) -> Result<ZPrepared, String> {
1298        if cap_feats.len() != shape.l * self.cfg.cap_feat_dim || shape.l == 0 {
1299            return Err(format!(
1300                "caption features: {} floats for {} tokens of {}",
1301                cap_feats.len(),
1302                shape.l,
1303                self.cfg.cap_feat_dim
1304            ));
1305        }
1306        let rope = ids_and_rope(shape.grid, shape.l, self.cfg.rope_theta, self.cfg.axes_dims);
1307        let mut cap = self.embed_caption(cap_feats, shape.l);
1308        let refs = if device_refine { self.block_refs() } else { None };
1309        let geom = self.geom();
1310        let dev_refined = match (&refs, &self.model) {
1311            (Some(r), Some(m)) => crate::gpu::zimage_refine_caption(
1312                m,
1313                &geom,
1314                &r.context_refiner,
1315                (&rope.cap.0, &rope.cap.1),
1316                &mut cap,
1317            ),
1318            _ => false,
1319        };
1320        if !dev_refined {
1321            self.refine_caption_cpu(&mut cap, (&rope.cap.0, &rope.cap.1));
1322        }
1323        Ok(ZPrepared {
1324            key,
1325            shape,
1326            cap,
1327            rope,
1328            device: false,
1329        })
1330    }
1331
1332    fn prepare_args<'a>(
1333        &'a self,
1334        m: &'a Arc<CmfModel>,
1335        r: &'a ZBlockRefs<'a>,
1336        p: &'a ZPrepared,
1337        key: u64,
1338        mods_all: Option<(&'a [f32], &'a [f32])>,
1339        neg: Option<&'a ZPrepared>,
1340    ) -> crate::gpu::ZPrepareArgs<'a> {
1341        let shape = p.shape;
1342        crate::gpu::ZPrepareArgs {
1343            model: m,
1344            geom: self.geom(),
1345            key,
1346            n_img: shape.n_img,
1347            n_img_p: shape.n_img_p,
1348            n_cap_p: shape.l_p,
1349            grid: shape.grid,
1350            cap: &p.cap,
1351            rope_img: (&p.rope.img.0, &p.rope.img.1),
1352            rope_joint: (&p.rope.joint.0, &p.rope.joint.1),
1353            x_emb_w: &self.x_emb_w,
1354            x_emb_b: &self.x_emb_b,
1355            x_pad: &self.x_pad,
1356            final_w: &self.final_w,
1357            final_b: &self.final_b,
1358            noise_refiner: &r.noise_refiner,
1359            layers: &r.layers,
1360            mods_all: mods_all.map(|m| m.0),
1361            final_scale_all: mods_all.map(|m| m.1),
1362            neg: neg.map(|n| crate::gpu::ZNegArgs {
1363                cap: &n.cap,
1364                n_cap_p: n.shape.l_p,
1365                rope_img: (&n.rope.img.0, &n.rope.img.1),
1366                rope_joint: (&n.rope.joint.0, &n.rope.joint.1),
1367            }),
1368        }
1369    }
1370
1371    /// Upload every device plane now (`gpu::zimage_preload`); independent
1372    /// of the caption, so it can run beside the text encoder.
1373    pub fn preload_device(&self) -> bool {
1374        match (self.block_refs(), &self.model) {
1375            (Some(r), Some(m)) => crate::gpu::zimage_preload(
1376                m,
1377                &self.geom(),
1378                &r.noise_refiner,
1379                &r.layers,
1380                &r.context_refiner,
1381            ),
1382            _ => false,
1383        }
1384    }
1385
1386    /// `gpu::zimage_prepare` for a host state (sets `p.device`).
1387    pub fn attach_device(&self, p: &mut ZPrepared, mods_all: Option<(&[f32], &[f32])>) -> bool {
1388        let refs = self.block_refs();
1389        p.device = match (&refs, &self.model) {
1390            (Some(r), Some(m)) => {
1391                crate::gpu::zimage_prepare(&self.prepare_args(m, r, p, p.key, mods_all, None))
1392            }
1393            _ => false,
1394        };
1395        p.device
1396    }
1397
1398    /// One batch-2 device program for a CFG pair (item 0 = `pos`, item 1 =
1399    /// `neg`, both at the same resolution) under `key`. `false` = the
1400    /// backend has no batch 2 here; the caller steps the items one by one.
1401    pub fn attach_device_pair(
1402        &self,
1403        pos: &ZPrepared,
1404        neg: &ZPrepared,
1405        key: u64,
1406        mods_all: Option<(&[f32], &[f32])>,
1407    ) -> bool {
1408        if pos.shape.grid != neg.shape.grid {
1409            return false;
1410        }
1411        let refs = self.block_refs();
1412        match (&refs, &self.model) {
1413            (Some(r), Some(m)) => {
1414                crate::gpu::zimage_prepare(&self.prepare_args(m, r, pos, key, mods_all, Some(neg)))
1415            }
1416            _ => false,
1417        }
1418    }
1419
1420    /// Both items of a CFG pair prepared by `attach_device_pair` under
1421    /// `key`, in one device forward: returns (v_pos, v_neg), or None when
1422    /// the backend declined (the caller steps the items separately).
1423    pub fn step_pair_device(
1424        &self,
1425        key: u64,
1426        n_img: usize,
1427        step: usize,
1428        x_tok: &[f32],
1429        mods: &[f32],
1430        final_scale: &[f32],
1431    ) -> Option<(Vec<f32>, Vec<f32>)> {
1432        let pd = self.geom().patch_dim;
1433        let mut out = vec![0f32; n_img * pd];
1434        let mut out_neg = vec![0f32; n_img * pd];
1435        crate::gpu::zimage_step(&mut crate::gpu::ZStepArgs {
1436            key,
1437            step,
1438            x_tok,
1439            mods,
1440            final_scale,
1441            out: &mut out,
1442            out_neg: Some(&mut out_neg),
1443        })
1444        .then_some((out, out_neg))
1445    }
1446
1447    /// One DiT forward: `gpu::zimage_step` if `p.device`, else `step_cpu`.
1448    /// `x_tok` [n_img_p, 64] (pad rows = copies of the last row), `mods`
1449    /// [(2 + n_layers) · 4 · dim] of this step, `final_scale` [dim].
1450    /// Returns v [n_img, 64] (before the pipeline's negation).
1451    pub fn step(
1452        &self,
1453        p: &ZPrepared,
1454        step: usize,
1455        x_tok: &[f32],
1456        mods: &[f32],
1457        final_scale: &[f32],
1458    ) -> Vec<f32> {
1459        if p.device {
1460            let mut out = vec![0f32; p.shape.n_img * self.geom().patch_dim];
1461            if crate::gpu::zimage_step(&mut crate::gpu::ZStepArgs {
1462                key: p.key,
1463                step,
1464                x_tok,
1465                mods,
1466                final_scale,
1467                out: &mut out,
1468                out_neg: None,
1469            }) {
1470                return out;
1471            }
1472        }
1473        self.step_cpu(p, x_tok, mods, final_scale)
1474    }
1475
1476    /// The step input for a latent [16, h_lat, w_lat]: patchify, then pad
1477    /// to n_img_p rows by repeating the last row.
1478    pub fn tokens(&self, latent: &[f32], shape: &ZShape) -> Vec<f32> {
1479        pad_rows_repeat_last(
1480            &patchify(latent, self.cfg.in_channels, shape.h_lat, shape.w_lat),
1481            shape.n_img,
1482            shape.n_img_p,
1483            self.geom().patch_dim,
1484        )
1485    }
1486
1487    /// x_tok [n_img_p, 64] → [n_img_p, dim]: Linear + b, rows ≥ n_img :=
1488    /// x_pad_token.
1489    pub fn embed_image(&self, x_tok: &[f32], n_img: usize, n_img_p: usize) -> Vec<f32> {
1490        let (dim, pd) = (self.cfg.dim, self.geom().patch_dim);
1491        let mut x = vec![0f32; n_img_p * dim];
1492        host_gemm::gemm_nt(
1493            &x_tok[..n_img * pd],
1494            &self.x_emb_w,
1495            &mut x[..n_img * dim],
1496            n_img,
1497            pd,
1498            dim,
1499            self.pool(),
1500        );
1501        for row in x[..n_img * dim].chunks_exact_mut(dim) {
1502            for (v, &b) in row.iter_mut().zip(&self.x_emb_b) {
1503                *v += b;
1504            }
1505        }
1506        for row in x[n_img * dim..].chunks_exact_mut(dim) {
1507            row.copy_from_slice(&self.x_pad);
1508        }
1509        x
1510    }
1511
1512    /// Final layer on the first `n` rows of `u`: LayerNorm(eps 1e-6, no
1513    /// affine) · final_scale → Linear(dim → 64) + b.
1514    pub fn final_layer(&self, u: &[f32], n: usize, final_scale: &[f32]) -> Vec<f32> {
1515        let (dim, pd) = (self.cfg.dim, self.geom().patch_dim);
1516        let eps = self.cfg.final_eps as f64;
1517        let mut y = vec![0f32; n * dim];
1518        {
1519            let sy = SendRows(y.as_mut_ptr());
1520            pool_rows(self.pool(), n, &|lo, hi| {
1521                for p in lo..hi {
1522                    let x = &u[p * dim..(p + 1) * dim];
1523                    let mean = x.iter().map(|&v| v as f64).sum::<f64>() / dim as f64;
1524                    let var =
1525                        x.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / dim as f64;
1526                    let inv = 1.0 / (var + eps).sqrt();
1527                    // SAFETY: disjoint rows.
1528                    let d = unsafe { sy.row(p * dim, dim) };
1529                    for ((o, &v), &s) in d.iter_mut().zip(x).zip(final_scale) {
1530                        *o = ((v as f64 - mean) * inv) as f32 * s;
1531                    }
1532                }
1533            });
1534        }
1535        let mut out = vec![0f32; n * pd];
1536        host_gemm::gemm_nt(&y, &self.final_w, &mut out, n, dim, pd, self.pool());
1537        for row in out.chunks_exact_mut(pd) {
1538            for (v, &b) in row.iter_mut().zip(&self.final_b) {
1539                *v += b;
1540            }
1541        }
1542        out
1543    }
1544
1545    /// The host reference forward (WP2/WP3 gate against this):
1546    /// embed → pad rows := x_pad_token → noise refiner (img only) →
1547    /// concat [img, cap] → layers → LayerNorm(1e-6)·final_scale → Linear →
1548    /// image rows.
1549    pub fn step_cpu(&self, p: &ZPrepared, x_tok: &[f32], mods: &[f32], final_scale: &[f32]) -> Vec<f32> {
1550        self.step_cpu_taps(p, x_tok, mods, final_scale, &mut |_, _| {})
1551    }
1552
1553    /// `step_cpu` with a tap callback: (name, tensor) at the oracle's tap
1554    /// points (`x_seq`, `nr{i}_out`, `u_in`, `l{i}_out`, `final_out`).
1555    pub fn step_cpu_taps(
1556        &self,
1557        p: &ZPrepared,
1558        x_tok: &[f32],
1559        mods: &[f32],
1560        final_scale: &[f32],
1561        tap: &mut dyn FnMut(&str, &[f32]),
1562    ) -> Vec<f32> {
1563        let s = p.shape;
1564        let dim = self.cfg.dim;
1565        let md = 4 * dim;
1566        let mut x = self.embed_image(x_tok, s.n_img, s.n_img_p);
1567        tap("x_seq", &x);
1568        for i in 0..self.noise_refiner.len() {
1569            let m = &mods[i * md..(i + 1) * md];
1570            self.block_cpu(
1571                ZBlockId::NoiseRefiner(i),
1572                &mut x,
1573                s.n_img_p,
1574                (&p.rope.img.0, &p.rope.img.1),
1575                Some(m),
1576            );
1577            tap(&format!("nr{i}_out"), &x);
1578        }
1579        x.extend_from_slice(&p.cap);
1580        let n = s.seq();
1581        tap("u_in", &x);
1582        let nr = self.noise_refiner.len();
1583        for i in 0..self.layers.len() {
1584            let m = &mods[(nr + i) * md..(nr + i + 1) * md];
1585            self.block_cpu(
1586                ZBlockId::Layer(i),
1587                &mut x,
1588                n,
1589                (&p.rope.joint.0, &p.rope.joint.1),
1590                Some(m),
1591            );
1592            tap(&format!("l{i}_out"), &x);
1593        }
1594        let out = self.final_layer(&x, s.n_img, final_scale);
1595        tap("final_out", &out);
1596        out
1597    }
1598}
1599
1600/// Device paths are off under `CMF_GPU=0` or `CMF_ZIMAGE_GPU=0` (the
1601/// CPU-exact reference run).
1602/// The Z-Image device path may run (`CMF_ZIMAGE_GPU=0` / `CMF_GPU=0` off).
1603pub fn gpu_allowed() -> bool {
1604    !matches!(std::env::var("CMF_ZIMAGE_GPU").as_deref(), Ok("0"))
1605        && !matches!(std::env::var("CMF_GPU").as_deref(), Ok("0"))
1606}
1607
1608#[cfg(test)]
1609mod tests {
1610    use super::*;
1611
1612    #[test]
1613    fn sigmas_match_the_spec_table() {
1614        let s = sigmas_torch_f32(8, 3.0);
1615        let spec = [
1616            1.0f32,
1617            0.954545438,
1618            0.899999976,
1619            0.833333313,
1620            0.75,
1621            0.642857134,
1622            0.5,
1623            0.300000012,
1624            0.0,
1625        ];
1626        assert_eq!(s.len(), 9);
1627        for (a, b) in s.iter().zip(spec) {
1628            assert!((a - b).abs() <= 1e-7, "{s:?}");
1629        }
1630        let s4 = sigmas_torch_f32(4, 3.0);
1631        for (a, b) in s4.iter().zip([1.0f32, 0.9, 0.75, 0.5, 0.0]) {
1632            assert!((a - b).abs() <= 1e-6, "{s4:?}");
1633        }
1634        assert_eq!(t_model(1.0), 0.0);
1635        assert_eq!(t_model(0.5), 0.5);
1636    }
1637
1638    #[test]
1639    fn patchify_roundtrip_and_order() {
1640        let (c, h, w) = (16, 6, 4);
1641        let lat: Vec<f32> = (0..c * h * w).map(|i| i as f32).collect();
1642        let t = patchify(&lat, c, h, w);
1643        // token (1, 0), feature (dy=1, dx=0, ch=3) = latent[3, 3, 0]
1644        assert_eq!(t[(1 * 2) * 64 + (1 * 2) * 16 + 3], lat[(3 * h + 3) * w]);
1645        assert_eq!(unpatchify(&t, c, h, w), lat);
1646    }
1647
1648    #[test]
1649    fn ids_and_pads() {
1650        assert_eq!(ceil32(22), 32);
1651        assert_eq!(ceil32(32), 32);
1652        assert_eq!(ceil32(33), 64);
1653        let r = ids_and_rope((2, 3), 5, 256.0, [32, 48, 48]);
1654        // 6 image tokens → 32 rows, caption 5 → 32 rows, 64 pairs each
1655        assert_eq!(r.img.0.len(), 32 * 64);
1656        assert_eq!(r.cap.0.len(), 32 * 64);
1657        assert_eq!(r.joint.0.len(), 64 * 64);
1658        // image pad row (id 0,0,0): all angles 0
1659        assert!(r.img.0[31 * 64..].iter().all(|&c| c == 1.0));
1660        // first caption row: axis-0 id 1, pair 0 → angle 1
1661        assert!((r.cap.0[0] - 1f32.cos()).abs() < 1e-7);
1662        // first image row: axis-0 id l_p+1 = 33
1663        assert!((r.img.0[0] - 33f32.cos()).abs() < 1e-6);
1664        let x = [1.0f32, 2.0, 3.0, 4.0];
1665        assert_eq!(
1666            pad_rows_repeat_last(&x, 2, 4, 2),
1667            vec![1.0, 2.0, 3.0, 4.0, 3.0, 4.0, 3.0, 4.0]
1668        );
1669    }
1670}