Skip to main content

cortiq_engine/
qwen3te.rs

1//! Qwen3-VL as MiniMax-H3's prompt encoder: token ids → the
2//! unnormalized hidden state after layer 50.
3//!
4//! The conditioning checkpoint is the 32 B model truncated at layer 50,
5//! and the DiT consumes that stream directly — no final norm, no LM
6//! head, and no chat template either: the H3 presentation is raw prompt
7//! text with no special tokens at all.
8//!
9//! Same shape as the Gemma encoder next door and deliberately as plain:
10//! a full-sequence causal forward, no KV cache, no sampling. Qwen3
11//! specifics carried exactly — per-head RMSNorm on q and k BEFORE the
12//! rotation, GQA 64/8, split-half RoPE at θ=5e6, SwiGLU, plain-w
13//! RMSNorm (not Gemma's 1+w) and no embedding scale.
14//!
15//! The rotation is Qwen3-VL's interleaved MRoPE. A text token's three
16//! axis positions are equal, so every frequency slot sees the same
17//! angle and the interleave is invisible; an image span pins the time
18//! axis and lays its merged grid out on the other two, and the tokens
19//! after it resume from the grid's larger side rather than from the
20//! span's length. `encode` is the text-only entry point and
21//! `encode_with_images` the general one.
22//!
23//! Deepstack features go in at LM layers 0, 1, 2 — the FIRST layers.
24//! `deepstack_visual_indexes` (8, 16, 24) names the vision layers the
25//! features are taken FROM, which is a different list and an easy one
26//! to conflate.
27
28use crate::dit::Proj;
29use crate::pool::Pool;
30use crate::qtensor::QTensor;
31use cortiq_core::CmfModel;
32use std::sync::Arc;
33
34struct Layer {
35    input_norm: Vec<f32>,
36    q: Proj,
37    k: Proj,
38    v: Proj,
39    o: Proj,
40    q_norm: Vec<f32>, // [head_dim]
41    k_norm: Vec<f32>,
42    post_attn_norm: Vec<f32>,
43    gate: Proj,
44    up: Proj,
45    down: Proj,
46}
47
48/// From this many tokens the attention runs as per-head GEMMs
49/// ([`Qwen3Encoder::attention_gemm`]); below it, the exact per-pair loop.
50/// (The projections stay on `matmat`: a dequantize-and-host-GEMM arm
51/// measured 29 → 55 s for 1064 tokens on a 28-core AVX2 host.)
52const GEMM_ATTN_MIN: usize = 256;
53
54pub struct Qwen3Encoder {
55    embed: QTensor,
56    layers: Vec<Layer>,
57    final_norm: Option<Vec<f32>>,
58    proj: Option<ClipProj>,
59    /// Optional vision-row twin (`te.proj.vis.*`): a projection fitted
60    /// on VISION activations, routed to image-span rows only. The base
61    /// projection was fitted on text and holds R^2 0.65 there while
62    /// actively wrong on vision (-0.57); one affine map cannot serve
63    /// both distributions — measured, 2026-08-15.
64    proj_vis: Option<ClipProj>,
65    pool: Option<Arc<Pool>>,
66    pub hidden: usize,
67    nh: usize,
68    nkv: usize,
69    hd: usize,
70    theta: f32,
71    eps: f64,
72    /// Weight-only exact q8 projections (`q8_exact_matmat`); off by
73    /// default — the Z-Image pipeline turns it on.
74    exact_q8: bool,
75    /// Projections (bit per `PROJ_NAMES` entry) that go through the
76    /// device GEMM of their codec (`QTensor::device_matmat`) before the
77    /// host kernels — the device-text-encoder experiment (vk2). 0 = off.
78    dev_ops: u8,
79}
80
81/// Projection names, in `dev_ops` bit order.
82pub const PROJ_NAMES: [&str; 7] = ["q", "k", "v", "o", "gate", "up", "down"];
83
84fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
85    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
86    let inv = 1.0 / (ss + eps).sqrt();
87    for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
88        *d = (v as f64 * inv) as f32 * g;
89    }
90}
91
92fn silu(v: f32) -> f32 {
93    v / (1.0 + (-v).exp())
94}
95
96struct SendPtr(*mut f32);
97unsafe impl Send for SendPtr {}
98unsafe impl Sync for SendPtr {}
99impl SendPtr {
100    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
101    #[allow(clippy::mut_from_ref)]
102    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
103        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
104    }
105}
106
107/// Gauss error function, Abramowitz & Stegun 7.1.26 (|ε| < 1.5e-7).
108/// `nn.GELU()` with no `approximate=` is the erf form, and the residual
109/// this feeds was fitted through exactly that.
110fn erf(x: f64) -> f64 {
111    let s = if x < 0.0 { -1.0 } else { 1.0 };
112    let x = x.abs();
113    let t = 1.0 / (1.0 + 0.327_591_1 * x);
114    let y = 1.0
115        - (((((1.061_405_429 * t - 1.453_152_027) * t) + 1.421_413_741) * t - 0.284_496_736) * t
116            + 0.254_829_592)
117            * t
118            * (-x * x).exp();
119    s * y
120}
121
122fn gelu(v: f32) -> f32 {
123    (0.5 * v as f64 * (1.0 + erf(v as f64 / std::f64::consts::SQRT_2))) as f32
124}
125
126/// The GELU residual of a `-mlp` ClipProj: `d_in → hidden → d_out`,
127/// added to the ridge matrix's output in the STANDARDIZED space.
128struct ProjMlp {
129    w0: Vec<f32>, // [hidden, d_in], torch Linear layout
130    b0: Vec<f32>,
131    w2: Vec<f32>, // [d_out, hidden]
132    b2: Vec<f32>,
133    hidden: usize,
134}
135
136/// ClipProj: the fitted map that lets a SMALL Qwen3-VL stand in for the
137/// 32 B prompt encoder.
138///
139/// ```text
140/// cond = ((h - mean_in) / std_in) @ W [+ mlp(...)] * std_out + mean_out
141/// ```
142///
143/// Token 0 is not projected but OVERWRITTEN with `sink_out`: it is the
144/// attention sink, an outlier the ridge fit cannot represent and would
145/// otherwise smear across the whole conditioning.
146struct ClipProj {
147    w: Vec<f32>, // [d_in, d_out], so the product is a plain row sweep
148    mean_in: Vec<f32>,
149    std_in: Vec<f32>,
150    mean_out: Vec<f32>,
151    std_out: Vec<f32>,
152    sink_out: Vec<f32>,
153    mlp: Option<ProjMlp>,
154    d_in: usize,
155    d_out: usize,
156}
157
158impl ClipProj {
159    /// `[n, d_in]` tapped hidden state → `[n, d_out]` conditioning.
160    fn apply(&self, h: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
161        self.apply_inner(h, n, pool, true)
162    }
163
164    /// `sink=false` skips the token-0 overwrite — for span-gathered
165    /// rows, whose first row is NOT the sequence's attention sink.
166    fn apply_inner(&self, h: &[f32], n: usize, pool: Option<&Pool>, sink: bool) -> Vec<f32> {
167        let (di, dout) = (self.d_in, self.d_out);
168        let mut out = vec![0f32; n * dout];
169        let ptr = SendPtr(out.as_mut_ptr());
170        let work = |lo: usize, hi: usize| {
171            let mut xn = vec![0f32; di];
172            let mut g = vec![0f32; self.mlp.as_ref().map_or(0, |m| m.hidden)];
173            for p in lo..hi {
174                let hp = &h[p * di..(p + 1) * di];
175                for i in 0..di {
176                    xn[i] = (hp[i] - self.mean_in[i]) / self.std_in[i];
177                }
178                // SAFETY: workers own disjoint token ranges.
179                let o = unsafe { ptr.row(p * dout, dout) };
180                o.fill(0.0);
181                for i in 0..di {
182                    let x = xn[i];
183                    let row = &self.w[i * dout..(i + 1) * dout];
184                    for (d, &wv) in o.iter_mut().zip(row) {
185                        *d += x * wv;
186                    }
187                }
188                if let Some(m) = &self.mlp {
189                    for (oi, gv) in g.iter_mut().enumerate() {
190                        let row = &m.w0[oi * di..(oi + 1) * di];
191                        let mut s = m.b0[oi];
192                        for (&r, &x) in row.iter().zip(&xn) {
193                            s += r * x;
194                        }
195                        *gv = gelu(s);
196                    }
197                    for (j, d) in o.iter_mut().enumerate() {
198                        let row = &m.w2[j * m.hidden..(j + 1) * m.hidden];
199                        let mut s = m.b2[j];
200                        for (&r, &gv) in row.iter().zip(&g) {
201                            s += r * gv;
202                        }
203                        *d += s;
204                    }
205                }
206                for (j, d) in o.iter_mut().enumerate() {
207                    *d = *d * self.std_out[j] + self.mean_out[j];
208                }
209            }
210        };
211        match pool {
212            Some(pl) => pl.run_rows(n, &work),
213            None => work(0, n),
214        }
215        if sink && n > 0 {
216            out[..dout].copy_from_slice(&self.sink_out);
217        }
218        out
219    }
220}
221
222/// One image spliced into the prompt: where its tokens start, how many
223/// there are, and the merged patch grid they came from.
224pub struct ImageSpan {
225    pub start: usize,
226    pub len: usize,
227    pub merged_h: usize,
228    pub merged_w: usize,
229}
230
231impl Qwen3Encoder {
232    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
233        let cfg: serde_json::Value = serde_json::from_slice(
234            model
235                .tensor_bytes("te.config_json")
236                .map_err(|e| e.to_string())?,
237        )
238        .map_err(|e| format!("te.config_json: {e}"))?;
239        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
240        // `CMF_TE_TAP` runs FEWER layers than the file carries. The tap
241        // a ClipProj was fitted on is an index, and index conventions
242        // differ by one between frameworks; packing one layer spare and
243        // calibrating against the teacher beats repacking to find out.
244        let nl = match std::env::var("CMF_TE_TAP")
245            .ok()
246            .and_then(|v| v.parse().ok())
247        {
248            Some(t) if t > 0 && t <= u("num_hidden_layers", 0) => t,
249            _ => u("num_hidden_layers", 0),
250        };
251        let f32v = |n: &str| crate::dit::cmf_f32(model, n);
252        let mut layers = Vec::with_capacity(nl);
253        for l in 0..nl {
254            let p = format!("te.layers.{l}");
255            layers.push(Layer {
256                input_norm: f32v(&format!("{p}.input_layernorm.weight"))?,
257                q: Proj::from_model(model, &format!("{p}.self_attn.q_proj.weight"))?,
258                k: Proj::from_model(model, &format!("{p}.self_attn.k_proj.weight"))?,
259                v: Proj::from_model(model, &format!("{p}.self_attn.v_proj.weight"))?,
260                o: Proj::from_model(model, &format!("{p}.self_attn.o_proj.weight"))?,
261                q_norm: f32v(&format!("{p}.self_attn.q_norm.weight"))?,
262                k_norm: f32v(&format!("{p}.self_attn.k_norm.weight"))?,
263                post_attn_norm: f32v(&format!("{p}.post_attention_layernorm.weight"))?,
264                gate: Proj::from_model(model, &format!("{p}.mlp.gate_proj.weight"))?,
265                up: Proj::from_model(model, &format!("{p}.mlp.up_proj.weight"))?,
266                down: Proj::from_model(model, &format!("{p}.mlp.down_proj.weight"))?,
267            });
268        }
269        let hidden = u("hidden_size", 0);
270        let proj = match model.tensor_bytes("te.proj.config_json") {
271            Ok(b) => {
272                let pc: serde_json::Value =
273                    serde_json::from_slice(b).map_err(|e| format!("te.proj.config_json: {e}"))?;
274                let pu = |k: &str| pc[k].as_u64().unwrap_or(0) as usize;
275                let (d_in, d_out) = (pu("d_in"), pu("d_out"));
276                if d_in != hidden {
277                    return Err(format!(
278                        "te.proj expects a {d_in}-wide encoder, the packed one is {hidden}: \
279                         the projection and the encoder come from different models"
280                    ));
281                }
282                let mlp = match pc["mlp"].as_bool() {
283                    Some(true) => Some(ProjMlp {
284                        w0: f32v("te.proj.mlp.0.weight")?,
285                        b0: f32v("te.proj.mlp.0.bias")?,
286                        w2: f32v("te.proj.mlp.2.weight")?,
287                        b2: f32v("te.proj.mlp.2.bias")?,
288                        hidden: pu("mlp_hidden"),
289                    }),
290                    _ => None,
291                };
292                Some(ClipProj {
293                    w: f32v("te.proj.W")?,
294                    mean_in: f32v("te.proj.mean_in")?,
295                    std_in: f32v("te.proj.std_in")?,
296                    mean_out: f32v("te.proj.mean_out")?,
297                    std_out: f32v("te.proj.std_out")?,
298                    sink_out: f32v("te.proj.sink_out")?,
299                    mlp,
300                    d_in,
301                    d_out,
302                })
303            }
304            Err(_) => None,
305        };
306        let proj_vis = match (&proj, model.tensor_bytes("te.proj.vis.W").is_ok()) {
307            (Some(base), true) => {
308                let g = |n: &str| crate::dit::cmf_f32(model, n);
309                Some(ClipProj {
310                    w: g("te.proj.vis.W")?,
311                    mean_in: g("te.proj.vis.mean_in")?,
312                    std_in: g("te.proj.vis.std_in")?,
313                    mean_out: g("te.proj.vis.mean_out")?,
314                    std_out: g("te.proj.vis.std_out")?,
315                    // vision rows never include token 0; the base sink is
316                    // authoritative. Kept for struct uniformity.
317                    sink_out: base.sink_out.clone(),
318                    mlp: None,
319                    d_in: base.d_in,
320                    d_out: base.d_out,
321                })
322            }
323            _ => None,
324        };
325        Ok(Self {
326            embed: QTensor::from_model(model, "te.embed_tokens.weight")?,
327            layers,
328            final_norm: match cfg["final_norm"].as_bool() {
329                Some(false) | None => None,
330                Some(true) => Some(f32v("te.norm.weight")?),
331            },
332            proj,
333            proj_vis,
334            pool: Pool::from_env(),
335            hidden,
336            nh: u("num_attention_heads", 0),
337            nkv: u("num_key_value_heads", 0),
338            hd: u("head_dim", 128),
339            theta: cfg["rope_theta"].as_f64().unwrap_or(5e6) as f32,
340            eps: cfg["rms_norm_eps"].as_f64().unwrap_or(1e-6),
341            exact_q8: false,
342            dev_ops: 0,
343        })
344    }
345
346    /// Weight-only exact q8 projections (f32 activations; see
347    /// `q8_exact_matmat`). Off by default, so every existing caller keeps
348    /// its numbers; Z-Image turns it on.
349    pub fn set_exact_q8(&mut self, on: bool) {
350        self.exact_q8 = on;
351    }
352
353    /// Route the named projections (`all`, or a comma list of
354    /// `PROJ_NAMES`) through the device GEMM of their codec; the host
355    /// kernels stay the fallback. Experiment knob (`CMF_ZIMAGE_TE_DEV` in the Z-Image pipeline, `ZC_TE_DEV` in examples/zimage_techeck.rs).
356    pub fn set_device_ops(&mut self, spec: &str) {
357        self.dev_ops = 0;
358        for t in spec.split(',').map(str::trim) {
359            if t == "all" {
360                self.dev_ops = 0x7f;
361            } else if let Some(i) = PROJ_NAMES.iter().position(|n| *n == t) {
362                self.dev_ops |= 1 << i;
363            }
364        }
365    }
366
367    fn mm(&self, p: &Proj, which: usize, xs: &[f32], n: usize, out: &mut [f32], pool: Option<&Pool>) {
368        if self.dev_ops & (1 << which) != 0 {
369            if let Proj::Q(q) = p {
370                if q.device_matmat(&xs[..n * q.cols()], n, &mut out[..n * q.rows()]) {
371                    return;
372                }
373            }
374        }
375        if self.exact_q8 && q8_exact_matmat(p, xs, n, out, pool).is_some() {
376            return;
377        }
378        // The projections kept at 16 bit (Z-Image: layer 6's down_proj,
379        // the massive-activation layer) are F32 here, and `matmat` sends a
380        // large F32 GEMM to the device whenever the device is on — the
381        // cooperative f32 GEMM, which is tf32-class (vk2: it alone moved
382        // the residual by 2.2e-4 at layer 6 and kept it there). The exact
383        // mode keeps them on the host f32 GEMM (0.5 GFLOP at 22 tokens).
384        if self.exact_q8 {
385            if let Proj::F32 { w, rows, cols } = p {
386                crate::fcd_ops::gemm_nt_host(xs, w, out, n, *cols, *rows, pool);
387                return;
388            }
389        }
390        p.matmat(xs, n, out, pool);
391    }
392
393    /// Per-head RMSNorm then split-half RoPE over the whole head, in
394    /// place across a `[n, heads·hd]` buffer.
395    fn norm_rope(&self, all: &mut [f32], n: usize, heads: usize, w: &[f32], pos: &[[f32; 3]]) {
396        let hd = self.hd;
397        let half = hd / 2;
398        let pool = self.pool.as_deref();
399        let ptr = SendPtr(all.as_mut_ptr());
400        let work = |lo: usize, hi: usize| {
401            for p in lo..hi {
402                for h in 0..heads {
403                    // SAFETY: workers own disjoint token ranges.
404                    let x = unsafe { ptr.row((p * heads + h) * hd, hd) };
405                    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hd as f64;
406                    let inv = 1.0 / (ss + self.eps).sqrt();
407                    for (d, &g) in x.iter_mut().zip(w) {
408                        *d = (*d as f64 * inv) as f32 * g;
409                    }
410                    for i in 0..half {
411                        let freq = 1.0 / self.theta.powf(2.0 * i as f32 / hd as f32);
412                        // Qwen3-VL's interleaved MRoPE: the time axis by
413                        // default, with height and width taking every
414                        // third slot below 3·rope_dims. All three axes
415                        // carry the same value on a text token, so this
416                        // is `p` there whatever the slot.
417                        let axis = if i < 60 && i % 3 == 1 {
418                            1
419                        } else if i < 60 && i % 3 == 2 {
420                            2
421                        } else {
422                            0
423                        };
424                        let (s, c) = (pos[p][axis] * freq).sin_cos();
425                        let (a, b) = (x[i], x[i + half]);
426                        x[i] = a * c - b * s;
427                        x[i + half] = a * s + b * c;
428                    }
429                }
430            }
431        };
432        match pool {
433            Some(pl) => pl.run_rows(n, &work),
434            None => work(0, n),
435        }
436    }
437
438    /// Causal attention of a long sequence as two GEMMs per head
439    /// (`Q·Kᵀ`, masked row softmax, `P·V`) on the host GEMM — an
440    /// image-conditioned prompt is ~1–4k tokens, where the per-pair dot
441    /// loop above costs tens of seconds. Short prompts keep the loop (and
442    /// its exact numbers).
443    fn attention_gemm(&self, q_all: &[f32], k_all: &[f32], v_all: &[f32], n: usize, attn: &mut [f32]) {
444        use crate::zimage::host_gemm;
445        let (nh, nkv, hd) = (self.nh, self.nkv, self.hd);
446        let hpk = nh / nkv;
447        let pool = self.pool.as_deref();
448        let scale = 1.0 / (hd as f32).sqrt();
449        let mut qh = vec![0f32; n * hd];
450        let mut kh = vec![0f32; n * hd];
451        let mut vt = vec![0f32; hd * n];
452        let mut scores = vec![0f32; n * n];
453        let mut oh = vec![0f32; n * hd];
454        let mut kv_cur = usize::MAX;
455        for hh in 0..nh {
456            let kv = hh / hpk;
457            for p in 0..n {
458                for d in 0..hd {
459                    qh[p * hd + d] = q_all[(p * nh + hh) * hd + d] * scale;
460                }
461            }
462            if kv != kv_cur {
463                kv_cur = kv;
464                for p in 0..n {
465                    kh[p * hd..(p + 1) * hd].copy_from_slice(&k_all[(p * nkv + kv) * hd..(p * nkv + kv + 1) * hd]);
466                    for d in 0..hd {
467                        vt[d * n + p] = v_all[(p * nkv + kv) * hd + d];
468                    }
469                }
470            }
471            host_gemm::gemm_nt(&qh, &kh, &mut scores, n, hd, n, pool);
472            {
473                let sp = SendPtr(scores.as_mut_ptr());
474                let soft = |lo: usize, hi: usize| {
475                    for r in lo..hi {
476                        // SAFETY: workers own disjoint score rows.
477                        let row = unsafe { sp.row(r * n, n) };
478                        let (live, dead) = row.split_at_mut(r + 1);
479                        let mx = live.iter().cloned().fold(f32::MIN, f32::max);
480                        let mut den = 0f32;
481                        for x in live.iter_mut() {
482                            *x = (*x - mx).exp();
483                            den += *x;
484                        }
485                        let inv = 1.0 / den;
486                        for x in live.iter_mut() {
487                            *x *= inv;
488                        }
489                        dead.fill(0.0);
490                    }
491                };
492                match pool {
493                    Some(pl) => pl.run_rows(n, &soft),
494                    None => soft(0, n),
495                }
496            }
497            host_gemm::gemm_nt(&scores, &vt, &mut oh, n, n, hd, pool);
498            for p in 0..n {
499                attn[(p * nh + hh) * hd..(p * nh + hh + 1) * hd].copy_from_slice(&oh[p * hd..(p + 1) * hd]);
500            }
501        }
502    }
503
504    /// Causal full-sequence forward. Returns `[n, hidden]` — the
505    /// residual stream leaving the last layer, normed only if the
506    /// config says the checkpoint has a final norm (H3's does not).
507    pub fn encode(&self, ids: &[u32]) -> Vec<f32> {
508        self.encode_with_images(ids, &[], &[], &[])
509    }
510
511    /// The 3-axis MRoPE position of every token.
512    ///
513    /// Text runs sequentially on all three axes; an image span pins the
514    /// time axis and lays its merged grid out on the other two, and
515    /// everything after it resumes from the grid's larger side rather
516    /// than from the span's length. With no image the three axes are
517    /// equal and this reduces to `0..n` — which is why the text-only
518    /// path can ignore the interleave entirely.
519    fn mrope_positions(&self, n: usize, spans: &[ImageSpan]) -> Vec<[f32; 3]> {
520        let mut pos: Vec<[f32; 3]> = (0..n).map(|i| [i as f32; 3]).collect();
521        let mut offset = 0i64;
522        for sp in spans {
523            let (start, end) = (sp.start, sp.start + sp.len);
524            let len_max = sp.merged_h.max(sp.merged_w) as i64;
525            let base = start as i64 + offset;
526            for i in start..end {
527                let k = i - start;
528                pos[i] = [
529                    base as f32,
530                    (base + (k / sp.merged_w) as i64) as f32,
531                    (base + (k % sp.merged_w) as i64) as f32,
532                ];
533            }
534            let next = len_max + start as i64;
535            for (j, p) in pos.iter_mut().enumerate().skip(end) {
536                let v = (next + offset + (j - end) as i64) as f32;
537                *p = [v; 3];
538            }
539            offset += len_max - sp.len as i64;
540        }
541        pos
542    }
543
544    /// Full forward with images. `embeds` replaces the token embedding
545    /// at `[span.start, span.start + span.len)` with the vision tower's
546    /// merged tokens; `deepstack[k]` is added at the visual positions
547    /// after LM layer k — the first layers, not the vision layers the
548    /// features were taken from.
549    pub fn encode_with_images(
550        &self,
551        ids: &[u32],
552        spans: &[ImageSpan],
553        embeds: &[Vec<f32>],
554        deepstack: &[Vec<f32>],
555    ) -> Vec<f32> {
556        let n = ids.len();
557        let hs = self.hidden;
558        let (nh, nkv, hd) = (self.nh, self.nkv, self.hd);
559        let hpk = nh / nkv;
560        let pool = self.pool.as_deref();
561        let scale = 1.0 / (hd as f32).sqrt();
562
563        let mut h = vec![0f32; n * hs];
564        for (i, &id) in ids.iter().enumerate() {
565            self.embed
566                .row_f32(id as usize, &mut h[i * hs..(i + 1) * hs]);
567        }
568        for (sp, e) in spans.iter().zip(embeds) {
569            h[sp.start * hs..(sp.start + sp.len) * hs].copy_from_slice(&e[..sp.len * hs]);
570        }
571        let pos = self.mrope_positions(n, spans);
572        // Which rows a deepstack feature lands on, in span order.
573        let visual: Vec<usize> = spans
574            .iter()
575            .flat_map(|s| s.start..s.start + s.len)
576            .collect();
577        let mut xn = vec![0f32; n * hs];
578        let mut q_all = vec![0f32; n * nh * hd];
579        let mut k_all = vec![0f32; n * nkv * hd];
580        let mut v_all = vec![0f32; n * nkv * hd];
581        let mut attn = vec![0f32; n * nh * hd];
582        let mut proj = vec![0f32; n * hs];
583
584        // `CMF_TE_TAPS=<dir>`: the residual after every layer and the
585        // intermediates of every layer (parity work).
586        let taps = std::env::var("CMF_TE_TAPS").ok();
587        let tap = |name: String, v: &[f32]| {
588            if let Some(dir) = &taps {
589                let _ = std::fs::create_dir_all(dir);
590                let _ = std::fs::write(
591                    std::path::Path::new(dir).join(format!("{name}.f32")),
592                    v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>(),
593                );
594            }
595        };
596        for (li, layer) in self.layers.iter().enumerate() {
597            for (o, src) in xn.chunks_exact_mut(hs).zip(h.chunks_exact(hs)) {
598                rms_norm_into(src, &layer.input_norm, self.eps, o);
599            }
600            self.mm(&layer.q, 0, &xn, n, &mut q_all, pool);
601            self.mm(&layer.k, 1, &xn, n, &mut k_all, pool);
602            self.mm(&layer.v, 2, &xn, n, &mut v_all, pool);
603            self.norm_rope(&mut q_all, n, nh, &layer.q_norm, &pos);
604            self.norm_rope(&mut k_all, n, nkv, &layer.k_norm, &pos);
605
606            attn.fill(0.0);
607            let ap = SendPtr(attn.as_mut_ptr());
608            let heads = |lo: usize, hi: usize| {
609                let mut row = vec![0f32; n];
610                for hh in lo..hi {
611                    let kv = hh / hpk;
612                    for p in 0..n {
613                        let qv = &q_all[(p * nh + hh) * hd..(p * nh + hh + 1) * hd];
614                        for (j, r) in row[..=p].iter_mut().enumerate() {
615                            let kvv = &k_all[(j * nkv + kv) * hd..(j * nkv + kv + 1) * hd];
616                            *r = qv.iter().zip(kvv).map(|(&a, &b)| a * b).sum::<f32>() * scale;
617                        }
618                        let mx = row[..=p].iter().cloned().fold(f32::MIN, f32::max);
619                        let mut den = 0f32;
620                        for r in row[..=p].iter_mut() {
621                            *r = (*r - mx).exp();
622                            den += *r;
623                        }
624                        let inv = 1.0 / den;
625                        // SAFETY: workers own disjoint head ranges, and
626                        // one head's slice is disjoint per token.
627                        let out = unsafe { ap.row((p * nh + hh) * hd, hd) };
628                        for (j, &rw) in row[..=p].iter().enumerate() {
629                            let vv = &v_all[(j * nkv + kv) * hd..(j * nkv + kv + 1) * hd];
630                            let wgt = rw * inv;
631                            for (o, &s) in out.iter_mut().zip(vv) {
632                                *o += wgt * s;
633                            }
634                        }
635                    }
636                }
637            };
638            if n >= GEMM_ATTN_MIN {
639                self.attention_gemm(&q_all, &k_all, &v_all, n, &mut attn);
640            } else {
641                match pool {
642                    Some(pl) => pl.run_rows(nh, &heads),
643                    None => heads(0, nh),
644                }
645            }
646
647            tap(format!("layer{li}_q"), &q_all);
648            tap(format!("layer{li}_attn"), &attn);
649            self.mm(&layer.o, 3, &attn, n, &mut proj, pool);
650            for (d, &v) in h.iter_mut().zip(&proj) {
651                *d += v;
652            }
653            tap(format!("layer{li}_h_mid"), &h);
654
655            for (o, src) in xn.chunks_exact_mut(hs).zip(h.chunks_exact(hs)) {
656                rms_norm_into(src, &layer.post_attn_norm, self.eps, o);
657            }
658            let inter = layer.gate.rows();
659            let mut g = vec![0f32; n * inter];
660            let mut u = vec![0f32; n * inter];
661            self.mm(&layer.gate, 4, &xn, n, &mut g, pool);
662            self.mm(&layer.up, 5, &xn, n, &mut u, pool);
663            tap(format!("layer{li}_gate"), &g);
664            for (a, &b) in g.iter_mut().zip(&u) {
665                *a = silu(*a) * b;
666            }
667            tap(format!("layer{li}_act"), &g);
668            self.mm(&layer.down, 6, &g, n, &mut proj, pool);
669            tap(format!("layer{li}_down"), &proj);
670            for (d, &v) in h.iter_mut().zip(&proj) {
671                *d += v;
672            }
673            tap(format!("layer{li}"), &h);
674            if let Some(f) = deepstack.get(li) {
675                for (k, &row) in visual.iter().enumerate() {
676                    for c in 0..hs {
677                        h[row * hs + c] += f[k * hs + c];
678                    }
679                }
680            }
681        }
682        if let Some(w) = &self.final_norm {
683            let mut out = vec![0f32; n * hs];
684            for (o, src) in out.chunks_exact_mut(hs).zip(h.chunks_exact(hs)) {
685                rms_norm_into(src, w, self.eps, o);
686            }
687            return out;
688        }
689        // A ClipProj file makes this a STAND-IN encoder: the stream
690        // leaving the tap is in the small model's space and the DiT
691        // never sees it raw.
692        // `CMF_CP_DUMP=<path>`: the RAW tapped hidden state, before the
693        // projection — one side of a (student, teacher) pair a refit
694        // regresses on. Same [u64 n][u64 width] + f32 rows framing as
695        // CMF_TE_DUMP, so one reader serves both.
696        if let (Ok(path), Some(_)) = (std::env::var("CMF_CP_DUMP"), &self.proj) {
697            let mut b = Vec::with_capacity(16 + h.len() * 4);
698            b.extend_from_slice(&(n as u64).to_le_bytes());
699            b.extend_from_slice(&(self.hidden as u64).to_le_bytes());
700            for v in &h {
701                b.extend_from_slice(&v.to_le_bytes());
702            }
703            if let Err(e) = std::fs::write(&path, &b) {
704                tracing::warn!("CMF_CP_DUMP {path}: {e}");
705            } else {
706                eprintln!("cp dump: {n} tokens x {} -> {path}", self.hidden);
707            }
708        }
709        match &self.proj {
710            Some(p) => {
711                let mut out = p.apply(&h, n, self.pool.as_deref());
712                // Vision rows go through their own map when the file
713                // ships one: gather span rows, project, scatter back.
714                if let Some(pv) = &self.proj_vis {
715                    if !visual.is_empty() {
716                        let (din, dout) = (pv.d_in, pv.d_out);
717                        let mut hv = Vec::with_capacity(visual.len() * din);
718                        for &r in &visual {
719                            hv.extend_from_slice(&h[r * din..(r + 1) * din]);
720                        }
721                        let ov = pv.apply_inner(&hv, visual.len(), self.pool.as_deref(), false);
722                        for (i, &r) in visual.iter().enumerate() {
723                            out[r * dout..(r + 1) * dout]
724                                .copy_from_slice(&ov[i * dout..(i + 1) * dout]);
725                        }
726                    }
727                }
728                out
729            }
730            None => h,
731        }
732    }
733
734    /// Width of what `encode` returns — the DiT's conditioning width,
735    /// which is the projection's output when one is packed and the
736    /// encoder's own hidden size otherwise.
737    pub fn out_hidden(&self) -> usize {
738        self.proj.as_ref().map_or(self.hidden, |p| p.d_out)
739    }
740}
741
742// ── Exact q8 projections (Z-Image, B2) ───────────────────────────────
743//
744// The default CPU q8 matmat quantizes the ACTIVATIONS to int8 too (the
745// a8w8 / SDOT contract). For the Z-Image caption that costs a factor of
746// three in accuracy: h_m2 rel 1.35e-2 against the fp32 oracle, against
747// 4.1e-3 weight-only (and 8.7e-3 for diffusers bf16). The exact scalar
748// path (`CMF_SDOT=0`) gets 4.1e-3 but is 7× slower (3.2 s for 22
749// tokens). This kernel is weight-only exact AND fast: f32 activations
750// pre-multiplied by the q8_2f column field, int8 rows widened to f32 in
751// registers, f32 FMA, the row scale applied once per output
752// (x86_64 AVX2+FMA; elsewhere the caller keeps the default path).
753
754/// `Some(())` when `p` is a q8_row / q8_2f weight the fast exact kernel
755/// handled; `None` = run the default `matmat`.
756fn q8_exact_matmat(p: &Proj, xs: &[f32], n: usize, out: &mut [f32], pool: Option<&Pool>) -> Option<()> {
757    #[cfg(target_arch = "x86_64")]
758    {
759        use crate::qtensor::QTensor;
760        use cortiq_core::TensorDtype as T;
761        if !(std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma")) {
762            return None;
763        }
764        let Proj::Q(QTensor::Mapped { model, idx, dtype, rows, cols, row_scale, col_field, .. }) = p else {
765            return None;
766        };
767        if !matches!(dtype, T::Q8_2f | T::Q8Row) || row_scale.len() != *rows {
768            return None;
769        }
770        let (rows, cols) = (*rows, *cols);
771        let bytes = model.entry_bytes(&model.tensors[*idx]);
772        if bytes.len() < rows * cols || xs.len() < n * cols || out.len() < n * rows {
773            return None;
774        }
775        // x' = x ⊙ col_field (q8_2f), so w·x = rs · Σ q·x'.
776        let xs2: Vec<f32> = if *dtype == T::Q8_2f && col_field.len() == cols {
777            xs[..n * cols].chunks_exact(cols).flat_map(|r| r.iter().zip(col_field).map(|(a, c)| a * c)).collect()
778        } else {
779            xs[..n * cols].to_vec()
780        };
781        let q = &bytes[..rows * cols];
782        let op = SendPtr(out.as_mut_ptr());
783        let work = |lo: usize, hi: usize| {
784            for o in lo..hi {
785                let wrow = &q[o * cols..(o + 1) * cols];
786                let mut b0 = 0;
787                while b0 < n {
788                    let nb = (n - b0).min(4);
789                    let mut acc = [0f32; 4];
790                    // SAFETY: AVX2+FMA checked above; slices sized above.
791                    unsafe { q8_dot4(wrow, &xs2[b0 * cols..], cols, nb, &mut acc) };
792                    for (k, a) in acc.iter().take(nb).enumerate() {
793                        // SAFETY: each (token, row) cell has one writer.
794                        unsafe { op.row((b0 + k) * rows + o, 1)[0] = a * row_scale[o] };
795                    }
796                    b0 += nb;
797                }
798            }
799        };
800        match pool {
801            Some(pl) => pl.run_rows(rows, &work),
802            None => work(0, rows),
803        }
804        Some(())
805    }
806    #[cfg(not(target_arch = "x86_64"))]
807    {
808        let _ = (p, xs, n, out, pool);
809        None
810    }
811}
812
813/// Σ_j w[j]·x_k[j] for up to 4 activation rows x_k = xs[k·cols..].
814#[cfg(target_arch = "x86_64")]
815#[target_feature(enable = "avx2,fma")]
816unsafe fn q8_dot4(w: &[u8], xs: &[f32], cols: usize, nb: usize, acc: &mut [f32; 4]) {
817    use std::arch::x86_64::*;
818    unsafe {
819        let mut a = [_mm256_setzero_ps(); 4];
820        let wp = w.as_ptr() as *const i8;
821        let xp = xs.as_ptr();
822        let mut j = 0;
823        while j + 8 <= cols {
824            let wi = _mm_loadl_epi64(wp.add(j) as *const __m128i);
825            let wf = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(wi));
826            for k in 0..4 {
827                if k < nb {
828                    a[k] = _mm256_fmadd_ps(wf, _mm256_loadu_ps(xp.add(k * cols + j)), a[k]);
829                }
830            }
831            j += 8;
832        }
833        for k in 0..nb {
834            let v = a[k];
835            let hi = _mm256_extractf128_ps(v, 1);
836            let lo = _mm256_castps256_ps128(v);
837            let s = _mm_add_ps(lo, hi);
838            let s = _mm_hadd_ps(s, s);
839            let s = _mm_hadd_ps(s, s);
840            let mut t = _mm_cvtss_f32(s);
841            for jj in j..cols {
842                t += (w[jj] as i8) as f32 * *xp.add(k * cols + jj);
843            }
844            acc[k] = t;
845        }
846    }
847}