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
48pub struct Qwen3Encoder {
49    embed: QTensor,
50    layers: Vec<Layer>,
51    final_norm: Option<Vec<f32>>,
52    proj: Option<ClipProj>,
53    pool: Option<Arc<Pool>>,
54    pub hidden: usize,
55    nh: usize,
56    nkv: usize,
57    hd: usize,
58    theta: f32,
59    eps: f64,
60}
61
62fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
63    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
64    let inv = 1.0 / (ss + eps).sqrt();
65    for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
66        *d = (v as f64 * inv) as f32 * g;
67    }
68}
69
70fn silu(v: f32) -> f32 {
71    v / (1.0 + (-v).exp())
72}
73
74struct SendPtr(*mut f32);
75unsafe impl Send for SendPtr {}
76unsafe impl Sync for SendPtr {}
77impl SendPtr {
78    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
79    #[allow(clippy::mut_from_ref)]
80    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
81        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
82    }
83}
84
85/// Gauss error function, Abramowitz & Stegun 7.1.26 (|ε| < 1.5e-7).
86/// `nn.GELU()` with no `approximate=` is the erf form, and the residual
87/// this feeds was fitted through exactly that.
88fn erf(x: f64) -> f64 {
89    let s = if x < 0.0 { -1.0 } else { 1.0 };
90    let x = x.abs();
91    let t = 1.0 / (1.0 + 0.327_591_1 * x);
92    let y = 1.0
93        - (((((1.061_405_429 * t - 1.453_152_027) * t) + 1.421_413_741) * t - 0.284_496_736) * t
94            + 0.254_829_592)
95            * t
96            * (-x * x).exp();
97    s * y
98}
99
100fn gelu(v: f32) -> f32 {
101    (0.5 * v as f64 * (1.0 + erf(v as f64 / std::f64::consts::SQRT_2))) as f32
102}
103
104/// The GELU residual of a `-mlp` ClipProj: `d_in → hidden → d_out`,
105/// added to the ridge matrix's output in the STANDARDIZED space.
106struct ProjMlp {
107    w0: Vec<f32>, // [hidden, d_in], torch Linear layout
108    b0: Vec<f32>,
109    w2: Vec<f32>, // [d_out, hidden]
110    b2: Vec<f32>,
111    hidden: usize,
112}
113
114/// ClipProj: the fitted map that lets a SMALL Qwen3-VL stand in for the
115/// 32 B prompt encoder.
116///
117/// ```text
118/// cond = ((h - mean_in) / std_in) @ W [+ mlp(...)] * std_out + mean_out
119/// ```
120///
121/// Token 0 is not projected but OVERWRITTEN with `sink_out`: it is the
122/// attention sink, an outlier the ridge fit cannot represent and would
123/// otherwise smear across the whole conditioning.
124struct ClipProj {
125    w: Vec<f32>, // [d_in, d_out], so the product is a plain row sweep
126    mean_in: Vec<f32>,
127    std_in: Vec<f32>,
128    mean_out: Vec<f32>,
129    std_out: Vec<f32>,
130    sink_out: Vec<f32>,
131    mlp: Option<ProjMlp>,
132    d_in: usize,
133    d_out: usize,
134}
135
136impl ClipProj {
137    /// `[n, d_in]` tapped hidden state → `[n, d_out]` conditioning.
138    fn apply(&self, h: &[f32], n: usize, pool: Option<&Pool>) -> Vec<f32> {
139        let (di, dout) = (self.d_in, self.d_out);
140        let mut out = vec![0f32; n * dout];
141        let ptr = SendPtr(out.as_mut_ptr());
142        let work = |lo: usize, hi: usize| {
143            let mut xn = vec![0f32; di];
144            let mut g = vec![0f32; self.mlp.as_ref().map_or(0, |m| m.hidden)];
145            for p in lo..hi {
146                let hp = &h[p * di..(p + 1) * di];
147                for i in 0..di {
148                    xn[i] = (hp[i] - self.mean_in[i]) / self.std_in[i];
149                }
150                // SAFETY: workers own disjoint token ranges.
151                let o = unsafe { ptr.row(p * dout, dout) };
152                o.fill(0.0);
153                for i in 0..di {
154                    let x = xn[i];
155                    let row = &self.w[i * dout..(i + 1) * dout];
156                    for (d, &wv) in o.iter_mut().zip(row) {
157                        *d += x * wv;
158                    }
159                }
160                if let Some(m) = &self.mlp {
161                    for (oi, gv) in g.iter_mut().enumerate() {
162                        let row = &m.w0[oi * di..(oi + 1) * di];
163                        let mut s = m.b0[oi];
164                        for (&r, &x) in row.iter().zip(&xn) {
165                            s += r * x;
166                        }
167                        *gv = gelu(s);
168                    }
169                    for (j, d) in o.iter_mut().enumerate() {
170                        let row = &m.w2[j * m.hidden..(j + 1) * m.hidden];
171                        let mut s = m.b2[j];
172                        for (&r, &gv) in row.iter().zip(&g) {
173                            s += r * gv;
174                        }
175                        *d += s;
176                    }
177                }
178                for (j, d) in o.iter_mut().enumerate() {
179                    *d = *d * self.std_out[j] + self.mean_out[j];
180                }
181            }
182        };
183        match pool {
184            Some(pl) => pl.run_rows(n, &work),
185            None => work(0, n),
186        }
187        if n > 0 {
188            out[..dout].copy_from_slice(&self.sink_out);
189        }
190        out
191    }
192}
193
194/// One image spliced into the prompt: where its tokens start, how many
195/// there are, and the merged patch grid they came from.
196pub struct ImageSpan {
197    pub start: usize,
198    pub len: usize,
199    pub merged_h: usize,
200    pub merged_w: usize,
201}
202
203impl Qwen3Encoder {
204    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
205        let cfg: serde_json::Value = serde_json::from_slice(
206            model.tensor_bytes("te.config_json").map_err(|e| e.to_string())?,
207        )
208        .map_err(|e| format!("te.config_json: {e}"))?;
209        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
210        // `CMF_TE_TAP` runs FEWER layers than the file carries. The tap
211        // a ClipProj was fitted on is an index, and index conventions
212        // differ by one between frameworks; packing one layer spare and
213        // calibrating against the teacher beats repacking to find out.
214        let nl = match std::env::var("CMF_TE_TAP").ok().and_then(|v| v.parse().ok()) {
215            Some(t) if t > 0 && t <= u("num_hidden_layers", 0) => t,
216            _ => u("num_hidden_layers", 0),
217        };
218        let f32v = |n: &str| crate::dit::cmf_f32(model, n);
219        let mut layers = Vec::with_capacity(nl);
220        for l in 0..nl {
221            let p = format!("te.layers.{l}");
222            layers.push(Layer {
223                input_norm: f32v(&format!("{p}.input_layernorm.weight"))?,
224                q: Proj::from_model(model, &format!("{p}.self_attn.q_proj.weight"))?,
225                k: Proj::from_model(model, &format!("{p}.self_attn.k_proj.weight"))?,
226                v: Proj::from_model(model, &format!("{p}.self_attn.v_proj.weight"))?,
227                o: Proj::from_model(model, &format!("{p}.self_attn.o_proj.weight"))?,
228                q_norm: f32v(&format!("{p}.self_attn.q_norm.weight"))?,
229                k_norm: f32v(&format!("{p}.self_attn.k_norm.weight"))?,
230                post_attn_norm: f32v(&format!("{p}.post_attention_layernorm.weight"))?,
231                gate: Proj::from_model(model, &format!("{p}.mlp.gate_proj.weight"))?,
232                up: Proj::from_model(model, &format!("{p}.mlp.up_proj.weight"))?,
233                down: Proj::from_model(model, &format!("{p}.mlp.down_proj.weight"))?,
234            });
235        }
236        let hidden = u("hidden_size", 0);
237        let proj = match model.tensor_bytes("te.proj.config_json") {
238            Ok(b) => {
239                let pc: serde_json::Value =
240                    serde_json::from_slice(b).map_err(|e| format!("te.proj.config_json: {e}"))?;
241                let pu = |k: &str| pc[k].as_u64().unwrap_or(0) as usize;
242                let (d_in, d_out) = (pu("d_in"), pu("d_out"));
243                if d_in != hidden {
244                    return Err(format!(
245                        "te.proj expects a {d_in}-wide encoder, the packed one is {hidden}: \
246                         the projection and the encoder come from different models"
247                    ));
248                }
249                let mlp = match pc["mlp"].as_bool() {
250                    Some(true) => Some(ProjMlp {
251                        w0: f32v("te.proj.mlp.0.weight")?,
252                        b0: f32v("te.proj.mlp.0.bias")?,
253                        w2: f32v("te.proj.mlp.2.weight")?,
254                        b2: f32v("te.proj.mlp.2.bias")?,
255                        hidden: pu("mlp_hidden"),
256                    }),
257                    _ => None,
258                };
259                Some(ClipProj {
260                    w: f32v("te.proj.W")?,
261                    mean_in: f32v("te.proj.mean_in")?,
262                    std_in: f32v("te.proj.std_in")?,
263                    mean_out: f32v("te.proj.mean_out")?,
264                    std_out: f32v("te.proj.std_out")?,
265                    sink_out: f32v("te.proj.sink_out")?,
266                    mlp,
267                    d_in,
268                    d_out,
269                })
270            }
271            Err(_) => None,
272        };
273        Ok(Self {
274            embed: QTensor::from_model(model, "te.embed_tokens.weight")?,
275            layers,
276            final_norm: match cfg["final_norm"].as_bool() {
277                Some(false) | None => None,
278                Some(true) => Some(f32v("te.norm.weight")?),
279            },
280            proj,
281            pool: Pool::from_env(),
282            hidden,
283            nh: u("num_attention_heads", 0),
284            nkv: u("num_key_value_heads", 0),
285            hd: u("head_dim", 128),
286            theta: cfg["rope_theta"].as_f64().unwrap_or(5e6) as f32,
287            eps: cfg["rms_norm_eps"].as_f64().unwrap_or(1e-6),
288        })
289    }
290
291    /// Per-head RMSNorm then split-half RoPE over the whole head, in
292    /// place across a `[n, heads·hd]` buffer.
293    fn norm_rope(&self, all: &mut [f32], n: usize, heads: usize, w: &[f32], pos: &[[f32; 3]]) {
294        let hd = self.hd;
295        let half = hd / 2;
296        let pool = self.pool.as_deref();
297        let ptr = SendPtr(all.as_mut_ptr());
298        let work = |lo: usize, hi: usize| {
299            for p in lo..hi {
300                for h in 0..heads {
301                    // SAFETY: workers own disjoint token ranges.
302                    let x = unsafe { ptr.row((p * heads + h) * hd, hd) };
303                    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hd as f64;
304                    let inv = 1.0 / (ss + self.eps).sqrt();
305                    for (d, &g) in x.iter_mut().zip(w) {
306                        *d = (*d as f64 * inv) as f32 * g;
307                    }
308                    for i in 0..half {
309                        let freq = 1.0 / self.theta.powf(2.0 * i as f32 / hd as f32);
310                        // Qwen3-VL's interleaved MRoPE: the time axis by
311                        // default, with height and width taking every
312                        // third slot below 3·rope_dims. All three axes
313                        // carry the same value on a text token, so this
314                        // is `p` there whatever the slot.
315                        let axis = if i < 60 && i % 3 == 1 {
316                            1
317                        } else if i < 60 && i % 3 == 2 {
318                            2
319                        } else {
320                            0
321                        };
322                        let (s, c) = (pos[p][axis] * freq).sin_cos();
323                        let (a, b) = (x[i], x[i + half]);
324                        x[i] = a * c - b * s;
325                        x[i + half] = a * s + b * c;
326                    }
327                }
328            }
329        };
330        match pool {
331            Some(pl) => pl.run_rows(n, &work),
332            None => work(0, n),
333        }
334    }
335
336    /// Causal full-sequence forward. Returns `[n, hidden]` — the
337    /// residual stream leaving the last layer, normed only if the
338    /// config says the checkpoint has a final norm (H3's does not).
339    pub fn encode(&self, ids: &[u32]) -> Vec<f32> {
340        self.encode_with_images(ids, &[], &[], &[])
341    }
342
343    /// The 3-axis MRoPE position of every token.
344    ///
345    /// Text runs sequentially on all three axes; an image span pins the
346    /// time axis and lays its merged grid out on the other two, and
347    /// everything after it resumes from the grid's larger side rather
348    /// than from the span's length. With no image the three axes are
349    /// equal and this reduces to `0..n` — which is why the text-only
350    /// path can ignore the interleave entirely.
351    fn mrope_positions(&self, n: usize, spans: &[ImageSpan]) -> Vec<[f32; 3]> {
352        let mut pos: Vec<[f32; 3]> = (0..n).map(|i| [i as f32; 3]).collect();
353        let mut offset = 0i64;
354        for sp in spans {
355            let (start, end) = (sp.start, sp.start + sp.len);
356            let len_max = sp.merged_h.max(sp.merged_w) as i64;
357            let base = start as i64 + offset;
358            for i in start..end {
359                let k = i - start;
360                pos[i] = [
361                    base as f32,
362                    (base + (k / sp.merged_w) as i64) as f32,
363                    (base + (k % sp.merged_w) as i64) as f32,
364                ];
365            }
366            let next = len_max + start as i64;
367            for (j, p) in pos.iter_mut().enumerate().skip(end) {
368                let v = (next + offset + (j - end) as i64) as f32;
369                *p = [v; 3];
370            }
371            offset += len_max - sp.len as i64;
372        }
373        pos
374    }
375
376    /// Full forward with images. `embeds` replaces the token embedding
377    /// at `[span.start, span.start + span.len)` with the vision tower's
378    /// merged tokens; `deepstack[k]` is added at the visual positions
379    /// after LM layer k — the first layers, not the vision layers the
380    /// features were taken from.
381    pub fn encode_with_images(
382        &self,
383        ids: &[u32],
384        spans: &[ImageSpan],
385        embeds: &[Vec<f32>],
386        deepstack: &[Vec<f32>],
387    ) -> Vec<f32> {
388        let n = ids.len();
389        let hs = self.hidden;
390        let (nh, nkv, hd) = (self.nh, self.nkv, self.hd);
391        let hpk = nh / nkv;
392        let pool = self.pool.as_deref();
393        let scale = 1.0 / (hd as f32).sqrt();
394
395        let mut h = vec![0f32; n * hs];
396        for (i, &id) in ids.iter().enumerate() {
397            self.embed.row_f32(id as usize, &mut h[i * hs..(i + 1) * hs]);
398        }
399        for (sp, e) in spans.iter().zip(embeds) {
400            h[sp.start * hs..(sp.start + sp.len) * hs].copy_from_slice(&e[..sp.len * hs]);
401        }
402        let pos = self.mrope_positions(n, spans);
403        // Which rows a deepstack feature lands on, in span order.
404        let visual: Vec<usize> = spans
405            .iter()
406            .flat_map(|s| s.start..s.start + s.len)
407            .collect();
408        let mut xn = vec![0f32; n * hs];
409        let mut q_all = vec![0f32; n * nh * hd];
410        let mut k_all = vec![0f32; n * nkv * hd];
411        let mut v_all = vec![0f32; n * nkv * hd];
412        let mut attn = vec![0f32; n * nh * hd];
413        let mut proj = vec![0f32; n * hs];
414
415        for (li, layer) in self.layers.iter().enumerate() {
416            for (o, src) in xn.chunks_exact_mut(hs).zip(h.chunks_exact(hs)) {
417                rms_norm_into(src, &layer.input_norm, self.eps, o);
418            }
419            layer.q.matmat(&xn, n, &mut q_all, pool);
420            layer.k.matmat(&xn, n, &mut k_all, pool);
421            layer.v.matmat(&xn, n, &mut v_all, pool);
422            self.norm_rope(&mut q_all, n, nh, &layer.q_norm, &pos);
423            self.norm_rope(&mut k_all, n, nkv, &layer.k_norm, &pos);
424
425            attn.fill(0.0);
426            let ap = SendPtr(attn.as_mut_ptr());
427            let heads = |lo: usize, hi: usize| {
428                let mut row = vec![0f32; n];
429                for hh in lo..hi {
430                    let kv = hh / hpk;
431                    for p in 0..n {
432                        let qv = &q_all[(p * nh + hh) * hd..(p * nh + hh + 1) * hd];
433                        for (j, r) in row[..=p].iter_mut().enumerate() {
434                            let kvv = &k_all[(j * nkv + kv) * hd..(j * nkv + kv + 1) * hd];
435                            *r = qv.iter().zip(kvv).map(|(&a, &b)| a * b).sum::<f32>() * scale;
436                        }
437                        let mx = row[..=p].iter().cloned().fold(f32::MIN, f32::max);
438                        let mut den = 0f32;
439                        for r in row[..=p].iter_mut() {
440                            *r = (*r - mx).exp();
441                            den += *r;
442                        }
443                        let inv = 1.0 / den;
444                        // SAFETY: workers own disjoint head ranges, and
445                        // one head's slice is disjoint per token.
446                        let out = unsafe { ap.row((p * nh + hh) * hd, hd) };
447                        for (j, &rw) in row[..=p].iter().enumerate() {
448                            let vv = &v_all[(j * nkv + kv) * hd..(j * nkv + kv + 1) * hd];
449                            let wgt = rw * inv;
450                            for (o, &s) in out.iter_mut().zip(vv) {
451                                *o += wgt * s;
452                            }
453                        }
454                    }
455                }
456            };
457            match pool {
458                Some(pl) => pl.run_rows(nh, &heads),
459                None => heads(0, nh),
460            }
461
462            layer.o.matmat(&attn, n, &mut proj, pool);
463            for (d, &v) in h.iter_mut().zip(&proj) {
464                *d += v;
465            }
466
467            for (o, src) in xn.chunks_exact_mut(hs).zip(h.chunks_exact(hs)) {
468                rms_norm_into(src, &layer.post_attn_norm, self.eps, o);
469            }
470            let inter = layer.gate.rows();
471            let mut g = vec![0f32; n * inter];
472            let mut u = vec![0f32; n * inter];
473            layer.gate.matmat(&xn, n, &mut g, pool);
474            layer.up.matmat(&xn, n, &mut u, pool);
475            for (a, &b) in g.iter_mut().zip(&u) {
476                *a = silu(*a) * b;
477            }
478            layer.down.matmat(&g, n, &mut proj, pool);
479            for (d, &v) in h.iter_mut().zip(&proj) {
480                *d += v;
481            }
482            if let Some(f) = deepstack.get(li) {
483                for (k, &row) in visual.iter().enumerate() {
484                    for c in 0..hs {
485                        h[row * hs + c] += f[k * hs + c];
486                    }
487                }
488            }
489        }
490        if let Some(w) = &self.final_norm {
491            let mut out = vec![0f32; n * hs];
492            for (o, src) in out.chunks_exact_mut(hs).zip(h.chunks_exact(hs)) {
493                rms_norm_into(src, w, self.eps, o);
494            }
495            return out;
496        }
497        // A ClipProj file makes this a STAND-IN encoder: the stream
498        // leaving the tap is in the small model's space and the DiT
499        // never sees it raw.
500        match &self.proj {
501            Some(p) => p.apply(&h, n, self.pool.as_deref()),
502            None => h,
503        }
504    }
505
506    /// Width of what `encode` returns — the DiT's conditioning width,
507    /// which is the projection's output when one is packed and the
508    /// encoder's own hidden size otherwise.
509    pub fn out_hidden(&self) -> usize {
510        self.proj.as_ref().map_or(self.hidden, |p| p.d_out)
511    }
512}