Skip to main content

needle_core/v3/
model.rs

1//! Needle 3 model: weights and the whole-sequence forward pass.
2//!
3//! This is the prefill form — it computes every position at once, exactly as
4//! upstream's training graph does, and is what parity is established against.
5//! Incremental decode with a KV cache is layered on afterwards; getting the
6//! order the other way round means debugging a cache against logits that were
7//! never verified.
8
9extern crate alloc;
10use alloc::vec;
11use alloc::vec::Vec;
12
13use crate::cq::CqWeight;
14use crate::kernels::rms_unit_to;
15use crate::math::{cos, sin, sqrt};
16use crate::norm::zc_rms_norm_vec;
17use crate::ops::sigmoid;
18use crate::v3::attention::{
19    attend, attend_step, causal_depthwise_conv, norm_and_rope, AttnDims, KvStore, Ring,
20};
21use crate::v3::cache::{fake_quant_vec, quantize_rows, KvPrecision, Qkv, V3Cache};
22
23/// Positions per batched-prefill chunk.
24pub const DEFAULT_CHUNK: usize = 64;
25use crate::v3::config::V3Config;
26use crate::v3::engram::{engram_indices, ngram_valid, value_conv, EngramDims};
27use crate::v3::heads::{ProbeHead, ProbePool};
28use crate::v3::kernels::{hadamard_mlp, HadaMlp, HadaPerms};
29use crate::v3::mhc::{mix_down, scatter_up, Gates, LaneSite, MhcLayer};
30
31/// One transformer layer's weights.
32pub struct V3Layer {
33    pub norm_in: Vec<f32>,
34    pub q_proj: CqWeight,
35    pub k_proj: CqWeight,
36    pub v_proj: CqWeight,
37    /// `(taps, dim)` each; empty when the model declares no conv.
38    pub q_taps: Vec<f32>,
39    pub k_taps: Vec<f32>,
40    pub v_taps: Vec<f32>,
41    /// Per-head-dim ZCRMSNorm scales, `qk_head_dim` long.
42    pub q_norm: Vec<f32>,
43    pub k_norm: Vec<f32>,
44    pub gate_proj: CqWeight,
45    pub out_proj: CqWeight,
46    pub post_norm: Vec<f32>,
47    /// Stored pre-sigmoid, as upstream stores it.
48    pub attn_gate: f32,
49    pub pre_hada: Vec<f32>,
50    pub mlp: HadaMlp,
51}
52
53/// mHC parameters for the whole stack.
54pub struct V3Mhc {
55    /// `[num_layers]` each.
56    pub a_pre: Vec<f32>,
57    pub a_post: Vec<f32>,
58    pub a_res: Vec<f32>,
59    /// `[num_layers * lanes]`.
60    pub b_pre: Vec<f32>,
61    pub b_post: Vec<f32>,
62    /// `[num_layers * lanes * lanes]`.
63    pub b_res: Vec<f32>,
64    /// `[num_layers * lanes, mhc_width]` — row `layer * lanes + lane`.
65    pub phi_pre: CqWeight,
66    pub phi_post: CqWeight,
67    /// `[num_layers * lanes², mhc_width]`.
68    pub phi_res: CqWeight,
69}
70
71/// One Engram site.
72pub struct V3EngramSite {
73    /// `[num_tables * slots, sub_dim]`.
74    pub tables: CqWeight,
75    pub key_proj: CqWeight,
76    pub value_proj: CqWeight,
77    /// `[conv_taps, d_model]`.
78    pub taps: Vec<f32>,
79}
80
81/// A loaded Needle 3 model.
82pub struct V3Model {
83    pub cfg: V3Config,
84    /// Tied embedding, `[vocab_size, d_model]` — also the LM head.
85    pub embedding: CqWeight,
86    pub layers: Vec<V3Layer>,
87    pub mhc: V3Mhc,
88    pub engrams: Vec<V3EngramSite>,
89    pub final_norm: Vec<f32>,
90    pub perms: HadaPerms,
91    embed_scale: f32,
92}
93
94/// Apply upstream's per-head 8-bit activation quantisation in place, across
95/// `seq` positions of a `heads * head_dim` row.
96///
97/// This is `a8_fake_quant_kv` — `fake_quant(x, x.shape[-1], 8)` — which
98/// upstream applies to the query, the keys and the values alike.
99fn quant_rows(x: &mut [f32], seq: usize, heads: usize, head_dim: usize) {
100    for t in 0..seq {
101        for h in 0..heads {
102            let o = (t * heads + h) * head_dim;
103            fake_quant_vec(&mut x[o..o + head_dim], 8);
104        }
105    }
106}
107
108impl V3Model {
109    pub fn new(
110        cfg: V3Config,
111        embedding: CqWeight,
112        layers: Vec<V3Layer>,
113        mhc: V3Mhc,
114        engrams: Vec<V3EngramSite>,
115        final_norm: Vec<f32>,
116        perms: HadaPerms,
117    ) -> Result<Self, &'static str> {
118        if layers.len() != cfg.num_layers {
119            return Err("layer count does not match geometry");
120        }
121        if engrams.len() != cfg.engram.sites.len() {
122            return Err("engram site count does not match geometry");
123        }
124        if embedding.out_feat != cfg.vocab_size || embedding.in_feat != cfg.d_model {
125            return Err("embedding shape does not match geometry");
126        }
127        let embed_scale = sqrt(cfg.d_model as f32);
128        Ok(Self {
129            cfg,
130            embedding,
131            layers,
132            mhc,
133            engrams,
134            final_norm,
135            perms,
136            embed_scale,
137        })
138    }
139
140    /// One incremental decode step: feed `token`, get logits for its position.
141    ///
142    /// Mathematically identical to [`Self::forward_sequence`] at the same
143    /// position — the self-consistency test asserts exactly that.
144    ///
145    /// The mHC lane stream is deliberately **not** cached. Each position's
146    /// lanes start from its own embedding and evolve using only that
147    /// position's values; the sole cross-position dependencies are attention,
148    /// the Q/K/V convolution and the Engram, and the cache carries each of
149    /// those separately.
150    pub fn decode_step(&self, cache: &mut V3Cache, token: u32) -> Vec<f32> {
151        let cfg = &self.cfg;
152        let (d, n) = (cfg.d_model, cfg.mhc_lanes);
153        let pos = cache.pos();
154        let rows = cfg.logit_rows();
155
156        cache.push_token(token);
157
158        // ── Engram for this position ─────────────────────────────────────
159        // The hash reads the last `max(order)` tokens; the cache keeps
160        // exactly that many, oldest first, so the local index of "now" is the
161        // end of that history.
162        let e = &cfg.engram;
163        let hist = cache.token_history().to_vec();
164        let here = hist.len() - 1;
165        let idx = engram_indices(&hist, &e.orders, e.heads, e.slots as u32, e.seed_heads);
166        let num_tables = e.num_tables();
167
168        let mut ek = vec![0.0f32; e.sites.len() * d];
169        let mut ev = vec![0.0f32; e.sites.len() * d];
170        let mut fetched = vec![0.0f32; num_tables * e.sub_dim];
171        let mut row = vec![0.0f32; e.sub_dim];
172        for (s, site) in self.engrams.iter().enumerate() {
173            for t in 0..num_tables {
174                let dst = &mut fetched[t * e.sub_dim..(t + 1) * e.sub_dim];
175                // Validity is judged against the absolute position, not the
176                // retained history length.
177                if ngram_valid(pos, t, &e.orders, e.heads) {
178                    let slot = idx[here * num_tables + t] as usize;
179                    site.tables.dequantize_row(t * e.slots + slot, &mut row);
180                    dst.copy_from_slice(&row);
181                } else {
182                    dst.fill(0.0);
183                }
184            }
185            site.key_proj.matvec(&fetched, &mut ek[s * d..(s + 1) * d]);
186            site.value_proj
187                .matvec(&fetched, &mut ev[s * d..(s + 1) * d]);
188            let mut cur = ev[s * d..(s + 1) * d].to_vec();
189            cache.engram_conv_step(s, &site.taps, &mut cur);
190            ev[s * d..(s + 1) * d].copy_from_slice(&cur);
191        }
192
193        // ── Lane stream for this position ────────────────────────────────
194        let mut lanes = vec![0.0f32; n * d];
195        let mut emb = vec![0.0f32; d];
196        self.embedding
197            .dequantize_row(self.clamp_token(token), &mut emb);
198        for x in emb.iter_mut() {
199            *x *= self.embed_scale;
200        }
201        for lane in 0..n {
202            lanes[lane * d..(lane + 1) * d].copy_from_slice(&emb);
203        }
204
205        let dims = AttnDims {
206            seq: 1,
207            num_heads: cfg.num_heads,
208            num_kv_heads: cfg.num_kv_heads,
209            qk_head_dim: cfg.qk_head_dim,
210            v_head_dim: cfg.v_head_dim,
211        };
212        let half = cfg.qk_head_dim / 2;
213        let (rc, rs) = self.rope_at(pos);
214
215        let mut nx = vec![0.0f32; n * d];
216        let mut nx_prep = vec![0.0f32; self.mhc.phi_pre.prepared_len()];
217        let mut hpre = vec![0.0f32; n];
218        let mut hpost = vec![0.0f32; n];
219        let mut hres = vec![0.0f32; n * n];
220        let mut scratch = vec![0.0f32; n * d];
221        let mut u = vec![0.0f32; d];
222        let mut q = vec![0.0f32; cfg.q_dim()];
223        let mut k = vec![0.0f32; cfg.k_dim()];
224        let mut v = vec![0.0f32; cfg.v_dim()];
225        let mut attn = vec![0.0f32; cfg.attn_out_dim()];
226        let mut gate = vec![0.0f32; cfg.attn_out_dim()];
227        let mut proj = vec![0.0f32; d];
228        let mut mlp_out = vec![0.0f32; d];
229
230        for li in 0..cfg.num_layers {
231            let layer = &self.layers[li];
232            let m = MhcLayer {
233                a_pre: self.mhc.a_pre[li],
234                a_post: self.mhc.a_post[li],
235                a_res: self.mhc.a_res[li],
236                b_pre: &self.mhc.b_pre[li * n..(li + 1) * n],
237                b_post: &self.mhc.b_post[li * n..(li + 1) * n],
238                b_res: &self.mhc.b_res[li * n * n..(li + 1) * n * n],
239            };
240
241            rms_unit_to(&lanes, &mut nx);
242            self.mhc.phi_pre.prepare_input(&nx, &mut nx_prep);
243            self.mhc
244                .phi_pre
245                .matvec_rows_prepared(&nx_prep, li * n, &mut hpre);
246            mix_down(&lanes, &mut hpre, &m, li, n, d, &mut u);
247
248            let mut bx = u.clone();
249            if let Some(site) = cfg.engram.site_of(li) {
250                crate::v3::engram::apply_site(
251                    &mut bx,
252                    &ek[site * d..(site + 1) * d],
253                    &ev[site * d..(site + 1) * d],
254                    d,
255                );
256            }
257
258            let mut h = bx.clone();
259            zc_rms_norm_vec(&mut h, &layer.norm_in);
260            layer.q_proj.matvec(&h, &mut q);
261            layer.k_proj.matvec(&h, &mut k);
262            layer.v_proj.matvec(&h, &mut v);
263
264            if cfg.qkv_conv_taps > 0 {
265                cache.conv_step(li, Qkv::Q, &layer.q_taps, &mut q);
266                cache.conv_step(li, Qkv::K, &layer.k_taps, &mut k);
267                cache.conv_step(li, Qkv::V, &layer.v_taps, &mut v);
268            }
269            norm_and_rope(
270                &mut q,
271                &layer.q_norm,
272                &rc,
273                &rs,
274                1,
275                cfg.num_heads,
276                cfg.qk_head_dim,
277            );
278            norm_and_rope(
279                &mut k,
280                &layer.k_norm,
281                &rc,
282                &rs,
283                1,
284                cfg.num_kv_heads,
285                cfg.qk_head_dim,
286            );
287            let _ = half;
288
289            if let KvPrecision::Int8 = cache.precision() {
290                // `write_kv` quantises the keys and values; the query is the
291                // other half of upstream's quantised attention.
292                quant_rows(&mut q, 1, cfg.num_heads, cfg.qk_head_dim);
293            }
294            cache.write_kv(li, pos, &k, &v);
295            let (lo, hi) = cache.span(li, pos);
296            let slots = cache.slots(li);
297            attend_step(
298                &q,
299                Ring {
300                    kv: cache.kv(li),
301                    slots,
302                    lo,
303                    hi,
304                },
305                dims,
306                &mut attn,
307            );
308
309            layer.gate_proj.matvec(&h, &mut gate);
310            for (ai, &gi) in attn.iter_mut().zip(gate.iter()) {
311                *ai *= sigmoid(gi);
312            }
313            layer.out_proj.matvec(&attn, &mut proj);
314            zc_rms_norm_vec(&mut proj, &layer.post_norm);
315            let agate = sigmoid(layer.attn_gate);
316            for (si, &pi) in bx.iter_mut().zip(proj.iter()) {
317                *si += agate * pi;
318            }
319
320            proj.copy_from_slice(&bx);
321            zc_rms_norm_vec(&mut proj, &layer.pre_hada);
322            hadamard_mlp(&proj, &layer.mlp, &self.perms, cfg.hada_n, &mut mlp_out);
323            for (si, &mi) in bx.iter_mut().zip(mlp_out.iter()) {
324                *si += mi;
325            }
326
327            let mut y = vec![0.0f32; d];
328            for c in 0..d {
329                y[c] = bx[c] - u[c];
330            }
331
332            rms_unit_to(&lanes, &mut nx);
333            self.mhc.phi_post.prepare_input(&nx, &mut nx_prep);
334            self.mhc
335                .phi_post
336                .matvec_rows_prepared(&nx_prep, li * n, &mut hpost);
337            self.mhc.phi_res.prepare_input(&nx, &mut nx_prep);
338            self.mhc
339                .phi_res
340                .matvec_rows_prepared(&nx_prep, li * n * n, &mut hres);
341            scatter_up(
342                &mut lanes,
343                Gates {
344                    post: &mut hpost,
345                    res: &mut hres,
346                },
347                &y,
348                &m,
349                LaneSite {
350                    layer: li,
351                    lanes: n,
352                    d_model: d,
353                },
354                &mut scratch,
355            );
356        }
357
358        cache.advance();
359
360        let mut x = vec![0.0f32; d];
361        for (c, xc) in x.iter_mut().enumerate() {
362            let mut acc = 0.0f32;
363            for lane in 0..n {
364                acc += lanes[lane * d + c];
365            }
366            *xc = acc / n as f32;
367        }
368        zc_rms_norm_vec(&mut x, &self.final_norm);
369        let mut lm_prep = vec![0.0f32; self.embedding.prepared_len()];
370        let mut full = vec![0.0f32; cfg.vocab_size];
371        self.embedding.prepare_input(&x, &mut lm_prep);
372        self.embedding.matvec_prepared(&lm_prep, &mut full);
373        full.truncate(rows);
374        full
375    }
376
377    /// RoPE cos/sin for a single absolute position.
378    fn rope_at(&self, pos: usize) -> (Vec<f32>, Vec<f32>) {
379        let half = self.cfg.qk_head_dim / 2;
380        let mut c = vec![0.0f32; half];
381        let mut s = vec![0.0f32; half];
382        for i in 0..half {
383            let freq = 1.0
384                / powf_f32(
385                    self.cfg.rope_theta,
386                    (2 * i) as f32 / self.cfg.qk_head_dim as f32,
387                );
388            let angle = pos as f32 * freq;
389            c[i] = cos(angle);
390            s[i] = sin(angle);
391        }
392        (c, s)
393    }
394
395    /// RoPE tables for `seq` positions: `(seq, qk_head_dim / 2)` each.
396    fn rope(&self, seq: usize) -> (Vec<f32>, Vec<f32>) {
397        let half = self.cfg.qk_head_dim / 2;
398        let mut c = vec![0.0f32; seq * half];
399        let mut s = vec![0.0f32; seq * half];
400        for i in 0..half {
401            let freq = 1.0
402                / powf_f32(
403                    self.cfg.rope_theta,
404                    (2 * i) as f32 / self.cfg.qk_head_dim as f32,
405                );
406            for t in 0..seq {
407                let angle = t as f32 * freq;
408                c[t * half + i] = cos(angle);
409                s[t * half + i] = sin(angle);
410            }
411        }
412        (c, s)
413    }
414
415    /// Engram keys and values for every site, `(sites, seq, d_model)` each.
416    ///
417    /// With `want_raw`, also returns the **pre**-convolution values, which a
418    /// cache needs to continue the dilated tap convolution from where a
419    /// prefill left off.
420    fn engram_kv_with_raw(&self, tokens: &[u32], want_raw: bool) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
421        let cfg = &self.cfg;
422        let seq = tokens.len();
423        let d = cfg.d_model;
424        let e = &cfg.engram;
425        let dims = EngramDims {
426            num_tables: e.num_tables(),
427            slots: e.slots,
428            sub_dim: e.sub_dim,
429            d_model: d,
430            conv_taps: e.conv_taps,
431            conv_dilation: e.conv_dilation,
432            max_order: *e.orders.iter().max().unwrap_or(&1),
433        };
434        let indices = engram_indices(tokens, &e.orders, e.heads, e.slots as u32, e.seed_heads);
435
436        let sites = e.sites.len();
437        let mut ks = vec![0.0f32; sites * seq * d];
438        let mut vs = vec![0.0f32; sites * seq * d];
439        let mut raw = if want_raw {
440            vec![0.0f32; sites * seq * d]
441        } else {
442            Vec::new()
443        };
444        let fetch_dim = dims.num_tables * dims.sub_dim;
445        let mut fetched = vec![0.0f32; fetch_dim];
446        let mut row = vec![0.0f32; dims.sub_dim];
447
448        for (s, site) in self.engrams.iter().enumerate() {
449            for p in 0..seq {
450                for t in 0..dims.num_tables {
451                    let dst = &mut fetched[t * dims.sub_dim..(t + 1) * dims.sub_dim];
452                    if ngram_valid(p, t, &e.orders, e.heads) {
453                        let slot = indices[p * dims.num_tables + t] as usize;
454                        site.tables.dequantize_row(t * dims.slots + slot, &mut row);
455                        dst.copy_from_slice(&row);
456                    } else {
457                        dst.fill(0.0);
458                    }
459                }
460                let base = (s * seq + p) * d;
461                site.key_proj.matvec(&fetched, &mut ks[base..base + d]);
462                site.value_proj.matvec(&fetched, &mut vs[base..base + d]);
463            }
464            let off = s * seq * d;
465            if want_raw {
466                raw[off..off + seq * d].copy_from_slice(&vs[off..off + seq * d]);
467            }
468            value_conv(&mut vs[off..off + seq * d], &site.taps, seq, &dims);
469        }
470        (ks, vs, raw)
471    }
472
473    /// Logits for every position, `(seq, logit_rows)` row-major.
474    ///
475    /// Mirrors upstream's graph: the lane stream carries `mhc_lanes` copies of
476    /// the scaled embedding, each layer mixes down and scatters back, and the
477    /// head is the embedding itself, sliced to `out_vocab`.
478    pub fn forward_sequence(&self, tokens: &[u32]) -> Vec<f32> {
479        self.forward_impl(tokens, None)
480    }
481
482    /// Run `head` over the sequence without ever holding all the cells.
483    ///
484    /// Prefer this to `forward_cells` + [`ProbeHead::forward`]: the cells cost
485    /// `seq * (layers + 1) * d_model` floats — 504 MB at full context on the
486    /// shipped model — where the streaming pool holds 252 KB regardless of
487    /// length.
488    pub fn forward_head(&self, tokens: &[u32], head: &ProbeHead) -> Vec<f32> {
489        let mut pool = ProbePool::new(head, self.cfg.d_model);
490        self.forward_impl_pooled(tokens, None, None, Some(&mut pool));
491        pool.finish()
492    }
493
494    /// Per-layer pooled states, `(seq, num_layers + 1, d_model)`.
495    ///
496    /// Cell 0 is the scaled input embedding; cell `1 + i` is the lane stream
497    /// after layer `i`, meaned over lanes. These are what the probe heads read.
498    pub fn forward_cells(&self, tokens: &[u32]) -> Vec<f32> {
499        let mut cells = Vec::new();
500        self.forward_impl(tokens, Some(&mut cells));
501        cells
502    }
503
504    /// Batched prefill that also fills `cache`, so generation can continue
505    /// from it with [`Self::decode_step`].
506    ///
507    /// Returns the logits for the **last** position, which is what a caller
508    /// needs to sample the first new token. This is the path generation should
509    /// use for a prompt: stepping the prompt through `decode_step` costs one
510    /// full pass per position, where this pays the weight traffic once per
511    /// chunk.
512    pub fn prefill(&self, tokens: &[u32], cache: &mut V3Cache) -> Vec<f32> {
513        // An empty prompt has no last position to report. Returning empty
514        // rather than underflowing keeps this callable from a binding, where a
515        // panic is not recoverable.
516        if tokens.is_empty() {
517            return Vec::new();
518        }
519        let rows = self.cfg.logit_rows();
520        let all = self.forward_impl_cached(tokens, None, Some(cache));
521        let last = tokens.len() - 1;
522        all[last * rows..(last + 1) * rows].to_vec()
523    }
524
525    /// Clamp a token id into the embedding table.
526    ///
527    /// Out-of-range ids are a caller error, but this is reachable from every
528    /// binding and a panic in WebAssembly takes the whole module down, so it
529    /// saturates instead. The engine cannot produce one — sampling is bounded
530    /// by `logit_rows()` — so this only guards direct callers driving their own
531    /// decode loop.
532    #[inline]
533    fn clamp_token(&self, tok: u32) -> usize {
534        (tok as usize).min(self.cfg.vocab_size - 1)
535    }
536
537    fn forward_impl(&self, tokens: &[u32], cells: Option<&mut Vec<f32>>) -> Vec<f32> {
538        self.forward_impl_cached(tokens, cells, None)
539    }
540
541    fn forward_impl_cached(
542        &self,
543        tokens: &[u32],
544        cells: Option<&mut Vec<f32>>,
545        cache: Option<&mut V3Cache>,
546    ) -> Vec<f32> {
547        self.forward_impl_pooled(tokens, cells, cache, None)
548    }
549
550    fn forward_impl_pooled(
551        &self,
552        tokens: &[u32],
553        mut cells: Option<&mut Vec<f32>>,
554        mut cache: Option<&mut V3Cache>,
555        mut pool: Option<&mut ProbePool<'_>>,
556    ) -> Vec<f32> {
557        let cfg = &self.cfg;
558        let (seq, d, n) = (tokens.len(), cfg.d_model, cfg.mhc_lanes);
559        let rows = cfg.logit_rows();
560        let quantised = cache
561            .as_deref()
562            .is_some_and(|c| c.precision() == KvPrecision::Int8);
563        let (rc, rs) = self.rope(seq);
564        let (ek, ev, ev_raw) = self.engram_kv_with_raw(tokens, cache.is_some());
565
566        // Lane stream: (seq, lanes, d_model).
567        let l1 = cfg.num_layers + 1;
568        if let Some(c) = cells.as_deref_mut() {
569            c.clear();
570            c.resize(seq * l1 * d, 0.0);
571        }
572        let mut lanes = vec![0.0f32; seq * n * d];
573        let mut emb = vec![0.0f32; d];
574        for (t, &tok) in tokens.iter().enumerate() {
575            self.embedding
576                .dequantize_row(self.clamp_token(tok), &mut emb);
577            for e in emb.iter_mut() {
578                *e *= self.embed_scale;
579            }
580            if let Some(c) = cells.as_deref_mut() {
581                // Cell 0 is the scaled embedding, before the lane broadcast.
582                c[(t * l1) * d..(t * l1 + 1) * d].copy_from_slice(&emb);
583            }
584            if let Some(pl) = pool.as_deref_mut() {
585                pl.observe(0, &emb);
586            }
587            for lane in 0..n {
588                let off = (t * n + lane) * d;
589                lanes[off..off + d].copy_from_slice(&emb);
590            }
591        }
592
593        let dims = AttnDims {
594            seq,
595            num_heads: cfg.num_heads,
596            num_kv_heads: cfg.num_kv_heads,
597            qk_head_dim: cfg.qk_head_dim,
598            v_head_dim: cfg.v_head_dim,
599        };
600        let (q_dim, k_dim, v_dim) = (cfg.q_dim(), cfg.k_dim(), cfg.v_dim());
601        let o_dim = cfg.attn_out_dim();
602
603        let mut nx = vec![0.0f32; n * d];
604        let mut nx_prep = vec![0.0f32; self.mhc.phi_pre.prepared_len()];
605        let mut hpre = vec![0.0f32; n];
606        let mut hpost = vec![0.0f32; n];
607        let mut hres = vec![0.0f32; n * n];
608        let mut scratch = vec![0.0f32; n * d];
609
610        let mut u = vec![0.0f32; seq * d];
611        let mut bx = vec![0.0f32; seq * d];
612        let mut q = vec![0.0f32; seq * q_dim];
613        let mut k = vec![0.0f32; seq * k_dim];
614        let mut v = vec![0.0f32; seq * v_dim];
615        let mut attn = vec![0.0f32; seq * o_dim];
616        let mut gates = vec![0.0f32; seq * o_dim];
617
618        // Chunked projection scratch. All four input projections share one
619        // prepared activation, which is only sound because they share
620        // `in_feat` and `group` — asserted rather than assumed.
621        let chunk = DEFAULT_CHUNK.min(seq.max(1));
622        let prep_len = self.layers[0].q_proj.prepared_len();
623        debug_assert!(self.layers.iter().all(|l| {
624            l.k_proj.prepared_len() == prep_len
625                && l.v_proj.prepared_len() == prep_len
626                && l.gate_proj.prepared_len() == prep_len
627        }));
628        let mut xh = vec![0.0f32; chunk * prep_len];
629        let out_prep = self.layers[0].out_proj.prepared_len();
630        let mut oh = vec![0.0f32; chunk * out_prep];
631        let mut projected = vec![0.0f32; seq * d];
632        let mut acc = vec![0.0f32; chunk];
633        let mut mlp_out = vec![0.0f32; d];
634
635        for li in 0..cfg.num_layers {
636            let layer = &self.layers[li];
637            let m = MhcLayer {
638                a_pre: self.mhc.a_pre[li],
639                a_post: self.mhc.a_post[li],
640                a_res: self.mhc.a_res[li],
641                b_pre: &self.mhc.b_pre[li * n..(li + 1) * n],
642                b_post: &self.mhc.b_post[li * n..(li + 1) * n],
643                b_res: &self.mhc.b_res[li * n * n..(li + 1) * n * n],
644            };
645
646            // Mix the lanes down to this layer's block input, per position.
647            for t in 0..seq {
648                let lane_slice = &lanes[t * n * d..(t + 1) * n * d];
649                rms_unit_to(lane_slice, &mut nx);
650                self.mhc.phi_pre.prepare_input(&nx, &mut nx_prep);
651                self.mhc
652                    .phi_pre
653                    .matvec_rows_prepared(&nx_prep, li * n, &mut hpre);
654                mix_down(
655                    lane_slice,
656                    &mut hpre,
657                    &m,
658                    li,
659                    n,
660                    d,
661                    &mut u[t * d..(t + 1) * d],
662                );
663            }
664
665            // Engram gate, if this layer carries a site.
666            bx.copy_from_slice(&u);
667            if let Some(site) = cfg.engram.site_of(li) {
668                for t in 0..seq {
669                    let base = (site * seq + t) * d;
670                    crate::v3::engram::apply_site(
671                        &mut bx[t * d..(t + 1) * d],
672                        &ek[base..base + d],
673                        &ev[base..base + d],
674                        d,
675                    );
676                }
677            }
678
679            // ── Block ────────────────────────────────────────────────────
680            // Pre-attention norm, then projections in chunks.
681            //
682            // A matvec streams the whole packed weight set once per position —
683            // past any core's private cache — so a per-position loop re-reads
684            // it every time. Batching decodes each group once and applies it to
685            // the whole chunk, and `matmul_rows_prepared` is bit-identical to
686            // repeated matvecs rather than merely close.
687            //
688            // q, k, v and the gate all read the same activation with the same
689            // group geometry, so the Hadamard preparation is paid once for all
690            // four rather than four times.
691            let mut h = bx.clone();
692            for t in 0..seq {
693                zc_rms_norm_vec(&mut h[t * d..(t + 1) * d], &layer.norm_in);
694            }
695            for c0 in (0..seq).step_by(chunk) {
696                let b = (seq - c0).min(chunk);
697                for i in 0..b {
698                    layer.q_proj.prepare_input(
699                        &h[(c0 + i) * d..(c0 + i + 1) * d],
700                        &mut xh[i * prep_len..(i + 1) * prep_len],
701                    );
702                }
703                let xb = &xh[..b * prep_len];
704                layer.q_proj.matmul_rows_prepared(
705                    xb,
706                    b,
707                    0,
708                    q_dim,
709                    &mut q[c0 * q_dim..(c0 + b) * q_dim],
710                    &mut acc,
711                );
712                layer.k_proj.matmul_rows_prepared(
713                    xb,
714                    b,
715                    0,
716                    k_dim,
717                    &mut k[c0 * k_dim..(c0 + b) * k_dim],
718                    &mut acc,
719                );
720                layer.v_proj.matmul_rows_prepared(
721                    xb,
722                    b,
723                    0,
724                    v_dim,
725                    &mut v[c0 * v_dim..(c0 + b) * v_dim],
726                    &mut acc,
727                );
728                layer.gate_proj.matmul_rows_prepared(
729                    xb,
730                    b,
731                    0,
732                    o_dim,
733                    &mut gates[c0 * o_dim..(c0 + b) * o_dim],
734                    &mut acc,
735                );
736            }
737
738            // The conv runs in place, so the last few pre-conv vectors have to
739            // be kept if the cache is going to continue from here.
740            let (mut q_raw_tail, mut k_raw_tail, mut v_raw_tail) =
741                (Vec::new(), Vec::new(), Vec::new());
742            if cache.is_some() && cfg.qkv_conv_taps > 0 {
743                let n = cfg.qkv_conv_taps - 1;
744                q_raw_tail = last_rows(&q, seq, q_dim, n);
745                k_raw_tail = last_rows(&k, seq, k_dim, n);
746                v_raw_tail = last_rows(&v, seq, v_dim, n);
747            }
748            if cfg.qkv_conv_taps > 0 {
749                causal_depthwise_conv(&mut q, &layer.q_taps, seq, q_dim, cfg.qkv_conv_taps);
750                causal_depthwise_conv(&mut k, &layer.k_taps, seq, k_dim, cfg.qkv_conv_taps);
751                causal_depthwise_conv(&mut v, &layer.v_taps, seq, v_dim, cfg.qkv_conv_taps);
752            }
753            norm_and_rope(
754                &mut q,
755                &layer.q_norm,
756                &rc,
757                &rs,
758                seq,
759                cfg.num_heads,
760                cfg.qk_head_dim,
761            );
762            norm_and_rope(
763                &mut k,
764                &layer.k_norm,
765                &rc,
766                &rs,
767                seq,
768                cfg.num_kv_heads,
769                cfg.qk_head_dim,
770            );
771
772            // Upstream applies `maybe_quant_query` and `maybe_quant_kv` to the
773            // whole batch, immediately after RoPE and before attention — so a
774            // quantised session must attend the *prompt* at 8 bits too. Doing
775            // this only on the decode path would leave prefill reading f32
776            // while decode read int8: a split upstream does not have, and one
777            // that batched prefill would hide, since it never steps a position
778            // the other way.
779            if quantised {
780                quant_rows(&mut q, seq, cfg.num_heads, cfg.qk_head_dim);
781                quant_rows(&mut k, seq, cfg.num_kv_heads, cfg.qk_head_dim);
782                quant_rows(&mut v, seq, cfg.num_kv_heads, cfg.v_head_dim);
783            }
784
785            if let Some(c) = cache.as_deref_mut() {
786                // Post-conv, post-norm, post-rope keys and values: exactly what
787                // decode_step will attend against when it continues from here.
788                // Already quantised above when the session is, and `fake_quant`
789                // is idempotent — the absolute maximum is preserved exactly, so
790                // the scale and every rounded integer come out the same.
791                for t in 0..seq {
792                    c.write_kv(
793                        li,
794                        t,
795                        &k[t * k_dim..(t + 1) * k_dim],
796                        &v[t * v_dim..(t + 1) * v_dim],
797                    );
798                }
799                if cfg.qkv_conv_taps > 0 {
800                    c.seed_conv_tail(li, Qkv::Q, &q_raw_tail);
801                    c.seed_conv_tail(li, Qkv::K, &k_raw_tail);
802                    c.seed_conv_tail(li, Qkv::V, &v_raw_tail);
803                }
804            }
805
806            // Attend through the same representation the decode path reads.
807            // Quantising and then attending over the dequantised `f32` would
808            // be algebraically identical and numerically not: the stored-int
809            // reader hoists the per-head scale out of the dot product, so a
810            // prefilled session would disagree with a stepped one on the third
811            // decimal of every logit. Building the integers here costs one pass
812            // and makes the two paths bit-identical.
813            let store = if quantised {
814                let (kq, ks) = quantize_rows(&k, cfg.qk_head_dim);
815                let (vq, vs) = quantize_rows(&v, cfg.v_head_dim);
816                Some((kq, ks, vq, vs))
817            } else {
818                None
819            };
820            let kv_store = match &store {
821                Some((kq, ks, vq, vs)) => KvStore::Int8 {
822                    k: kq,
823                    k_scale: ks,
824                    v: vq,
825                    v_scale: vs,
826                },
827                None => KvStore::F32 { k: &k, v: &v },
828            };
829            attend(&q, kv_store, dims, cfg.attention_span(li), &mut attn);
830
831            let agate = sigmoid(layer.attn_gate);
832            for t in 0..seq {
833                let a = &mut attn[t * o_dim..(t + 1) * o_dim];
834                for (ai, &gi) in a.iter_mut().zip(&gates[t * o_dim..(t + 1) * o_dim]) {
835                    *ai *= sigmoid(gi);
836                }
837            }
838            // out_proj reads the gated attention output, so it gets its own
839            // preparation pass — a different activation and a different width
840            // from the four input projections.
841            for c0 in (0..seq).step_by(chunk) {
842                let b = (seq - c0).min(chunk);
843                for i in 0..b {
844                    layer.out_proj.prepare_input(
845                        &attn[(c0 + i) * o_dim..(c0 + i + 1) * o_dim],
846                        &mut oh[i * out_prep..(i + 1) * out_prep],
847                    );
848                }
849                layer.out_proj.matmul_rows_prepared(
850                    &oh[..b * out_prep],
851                    b,
852                    0,
853                    d,
854                    &mut projected[c0 * d..(c0 + b) * d],
855                    &mut acc,
856                );
857            }
858            for t in 0..seq {
859                let proj = &mut projected[t * d..(t + 1) * d];
860                zc_rms_norm_vec(proj, &layer.post_norm);
861
862                // Attention residual, then the MLP residual.
863                let s1 = &mut bx[t * d..(t + 1) * d];
864                for (si, &pi) in s1.iter_mut().zip(proj.iter()) {
865                    *si += agate * pi;
866                }
867                proj.copy_from_slice(s1);
868                zc_rms_norm_vec(proj, &layer.pre_hada);
869                hadamard_mlp(proj, &layer.mlp, &self.perms, cfg.hada_n, &mut mlp_out);
870                for (si, &mi) in s1.iter_mut().zip(mlp_out.iter()) {
871                    *si += mi;
872                }
873            }
874
875            // Scatter the block delta back across the lanes.
876            for t in 0..seq {
877                let lane_slice = &lanes[t * n * d..(t + 1) * n * d];
878                rms_unit_to(lane_slice, &mut nx);
879                self.mhc.phi_post.prepare_input(&nx, &mut nx_prep);
880                self.mhc
881                    .phi_post
882                    .matvec_rows_prepared(&nx_prep, li * n, &mut hpost);
883                self.mhc.phi_res.prepare_input(&nx, &mut nx_prep);
884                self.mhc
885                    .phi_res
886                    .matvec_rows_prepared(&nx_prep, li * n * n, &mut hres);
887
888                // y = block(u) - u
889                let mut y = vec![0.0f32; d];
890                for c in 0..d {
891                    y[c] = bx[t * d + c] - u[t * d + c];
892                }
893                scatter_up(
894                    &mut lanes[t * n * d..(t + 1) * n * d],
895                    Gates {
896                        post: &mut hpost,
897                        res: &mut hres,
898                    },
899                    &y,
900                    &m,
901                    LaneSite {
902                        layer: li,
903                        lanes: n,
904                        d_model: d,
905                    },
906                    &mut scratch,
907                );
908            }
909
910            if cells.is_some() || pool.is_some() {
911                let mut cell = vec![0.0f32; d];
912                for t in 0..seq {
913                    for (cc, o) in cell.iter_mut().enumerate() {
914                        let mut a = 0.0f32;
915                        for lane in 0..n {
916                            a += lanes[(t * n + lane) * d + cc];
917                        }
918                        *o = a / n as f32;
919                    }
920                    if let Some(c) = cells.as_deref_mut() {
921                        c[(t * l1 + li + 1) * d..(t * l1 + li + 2) * d].copy_from_slice(&cell);
922                    }
923                    if let Some(pl) = pool.as_deref_mut() {
924                        pl.observe(li + 1, &cell);
925                    }
926                }
927            }
928        }
929
930        if let Some(c) = cache {
931            c.set_pos(seq);
932            c.seed_tokens(tokens);
933            let reach = cfg.engram.conv_taps.saturating_sub(1) * cfg.engram.conv_dilation;
934            if reach > 0 {
935                for s in 0..cfg.engram.sites.len() {
936                    let off = s * seq * d;
937                    c.seed_engram_tail(s, &last_rows(&ev_raw[off..off + seq * d], seq, d, reach));
938                }
939            }
940        }
941
942        // Mean over lanes, final norm, then the tied head — batched, because
943        // at 8192 x 768 it is a fifth of the whole forward pass.
944        let mut logits = vec![0.0f32; seq * rows];
945        let mut pooled = vec![0.0f32; seq * d];
946        for t in 0..seq {
947            let out = &mut pooled[t * d..(t + 1) * d];
948            for (c, o) in out.iter_mut().enumerate() {
949                let mut a = 0.0f32;
950                for lane in 0..n {
951                    a += lanes[(t * n + lane) * d + c];
952                }
953                *o = a / n as f32;
954            }
955            zc_rms_norm_vec(out, &self.final_norm);
956        }
957
958        let lm_prep_len = self.embedding.prepared_len();
959        let mut lh = vec![0.0f32; chunk * lm_prep_len];
960        for c0 in (0..seq).step_by(chunk) {
961            let b = (seq - c0).min(chunk);
962            for i in 0..b {
963                self.embedding.prepare_input(
964                    &pooled[(c0 + i) * d..(c0 + i + 1) * d],
965                    &mut lh[i * lm_prep_len..(i + 1) * lm_prep_len],
966                );
967            }
968            // `rows` may be a prefix of the vocabulary: rows past `out_vocab`
969            // are input-only code embeddings and are never scored.
970            self.embedding.matmul_rows_prepared(
971                &lh[..b * lm_prep_len],
972                b,
973                0,
974                rows,
975                &mut logits[c0 * rows..(c0 + b) * rows],
976                &mut acc,
977            );
978        }
979        logits
980    }
981}
982
983/// The last `n` rows of a `(seq, dim)` buffer, oldest first, zero-padded at the
984/// front when the sequence is shorter than `n`.
985fn last_rows(buf: &[f32], seq: usize, dim: usize, n: usize) -> Vec<f32> {
986    let mut out = vec![0.0f32; n * dim];
987    for j in 0..n.min(seq) {
988        // out is oldest-first, so the newest row lands last.
989        let src = seq - n.min(seq) + j;
990        let dst = n - n.min(seq) + j;
991        out[dst * dim..(dst + 1) * dim].copy_from_slice(&buf[src * dim..(src + 1) * dim]);
992    }
993    out
994}
995
996/// `base^exp` for the RoPE frequency table. `crate::math::powf` exists but is
997/// spelled differently across the `std` and `libm` backends.
998fn powf_f32(base: f32, exp: f32) -> f32 {
999    crate::math::powf(base, exp)
1000}