Skip to main content

cortiq_engine/
linear_core.rs

1//! Linear-attention cores, selected by `arch.linear_core.kind`
2//! (descriptor-driven operators — Patent 15 claim 8).
3//!
4//! Two tracks (owner decision 2026-07-04):
5//!
6//! * `gated_delta_net` — the faithful vendor operator (Qwen3.5 /
7//!   Qwen3-Next). Default for models that ship GDN weights: conversion
8//!   carries the tensors 1:1 and needs no training. Port of the
9//!   validated `gated_delta_net` (vmfcore/rust/src/forward.rs) against
10//!   the numpy/torch oracle (vmfcore/gdn_layer.py).
11//!
12//! * `vmf_phase` — the legacy additive core: token carries a phase θ; kernel
13//!   φ(θ) = [cos θ; sin θ] gives a linear factorization; the recurrent
14//!   state S[head][p2, dv] uses decay exp(−exp(A_log)).
15//!   Noise-robust and simpler than vendor recurrences. Exotic operators
16//!   are folded onto it at CONVERT time (`--linear-core vmf_phase`) and
17//!   quality is restored by the offline heal — the research track and
18//!   the production mechanism for Patent-15 skills (mask→heal→compress).
19//!
20//! * `vmf_phase_delta_v1` — the operator-tagged normalized in-place
21//!   Phase-Delta variant: decay first, read the old value under k, write the
22//!   gated residual, then read under q. It shares all VMF tensors/state but
23//!   is selected per layer by the CMF linear-core record.
24//!
25//! Both cores implement the same contract: `*_forward` (one position,
26//! advances the state) and `*_pair` (fused two positions; lane 1
27//! commits, lane 2 is tentative in `scratch` for speculative verify).
28//! State lives in the layer's `linear_state: Vec<f32>` and is resized
29//! lazily by the core itself.
30
31use crate::pool::Pool;
32use crate::qtensor::QTensor;
33use cortiq_core::TensorDtype;
34use std::sync::OnceLock;
35use std::sync::atomic::{AtomicU64, Ordering};
36
37static PERF_GDN_FORWARD_CALLS: AtomicU64 = AtomicU64::new(0);
38static PERF_GDN_FORWARD_NS: AtomicU64 = AtomicU64::new(0);
39static PERF_GDN_BATCH_CALLS: AtomicU64 = AtomicU64::new(0);
40static PERF_GDN_BATCH_NS: AtomicU64 = AtomicU64::new(0);
41static PERF_GDN_STEP_CALLS: AtomicU64 = AtomicU64::new(0);
42static PERF_GDN_STEP_NS: AtomicU64 = AtomicU64::new(0);
43
44fn perf_enabled() -> bool {
45    static ON: OnceLock<bool> = OnceLock::new();
46    *ON.get_or_init(|| std::env::var("CMF_PERF_PROFILE").as_deref() == Ok("1"))
47}
48
49/// Weights of one vmf_phase layer (`model.layers.{i}.vmf_attn.*`).
50pub struct VmfPhaseWeights {
51    /// [nh·nphase, hidden] — query phase projection
52    pub thq: QTensor,
53    /// [nh·nphase, hidden] — key phase projection
54    pub thk: QTensor,
55    /// [nh·dv, hidden]
56    pub v_proj: QTensor,
57    /// [hidden, nh·dv]
58    pub out_proj: QTensor,
59    /// Per-component decay exp(−exp(A_log)), len nh·2·nphase (precomputed).
60    pub decay: Vec<f64>,
61    /// Short causal depthwise conv before the projections (embryo genomes
62    /// born with `--conv-k`): flat `[hidden·k]` taps, identity-initialised
63    /// at training start. The projections were TRAINED on the conv output —
64    /// running without it scrambles the layer (measured: train-val ppl 60
65    /// exported to runtime ppl 93 539). None = pre-conv genomes, untouched.
66    pub conv: Option<Vec<f32>>,
67    /// Selective-write input gate κ (hybrid_k core, stage 71): weight
68    /// [nh, hidden] + bias [nh]; κ_h = σ(W_k·x + b)_h multiplies the
69    /// state WRITE (S = decay·S + κ·φk⊗v). None = classic phase core,
70    /// bit-identical to the pre-κ kernel. Measured at mechanism level:
71    /// knee ×2–6 earlier, restores correlated-noise robustness, LM
72    /// crossover vs softmax at SEQ 512 (experiments/lc_final_merged.json).
73    pub k_gate: Option<(QTensor, Vec<f32>)>,
74    /// Select the normalized in-place Phase-Delta recurrence. This bit is
75    /// derived from the versioned CMF selector and is never inferred from
76    /// tensor presence, so legacy `vmf_phase` files retain their exact
77    /// additive semantics.
78    pub phase_delta: bool,
79}
80
81#[derive(Clone, Copy)]
82pub struct VmfPhaseCfg {
83    pub num_heads: usize,
84    pub nphase: usize,
85    pub value_head_dim: usize,
86    pub hidden_size: usize,
87    /// Phase-mass correction: scales the phase toward zero —
88    /// θ_eff = θ/(1+mass) — which widens the phase kernel.
89    /// Measured (experiments/vmf_native_core*.py) to restore noise
90    /// robustness when the phase projection is FIXED (exactly CMF's
91    /// fold-before-heal regime: thq/thk are init, not trained) — recall
92    /// 3%→91% at moderate noise; redundant once the projection is
93    /// healed. 0.0 = massless Goldstone (bit-identical to prior kernel).
94    /// Set via CMF_PHASE_MASS. Validated at mechanism level, not yet LM.
95    pub phase_mass: f32,
96}
97
98impl VmfPhaseCfg {
99    pub fn state_len(&self) -> usize {
100        self.num_heads * 2 * self.nphase * self.value_head_dim
101    }
102}
103
104/// One normalized in-place Phase-Delta step.  The state is feature-major
105/// `[2*nphase, value_head_dim]`; `state` may carry a convolution ring after
106/// that prefix, so this helper only touches the recurrent matrix.  Keeping
107/// the decay/read/write/read ordering literal is what gives beta=1 and
108/// q=k the exact-overwrite witness from the trainer contract.
109fn phase_delta_step_f32(
110    thq: &[f32],
111    thk: &[f32],
112    v: &[f32],
113    decay: &[f64],
114    kap: Option<&[f32]>,
115    cfg: &VmfPhaseCfg,
116    state: &mut [f32],
117    out: &mut [f32],
118) {
119    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
120    let p2 = 2 * nph;
121    let scale = 1.0f64 / (nph as f64).sqrt();
122    debug_assert_eq!(thq.len(), nh * nph);
123    debug_assert_eq!(thk.len(), nh * nph);
124    debug_assert_eq!(v.len(), nh * dv);
125    debug_assert_eq!(decay.len(), nh * p2);
126    debug_assert!(state.len() >= nh * p2 * dv);
127    debug_assert!(out.len() >= nh * dv);
128
129    // Reuse one head's workspace. The old-value reduction must finish before
130    // any cell is overwritten, but neither it nor the phase features needs a
131    // fresh allocation per head.
132    let mut r = vec![0.0f64; dv];
133    let mut key = vec![0.0f64; p2];
134    for h in 0..nh {
135        let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
136        let thk_h = &thk[h * nph..(h + 1) * nph];
137        let thq_h = &thq[h * nph..(h + 1) * nph];
138        let vt = &v[h * dv..(h + 1) * dv];
139        let ot = &mut out[h * dv..(h + 1) * dv];
140        let dec = &decay[h * p2..(h + 1) * p2];
141        let beta = kap.map_or(1.0f64, |k| k[h] as f64);
142
143        // P = D_gamma S_prev; r = k^T P.  P is kept as f64 only for this
144        // token's arithmetic, while the recurrent cells remain f32 exactly
145        // like the established runtime VMF path.
146        r.fill(0.0);
147        for f in 0..p2 {
148            let kf = if f < nph {
149                scale * (thk_h[f] as f64).cos()
150            } else {
151                scale * (thk_h[f - nph] as f64).sin()
152            };
153            key[f] = kf;
154            let row = &s[f * dv..(f + 1) * dv];
155            for d in 0..dv {
156                r[d] += kf * (dec[f] * row[d] as f64);
157            }
158        }
159
160        // S = P + beta*k*(v-r), then o = q^T S (post-write).
161        // On ARM fuse the write and read of each row: no third state sweep.
162        // On x86 keep separate loops: mixing f64 updates and f32 reductions
163        // prevents vectorization and regressed the measured Xeon kernel.
164        // Both paths avoid reevaluating the key's transcendental functions.
165        // Crucially read the rounded f32 cell, NOT its f64 precursor. Keep
166        // feature accumulation order and the f32 add exactly as before.
167        for f in 0..p2 {
168            let kf = key[f];
169            #[cfg(target_arch = "aarch64")]
170            let qf = if f < nph {
171                scale * (thq_h[f] as f64).cos()
172            } else {
173                scale * (thq_h[f - nph] as f64).sin()
174            };
175            let row = &mut s[f * dv..(f + 1) * dv];
176            for d in 0..dv {
177                let p = dec[f] * row[d] as f64;
178                row[d] = (p + beta * kf * (vt[d] as f64 - r[d])) as f32;
179                #[cfg(target_arch = "aarch64")]
180                {
181                    ot[d] += (qf * row[d] as f64) as f32;
182                }
183            }
184        }
185        #[cfg(not(target_arch = "aarch64"))]
186        for f in 0..p2 {
187            let qf = if f < nph {
188                scale * (thq_h[f] as f64).cos()
189            } else {
190                scale * (thq_h[f - nph] as f64).sin()
191            };
192            let row = &s[f * dv..(f + 1) * dv];
193            for d in 0..dv {
194                ot[d] += (qf * row[d] as f64) as f32;
195            }
196        }
197    }
198}
199
200/// One recurrent step for one head-set given projected phases/values.
201/// `state` is S[nh][p2, dv] stored f32 (per-element math in f64 — the
202/// storage halves, each step's arithmetic keeps the old precision).
203fn phase_step(
204    thq: &[f32],
205    thk: &[f32],
206    v: &[f32],
207    decay: &[f64],
208    kap: Option<&[f32]>,
209    cfg: &VmfPhaseCfg,
210    state: &mut [f32],
211    out: &mut [f32],
212    phase_delta: bool,
213) {
214    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
215    if phase_delta {
216        phase_delta_step_f32(thq, thk, v, decay, kap, cfg, state, out);
217        return;
218    }
219    // Phase-mass correction: θ_eff = θ/(1+mass). mass=0 → factor 1 → no-op.
220    let mscale = 1.0f64 / (1.0 + cfg.phase_mass as f64);
221    let p2 = 2 * nph;
222    for h in 0..nh {
223        let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
224        let thk_h = &thk[h * nph..(h + 1) * nph];
225        let thq_h = &thq[h * nph..(h + 1) * nph];
226        let vt = &v[h * dv..(h + 1) * dv];
227        let ot = &mut out[h * dv..(h + 1) * dv];
228        let dec = &decay[h * p2..(h + 1) * p2];
229        // Selective write (hybrid_k): κ scales what enters the recurrent state.
230        let kh = kap.map_or(1.0f64, |k| k[h] as f64);
231        for f in 0..p2 {
232            // φ(θ) = [cos·nph, sin·nph], θ scaled by the correction factor.
233            let (fk, fq) = if f < nph {
234                (
235                    (thk_h[f] as f64 * mscale).cos(),
236                    (thq_h[f] as f64 * mscale).cos(),
237                )
238            } else {
239                (
240                    (thk_h[f - nph] as f64 * mscale).sin(),
241                    (thq_h[f - nph] as f64 * mscale).sin(),
242                )
243            };
244            let fkw = fk * kh;
245            let row = &mut s[f * dv..(f + 1) * dv];
246            let dcf = dec[f];
247            for d in 0..dv {
248                // S = decay·S + κ·φk⊗v (f64 math, f32 cell)
249                let cell = dcf * row[d] as f64 + fkw * vt[d] as f64;
250                row[d] = cell as f32;
251                ot[d] += (fq * cell) as f32; // o = Σ φq·S
252            }
253        }
254    }
255}
256
257/// κ_h = σ(W_k·x + b)_h — the per-head write gate (None when the layer
258/// has no k_gate tensors: classic phase core).
259fn kappa_of(x: &[f32], w: &VmfPhaseWeights, nh: usize, pool: Option<&Pool>) -> Option<Vec<f32>> {
260    let (kw, kb) = w.k_gate.as_ref()?;
261    let mut k = vec![0.0f32; nh];
262    kw.matvec(x, &mut k, pool);
263    for (v, b) in k.iter_mut().zip(kb) {
264        *v = 1.0 / (1.0 + (-(*v + b)).exp());
265    }
266    Some(k)
267}
268
269/// Causal depthwise conv over the mixer input, with the last k−1 inputs
270/// ringed at the TAIL of the layer state (oldest first). Returns the
271/// convolved input; a layer without conv taps passes through untouched.
272fn conv_in(
273    x: &[f32],
274    w: &VmfPhaseWeights,
275    cfg: &VmfPhaseCfg,
276    state: &mut Vec<f32>,
277) -> Option<Vec<f32>> {
278    let taps = w.conv.as_ref()?;
279    let h = cfg.hidden_size;
280    let k = taps.len() / h.max(1);
281    if k < 2 || taps.len() != h * k {
282        return None;
283    }
284    let ring = (k - 1) * h;
285    let base = cfg.state_len();
286    if state.len() != base + ring {
287        // The phase part resets alongside — a fresh sequence either way.
288        let mut ns = vec![0f32; base + ring];
289        let n = state.len().min(base);
290        ns[..n].copy_from_slice(&state[..n]);
291        *state = ns;
292    }
293    let mut y = vec![0.0f32; h];
294    for c in 0..h {
295        // taps j = 0..k−2 read the ring (oldest first), tap k−1 reads x.
296        let mut acc = taps[c * k + k - 1] * x[c];
297        for j in 0..k - 1 {
298            acc += taps[c * k + j] * state[base + j * h + c];
299        }
300        y[c] = acc;
301    }
302    conv_ring_push(x, h, base, state);
303    Some(y)
304}
305
306/// Rotate the conv ring at `base`: drop the oldest input, append `x`.
307fn conv_ring_push(x: &[f32], h: usize, base: usize, state: &mut [f32]) {
308    let ring = state.len() - base;
309    state.copy_within(base + h.., base);
310    let at = base + ring - h;
311    state[at..at + h].copy_from_slice(&x[..h]);
312}
313
314/// Forward one position through a vmf_phase layer, advancing `state`.
315pub fn vmf_phase_forward(
316    x: &[f32],
317    w: &VmfPhaseWeights,
318    cfg: &VmfPhaseCfg,
319    state: &mut Vec<f32>,
320    pool: Option<&Pool>,
321) -> Vec<f32> {
322    if w.conv.is_none() && state.len() != cfg.state_len() {
323        *state = vec![0f32; cfg.state_len()];
324    }
325    let xc = conv_in(x, w, cfg, state);
326    let x = xc.as_deref().unwrap_or(x);
327    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
328
329    let mut thq = vec![0.0f32; nh * nph];
330    w.thq.matvec(x, &mut thq, pool);
331    let mut thk = vec![0.0f32; nh * nph];
332    w.thk.matvec(x, &mut thk, pool);
333    let mut v = vec![0.0f32; nh * dv];
334    w.v_proj.matvec(x, &mut v, pool);
335
336    let kap = kappa_of(x, w, nh, pool);
337    let mut o = vec![0.0f32; nh * dv];
338    phase_step(
339        &thq,
340        &thk,
341        &v,
342        &w.decay,
343        kap.as_deref(),
344        cfg,
345        state,
346        &mut o,
347        w.phase_delta,
348    );
349
350    let mut out = vec![0.0f32; cfg.hidden_size];
351    w.out_proj.matvec(&o, &mut out, pool);
352    out
353}
354
355/// Fused two-position forward (speculative verify). Lane 1 commits into
356/// `state` (its token is always committed); lane 2's tentative state
357/// goes into `scratch` — the caller swaps it in on draft acceptance and
358/// simply drops it on rejection.
359#[allow(clippy::too_many_arguments)]
360pub fn vmf_phase_pair(
361    x1: &[f32],
362    x2: &[f32],
363    w: &VmfPhaseWeights,
364    cfg: &VmfPhaseCfg,
365    state: &mut Vec<f32>,
366    scratch: &mut Vec<f32>,
367    pool: Option<&Pool>,
368) -> (Vec<f32>, Vec<f32>) {
369    if w.conv.is_none() && state.len() != cfg.state_len() {
370        *state = vec![0f32; cfg.state_len()];
371    }
372    // Lane 1 commits its ring advance into the real state; lane 2 works on
373    // the tentative copy exactly like the phase state itself.
374    let xc1 = conv_in(x1, w, cfg, state);
375    let x1 = xc1.as_deref().unwrap_or(x1);
376    let (xc2, x2raw) = if w.conv.is_some() {
377        let mut tmp = state.clone();
378        (conv_in(x2, w, cfg, &mut tmp), Some(x2))
379    } else {
380        (None, None)
381    };
382    let x2 = xc2.as_deref().unwrap_or(x2);
383    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
384
385    let mut thq1 = vec![0.0f32; nh * nph];
386    let mut thq2 = vec![0.0f32; nh * nph];
387    w.thq.matvec2(x1, x2, &mut thq1, &mut thq2, pool);
388    let mut thk1 = vec![0.0f32; nh * nph];
389    let mut thk2 = vec![0.0f32; nh * nph];
390    w.thk.matvec2(x1, x2, &mut thk1, &mut thk2, pool);
391    let mut v1 = vec![0.0f32; nh * dv];
392    let mut v2 = vec![0.0f32; nh * dv];
393    w.v_proj.matvec2(x1, x2, &mut v1, &mut v2, pool);
394
395    // Lane 1 commits into the real state.
396    let kap1 = kappa_of(x1, w, nh, pool);
397    let mut o1 = vec![0.0f32; nh * dv];
398    phase_step(
399        &thq1,
400        &thk1,
401        &v1,
402        &w.decay,
403        kap1.as_deref(),
404        cfg,
405        state,
406        &mut o1,
407        w.phase_delta,
408    );
409
410    // Lane 2 runs on a copy — tentative until the draft is verified.
411    let kap2 = kappa_of(x2, w, nh, pool);
412    scratch.clear();
413    scratch.extend_from_slice(state);
414    let mut o2 = vec![0.0f32; nh * dv];
415    phase_step(
416        &thq2,
417        &thk2,
418        &v2,
419        &w.decay,
420        kap2.as_deref(),
421        cfg,
422        scratch,
423        &mut o2,
424        w.phase_delta,
425    );
426    // The scratch copy above re-took state's ring (advanced only through
427    // x1); commit x2's advance so an accepted draft leaves a correct ring.
428    if let Some(xr) = x2raw {
429        conv_ring_push(xr, cfg.hidden_size, cfg.state_len(), scratch);
430    }
431
432    let mut out1 = vec![0.0f32; cfg.hidden_size];
433    let mut out2 = vec![0.0f32; cfg.hidden_size];
434    w.out_proj.matvec2(&o1, &o2, &mut out1, &mut out2, pool);
435    (out1, out2)
436}
437
438// ───────────────────────── GatedDeltaNet (faithful vendor operator) ─────────────────────────
439
440/// Weights of one GatedDeltaNet layer (`model.layers.{i}.linear_attn.*`,
441/// names 1:1 with the source model — no fold, no training).
442pub struct GdnWeights {
443    /// [2·nk·dk + nv·dv, hidden] — fused q/k/v projection
444    pub in_proj_qkv: QTensor,
445    /// [nv·dv, hidden] — output-gate projection z
446    pub in_proj_z: QTensor,
447    /// [nv, hidden] — decay modulation a
448    pub in_proj_a: QTensor,
449    /// [nv, hidden] — write-strength b (β = σ(b))
450    pub in_proj_b: QTensor,
451    /// [c_dim · kk] — depthwise causal conv taps, flattened [c][tap]
452    pub conv1d: Vec<f32>,
453    /// [nv]
454    pub a_log: Vec<f32>,
455    /// [nv]
456    pub dt_bias: Vec<f32>,
457    /// [dv] — gated RMSNorm weight (plain x̂·w, validated by the oracle)
458    pub norm: Vec<f32>,
459    /// [hidden, nv·dv]
460    pub out_proj: QTensor,
461}
462
463#[derive(Clone, Copy)]
464pub struct GdnCfg {
465    pub num_v_heads: usize,
466    pub num_k_heads: usize,
467    pub key_head_dim: usize,
468    pub value_head_dim: usize,
469    pub conv_kernel: usize,
470    pub hidden_size: usize,
471    pub rms_eps: f64,
472    /// Qwen4-exp trains the output gate as sigmoid(z); older GDN families
473    /// use SiLU(z). This is part of the operator, not a sampling option.
474    pub output_gate_sigmoid: bool,
475}
476
477impl GdnCfg {
478    pub fn conv_dim(&self) -> usize {
479        2 * self.num_k_heads * self.key_head_dim + self.num_v_heads * self.value_head_dim
480    }
481
482    /// Packed state: [conv ring (kk−1)·c_dim | S nv·dk·dv], one Vec<f64>
483    /// so the speculative scratch-swap moves ring and recurrent state together.
484    pub fn state_len(&self) -> usize {
485        (self.conv_kernel - 1) * self.conv_dim()
486            + self.num_v_heads * self.key_head_dim * self.value_head_dim
487    }
488}
489
490fn softplus(x: f64) -> f64 {
491    if x > 20.0 { x } else { x.exp().ln_1p() }
492}
493
494fn sigmoid(x: f64) -> f64 {
495    1.0 / (1.0 + (-x).exp())
496}
497
498fn silu(x: f64) -> f64 {
499    x / (1.0 + (-x).exp())
500}
501
502/// `*mut f32` that may cross worker threads; safety comes from the
503/// disjoint (head, element) ranges each worker writes.
504#[derive(Clone, Copy)]
505struct SendMutF32(*mut f32);
506unsafe impl Send for SendMutF32 {}
507unsafe impl Sync for SendMutF32 {}
508
509/// One recurrent step given the raw (pre-conv) projections of this
510/// position. Advances the packed state (conv ring + S) and writes the
511/// gated per-head output into `of` [nv·dv].
512///
513/// The recurrent-state math runs in f32 (the vendor operator's own dtype —
514/// `mamba_ssm_dtype: float32` in the source configs; the old f64 was
515/// over-precision at 4× the traffic and no SIMD). The two S passes are
516/// element-wise in `dj` with no cross-lane reduction, so LLVM
517/// auto-vectorizes them (fmla on NEON, FMA on AVX2). Heads are
518/// independent given the conv output and run across the pool — on a
519/// Qwen3.5-27B this loop is 48 heads × 128×128 × 48 layers per token,
520/// the single biggest serial block in the hybrid's decode.
521#[allow(clippy::too_many_arguments)]
522fn gdn_step(
523    qkv: &[f32],
524    z: &[f32],
525    a: &[f32],
526    b: &[f32],
527    w: &GdnWeights,
528    cfg: &GdnCfg,
529    state: &mut [f32],
530    of: &mut [f32],
531    pool: Option<&Pool>,
532) {
533    let perf_t0 = perf_enabled().then(std::time::Instant::now);
534    let (nv, nk, dk, dv, kk) = (
535        cfg.num_v_heads,
536        cfg.num_k_heads,
537        cfg.key_head_dim,
538        cfg.value_head_dim,
539        cfg.conv_kernel,
540    );
541    let c_dim = cfg.conv_dim();
542    let (kd, rep) = (nk * dk, nv / nk);
543    let (ring, s_all) = state.split_at_mut((kk - 1) * c_dim);
544
545    // Depthwise causal conv over [ring…, current] + SiLU. Taps are
546    // ordered oldest→newest; tap kk−1 multiplies the current position.
547    // (Tiny: c_dim × kk — f64 accumulation kept.)
548    let mut cq = vec![0f32; c_dim];
549    for c in 0..c_dim {
550        let taps = &w.conv1d[c * kk..(c + 1) * kk];
551        let mut acc = qkv[c] as f64 * taps[kk - 1] as f64;
552        for j in 0..kk - 1 {
553            acc += ring[j * c_dim + c] as f64 * taps[j] as f64;
554        }
555        cq[c] = silu(acc) as f32;
556    }
557    // Ring shift: drop the oldest position, append the raw current one.
558    if kk > 1 {
559        ring.copy_within(c_dim.., 0);
560        let tail = (kk - 2) * c_dim;
561        ring[tail..tail + c_dim].copy_from_slice(&qkv[..c_dim]);
562    }
563
564    let cq = &cq;
565    let s_ptr = SendMutF32(s_all.as_mut_ptr());
566    let of_ptr = SendMutF32(of.as_mut_ptr());
567    let head_range = |h0: usize, h1: usize| {
568        // Rebind the Sync wrappers whole — edition-2021 disjoint capture
569        // would otherwise grab the raw `.0` fields and lose Send/Sync.
570        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
571        // Per-worker scratch, recycled across calls (thread-local freelists).
572        let mut kv = crate::attention::take_buf(dv);
573        let mut delta = crate::attention::take_buf(dv);
574        let mut o = crate::attention::take_buf(dv);
575        let mut kf = crate::attention::take_buf(dk);
576        let mut qf = crate::attention::take_buf(dk);
577        for h in h0..h1 {
578            let ko = h / rep; // source q/k head (GQA)
579            let (qs, ks) = (ko * dk, kd + ko * dk);
580            // l2-normalize q and k; q additionally scaled by 1/√dk.
581            let (mut nq, mut nkn) = (0f64, 0f64);
582            for d in 0..dk {
583                nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
584                nkn += (cq[ks + d] as f64) * (cq[ks + d] as f64);
585            }
586            let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
587            let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
588            for d in 0..dk {
589                qf[d] = cq[qs + d] * invq;
590                kf[d] = cq[ks + d] * invk;
591            }
592
593            let g = (-(w.a_log[h] as f64).exp() * softplus(a[h] as f64 + w.dt_bias[h] as f64)).exp()
594                as f32;
595            let beta = sigmoid(b[h] as f64) as f32;
596
597            // SAFETY: disjoint per-head S and output slices per worker.
598            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
599            let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
600            let vt = &cq[2 * kd + h * dv..2 * kd + (h + 1) * dv];
601
602            // S ← g·S;  kv = kᵀS;  S += k ⊗ β(v − kv);  o = qᵀS —
603            // algebraically regrouped so S is READ twice and WRITTEN
604            // once: kv over S_old (then ×g), one fused update+query pass.
605            kv[..dv].fill(0.0);
606            for di in 0..dk {
607                let kfd = kf[di];
608                let row = &s[di * dv..(di + 1) * dv];
609                for dj in 0..dv {
610                    kv[dj] += row[dj] * kfd; // elementwise in dj → SIMD
611                }
612            }
613            for dj in 0..dv {
614                delta[dj] = (vt[dj] - g * kv[dj]) * beta;
615            }
616            o[..dv].fill(0.0);
617            for di in 0..dk {
618                let kfd = kf[di];
619                let qfd = qf[di];
620                let row = &mut s[di * dv..(di + 1) * dv];
621                for dj in 0..dv {
622                    let cell = g * row[dj] + kfd * delta[dj];
623                    row[dj] = cell;
624                    o[dj] += qfd * cell; // elementwise in dj → SIMD
625                }
626            }
627            // Gated RMSNorm per head: x̂·w·silu(z) (oracle-validated form).
628            let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
629            let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
630            for dj in 0..dv {
631                let gate = if cfg.output_gate_sigmoid {
632                    sigmoid(z[h * dv + dj] as f64)
633                } else {
634                    silu(z[h * dv + dj] as f64)
635                };
636                oh[dj] = ((o[dj] as f64 * inv) * w.norm[dj] as f64 * gate) as f32;
637            }
638        }
639        crate::attention::recycle_buf(&mut kv);
640        crate::attention::recycle_buf(&mut delta);
641        crate::attention::recycle_buf(&mut o);
642        crate::attention::recycle_buf(&mut kf);
643        crate::attention::recycle_buf(&mut qf);
644    };
645    match pool {
646        Some(pool) if nv >= 4 => pool.run(&|widx, n| {
647            let chunk = nv.div_ceil(n);
648            let h0 = (widx * chunk).min(nv);
649            let h1 = (h0 + chunk).min(nv);
650            if h0 < h1 {
651                head_range(h0, h1);
652            }
653        }),
654        _ => head_range(0, nv),
655    }
656    if let Some(t0) = perf_t0 {
657        PERF_GDN_STEP_CALLS.fetch_add(1, Ordering::Relaxed);
658        PERF_GDN_STEP_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
659    }
660}
661
662/// `CMF_GDN_SCAN=0`: the batched prefill walks `gdn_step` position by
663/// position (one pool barrier each) instead of [`gdn_scan_batch`].
664fn gdn_scan_on() -> bool {
665    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
666    *ON.get_or_init(|| std::env::var("CMF_GDN_SCAN").as_deref() != Ok("0"))
667}
668
669/// The recurrence of `b` consecutive positions in one pool dispatch per
670/// phase instead of one per position: the depthwise conv runs over the
671/// whole chunk first (channels split over the workers, each channel walking
672/// its positions in order through the same ring), then every worker takes
673/// whole heads and walks the positions in order. Per (position, head) the
674/// arithmetic is `gdn_step`'s, in the same order, so the outputs and the
675/// final state are bit-identical to `b` calls of it.
676///
677/// A 27B calibration chunk (512 positions × 48 GDN layers) paid ~25 k pool
678/// barriers per window for ~3 Mflop of work each; this pays two per layer.
679#[allow(clippy::too_many_arguments)]
680fn gdn_scan_batch(
681    qkv: &[f32],
682    z: &[f32],
683    a: &[f32],
684    bb: &[f32],
685    b: usize,
686    w: &GdnWeights,
687    cfg: &GdnCfg,
688    state: &mut [f32],
689    of: &mut [f32],
690    pool: Option<&Pool>,
691) {
692    let (nv, nk, dk, dv, kk) = (
693        cfg.num_v_heads,
694        cfg.num_k_heads,
695        cfg.key_head_dim,
696        cfg.value_head_dim,
697        cfg.conv_kernel,
698    );
699    let c_dim = cfg.conv_dim();
700    let vd = nv * dv;
701    let (kd, rep) = (nk * dk, nv / nk);
702    let hist = kk.saturating_sub(1);
703    let (ring, s_all) = state.split_at_mut(hist * c_dim);
704
705    // Depthwise causal conv + SiLU for every position. The raw input of
706    // position t−hist+j is the chunk's own row when that index is ≥ 0 and
707    // ring row t+j otherwise (the ring holds the last `hist` raw rows,
708    // oldest first) — exactly what `gdn_step` reads from its ring.
709    let mut cq = vec![0f32; b * c_dim];
710    {
711        let ring_ro: &[f32] = ring;
712        let cq_ptr = SendMutF32(cq.as_mut_ptr());
713        let conv_range = |c0: usize, c1: usize| {
714            let cq_ptr = cq_ptr;
715            for c in c0..c1 {
716                let taps = &w.conv1d[c * kk..(c + 1) * kk];
717                for t in 0..b {
718                    let mut acc = qkv[t * c_dim + c] as f64 * taps[kk - 1] as f64;
719                    for j in 0..hist {
720                        let raw = if t + j >= hist {
721                            qkv[(t + j - hist) * c_dim + c]
722                        } else {
723                            ring_ro[(t + j) * c_dim + c]
724                        };
725                        acc += raw as f64 * taps[j] as f64;
726                    }
727                    // SAFETY: each worker owns a disjoint channel range.
728                    unsafe { *cq_ptr.0.add(t * c_dim + c) = silu(acc) as f32 };
729                }
730            }
731        };
732        match pool {
733            Some(pool) if c_dim >= 256 => pool.run(&|widx, n| {
734                let chunk = c_dim.div_ceil(n);
735                let c0 = (widx * chunk).min(c_dim);
736                let c1 = (c0 + chunk).min(c_dim);
737                if c0 < c1 {
738                    conv_range(c0, c1);
739                }
740            }),
741            _ => conv_range(0, c_dim),
742        }
743    }
744    // The ring after the chunk: its last `hist` raw rows (old ring rows
745    // when the chunk is shorter than the ring).
746    if hist > 0 {
747        let mut next = vec![0f32; hist * c_dim];
748        for j in 0..hist {
749            let src = if b + j >= hist {
750                &qkv[(b + j - hist) * c_dim..(b + j - hist + 1) * c_dim]
751            } else {
752                &ring[(b + j) * c_dim..(b + j + 1) * c_dim]
753            };
754            next[j * c_dim..(j + 1) * c_dim].copy_from_slice(src);
755        }
756        ring.copy_from_slice(&next);
757    }
758
759    let cq = &cq;
760    let s_ptr = SendMutF32(s_all.as_mut_ptr());
761    let of_ptr = SendMutF32(of.as_mut_ptr());
762    let head_range = |h0: usize, h1: usize| {
763        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
764        let mut kv = crate::attention::take_buf(dv);
765        let mut delta = crate::attention::take_buf(dv);
766        let mut o = crate::attention::take_buf(dv);
767        let mut kf = crate::attention::take_buf(dk);
768        let mut qf = crate::attention::take_buf(dk);
769        for h in h0..h1 {
770            let ko = h / rep;
771            let (qs, ks) = (ko * dk, kd + ko * dk);
772            let g_base = -(w.a_log[h] as f64).exp();
773            // SAFETY: disjoint per-head S and output slices per worker.
774            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
775            for t in 0..b {
776                let cqt = &cq[t * c_dim..(t + 1) * c_dim];
777                let (mut nq, mut nkn) = (0f64, 0f64);
778                for d in 0..dk {
779                    nq += (cqt[qs + d] as f64) * (cqt[qs + d] as f64);
780                    nkn += (cqt[ks + d] as f64) * (cqt[ks + d] as f64);
781                }
782                let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
783                let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
784                for d in 0..dk {
785                    qf[d] = cqt[qs + d] * invq;
786                    kf[d] = cqt[ks + d] * invk;
787                }
788                let g = (g_base * softplus(a[t * nv + h] as f64 + w.dt_bias[h] as f64)).exp() as f32;
789                let beta = sigmoid(bb[t * nv + h] as f64) as f32;
790                let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(t * vd + h * dv), dv) };
791                let vt = &cqt[2 * kd + h * dv..2 * kd + (h + 1) * dv];
792                kv[..dv].fill(0.0);
793                for di in 0..dk {
794                    let kfd = kf[di];
795                    let row = &s[di * dv..(di + 1) * dv];
796                    for dj in 0..dv {
797                        kv[dj] += row[dj] * kfd;
798                    }
799                }
800                for dj in 0..dv {
801                    delta[dj] = (vt[dj] - g * kv[dj]) * beta;
802                }
803                o[..dv].fill(0.0);
804                for di in 0..dk {
805                    let kfd = kf[di];
806                    let qfd = qf[di];
807                    let row = &mut s[di * dv..(di + 1) * dv];
808                    for dj in 0..dv {
809                        let cell = g * row[dj] + kfd * delta[dj];
810                        row[dj] = cell;
811                        o[dj] += qfd * cell;
812                    }
813                }
814                let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
815                let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
816                let zt = &z[t * vd..(t + 1) * vd];
817                for dj in 0..dv {
818                    let gate = if cfg.output_gate_sigmoid {
819                        sigmoid(zt[h * dv + dj] as f64)
820                    } else {
821                        silu(zt[h * dv + dj] as f64)
822                    };
823                    oh[dj] = ((o[dj] as f64 * inv) * w.norm[dj] as f64 * gate) as f32;
824                }
825            }
826        }
827        crate::attention::recycle_buf(&mut kv);
828        crate::attention::recycle_buf(&mut delta);
829        crate::attention::recycle_buf(&mut o);
830        crate::attention::recycle_buf(&mut kf);
831        crate::attention::recycle_buf(&mut qf);
832    };
833    match pool {
834        Some(pool) if nv >= 4 => pool.run(&|widx, n| {
835            let chunk = nv.div_ceil(n);
836            let h0 = (widx * chunk).min(nv);
837            let h1 = (h0 + chunk).min(nv);
838            if h0 < h1 {
839                head_range(h0, h1);
840            }
841        }),
842        _ => head_range(0, nv),
843    }
844}
845
846/// Forward one position through a GatedDeltaNet layer, advancing `state`.
847pub fn gdn_forward(
848    x: &[f32],
849    w: &GdnWeights,
850    cfg: &GdnCfg,
851    state: &mut Vec<f32>,
852    pool: Option<&Pool>,
853) -> Vec<f32> {
854    let perf_t0 = perf_enabled().then(std::time::Instant::now);
855    if state.len() != cfg.state_len() {
856        *state = vec![0f32; cfg.state_len()];
857    }
858    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
859
860    let mut qkv = vec![0.0f32; c_dim];
861    let mut z = vec![0.0f32; vd];
862    let mut a = vec![0.0f32; cfg.num_v_heads];
863    let mut b = vec![0.0f32; cfg.num_v_heads];
864    // D5: two heavy projections (the GDN mixer is ~half a hybrid layer's
865    // bytes) — one GPU submission; a/b are tiny and stay on CPU. The
866    // Batch probe arbitrates GPU vs the fused-CPU dispatch per machine.
867    let cpu_projs = |qkv: &mut Vec<f32>, z: &mut Vec<f32>, a: &mut Vec<f32>, b: &mut Vec<f32>| {
868        QTensor::matvec_many(
869            [&w.in_proj_qkv, &w.in_proj_z, &w.in_proj_a, &w.in_proj_b],
870            x,
871            [
872                qkv.as_mut_slice(),
873                z.as_mut_slice(),
874                a.as_mut_slice(),
875                b.as_mut_slice(),
876            ],
877            pool,
878        );
879    };
880    let mut done = false;
881    if crate::gpu::enabled_here() && gdn_projs_eligible(w) {
882        match crate::gpu::probe_arm(crate::gpu::OpClass::Batch) {
883            crate::gpu::ProbeArm::Gpu => {
884                let t0 = std::time::Instant::now();
885                if gdn_projs_gpu(w, x, &mut qkv, &mut z) {
886                    crate::gpu::probe_record(crate::gpu::OpClass::Batch, true, t0.elapsed());
887                    w.in_proj_a.matvec(x, &mut a, pool);
888                    w.in_proj_b.matvec(x, &mut b, pool);
889                    done = true;
890                } else {
891                    crate::gpu::probe_note_decline(crate::gpu::OpClass::Batch);
892                }
893            }
894            crate::gpu::ProbeArm::CpuTimed => {
895                let t0 = std::time::Instant::now();
896                crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
897                crate::gpu::probe_record(crate::gpu::OpClass::Batch, false, t0.elapsed());
898                done = true;
899            }
900            crate::gpu::ProbeArm::Cpu => {
901                crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
902                done = true;
903            }
904        }
905    }
906    if !done {
907        cpu_projs(&mut qkv, &mut z, &mut a, &mut b);
908    }
909
910    let mut of = vec![0.0f32; vd];
911    gdn_step(&qkv, &z, &a, &b, w, cfg, state, &mut of, pool);
912
913    let mut out = vec![0.0f32; cfg.hidden_size];
914    w.out_proj.matvec(&of, &mut out, pool);
915    if let Some(t0) = perf_t0 {
916        PERF_GDN_FORWARD_CALLS.fetch_add(1, Ordering::Relaxed);
917        PERF_GDN_FORWARD_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
918    }
919    out
920}
921
922/// Batched GDN forward (prefill-GEMM): the qkv/z/a/b and out_proj
923/// projections are matmat over the batch (a weight row once per chunk),
924/// the gdn_step recurrence runs sequentially over positions (state is the
925/// same as the sequential path; the math is elementwise identical).
926pub fn gdn_forward_batch(
927    xs: &[f32],
928    b: usize,
929    w: &GdnWeights,
930    cfg: &GdnCfg,
931    state: &mut Vec<f32>,
932    pool: Option<&Pool>,
933) -> Vec<f32> {
934    let perf_t0 = perf_enabled().then(std::time::Instant::now);
935    if state.len() != cfg.state_len() {
936        *state = vec![0f32; cfg.state_len()];
937    }
938    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
939    let nv = cfg.num_v_heads;
940
941    let mut qkv = vec![0.0f32; b * c_dim];
942    w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
943    let mut z = vec![0.0f32; b * vd];
944    w.in_proj_z.matmat(xs, b, &mut z, pool);
945    let mut a = vec![0.0f32; b * nv];
946    w.in_proj_a.matmat(xs, b, &mut a, pool);
947    let mut bb = vec![0.0f32; b * nv];
948    w.in_proj_b.matmat(xs, b, &mut bb, pool);
949
950    let mut of = vec![0.0f32; b * vd];
951    if b > 1 && gdn_scan_on() {
952        gdn_scan_batch(&qkv, &z, &a, &bb, b, w, cfg, state, &mut of, pool);
953    } else {
954        for bi in 0..b {
955            gdn_step(
956                &qkv[bi * c_dim..(bi + 1) * c_dim],
957                &z[bi * vd..(bi + 1) * vd],
958                &a[bi * nv..(bi + 1) * nv],
959                &bb[bi * nv..(bi + 1) * nv],
960                w,
961                cfg,
962                state,
963                &mut of[bi * vd..(bi + 1) * vd],
964                pool,
965            );
966        }
967    }
968    let mut out = vec![0.0f32; b * cfg.hidden_size];
969    w.out_proj.matmat(&of, b, &mut out, pool);
970    if std::env::var("CMF_GDN_TRACE").is_ok() {
971        let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
972        eprintln!(
973            "gdn-batch b={b}: |x0|={:.5} |qkv0|={:.5} |z0|={:.5} |a0|={:.5} |b0|={:.5} |of0|={:.5} |out0|={:.5} |state|={:.5}",
974            n(&xs[..cfg.hidden_size]),
975            n(&qkv[..c_dim]),
976            n(&z[..vd]),
977            n(&a[..nv]),
978            n(&bb[..nv]),
979            n(&of[..vd]),
980            n(&out[..cfg.hidden_size]),
981            n(state)
982        );
983    }
984    if let Some(t0) = perf_t0 {
985        PERF_GDN_BATCH_CALLS.fetch_add(1, Ordering::Relaxed);
986        PERF_GDN_BATCH_NS.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
987    }
988    out
989}
990
991/// Aggregate host-side GDN costs for the bounded warm-decode profile.
992pub fn perf_report() {
993    if !perf_enabled() {
994        return;
995    }
996    let fc = PERF_GDN_FORWARD_CALLS.load(Ordering::Relaxed);
997    let bc = PERF_GDN_BATCH_CALLS.load(Ordering::Relaxed);
998    let sc = PERF_GDN_STEP_CALLS.load(Ordering::Relaxed);
999    eprintln!(
1000        "[perf-gdn] forward_calls={} forward_ms={:.3} forward_ms_per_call={:.3} batch_calls={} batch_ms={:.3} step_calls={} step_ms={:.3} step_ms_per_call={:.3}",
1001        fc,
1002        PERF_GDN_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6,
1003        PERF_GDN_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6 / fc.max(1) as f64,
1004        bc,
1005        PERF_GDN_BATCH_NS.load(Ordering::Relaxed) as f64 / 1e6,
1006        sc,
1007        PERF_GDN_STEP_NS.load(Ordering::Relaxed) as f64 / 1e6,
1008        PERF_GDN_STEP_NS.load(Ordering::Relaxed) as f64 / 1e6 / sc.max(1) as f64,
1009    );
1010}
1011
1012/// GDN qkv+z GPU eligibility: q1 mixers offload by default (the CPU q1
1013/// kernel is compute-bound); q8 stays opt-in via CMF_GPU_GDN=1 (measured
1014/// neutral). The probe in `gdn_forward` still arbitrates either way.
1015fn gdn_projs_eligible(w: &GdnWeights) -> bool {
1016    // Any tile-embedded-scale layout, not just q1: the batched matvec now has
1017    // kernels for all of them, and the probe arbitrates whether it pays.
1018    // `CMF_GPU_GDN=0` forces the CPU projections back — the switch exists so
1019    // the two arms can be compared inside ONE run: this machine throttles far
1020    // enough that measurements minutes apart are not comparable.
1021    if std::env::var("CMF_GPU_GDN")
1022        .map(|v| v == "0")
1023        .unwrap_or(false)
1024    {
1025        return false;
1026    }
1027    w.in_proj_qkv.is_q1()
1028        || w.in_proj_qkv.q4t_parts().is_some()
1029        || w.in_proj_qkv.q4tp_parts().is_some()
1030        || std::env::var("CMF_GPU_GDN")
1031            .map(|v| v == "1")
1032            .unwrap_or(false)
1033}
1034
1035/// GDN qkv+z on GPU in a single submission (independent matvecs of one input).
1036fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
1037    use crate::gpu::matvec_batch;
1038    use crate::qtensor::QTensor;
1039    if !crate::gpu::enabled_here() {
1040        return false;
1041    }
1042    fn part<'a>(
1043        t: &'a QTensor,
1044        x: &[f32],
1045    ) -> Option<(
1046        std::sync::Arc<cortiq_core::CmfModel>,
1047        crate::gpu::BatchJob<'a>,
1048    )> {
1049        use crate::gpu::BatchJob;
1050        use crate::qtensor::prescale;
1051        use cortiq_core::TensorDtype;
1052        match t {
1053            QTensor::Mapped {
1054                model,
1055                idx,
1056                dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
1057                rows,
1058                cols,
1059                row_scale,
1060                col_field,
1061                ..
1062            } => Some((
1063                model.clone(),
1064                BatchJob {
1065                    idx: *idx,
1066                    rows: *rows,
1067                    cols: *cols,
1068                    row_scale,
1069                    xs: prescale(x, col_field, *dt).into_owned(),
1070                    layout: crate::gpu::BatchLayout::Q8,
1071                },
1072            )),
1073            QTensor::Mapped {
1074                model,
1075                idx,
1076                dtype: TensorDtype::Q1,
1077                rows,
1078                cols,
1079                ..
1080            } => Some((
1081                model.clone(),
1082                BatchJob {
1083                    idx: *idx,
1084                    rows: *rows,
1085                    cols: *cols,
1086                    row_scale: &[],
1087                    xs: x.to_vec(),
1088                    layout: crate::gpu::BatchLayout::Q1,
1089                },
1090            )),
1091            QTensor::Mapped {
1092                model,
1093                idx,
1094                dtype: TensorDtype::Q1G,
1095                rows,
1096                cols,
1097                ..
1098            } => Some((
1099                model.clone(),
1100                BatchJob {
1101                    idx: *idx,
1102                    rows: *rows,
1103                    cols: *cols,
1104                    row_scale: &[],
1105                    xs: x.to_vec(),
1106                    layout: crate::gpu::BatchLayout::Q1g,
1107                },
1108            )),
1109            // q4t/q4tp: same tile-embedded-scale contract as q1, different
1110            // stride. Missing here, a hybrid model's GDN projections fell to
1111            // the CPU on every layer while its experts rode the device.
1112            QTensor::Mapped {
1113                model,
1114                idx,
1115                dtype: dt @ (TensorDtype::Q4Tiled | TensorDtype::Q4TiledP),
1116                rows,
1117                cols,
1118                ..
1119            } => Some((
1120                model.clone(),
1121                BatchJob {
1122                    idx: *idx,
1123                    rows: *rows,
1124                    cols: *cols,
1125                    row_scale: &[],
1126                    xs: x.to_vec(),
1127                    layout: if *dt == TensorDtype::Q4TiledP {
1128                        crate::gpu::BatchLayout::Q4tp
1129                    } else {
1130                        crate::gpu::BatchLayout::Q4t
1131                    },
1132                },
1133            )),
1134            _ => None,
1135        }
1136    }
1137    let Some((model, jq)) = part(&w.in_proj_qkv, x) else {
1138        return false;
1139    };
1140    let Some((_, jz)) = part(&w.in_proj_z, x) else {
1141        return false;
1142    };
1143    matvec_batch(&model, &[jq, jz], &mut [qkv, z])
1144}
1145
1146/// Fused two-position forward (speculative verify): lane 1 commits into
1147/// `state`, lane 2 is tentative in `scratch` (ring + S move together).
1148#[allow(clippy::too_many_arguments)]
1149pub fn gdn_pair(
1150    x1: &[f32],
1151    x2: &[f32],
1152    w: &GdnWeights,
1153    cfg: &GdnCfg,
1154    state: &mut Vec<f32>,
1155    scratch: &mut Vec<f32>,
1156    pool: Option<&Pool>,
1157) -> (Vec<f32>, Vec<f32>) {
1158    if state.len() != cfg.state_len() {
1159        *state = vec![0f32; cfg.state_len()];
1160    }
1161    let (c_dim, vd, nv) = (
1162        cfg.conv_dim(),
1163        cfg.num_v_heads * cfg.value_head_dim,
1164        cfg.num_v_heads,
1165    );
1166
1167    let mut qkv1 = vec![0.0f32; c_dim];
1168    let mut qkv2 = vec![0.0f32; c_dim];
1169    w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
1170    let mut z1 = vec![0.0f32; vd];
1171    let mut z2 = vec![0.0f32; vd];
1172    w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
1173    let mut a1 = vec![0.0f32; nv];
1174    let mut a2 = vec![0.0f32; nv];
1175    w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
1176    let mut b1 = vec![0.0f32; nv];
1177    let mut b2 = vec![0.0f32; nv];
1178    w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
1179
1180    let mut of1 = vec![0.0f32; vd];
1181    gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1, pool);
1182
1183    scratch.clear();
1184    scratch.extend_from_slice(state);
1185    let mut of2 = vec![0.0f32; vd];
1186    gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2, pool);
1187
1188    let mut out1 = vec![0.0f32; cfg.hidden_size];
1189    let mut out2 = vec![0.0f32; cfg.hidden_size];
1190    w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
1191    (out1, out2)
1192}
1193
1194// ───────────────────────── ShortConv (LFM2 gated short convolution) ─────────────────────────
1195
1196/// Weights of one LFM2 short-convolution mixer
1197/// (`model.layers.{i}.short_conv.*`, renamed from the vendor `conv.*` at
1198/// convert time). No recurrent mixer state — the only state is the causal
1199/// conv ring (the last `kernel−1` gated inputs per channel).
1200pub struct ShortConvWeights {
1201    /// [3·hidden, hidden] — fused (B, C, x) projection.
1202    pub in_proj: QTensor,
1203    /// [hidden · kernel] depthwise conv taps, flattened `[channel][tap]`
1204    /// (the source `[hidden, 1, kernel]` with the singleton group axis
1205    /// dropped). Tap `kernel−1` multiplies the current position.
1206    pub conv: Vec<f32>,
1207    /// [hidden, hidden] — output projection.
1208    pub out_proj: QTensor,
1209}
1210
1211#[derive(Clone, Copy)]
1212pub struct ShortConvCfg {
1213    pub hidden_size: usize,
1214    /// Conv kernel width `L` (`conv_L_cache`; LFM2 uses 3).
1215    pub kernel: usize,
1216}
1217
1218impl ShortConvCfg {
1219    /// Conv ring: the last `kernel−1` gated inputs per channel.
1220    pub fn state_len(&self) -> usize {
1221        (self.kernel - 1) * self.hidden_size
1222    }
1223}
1224
1225/// One position through the gated conv, given the fused projection
1226/// `bcx = in_proj·x` [3·hidden] = [B | C | x]. Advances the conv ring and
1227/// writes the gated conv output `y = C ⊙ conv(B ⊙ x)` [hidden] into `y`.
1228///
1229/// The conv is PyTorch's causal depthwise `Conv1d(padding=kernel−1)`
1230/// truncated to the current length: for tap `k`, weight `w[c][k]` pairs
1231/// with the input `kernel−1−k` steps in the past, so `w[c][kernel−1]` is
1232/// the current position. The ring holds `in[t−1] … in[t−(kernel−1)]` at
1233/// slots `0 … kernel−2`.
1234fn short_conv_step(
1235    bcx: &[f32],
1236    conv: &[f32],
1237    cfg: &ShortConvCfg,
1238    ring_state: &mut [f32],
1239    y: &mut [f32],
1240) {
1241    let (h, k) = (cfg.hidden_size, cfg.kernel);
1242    let ring = k - 1;
1243    let (bg, cg, xg) = (&bcx[0..h], &bcx[h..2 * h], &bcx[2 * h..3 * h]);
1244    for c in 0..h {
1245        let bx = bg[c] * xg[c];
1246        let wc = &conv[c * k..(c + 1) * k];
1247        // Current tap, then the past taps read from the channel's ring.
1248        let mut acc = wc[k - 1] * bx;
1249        let rc = &mut ring_state[c * ring..c * ring + ring];
1250        for s in 0..ring {
1251            acc += wc[k - 2 - s] * rc[s];
1252        }
1253        y[c] = cg[c] * acc;
1254        // Shift newest-in-front: slot 0 becomes the just-seen input.
1255        for s in (1..ring).rev() {
1256            rc[s] = rc[s - 1];
1257        }
1258        if ring > 0 {
1259            rc[0] = bx;
1260        }
1261    }
1262}
1263
1264/// Forward one position through a short-conv layer, advancing `state`.
1265pub fn short_conv_forward(
1266    x: &[f32],
1267    w: &ShortConvWeights,
1268    cfg: &ShortConvCfg,
1269    state: &mut Vec<f32>,
1270    pool: Option<&Pool>,
1271) -> Vec<f32> {
1272    if state.len() != cfg.state_len() {
1273        *state = vec![0f32; cfg.state_len()];
1274    }
1275    let h = cfg.hidden_size;
1276    let mut bcx = vec![0.0f32; 3 * h];
1277    w.in_proj.matvec(x, &mut bcx, pool);
1278    let mut y = vec![0.0f32; h];
1279    short_conv_step(&bcx, &w.conv, cfg, state, &mut y);
1280    let mut out = vec![0.0f32; h];
1281    w.out_proj.matvec(&y, &mut out, pool);
1282    out
1283}
1284
1285/// Batched short-conv forward (prefill-GEMM): in_proj/out_proj are matmat
1286/// over the chunk (a weight row streamed once), the conv walks the
1287/// positions in order — the chunk is contiguous, so the ring state is
1288/// exactly the sequential path's and the math is elementwise identical.
1289pub fn short_conv_forward_batch(
1290    xs: &[f32],
1291    b: usize,
1292    w: &ShortConvWeights,
1293    cfg: &ShortConvCfg,
1294    state: &mut Vec<f32>,
1295    pool: Option<&Pool>,
1296) -> Vec<f32> {
1297    if state.len() != cfg.state_len() {
1298        *state = vec![0f32; cfg.state_len()];
1299    }
1300    let h = cfg.hidden_size;
1301    let mut bcx = vec![0.0f32; b * 3 * h];
1302    w.in_proj.matmat(xs, b, &mut bcx, pool);
1303    let mut y = vec![0.0f32; b * h];
1304    for bi in 0..b {
1305        short_conv_step(
1306            &bcx[bi * 3 * h..(bi + 1) * 3 * h],
1307            &w.conv,
1308            cfg,
1309            state,
1310            &mut y[bi * h..(bi + 1) * h],
1311        );
1312    }
1313    let mut out = vec![0.0f32; b * h];
1314    w.out_proj.matmat(&y, b, &mut out, pool);
1315    out
1316}
1317
1318/// Fused two-position forward (speculative verify). Lane 1 commits into
1319/// `state`; lane 2's tentative ring goes into `scratch` — swapped in on
1320/// draft acceptance, dropped on rejection. LFM2 ships no MTP head, so this
1321/// is exercised only by the pair-fusion micro-benchmark; kept correct.
1322#[allow(clippy::too_many_arguments)]
1323pub fn short_conv_pair(
1324    x1: &[f32],
1325    x2: &[f32],
1326    w: &ShortConvWeights,
1327    cfg: &ShortConvCfg,
1328    state: &mut Vec<f32>,
1329    scratch: &mut Vec<f32>,
1330    pool: Option<&Pool>,
1331) -> (Vec<f32>, Vec<f32>) {
1332    if state.len() != cfg.state_len() {
1333        *state = vec![0f32; cfg.state_len()];
1334    }
1335    let h = cfg.hidden_size;
1336    let mut bcx1 = vec![0.0f32; 3 * h];
1337    let mut bcx2 = vec![0.0f32; 3 * h];
1338    w.in_proj.matvec2(x1, x2, &mut bcx1, &mut bcx2, pool);
1339
1340    let mut y1 = vec![0.0f32; h];
1341    short_conv_step(&bcx1, &w.conv, cfg, state, &mut y1);
1342    scratch.clear();
1343    scratch.extend_from_slice(state);
1344    let mut y2 = vec![0.0f32; h];
1345    short_conv_step(&bcx2, &w.conv, cfg, scratch, &mut y2);
1346
1347    let mut out1 = vec![0.0f32; h];
1348    let mut out2 = vec![0.0f32; h];
1349    w.out_proj.matvec2(&y1, &y2, &mut out1, &mut out2, pool);
1350    (out1, out2)
1351}
1352
1353// ─── Kimi Delta Attention (KDA) ─────────────────────────────────────────
1354//
1355// Kimi Linear / Kimi-K3 linear mixer (reference: FLA naive_recurrent_kda
1356// + moonshotai modeling_kimi.py). Differences from GatedDeltaNet above:
1357// separate q/k/v projections each behind its OWN causal depthwise short
1358// convolution; the delta-rule decay is a PER-CHANNEL vector (diagonal)
1359// instead of a per-head scalar; the decay pre-activation comes from a
1360// low-rank projection f_b(f_a(x)); and the output gate norm uses
1361// sigmoid, not SiLU.
1362
1363pub struct KdaWeights {
1364    /// [nh·dk, hidden]
1365    pub q_proj: QTensor,
1366    /// [nh·dk, hidden]
1367    pub k_proj: QTensor,
1368    /// [nh·dv, hidden]
1369    pub v_proj: QTensor,
1370    /// [nh·dk × kk] — depthwise taps, oldest→newest (see GdnWeights.conv1d)
1371    pub conv_q: Vec<f32>,
1372    pub conv_k: Vec<f32>,
1373    /// [nh·dv × kk]
1374    pub conv_v: Vec<f32>,
1375    /// [rank, hidden] — low-rank decay projection, stage 1
1376    pub f_a: QTensor,
1377    /// [nh·dk, rank] — stage 2
1378    pub f_b: QTensor,
1379    /// [nh·dk]
1380    pub dt_bias: Vec<f32>,
1381    /// [nh] per-head (Kimi-Linear-48B) | [dk] per-dim (Kimi-K3) |
1382    /// [nh·dk] full — broadcast resolved by length.
1383    pub a_log: Vec<f32>,
1384    /// [nh, hidden] — β = σ(b_proj·x) per head
1385    pub b_proj: QTensor,
1386    /// Output gate: full-rank g_proj (K3) or low-rank g_b(g_a(x)) (48B).
1387    pub gate: KdaOutGate,
1388    /// [dv] — gated RMSNorm weight (per head over head_v_dim)
1389    pub o_norm: Vec<f32>,
1390    /// [hidden, nh·dv]
1391    pub o_proj: QTensor,
1392    /// Some(lb): log-decay = lb·σ(exp(A)·(f+bias)) (K3, lb=−5);
1393    /// None: −exp(A)·softplus(f+bias) (Kimi-Linear-48B).
1394    pub gate_lower_bound: Option<f32>,
1395}
1396
1397pub enum KdaOutGate {
1398    /// [nh·dv, hidden]
1399    Full(QTensor),
1400    /// g_a [rank, hidden], g_b [nh·dv, rank]
1401    LowRank(QTensor, QTensor),
1402}
1403
1404#[derive(Clone, Copy)]
1405pub struct KdaCfg {
1406    pub num_heads: usize,
1407    pub head_k_dim: usize,
1408    pub head_v_dim: usize,
1409    pub conv_kernel: usize,
1410    pub hidden_size: usize,
1411    pub rms_eps: f64,
1412}
1413
1414impl KdaCfg {
1415    /// Packed state: [q ring | k ring | v ring | S nh·dk·dv], one Vec —
1416    /// same single-buffer convention as GdnCfg::state_len.
1417    pub fn state_len(&self) -> usize {
1418        let (nh, dk, dv, kk) = (
1419            self.num_heads,
1420            self.head_k_dim,
1421            self.head_v_dim,
1422            self.conv_kernel,
1423        );
1424        (kk - 1) * (2 * nh * dk + nh * dv) + nh * dk * dv
1425    }
1426}
1427
1428/// Depthwise causal conv over [ring…, current] + SiLU, then ring shift.
1429/// Taps oldest→newest, tap kk−1 multiplies the current position.
1430fn kda_conv(raw: &[f32], taps: &[f32], ring: &mut [f32], kk: usize, out: &mut [f32]) {
1431    let c_dim = raw.len();
1432    for c in 0..c_dim {
1433        let t = &taps[c * kk..(c + 1) * kk];
1434        let mut acc = raw[c] as f64 * t[kk - 1] as f64;
1435        for j in 0..kk - 1 {
1436            acc += ring[j * c_dim + c] as f64 * t[j] as f64;
1437        }
1438        out[c] = silu(acc) as f32;
1439    }
1440    if kk > 1 {
1441        ring.copy_within(c_dim.., 0);
1442        let tail = (kk - 2) * c_dim;
1443        ring[tail..tail + c_dim].copy_from_slice(raw);
1444    }
1445}
1446
1447/// Per-channel log-decay for head-channel (h, d): resolves the A_log
1448/// broadcast by length and applies the configured gate formula.
1449#[inline]
1450fn kda_log_decay(w: &KdaWeights, cfg: &KdaCfg, h: usize, d: usize, f: f32) -> f64 {
1451    let (nh, dk) = (cfg.num_heads, cfg.head_k_dim);
1452    let a = if w.a_log.len() == nh {
1453        w.a_log[h] as f64
1454    } else if w.a_log.len() == dk {
1455        w.a_log[d] as f64
1456    } else {
1457        w.a_log[h * dk + d] as f64
1458    };
1459    let raw = f as f64 + w.dt_bias[h * dk + d] as f64;
1460    match w.gate_lower_bound {
1461        Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1462        None => -a.exp() * softplus(raw),
1463    }
1464}
1465
1466/// One recurrent step given this position's raw (pre-conv) projections.
1467/// Advances the packed state and writes the gated per-head output into
1468/// `of` [nh·dv]. Recurrence (FLA naive_recurrent_kda):
1469///   S ← Diag(exp(g))·S;  S += β·k ⊗ (v − kᵀS);  o = qᵀS
1470/// with q,k L2-normalized per head and q additionally scaled by 1/√dk —
1471/// regrouped into two S passes like gdn_step (per-channel decay folds
1472/// into the k readout of the first pass).
1473#[allow(clippy::too_many_arguments)]
1474fn kda_step(
1475    xq: &[f32],
1476    xk: &[f32],
1477    xv: &[f32],
1478    f: &[f32],
1479    b: &[f32],
1480    gate_out: &[f32],
1481    w: &KdaWeights,
1482    cfg: &KdaCfg,
1483    state: &mut [f32],
1484    of: &mut [f32],
1485    pool: Option<&Pool>,
1486) {
1487    let (nh, dk, dv, kk) = (
1488        cfg.num_heads,
1489        cfg.head_k_dim,
1490        cfg.head_v_dim,
1491        cfg.conv_kernel,
1492    );
1493    let (kd, vd) = (nh * dk, nh * dv);
1494    let ring_q_len = (kk - 1) * kd;
1495    let ring_v_len = (kk - 1) * vd;
1496    let (ring_q, rest) = state.split_at_mut(ring_q_len);
1497    let (ring_k, rest) = rest.split_at_mut(ring_q_len);
1498    let (ring_v, s_all) = rest.split_at_mut(ring_v_len);
1499
1500    let mut cq = vec![0f32; kd];
1501    let mut ck = vec![0f32; kd];
1502    let mut cv = vec![0f32; vd];
1503    kda_conv(xq, &w.conv_q, ring_q, kk, &mut cq);
1504    kda_conv(xk, &w.conv_k, ring_k, kk, &mut ck);
1505    kda_conv(xv, &w.conv_v, ring_v, kk, &mut cv);
1506
1507    let (cq, ck, cv) = (&cq, &ck, &cv);
1508    let s_ptr = SendMutF32(s_all.as_mut_ptr());
1509    let of_ptr = SendMutF32(of.as_mut_ptr());
1510    let head_range = |h0: usize, h1: usize| {
1511        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
1512        let mut kv = crate::attention::take_buf(dv);
1513        let mut delta = crate::attention::take_buf(dv);
1514        let mut o = crate::attention::take_buf(dv);
1515        let mut kf = crate::attention::take_buf(dk);
1516        let mut qf = crate::attention::take_buf(dk);
1517        let mut gd = crate::attention::take_buf(dk);
1518        for h in h0..h1 {
1519            let qs = h * dk;
1520            // l2-normalize q and k; q additionally scaled by 1/√dk.
1521            let (mut nq, mut nkn) = (0f64, 0f64);
1522            for d in 0..dk {
1523                nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
1524                nkn += (ck[qs + d] as f64) * (ck[qs + d] as f64);
1525            }
1526            let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
1527            let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
1528            for d in 0..dk {
1529                qf[d] = cq[qs + d] * invq;
1530                kf[d] = ck[qs + d] * invk;
1531                gd[d] = kda_log_decay(w, cfg, h, d, f[qs + d]).exp() as f32;
1532            }
1533            let beta = sigmoid(b[h] as f64) as f32;
1534
1535            // SAFETY: disjoint per-head S and output slices per worker.
1536            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
1537            let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
1538            let vt = &cv[h * dv..(h + 1) * dv];
1539
1540            // Pass 1: kv = kᵀ(Diag(gd)·S_old) — decay folded into k.
1541            kv[..dv].fill(0.0);
1542            for di in 0..dk {
1543                let kg = kf[di] * gd[di];
1544                let row = &s[di * dv..(di + 1) * dv];
1545                for dj in 0..dv {
1546                    kv[dj] += row[dj] * kg;
1547                }
1548            }
1549            for dj in 0..dv {
1550                delta[dj] = (vt[dj] - kv[dj]) * beta;
1551            }
1552            // Pass 2: S[di,:] = gd[di]·row + k[di]·delta;  o += q[di]·row.
1553            o[..dv].fill(0.0);
1554            for di in 0..dk {
1555                let (kfd, qfd, gdd) = (kf[di], qf[di], gd[di]);
1556                let row = &mut s[di * dv..(di + 1) * dv];
1557                for dj in 0..dv {
1558                    let cell = gdd * row[dj] + kfd * delta[dj];
1559                    row[dj] = cell;
1560                    o[dj] += qfd * cell;
1561                }
1562            }
1563            // Gated RMSNorm per head: x̂·w·σ(gate) — sigmoid, not SiLU.
1564            let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
1565            let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1566            for dj in 0..dv {
1567                oh[dj] = ((o[dj] as f64 * inv)
1568                    * w.o_norm[dj] as f64
1569                    * sigmoid(gate_out[h * dv + dj] as f64)) as f32;
1570            }
1571        }
1572        crate::attention::recycle_buf(&mut kv);
1573        crate::attention::recycle_buf(&mut delta);
1574        crate::attention::recycle_buf(&mut o);
1575        crate::attention::recycle_buf(&mut kf);
1576        crate::attention::recycle_buf(&mut qf);
1577        crate::attention::recycle_buf(&mut gd);
1578    };
1579    match pool {
1580        Some(pool) if nh >= 4 => pool.run(&|widx, n| {
1581            let chunk = nh.div_ceil(n);
1582            let h0 = (widx * chunk).min(nh);
1583            let h1 = (h0 + chunk).min(nh);
1584            if h0 < h1 {
1585                head_range(h0, h1);
1586            }
1587        }),
1588        _ => head_range(0, nh),
1589    }
1590}
1591
1592/// Project one position's raw q/k/v/f/β/gate inputs (shared by the
1593/// single and batched forwards; `bi` selects the row when batched).
1594fn kda_gate_out(w: &KdaWeights, x: &[f32], vd: usize, pool: Option<&Pool>) -> Vec<f32> {
1595    let mut g = vec![0.0f32; vd];
1596    match &w.gate {
1597        KdaOutGate::Full(gp) => gp.matvec(x, &mut g, pool),
1598        KdaOutGate::LowRank(ga, gb) => {
1599            let mut low = vec![0.0f32; ga.rows()];
1600            ga.matvec(x, &mut low, pool);
1601            gb.matvec(&low, &mut g, pool);
1602        }
1603    }
1604    g
1605}
1606
1607/// Forward one position through a KDA layer, advancing `state`.
1608pub fn kda_forward(
1609    x: &[f32],
1610    w: &KdaWeights,
1611    cfg: &KdaCfg,
1612    state: &mut Vec<f32>,
1613    pool: Option<&Pool>,
1614) -> Vec<f32> {
1615    if state.len() != cfg.state_len() {
1616        *state = vec![0f32; cfg.state_len()];
1617    }
1618    let (nh, dk, dv) = (cfg.num_heads, cfg.head_k_dim, cfg.head_v_dim);
1619    let (kd, vd) = (nh * dk, nh * dv);
1620
1621    let mut xq = vec![0.0f32; kd];
1622    let mut xk = vec![0.0f32; kd];
1623    let mut xv = vec![0.0f32; vd];
1624    let mut fl = vec![0.0f32; w.f_a.rows()];
1625    let mut b = vec![0.0f32; nh];
1626    // KDA's beta projection uses the same hidden input as q/k/v/f_a.  When
1627    // it shares their Q4TP layout, include it in the virtual row space so
1628    // the projection group pays one pool dispatch, not five.  The explicit
1629    // fallback keeps mixed-dtype checkpoints on their established kernels.
1630    let q4_group = [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a, &w.b_proj];
1631    let q4_uniform = q4_group.iter().all(|t| {
1632        matches!(
1633            t,
1634            QTensor::Mapped {
1635                dtype: TensorDtype::Q4TiledP,
1636                ..
1637            }
1638        )
1639    });
1640    if q4_uniform {
1641        QTensor::matvec_many(
1642            q4_group,
1643            x,
1644            [
1645                xq.as_mut_slice(),
1646                xk.as_mut_slice(),
1647                xv.as_mut_slice(),
1648                fl.as_mut_slice(),
1649                b.as_mut_slice(),
1650            ],
1651            pool,
1652        );
1653    } else {
1654        QTensor::matvec_many(
1655            [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a],
1656            x,
1657            [
1658                xq.as_mut_slice(),
1659                xk.as_mut_slice(),
1660                xv.as_mut_slice(),
1661                fl.as_mut_slice(),
1662            ],
1663            pool,
1664        );
1665        w.b_proj.matvec(x, &mut b, pool);
1666    }
1667    let mut f = vec![0.0f32; kd];
1668    w.f_b.matvec(&fl, &mut f, pool);
1669    let gate_out = kda_gate_out(w, x, vd, pool);
1670
1671    let mut of = vec![0.0f32; vd];
1672    kda_step(
1673        &xq, &xk, &xv, &f, &b, &gate_out, w, cfg, state, &mut of, pool,
1674    );
1675
1676    let mut out = vec![0.0f32; cfg.hidden_size];
1677    w.o_proj.matvec(&of, &mut out, pool);
1678    out
1679}
1680
1681/// Batched KDA forward (prefill-GEMM): projections as matmat over the
1682/// chunk, the recurrence sequential per position — elementwise identical
1683/// to the single-position path.
1684pub fn kda_forward_batch(
1685    xs: &[f32],
1686    bsz: usize,
1687    w: &KdaWeights,
1688    cfg: &KdaCfg,
1689    state: &mut Vec<f32>,
1690    pool: Option<&Pool>,
1691) -> Vec<f32> {
1692    if state.len() != cfg.state_len() {
1693        *state = vec![0f32; cfg.state_len()];
1694    }
1695    let (nh, dk, dv, hs) = (
1696        cfg.num_heads,
1697        cfg.head_k_dim,
1698        cfg.head_v_dim,
1699        cfg.hidden_size,
1700    );
1701    let (kd, vd) = (nh * dk, nh * dv);
1702
1703    let mut xq = vec![0.0f32; bsz * kd];
1704    w.q_proj.matmat(xs, bsz, &mut xq, pool);
1705    let mut xk = vec![0.0f32; bsz * kd];
1706    w.k_proj.matmat(xs, bsz, &mut xk, pool);
1707    let mut xv = vec![0.0f32; bsz * vd];
1708    w.v_proj.matmat(xs, bsz, &mut xv, pool);
1709    let rank = w.f_a.rows();
1710    let mut fl = vec![0.0f32; bsz * rank];
1711    w.f_a.matmat(xs, bsz, &mut fl, pool);
1712    let mut f = vec![0.0f32; bsz * kd];
1713    w.f_b.matmat(&fl, bsz, &mut f, pool);
1714    let mut b = vec![0.0f32; bsz * nh];
1715    w.b_proj.matmat(xs, bsz, &mut b, pool);
1716    let mut gate_out = vec![0.0f32; bsz * vd];
1717    match &w.gate {
1718        KdaOutGate::Full(gp) => gp.matmat(xs, bsz, &mut gate_out, pool),
1719        KdaOutGate::LowRank(ga, gb) => {
1720            let mut low = vec![0.0f32; bsz * ga.rows()];
1721            ga.matmat(xs, bsz, &mut low, pool);
1722            gb.matmat(&low, bsz, &mut gate_out, pool);
1723        }
1724    }
1725
1726    let mut of = vec![0.0f32; bsz * vd];
1727    for bi in 0..bsz {
1728        let mut oh = vec![0.0f32; vd];
1729        kda_step(
1730            &xq[bi * kd..(bi + 1) * kd],
1731            &xk[bi * kd..(bi + 1) * kd],
1732            &xv[bi * vd..(bi + 1) * vd],
1733            &f[bi * kd..(bi + 1) * kd],
1734            &b[bi * nh..(bi + 1) * nh],
1735            &gate_out[bi * vd..(bi + 1) * vd],
1736            w,
1737            cfg,
1738            state,
1739            &mut oh,
1740            pool,
1741        );
1742        of[bi * vd..(bi + 1) * vd].copy_from_slice(&oh);
1743    }
1744
1745    let mut out = vec![0.0f32; bsz * hs];
1746    w.o_proj.matmat(&of, bsz, &mut out, pool);
1747    out
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752    #[test]
1753    fn kda_forward_matches_naive_reference() {
1754        // Small deterministic KDA layer; the oracle is a literal port of
1755        // FLA naive_recurrent_kda + naive_kda_gate + the modeling glue
1756        // (conv→silu, low-rank decay, sigmoid-gated output norm), coded
1757        // straight from the reference — a different shape from the fused
1758        // two-pass production kernel.
1759        let (nh, dk, dv, kk, hs, rank) = (2usize, 4usize, 4usize, 3usize, 6usize, 3usize);
1760        let synth = |rows: usize, cols: usize, salt: usize| -> QTensor {
1761            QTensor::from_f32(
1762                (0..rows * cols)
1763                    .map(|i| (((i * 31 + salt * 17) % 101) as f32 / 101.0 - 0.5) * 0.6)
1764                    .collect(),
1765                rows,
1766                cols,
1767            )
1768        };
1769        let vecf = |n: usize, salt: usize| -> Vec<f32> {
1770            (0..n)
1771                .map(|i| (((i * 13 + salt * 7) % 89) as f32 / 89.0 - 0.5) * 0.8)
1772                .collect()
1773        };
1774        for (label, a_log, lb) in [
1775            ("per-head standard", vecf(nh, 40), None),
1776            ("per-dim lower-bound", vecf(dk, 41), Some(-5.0f32)),
1777        ] {
1778            let w = KdaWeights {
1779                q_proj: synth(nh * dk, hs, 1),
1780                k_proj: synth(nh * dk, hs, 2),
1781                v_proj: synth(nh * dv, hs, 3),
1782                conv_q: vecf(nh * dk * kk, 4),
1783                conv_k: vecf(nh * dk * kk, 5),
1784                conv_v: vecf(nh * dv * kk, 6),
1785                f_a: synth(rank, hs, 7),
1786                f_b: synth(nh * dk, rank, 8),
1787                dt_bias: vecf(nh * dk, 9),
1788                a_log: a_log.clone(),
1789                b_proj: synth(nh, hs, 10),
1790                gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1791                o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1792                o_proj: synth(hs, nh * dv, 13),
1793                gate_lower_bound: lb,
1794            };
1795            let cfg = KdaCfg {
1796                num_heads: nh,
1797                head_k_dim: dk,
1798                head_v_dim: dv,
1799                conv_kernel: kk,
1800                hidden_size: hs,
1801                rms_eps: 1e-6,
1802            };
1803            let xs: Vec<Vec<f32>> = (0..6)
1804                .map(|t| {
1805                    (0..hs)
1806                        .map(|i| ((t * hs + i) as f32 * 0.37).sin() * 0.5)
1807                        .collect()
1808                })
1809                .collect();
1810
1811            // Production path.
1812            let mut state = Vec::new();
1813            let got: Vec<Vec<f32>> = xs
1814                .iter()
1815                .map(|x| kda_forward(x, &w, &cfg, &mut state, None))
1816                .collect();
1817
1818            // Oracle.
1819            let mv = |t: &QTensor, x: &[f32]| -> Vec<f32> {
1820                let mut o = vec![0.0f32; t.rows()];
1821                t.matvec(x, &mut o, None);
1822                o
1823            };
1824            let mut hist: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = Vec::new(); // raw xq/xk/xv
1825            let mut s_state = vec![0f64; nh * dk * dv];
1826            let mut want: Vec<Vec<f32>> = Vec::new();
1827            for x in &xs {
1828                let (xq, xk, xv) = (mv(&w.q_proj, x), mv(&w.k_proj, x), mv(&w.v_proj, x));
1829                hist.push((xq, xk, xv));
1830                // conv over the raw history, taps oldest→newest.
1831                let conv = |sel: fn(&(Vec<f32>, Vec<f32>, Vec<f32>)) -> &Vec<f32>,
1832                            taps: &[f32],
1833                            n: usize|
1834                 -> Vec<f32> {
1835                    (0..n)
1836                        .map(|c| {
1837                            let t = &taps[c * kk..(c + 1) * kk];
1838                            let mut acc = 0f64;
1839                            for j in 0..kk {
1840                                let idx = hist.len() as i64 - (kk as i64 - j as i64);
1841                                if idx >= 0 {
1842                                    acc += sel(&hist[idx as usize])[c] as f64 * t[j] as f64;
1843                                }
1844                            }
1845                            silu(acc)
1846                        })
1847                        .map(|v| v as f32)
1848                        .collect()
1849                };
1850                let cq = conv(|h| &h.0, &w.conv_q, nh * dk);
1851                let ck = conv(|h| &h.1, &w.conv_k, nh * dk);
1852                let cv = conv(|h| &h.2, &w.conv_v, nh * dv);
1853                let f = mv(&w.f_b, &mv(&w.f_a, x));
1854                let bb = mv(&w.b_proj, x);
1855                let gate_out = match &w.gate {
1856                    KdaOutGate::LowRank(ga, gb) => mv(gb, &mv(ga, x)),
1857                    KdaOutGate::Full(g) => mv(g, x),
1858                };
1859                let mut of = vec![0f32; nh * dv];
1860                for h in 0..nh {
1861                    // l2norm + scale.
1862                    let q: Vec<f64> = {
1863                        let sl = &cq[h * dk..(h + 1) * dk];
1864                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
1865                        let inv = 1.0 / ((n + 1e-6).sqrt() * (dk as f64).sqrt());
1866                        sl.iter().map(|&v| v as f64 * inv).collect()
1867                    };
1868                    let k: Vec<f64> = {
1869                        let sl = &ck[h * dk..(h + 1) * dk];
1870                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
1871                        let inv = 1.0 / (n + 1e-6).sqrt();
1872                        sl.iter().map(|&v| v as f64 * inv).collect()
1873                    };
1874                    let v: Vec<f64> = cv[h * dv..(h + 1) * dv].iter().map(|&v| v as f64).collect();
1875                    // gate: g = −exp(A)·softplus(f+bias) | lb·σ(exp(A)·(f+bias))
1876                    let g: Vec<f64> = (0..dk)
1877                        .map(|d| {
1878                            let a = if w.a_log.len() == nh {
1879                                w.a_log[h] as f64
1880                            } else {
1881                                w.a_log[d] as f64
1882                            };
1883                            let raw = f[h * dk + d] as f64 + w.dt_bias[h * dk + d] as f64;
1884                            match w.gate_lower_bound {
1885                                Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1886                                None => -a.exp() * softplus(raw),
1887                            }
1888                        })
1889                        .collect();
1890                    let beta = sigmoid(bb[h] as f64);
1891                    let s = &mut s_state[h * dk * dv..(h + 1) * dk * dv];
1892                    // S = Diag(exp(g))·S
1893                    for di in 0..dk {
1894                        for dj in 0..dv {
1895                            s[di * dv + dj] *= g[di].exp();
1896                        }
1897                    }
1898                    // kv = kᵀS; S += β·k⊗(v−kv); o = qᵀS
1899                    let mut kv = vec![0f64; dv];
1900                    for di in 0..dk {
1901                        for dj in 0..dv {
1902                            kv[dj] += k[di] * s[di * dv + dj];
1903                        }
1904                    }
1905                    for di in 0..dk {
1906                        for dj in 0..dv {
1907                            s[di * dv + dj] += beta * k[di] * (v[dj] - kv[dj]);
1908                        }
1909                    }
1910                    let mut o = vec![0f64; dv];
1911                    for di in 0..dk {
1912                        for dj in 0..dv {
1913                            o[dj] += q[di] * s[di * dv + dj];
1914                        }
1915                    }
1916                    // sigmoid-gated RMSNorm
1917                    let ss: f64 = o.iter().map(|&v| v * v).sum();
1918                    let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1919                    for dj in 0..dv {
1920                        of[h * dv + dj] = (o[dj]
1921                            * inv
1922                            * w.o_norm[dj] as f64
1923                            * sigmoid(gate_out[h * dv + dj] as f64))
1924                            as f32;
1925                    }
1926                }
1927                want.push(mv(&w.o_proj, &of));
1928            }
1929
1930            for (t, (g, e)) in got.iter().zip(&want).enumerate() {
1931                for (i, (a, b)) in g.iter().zip(e.iter()).enumerate() {
1932                    assert!((a - b).abs() < 2e-4, "{label}: t={t} i={i}: {a} vs {b}");
1933                }
1934            }
1935        }
1936
1937        // Batched prefill must equal the sequential singles bit-close.
1938        let w = KdaWeights {
1939            q_proj: synth(nh * dk, hs, 1),
1940            k_proj: synth(nh * dk, hs, 2),
1941            v_proj: synth(nh * dv, hs, 3),
1942            conv_q: vecf(nh * dk * kk, 4),
1943            conv_k: vecf(nh * dk * kk, 5),
1944            conv_v: vecf(nh * dv * kk, 6),
1945            f_a: synth(rank, hs, 7),
1946            f_b: synth(nh * dk, rank, 8),
1947            dt_bias: vecf(nh * dk, 9),
1948            a_log: vecf(nh, 40),
1949            b_proj: synth(nh, hs, 10),
1950            gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1951            o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1952            o_proj: synth(hs, nh * dv, 13),
1953            gate_lower_bound: None,
1954        };
1955        let cfg = KdaCfg {
1956            num_heads: nh,
1957            head_k_dim: dk,
1958            head_v_dim: dv,
1959            conv_kernel: kk,
1960            hidden_size: hs,
1961            rms_eps: 1e-6,
1962        };
1963        let xs: Vec<f32> = (0..5 * hs).map(|i| (i as f32 * 0.29).cos() * 0.4).collect();
1964        let mut st1 = Vec::new();
1965        let seq: Vec<f32> = (0..5)
1966            .flat_map(|t| kda_forward(&xs[t * hs..(t + 1) * hs], &w, &cfg, &mut st1, None))
1967            .collect();
1968        let mut st2 = Vec::new();
1969        let bat = kda_forward_batch(&xs, 5, &w, &cfg, &mut st2, None);
1970        for (i, (a, b)) in seq.iter().zip(&bat).enumerate() {
1971            assert!((a - b).abs() < 1e-5, "batch i={i}: {a} vs {b}");
1972        }
1973        assert_eq!(st1, st2, "state must match after the chunk");
1974    }
1975
1976    use super::*;
1977
1978    fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
1979        let cfg = VmfPhaseCfg {
1980            num_heads: 2,
1981            nphase: 3,
1982            value_head_dim: 4,
1983            hidden_size: 8,
1984            phase_mass: 0.0,
1985        };
1986        let synth = |rows: usize, cols: usize, salt: usize| {
1987            QTensor::from_f32(
1988                (0..rows * cols)
1989                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
1990                    .collect(),
1991                rows,
1992                cols,
1993            )
1994        };
1995        let w = VmfPhaseWeights {
1996            thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
1997            thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
1998            v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
1999            out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
2000            decay: (0..cfg.num_heads * 2 * cfg.nphase)
2001                .map(|i| 0.9 + 0.005 * (i % 10) as f64)
2002                .collect(),
2003            conv: None,
2004            k_gate: None,
2005            phase_delta: false,
2006        };
2007        (w, cfg)
2008    }
2009
2010    #[test]
2011    fn state_persists_and_changes_output() {
2012        let (w, cfg) = tiny();
2013        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2014        let mut state = Vec::new();
2015        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2016        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2017        // Same input, evolved state → different output.
2018        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
2019        assert_eq!(state.len(), cfg.state_len());
2020    }
2021
2022    #[test]
2023    fn phase_delta_matches_independent_token_oracle_and_ignores_phase_mass() {
2024        // One head, two phase angles, one value channel.  The projections are
2025        // intentionally simple so this test computes the token oracle without
2026        // calling any implementation helper: q=[x0,x1], k=[x1,x0], v=x0,
2027        // and the first output row is qᵀS.
2028        let cfg = VmfPhaseCfg {
2029            num_heads: 1,
2030            nphase: 2,
2031            value_head_dim: 1,
2032            hidden_size: 2,
2033            // A nonzero legacy knob must not alter a v1 operator.
2034            phase_mass: 17.0,
2035        };
2036        let w = VmfPhaseWeights {
2037            thq: QTensor::from_f32(vec![1.0, 0.0, 0.0, 1.0], 2, 2),
2038            thk: QTensor::from_f32(vec![0.0, 1.0, 1.0, 0.0], 2, 2),
2039            v_proj: QTensor::from_f32(vec![1.0, 0.0], 1, 2),
2040            out_proj: QTensor::from_f32(vec![1.0, 0.0], 2, 1),
2041            decay: vec![0.9, 0.8, 0.7, 0.6],
2042            conv: None,
2043            k_gate: None, // beta=1
2044            phase_delta: true,
2045        };
2046        let xs = [[0.3f32, 0.7f32], [-0.2, 0.4], [0.8, -0.6]];
2047        let c = 1.0f64 / 2.0f64.sqrt();
2048        let mut oracle_state = [0.0f64; 4];
2049        let mut state = Vec::new();
2050        for x in xs {
2051            let q = [
2052                c * (x[0] as f64).cos(),
2053                c * (x[1] as f64).cos(),
2054                c * (x[0] as f64).sin(),
2055                c * (x[1] as f64).sin(),
2056            ];
2057            let k = [
2058                c * (x[1] as f64).cos(),
2059                c * (x[0] as f64).cos(),
2060                c * (x[1] as f64).sin(),
2061                c * (x[0] as f64).sin(),
2062            ];
2063            let value = x[0] as f64;
2064            let mut read = 0.0;
2065            for f in 0..4 {
2066                oracle_state[f] *= w.decay[f];
2067                read += k[f] * oracle_state[f];
2068            }
2069            for f in 0..4 {
2070                oracle_state[f] += k[f] * (value - read);
2071            }
2072            let want: f32 = q
2073                .iter()
2074                .zip(oracle_state)
2075                .map(|(qf, sf)| (qf * sf) as f32)
2076                .sum();
2077            let got = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2078            assert!(
2079                (got[0] - want).abs() < 2e-6,
2080                "oracle {want} vs runtime {}",
2081                got[0]
2082            );
2083            assert!(got[1].abs() < 2e-7);
2084        }
2085        assert_eq!(state.len(), cfg.state_len());
2086        for (got, want) in state.iter().zip(oracle_state) {
2087            assert!((*got as f64 - want).abs() < 2e-6);
2088        }
2089    }
2090
2091    #[test]
2092    fn phase_delta_fused_readout_is_bit_exact_over_long_continuation() {
2093        // Independent three-pass reference, with nonzero state/output and a
2094        // sentinel convolution tail. Exercise odd widths and the real shape.
2095        for (nh, nph, dv) in [(1, 1, 1), (3, 5, 7), (8, 32, 128)] {
2096            let cfg = VmfPhaseCfg {
2097                num_heads: nh,
2098                nphase: nph,
2099                value_head_dim: dv,
2100                hidden_size: nh * dv,
2101                phase_mass: 19.0,
2102            };
2103            let p2 = 2 * nph;
2104            let scale = 1.0f64 / (nph as f64).sqrt();
2105            let mut state: Vec<f32> = (0..cfg.state_len() + 11)
2106                .map(|i| (i as f32 * 0.37).sin())
2107                .collect();
2108            let mut reference = state.clone();
2109            let tail = state[cfg.state_len()..].to_vec();
2110            let decay: Vec<f64> = (0..nh * p2).map(|i| [0.0, 0.8, 0.99, 1.0][i % 4]).collect();
2111            for step in 0..128 {
2112                let q: Vec<f32> = (0..nh * nph)
2113                    .map(|i| ((i + step * 3) as f32 * 0.13).sin() * 4.0)
2114                    .collect();
2115                let k: Vec<f32> = (0..nh * nph)
2116                    .map(|i| ((i + step * 7) as f32 * 0.29).cos() * 4.0)
2117                    .collect();
2118                let v: Vec<f32> = (0..nh * dv)
2119                    .map(|i| ((i + step) as f32 * 0.43).sin())
2120                    .collect();
2121                let gates: Vec<f32> = (0..nh).map(|h| [0.0, 0.3, 1.0][(step + h) % 3]).collect();
2122                let kap = (step % 4 != 0).then_some(gates.as_slice());
2123                let mut out = vec![0.125f32; nh * dv];
2124                let mut want = out.clone();
2125                for h in 0..nh {
2126                    let feature = |theta: &[f32], f: usize| {
2127                        let angle = theta[h * nph + f % nph] as f64;
2128                        scale * if f < nph { angle.cos() } else { angle.sin() }
2129                    };
2130                    let beta = kap.map_or(1.0f64, |g| g[h] as f64);
2131                    let mut read = vec![0.0f64; dv];
2132                    for f in 0..p2 {
2133                        for d in 0..dv {
2134                            let at = (h * p2 + f) * dv + d;
2135                            read[d] += feature(&k, f) * (decay[h * p2 + f] * reference[at] as f64);
2136                        }
2137                    }
2138                    for f in 0..p2 {
2139                        for d in 0..dv {
2140                            let at = (h * p2 + f) * dv + d;
2141                            reference[at] = (decay[h * p2 + f] * reference[at] as f64
2142                                + beta * feature(&k, f) * (v[h * dv + d] as f64 - read[d]))
2143                                as f32;
2144                        }
2145                    }
2146                    for f in 0..p2 {
2147                        for d in 0..dv {
2148                            want[h * dv + d] +=
2149                                (feature(&q, f) * reference[(h * p2 + f) * dv + d] as f64) as f32;
2150                        }
2151                    }
2152                }
2153                phase_delta_step_f32(&q, &k, &v, &decay, kap, &cfg, &mut state, &mut out);
2154                assert_eq!(out, want, "shape {nh}/{nph}/{dv}, step {step}");
2155                assert_eq!(state, reference, "state at step {step}");
2156                assert_eq!(&state[cfg.state_len()..], tail);
2157            }
2158        }
2159    }
2160
2161    #[test]
2162    fn phase_delta_pair_reset_and_legacy_paths_are_distinct() {
2163        let (mut delta, mut cfg) = tiny();
2164        delta.phase_delta = true;
2165        // The phase normalization is observable for nphase=3, while the
2166        // legacy core remains unchanged and still uses raw cos/sin features.
2167        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2168        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2169        let mut seq_state = Vec::new();
2170        let d1 = vmf_phase_forward(&x1, &delta, &cfg, &mut seq_state, None);
2171        let d2 = vmf_phase_forward(&x2, &delta, &cfg, &mut seq_state, None);
2172
2173        let mut pair_state = Vec::new();
2174        let mut scratch = Vec::new();
2175        let (p1, p2) = vmf_phase_pair(&x1, &x2, &delta, &cfg, &mut pair_state, &mut scratch, None);
2176        assert_eq!(d1, p1);
2177        assert_eq!(d2, p2);
2178        std::mem::swap(&mut pair_state, &mut scratch);
2179        assert_eq!(seq_state, pair_state);
2180
2181        // Empty state is the runtime reset contract; it reproduces the first
2182        // token exactly and does not inherit the previous sequence.
2183        let mut reset = seq_state;
2184        reset.clear();
2185        let after_reset = vmf_phase_forward(&x1, &delta, &cfg, &mut reset, None);
2186        assert_eq!(after_reset, d1);
2187
2188        cfg.phase_mass = 0.0;
2189        let mut legacy = delta;
2190        legacy.phase_delta = false;
2191        let mut legacy_state = Vec::new();
2192        let old = vmf_phase_forward(&x1, &legacy, &cfg, &mut legacy_state, None);
2193        assert!(
2194            old.iter().zip(&d1).any(|(a, b)| (a - b).abs() > 1e-5),
2195            "legacy additive and normalized Phase-Delta paths must differ"
2196        );
2197    }
2198
2199    /// Phase-mass correction: mass=0 is bit-identical to the unscaled kernel; mass>0
2200    /// changes the output (phase narrowed → kernel widened). Guards the
2201    /// no-op default and that the knob is actually wired.
2202    #[test]
2203    fn phase_mass_zero_is_noop_and_positive_shifts() {
2204        let (w, cfg0) = tiny();
2205        let mut cfg_m = cfg0;
2206        cfg_m.phase_mass = 1.0;
2207        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
2208
2209        let mut s0 = Vec::new();
2210        let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
2211        // Re-run with mass=0 → must be bit-identical.
2212        let mut s0b = Vec::new();
2213        let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
2214        assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
2215        // mass=1 → output differs (θ halved before cos/sin).
2216        let mut sm = Vec::new();
2217        let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
2218        assert!(
2219            base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
2220            "mass>0 must change the output"
2221        );
2222        assert!(massed.iter().all(|v| v.is_finite()));
2223    }
2224
2225    /// κ write gate (hybrid_k): saturated-open gate (bias ≫ 0 → κ→1)
2226    /// matches the gateless kernel within fp tolerance; a closed gate
2227    /// (bias ≪ 0 → κ→0) writes nothing — the state stays zero and the
2228    /// output collapses to the empty-state readout.
2229    #[test]
2230    fn kappa_gate_open_matches_none_and_closed_writes_nothing() {
2231        let (mut w, cfg) = tiny();
2232        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2233
2234        let mut s_none = Vec::new();
2235        let base1 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
2236        let base2 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
2237
2238        // Open gate: W=0, bias=+20 → κ = σ(20) ≈ 1 − 2e−9.
2239        w.k_gate = Some((
2240            QTensor::from_f32(
2241                vec![0.0; cfg.num_heads * cfg.hidden_size],
2242                cfg.num_heads,
2243                cfg.hidden_size,
2244            ),
2245            vec![20.0; cfg.num_heads],
2246        ));
2247        let mut s_open = Vec::new();
2248        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
2249        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
2250        for (a, b) in base1.iter().zip(&o1).chain(base2.iter().zip(&o2)) {
2251            assert!(
2252                (a - b).abs() < 1e-5,
2253                "open κ must match gateless: {a} vs {b}"
2254            );
2255        }
2256
2257        // Closed gate: bias=−20 → κ ≈ 0 → nothing is written.
2258        w.k_gate = Some((
2259            QTensor::from_f32(
2260                vec![0.0; cfg.num_heads * cfg.hidden_size],
2261                cfg.num_heads,
2262                cfg.hidden_size,
2263            ),
2264            vec![-20.0; cfg.num_heads],
2265        ));
2266        let mut s_closed = Vec::new();
2267        let oc = vmf_phase_forward(&x, &w, &cfg, &mut s_closed, None);
2268        assert!(
2269            s_closed.iter().all(|&v| v.abs() < 1e-7),
2270            "closed κ: state must stay empty"
2271        );
2272        assert!(
2273            oc.iter().all(|&v| v.abs() < 1e-6),
2274            "closed κ: empty-state readout"
2275        );
2276    }
2277
2278    #[test]
2279    fn pair_matches_two_singles_bitexact() {
2280        let (w, cfg) = tiny();
2281        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2282        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2283
2284        // Reference: two sequential singles.
2285        let mut s_ref = Vec::new();
2286        let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
2287        let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
2288
2289        // Pair: lane1 commits, lane2 tentative in scratch.
2290        let mut s = Vec::new();
2291        let mut scratch = Vec::new();
2292        let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2293        assert_eq!(r1, p1, "lane 1 must be bit-identical");
2294        assert_eq!(r2, p2, "lane 2 must be bit-identical");
2295        // Accepting the draft = swapping scratch in → equals s_ref.
2296        std::mem::swap(&mut s, &mut scratch);
2297        assert_eq!(s, s_ref, "accepted state must equal sequential state");
2298    }
2299
2300    #[test]
2301    fn rejected_draft_leaves_state_at_lane1() {
2302        let (w, cfg) = tiny();
2303        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
2304        let x2 = vec![0.5f32; 8];
2305
2306        let mut s_ref = Vec::new();
2307        let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
2308
2309        let mut s = Vec::new();
2310        let mut scratch = Vec::new();
2311        let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2312        // Reject: state must be exactly the post-lane1 state.
2313        assert_eq!(s, s_ref);
2314    }
2315
2316    // ───────────── GatedDeltaNet ─────────────
2317
2318    fn tiny_gdn() -> (GdnWeights, GdnCfg) {
2319        let cfg = GdnCfg {
2320            num_v_heads: 4,
2321            num_k_heads: 2,
2322            key_head_dim: 3,
2323            value_head_dim: 5,
2324            conv_kernel: 4,
2325            hidden_size: 8,
2326            rms_eps: 1e-6,
2327            output_gate_sigmoid: false,
2328        };
2329        let c_dim = cfg.conv_dim();
2330        let vd = cfg.num_v_heads * cfg.value_head_dim;
2331        let synth = |rows: usize, cols: usize, salt: usize| {
2332            QTensor::from_f32(
2333                (0..rows * cols)
2334                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
2335                    .collect(),
2336                rows,
2337                cols,
2338            )
2339        };
2340        let vecf = |n: usize, salt: usize| -> Vec<f32> {
2341            (0..n)
2342                .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
2343                .collect()
2344        };
2345        let w = GdnWeights {
2346            in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
2347            in_proj_z: synth(vd, cfg.hidden_size, 2),
2348            in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
2349            in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
2350            conv1d: vecf(c_dim * cfg.conv_kernel, 5),
2351            a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
2352            dt_bias: vecf(cfg.num_v_heads, 6),
2353            norm: vec![1.0; cfg.value_head_dim],
2354            out_proj: synth(cfg.hidden_size, vd, 7),
2355        };
2356        (w, cfg)
2357    }
2358
2359    /// The chunked scan equals `gdn_step` position by position, outputs and
2360    /// state bit for bit — with a warm ring, a chunk shorter than the ring,
2361    /// and both gate forms.
2362    #[test]
2363    fn gdn_scan_matches_steps_bitexact() {
2364        for sigmoid_gate in [false, true] {
2365            let (w, mut cfg) = tiny_gdn();
2366            cfg.output_gate_sigmoid = sigmoid_gate;
2367            let (c_dim, vd, nv) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim, cfg.num_v_heads);
2368            let row = |n: usize, t: usize, salt: usize| -> Vec<f32> {
2369                (0..n).map(|i| (((i * 7 + t * 31 + salt * 3) % 83) as f32 / 83.0 - 0.5) * 1.3).collect()
2370            };
2371            let mut s_ref = vec![0f32; cfg.state_len()];
2372            let mut s_scan = s_ref.clone();
2373            for (chunk, b) in [(0usize, 2usize), (1, 7), (2, 1), (3, 5)] {
2374                let qkv: Vec<f32> = (0..b).flat_map(|t| row(c_dim, t + 10 * chunk, 1)).collect();
2375                let z: Vec<f32> = (0..b).flat_map(|t| row(vd, t + 10 * chunk, 2)).collect();
2376                let a: Vec<f32> = (0..b).flat_map(|t| row(nv, t + 10 * chunk, 3)).collect();
2377                let bb: Vec<f32> = (0..b).flat_map(|t| row(nv, t + 10 * chunk, 4)).collect();
2378                let mut of_ref = vec![0f32; b * vd];
2379                for t in 0..b {
2380                    gdn_step(
2381                        &qkv[t * c_dim..(t + 1) * c_dim],
2382                        &z[t * vd..(t + 1) * vd],
2383                        &a[t * nv..(t + 1) * nv],
2384                        &bb[t * nv..(t + 1) * nv],
2385                        &w,
2386                        &cfg,
2387                        &mut s_ref,
2388                        &mut of_ref[t * vd..(t + 1) * vd],
2389                        None,
2390                    );
2391                }
2392                let mut of_scan = vec![0f32; b * vd];
2393                gdn_scan_batch(&qkv, &z, &a, &bb, b, &w, &cfg, &mut s_scan, &mut of_scan, None);
2394                assert_eq!(of_scan, of_ref, "outputs, chunk {chunk} (b = {b})");
2395                assert_eq!(s_scan, s_ref, "state, chunk {chunk} (b = {b})");
2396            }
2397        }
2398    }
2399
2400    #[test]
2401    fn gdn_state_persists_and_changes_output() {
2402        let (w, cfg) = tiny_gdn();
2403        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2404        let mut state = Vec::new();
2405        let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
2406        let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
2407        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
2408        assert_eq!(state.len(), cfg.state_len());
2409    }
2410
2411    #[test]
2412    fn gdn_pair_matches_two_singles_bitexact() {
2413        let (w, cfg) = tiny_gdn();
2414        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2415        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2416
2417        let mut s_ref = Vec::new();
2418        let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
2419        let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
2420
2421        let mut s = Vec::new();
2422        let mut scratch = Vec::new();
2423        let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2424        assert_eq!(r1, p1, "lane 1 must be bit-identical");
2425        assert_eq!(r2, p2, "lane 2 must be bit-identical");
2426        std::mem::swap(&mut s, &mut scratch);
2427        assert_eq!(s, s_ref, "accepted state must equal sequential state");
2428    }
2429
2430    #[test]
2431    fn gdn_rejected_draft_leaves_state_at_lane1() {
2432        let (w, cfg) = tiny_gdn();
2433        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
2434        let x2 = vec![0.5f32; 8];
2435
2436        let mut s_ref = Vec::new();
2437        let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
2438
2439        let mut s = Vec::new();
2440        let mut scratch = Vec::new();
2441        let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2442        assert_eq!(s, s_ref);
2443    }
2444
2445    /// The conv ring must give the same result as an explicit causal
2446    /// conv over the whole sequence (oracle semantics: zero left-pad,
2447    /// tap kk−1 on the current position).
2448    #[test]
2449    fn gdn_conv_ring_matches_explicit_causal_conv() {
2450        let (w, cfg) = tiny_gdn();
2451        let seq: Vec<Vec<f32>> = (0..6)
2452            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
2453            .collect();
2454
2455        // Reference: recompute position t from scratch each time with a
2456        // fresh state built by replaying the prefix.
2457        let mut s_inc = Vec::new();
2458        for (t, x) in seq.iter().enumerate() {
2459            let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
2460            let mut s_replay = Vec::new();
2461            let mut replay = Vec::new();
2462            for xr in &seq[..=t] {
2463                replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
2464            }
2465            assert_eq!(inc, replay, "position {t}: ring must equal replay");
2466        }
2467    }
2468
2469    fn tiny_short_conv() -> (ShortConvWeights, ShortConvCfg) {
2470        let cfg = ShortConvCfg {
2471            hidden_size: 8,
2472            kernel: 3,
2473        };
2474        let synth = |rows: usize, cols: usize, salt: usize| {
2475            QTensor::from_f32(
2476                (0..rows * cols)
2477                    .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.5)
2478                    .collect(),
2479                rows,
2480                cols,
2481            )
2482        };
2483        let w = ShortConvWeights {
2484            in_proj: synth(3 * cfg.hidden_size, cfg.hidden_size, 1),
2485            conv: (0..cfg.hidden_size * cfg.kernel)
2486                .map(|i| ((i * 7 % 13) as f32 / 13.0 - 0.5) * 0.8)
2487                .collect(),
2488            out_proj: synth(cfg.hidden_size, cfg.hidden_size, 2),
2489        };
2490        (w, cfg)
2491    }
2492
2493    /// The incremental conv ring must equal a from-scratch causal replay
2494    /// of the prefix at every position — the decode/prefill contract.
2495    #[test]
2496    fn short_conv_ring_matches_explicit_causal_conv() {
2497        let (w, cfg) = tiny_short_conv();
2498        let seq: Vec<Vec<f32>> = (0..6)
2499            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.19).cos()).collect())
2500            .collect();
2501        let mut s_inc = Vec::new();
2502        for (t, x) in seq.iter().enumerate() {
2503            let inc = short_conv_forward(x, &w, &cfg, &mut s_inc, None);
2504            let mut s_replay = Vec::new();
2505            let mut replay = Vec::new();
2506            for xr in &seq[..=t] {
2507                replay = short_conv_forward(xr, &w, &cfg, &mut s_replay, None);
2508            }
2509            assert_eq!(inc, replay, "position {t}: ring must equal replay");
2510            assert_eq!(s_inc.len(), cfg.state_len());
2511        }
2512    }
2513
2514    /// The batched prefill path (matmat + sequential conv over the chunk)
2515    /// must reproduce the position-by-position decode path exactly.
2516    #[test]
2517    fn short_conv_batch_matches_sequential() {
2518        let (w, cfg) = tiny_short_conv();
2519        let b = 5;
2520        let xs: Vec<f32> = (0..b * cfg.hidden_size)
2521            .map(|i| (i as f32 * 0.13).sin() * 0.6)
2522            .collect();
2523
2524        let mut s_seq = Vec::new();
2525        let mut seq_out = vec![0.0f32; b * cfg.hidden_size];
2526        for bi in 0..b {
2527            let o = short_conv_forward(
2528                &xs[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size],
2529                &w,
2530                &cfg,
2531                &mut s_seq,
2532                None,
2533            );
2534            seq_out[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size].copy_from_slice(&o);
2535        }
2536
2537        let mut s_batch = Vec::new();
2538        let batch_out = short_conv_forward_batch(&xs, b, &w, &cfg, &mut s_batch, None);
2539        assert_eq!(
2540            seq_out, batch_out,
2541            "batch conv must match sequential decode"
2542        );
2543        assert_eq!(s_seq, s_batch, "ring state must match after the chunk");
2544    }
2545}