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/// The block-diffusion (Clover) preview weights of one GDN layer.
992pub struct GdnPreviewWeights {
993    /// [c_dim · (kk−1)] — right taps, flattened [c][distance−1]: tap d
994    /// reads the block row d positions AFTER the output row.
995    pub conv_right: Vec<f32>,
996    /// [nv] — per-head scale of the unscaled q̂·k̂ block logits.
997    pub logit_scale: Vec<f32>,
998    /// [nv] — the history slot's logit at a full 32-row block…
999    pub history_bias: Vec<f32>,
1000    /// [nv] — …and its slope in ln(valid rows / 32).
1001    pub history_c: Vec<f32>,
1002}
1003
1004/// Clover's preview of a GDN layer over one block, the committed state
1005/// read-only (`bd_gdn_conv1d` + `bd_gdn_local_memory` of the release):
1006///
1007/// * conv: the causal taps over [ring | block] plus right taps over the
1008///   next kk−1 rows OF THE BLOCK, then SiLU;
1009/// * mixer: per value head a softmax over the block's keys (logits
1010///   (q̂·k̂)·logit_scale, no β) and one history slot whose value is the
1011///   clean state's readout (q̂/√dk)·S decayed by this row's g — no write;
1012/// * the gated RMSNorm and out_proj of the causal path.
1013///
1014/// Every row of the block is valid (a preview always runs whole blocks).
1015#[allow(clippy::too_many_arguments)]
1016pub fn gdn_preview_batch(
1017    xs: &[f32],
1018    b: usize,
1019    w: &GdnWeights,
1020    pw: &GdnPreviewWeights,
1021    cfg: &GdnCfg,
1022    state: &[f32],
1023    pool: Option<&Pool>,
1024) -> Vec<f32> {
1025    let (nv, nk, dk, dv, kk) = (
1026        cfg.num_v_heads,
1027        cfg.num_k_heads,
1028        cfg.key_head_dim,
1029        cfg.value_head_dim,
1030        cfg.conv_kernel,
1031    );
1032    let (c_dim, vd) = (cfg.conv_dim(), nv * dv);
1033    let (kd, rep) = (nk * dk, nv / nk);
1034    let mut qkv = vec![0.0f32; b * c_dim];
1035    w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
1036    let mut z = vec![0.0f32; b * vd];
1037    w.in_proj_z.matmat(xs, b, &mut z, pool);
1038    let mut a = vec![0.0f32; b * nv];
1039    w.in_proj_a.matmat(xs, b, &mut a, pool);
1040
1041    // A fresh layer (no committed position yet) reads a zero state.
1042    let zeros;
1043    let state = if state.len() == cfg.state_len() {
1044        state
1045    } else {
1046        zeros = vec![0f32; cfg.state_len()];
1047        &zeros
1048    };
1049    let (ring, s_all) = state.split_at((kk - 1) * c_dim);
1050
1051    // conv: out[t] = Σ_j in[t + j]·taps[j] over in = [ring | block] (tap
1052    // kk−1 is row t itself) + Σ_d block[t + d]·right[d − 1], then SiLU.
1053    // Channels are independent: the pool takes channel ranges.
1054    let mut cq = vec![0.0f32; b * c_dim];
1055    let cq_ptr = SendMutF32(cq.as_mut_ptr());
1056    let qkv_r = &qkv;
1057    let conv = |c0: usize, c1: usize| {
1058        let cq_ptr = cq_ptr;
1059        for c in c0..c1 {
1060            let taps = &w.conv1d[c * kk..(c + 1) * kk];
1061            for t in 0..b {
1062                let mut acc = 0f64;
1063                for (j, &tap) in taps.iter().enumerate() {
1064                    let i = t + j; // index into [ring | block]
1065                    let x = if i < kk - 1 {
1066                        ring[i * c_dim + c]
1067                    } else {
1068                        qkv_r[(i - (kk - 1)) * c_dim + c]
1069                    };
1070                    acc += x as f64 * tap as f64;
1071                }
1072                for d in 1..kk.min(b - t) {
1073                    acc += qkv_r[(t + d) * c_dim + c] as f64
1074                        * pw.conv_right[c * (kk - 1) + d - 1] as f64;
1075                }
1076                // SAFETY: channel c is written by this worker only.
1077                unsafe { *cq_ptr.0.add(t * c_dim + c) = silu(acc) as f32 };
1078            }
1079        }
1080    };
1081    match pool {
1082        Some(p) => p.run_rows(c_dim, &conv),
1083        None => conv(0, c_dim),
1084    }
1085
1086    // l2-normalized q̂, k̂ per row and key head (the readout adds 1/√dk).
1087    let mut qn = vec![0.0f32; b * kd];
1088    let mut kn = vec![0.0f32; b * kd];
1089    for t in 0..b {
1090        for ko in 0..nk {
1091            for (dst, off) in [(&mut qn, 0), (&mut kn, kd)] {
1092                let src = &cq[t * c_dim + off + ko * dk..t * c_dim + off + (ko + 1) * dk];
1093                let ss: f64 = src.iter().map(|&x| x as f64 * x as f64).sum();
1094                let inv = (1.0 / (ss + 1e-6).sqrt()) as f32;
1095                for d in 0..dk {
1096                    dst[t * kd + ko * dk + d] = src[d] * inv;
1097                }
1098            }
1099        }
1100    }
1101    // The history slot's logit: valid rows = b, against the trained 32.
1102    let len_log = (b as f64 / 32.0).ln();
1103    let q_scale = 1.0 / (dk as f64).sqrt();
1104
1105    let mut of = vec![0.0f32; b * vd];
1106    let of_ptr = SendMutF32(of.as_mut_ptr());
1107    let (cq, qn, kn, z, a) = (&cq, &qn, &kn, &z, &a);
1108    let heads = |h0: usize, h1: usize| {
1109        let of_ptr = of_ptr;
1110        let mut logits = vec![0f64; b + 1];
1111        let mut o = vec![0f64; dv];
1112        for h in h0..h1 {
1113            let ko = h / rep;
1114            let s = &s_all[h * dk * dv..(h + 1) * dk * dv];
1115            let hist_logit = pw.history_bias[h] as f64 + pw.history_c[h] as f64 * len_log;
1116            let scale = pw.logit_scale[h] as f64;
1117            for t in 0..b {
1118                let q = &qn[t * kd + ko * dk..t * kd + (ko + 1) * dk];
1119                for (sx, lg) in logits[..b].iter_mut().enumerate() {
1120                    let k = &kn[sx * kd + ko * dk..sx * kd + (ko + 1) * dk];
1121                    let dot: f64 = q.iter().zip(k).map(|(&x, &y)| x as f64 * y as f64).sum();
1122                    *lg = dot * scale;
1123                }
1124                logits[b] = hist_logit;
1125                let mx = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1126                let mut sum = 0f64;
1127                for lg in logits.iter_mut() {
1128                    *lg = (*lg - mx).exp();
1129                    sum += *lg;
1130                }
1131                // History value: (q̂/√dk)ᵀS, decayed by this row's gate.
1132                let g = -(w.a_log[h] as f64).exp() * softplus(a[t * nv + h] as f64 + w.dt_bias[h] as f64);
1133                let w_hist = logits[b] / sum * g.exp() * q_scale;
1134                o.fill(0.0);
1135                for di in 0..dk {
1136                    let qd = q[di] as f64 * w_hist;
1137                    let row = &s[di * dv..(di + 1) * dv];
1138                    for dj in 0..dv {
1139                        o[dj] += qd * row[dj] as f64;
1140                    }
1141                }
1142                for sx in 0..b {
1143                    let ws = logits[sx] / sum;
1144                    let v = &cq[sx * c_dim + 2 * kd + h * dv..sx * c_dim + 2 * kd + (h + 1) * dv];
1145                    for dj in 0..dv {
1146                        o[dj] += ws * v[dj] as f64;
1147                    }
1148                }
1149                // Gated RMSNorm per head, as in `gdn_step`.
1150                let ss: f64 = o.iter().map(|&v| v * v).sum();
1151                let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1152                // SAFETY: head h of row t is written by this worker only.
1153                let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(t * vd + h * dv), dv) };
1154                for dj in 0..dv {
1155                    let zz = z[t * vd + h * dv + dj] as f64;
1156                    let gate = if cfg.output_gate_sigmoid { sigmoid(zz) } else { silu(zz) };
1157                    oh[dj] = (o[dj] * inv * w.norm[dj] as f64 * gate) as f32;
1158                }
1159            }
1160        }
1161    };
1162    match pool {
1163        Some(p) => p.run_rows(nv, &heads),
1164        None => heads(0, nv),
1165    }
1166    let mut out = vec![0.0f32; b * cfg.hidden_size];
1167    w.out_proj.matmat(&of, b, &mut out, pool);
1168    out
1169}
1170
1171/// Aggregate host-side GDN costs for the bounded warm-decode profile.
1172pub fn perf_report() {
1173    if !perf_enabled() {
1174        return;
1175    }
1176    let fc = PERF_GDN_FORWARD_CALLS.load(Ordering::Relaxed);
1177    let bc = PERF_GDN_BATCH_CALLS.load(Ordering::Relaxed);
1178    let sc = PERF_GDN_STEP_CALLS.load(Ordering::Relaxed);
1179    eprintln!(
1180        "[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}",
1181        fc,
1182        PERF_GDN_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6,
1183        PERF_GDN_FORWARD_NS.load(Ordering::Relaxed) as f64 / 1e6 / fc.max(1) as f64,
1184        bc,
1185        PERF_GDN_BATCH_NS.load(Ordering::Relaxed) as f64 / 1e6,
1186        sc,
1187        PERF_GDN_STEP_NS.load(Ordering::Relaxed) as f64 / 1e6,
1188        PERF_GDN_STEP_NS.load(Ordering::Relaxed) as f64 / 1e6 / sc.max(1) as f64,
1189    );
1190}
1191
1192/// GDN qkv+z GPU eligibility: q1 mixers offload by default (the CPU q1
1193/// kernel is compute-bound); q8 stays opt-in via CMF_GPU_GDN=1 (measured
1194/// neutral). The probe in `gdn_forward` still arbitrates either way.
1195fn gdn_projs_eligible(w: &GdnWeights) -> bool {
1196    // Any tile-embedded-scale layout, not just q1: the batched matvec now has
1197    // kernels for all of them, and the probe arbitrates whether it pays.
1198    // `CMF_GPU_GDN=0` forces the CPU projections back — the switch exists so
1199    // the two arms can be compared inside ONE run: this machine throttles far
1200    // enough that measurements minutes apart are not comparable.
1201    if std::env::var("CMF_GPU_GDN")
1202        .map(|v| v == "0")
1203        .unwrap_or(false)
1204    {
1205        return false;
1206    }
1207    w.in_proj_qkv.is_q1()
1208        || w.in_proj_qkv.q4t_parts().is_some()
1209        || w.in_proj_qkv.q4tp_parts().is_some()
1210        || std::env::var("CMF_GPU_GDN")
1211            .map(|v| v == "1")
1212            .unwrap_or(false)
1213}
1214
1215/// GDN qkv+z on GPU in a single submission (independent matvecs of one input).
1216fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
1217    use crate::gpu::matvec_batch;
1218    use crate::qtensor::QTensor;
1219    if !crate::gpu::enabled_here() {
1220        return false;
1221    }
1222    fn part<'a>(
1223        t: &'a QTensor,
1224        x: &[f32],
1225    ) -> Option<(
1226        std::sync::Arc<cortiq_core::CmfModel>,
1227        crate::gpu::BatchJob<'a>,
1228    )> {
1229        use crate::gpu::BatchJob;
1230        use crate::qtensor::prescale;
1231        use cortiq_core::TensorDtype;
1232        match t {
1233            QTensor::Mapped {
1234                model,
1235                idx,
1236                dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
1237                rows,
1238                cols,
1239                row_scale,
1240                col_field,
1241                ..
1242            } => Some((
1243                model.clone(),
1244                BatchJob {
1245                    idx: *idx,
1246                    rows: *rows,
1247                    cols: *cols,
1248                    row_scale,
1249                    xs: prescale(x, col_field, *dt).into_owned(),
1250                    layout: crate::gpu::BatchLayout::Q8,
1251                },
1252            )),
1253            QTensor::Mapped {
1254                model,
1255                idx,
1256                dtype: TensorDtype::Q1,
1257                rows,
1258                cols,
1259                ..
1260            } => Some((
1261                model.clone(),
1262                BatchJob {
1263                    idx: *idx,
1264                    rows: *rows,
1265                    cols: *cols,
1266                    row_scale: &[],
1267                    xs: x.to_vec(),
1268                    layout: crate::gpu::BatchLayout::Q1,
1269                },
1270            )),
1271            QTensor::Mapped {
1272                model,
1273                idx,
1274                dtype: TensorDtype::Q1G,
1275                rows,
1276                cols,
1277                ..
1278            } => Some((
1279                model.clone(),
1280                BatchJob {
1281                    idx: *idx,
1282                    rows: *rows,
1283                    cols: *cols,
1284                    row_scale: &[],
1285                    xs: x.to_vec(),
1286                    layout: crate::gpu::BatchLayout::Q1g,
1287                },
1288            )),
1289            // q4t/q4tp: same tile-embedded-scale contract as q1, different
1290            // stride. Missing here, a hybrid model's GDN projections fell to
1291            // the CPU on every layer while its experts rode the device.
1292            QTensor::Mapped {
1293                model,
1294                idx,
1295                dtype: dt @ (TensorDtype::Q4Tiled | TensorDtype::Q4TiledP),
1296                rows,
1297                cols,
1298                ..
1299            } => Some((
1300                model.clone(),
1301                BatchJob {
1302                    idx: *idx,
1303                    rows: *rows,
1304                    cols: *cols,
1305                    row_scale: &[],
1306                    xs: x.to_vec(),
1307                    layout: if *dt == TensorDtype::Q4TiledP {
1308                        crate::gpu::BatchLayout::Q4tp
1309                    } else {
1310                        crate::gpu::BatchLayout::Q4t
1311                    },
1312                },
1313            )),
1314            _ => None,
1315        }
1316    }
1317    let Some((model, jq)) = part(&w.in_proj_qkv, x) else {
1318        return false;
1319    };
1320    let Some((_, jz)) = part(&w.in_proj_z, x) else {
1321        return false;
1322    };
1323    matvec_batch(&model, &[jq, jz], &mut [qkv, z])
1324}
1325
1326/// Fused two-position forward (speculative verify): lane 1 commits into
1327/// `state`, lane 2 is tentative in `scratch` (ring + S move together).
1328#[allow(clippy::too_many_arguments)]
1329pub fn gdn_pair(
1330    x1: &[f32],
1331    x2: &[f32],
1332    w: &GdnWeights,
1333    cfg: &GdnCfg,
1334    state: &mut Vec<f32>,
1335    scratch: &mut Vec<f32>,
1336    pool: Option<&Pool>,
1337) -> (Vec<f32>, Vec<f32>) {
1338    if state.len() != cfg.state_len() {
1339        *state = vec![0f32; cfg.state_len()];
1340    }
1341    let (c_dim, vd, nv) = (
1342        cfg.conv_dim(),
1343        cfg.num_v_heads * cfg.value_head_dim,
1344        cfg.num_v_heads,
1345    );
1346
1347    let mut qkv1 = vec![0.0f32; c_dim];
1348    let mut qkv2 = vec![0.0f32; c_dim];
1349    w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
1350    let mut z1 = vec![0.0f32; vd];
1351    let mut z2 = vec![0.0f32; vd];
1352    w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
1353    let mut a1 = vec![0.0f32; nv];
1354    let mut a2 = vec![0.0f32; nv];
1355    w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
1356    let mut b1 = vec![0.0f32; nv];
1357    let mut b2 = vec![0.0f32; nv];
1358    w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
1359
1360    let mut of1 = vec![0.0f32; vd];
1361    gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1, pool);
1362
1363    scratch.clear();
1364    scratch.extend_from_slice(state);
1365    let mut of2 = vec![0.0f32; vd];
1366    gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2, pool);
1367
1368    let mut out1 = vec![0.0f32; cfg.hidden_size];
1369    let mut out2 = vec![0.0f32; cfg.hidden_size];
1370    w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
1371    (out1, out2)
1372}
1373
1374// ───────────────────────── ShortConv (LFM2 gated short convolution) ─────────────────────────
1375
1376/// Weights of one LFM2 short-convolution mixer
1377/// (`model.layers.{i}.short_conv.*`, renamed from the vendor `conv.*` at
1378/// convert time). No recurrent mixer state — the only state is the causal
1379/// conv ring (the last `kernel−1` gated inputs per channel).
1380pub struct ShortConvWeights {
1381    /// [3·hidden, hidden] — fused (B, C, x) projection.
1382    pub in_proj: QTensor,
1383    /// [hidden · kernel] depthwise conv taps, flattened `[channel][tap]`
1384    /// (the source `[hidden, 1, kernel]` with the singleton group axis
1385    /// dropped). Tap `kernel−1` multiplies the current position.
1386    pub conv: Vec<f32>,
1387    /// [hidden, hidden] — output projection.
1388    pub out_proj: QTensor,
1389}
1390
1391#[derive(Clone, Copy)]
1392pub struct ShortConvCfg {
1393    pub hidden_size: usize,
1394    /// Conv kernel width `L` (`conv_L_cache`; LFM2 uses 3).
1395    pub kernel: usize,
1396}
1397
1398impl ShortConvCfg {
1399    /// Conv ring: the last `kernel−1` gated inputs per channel.
1400    pub fn state_len(&self) -> usize {
1401        (self.kernel - 1) * self.hidden_size
1402    }
1403}
1404
1405/// One position through the gated conv, given the fused projection
1406/// `bcx = in_proj·x` [3·hidden] = [B | C | x]. Advances the conv ring and
1407/// writes the gated conv output `y = C ⊙ conv(B ⊙ x)` [hidden] into `y`.
1408///
1409/// The conv is PyTorch's causal depthwise `Conv1d(padding=kernel−1)`
1410/// truncated to the current length: for tap `k`, weight `w[c][k]` pairs
1411/// with the input `kernel−1−k` steps in the past, so `w[c][kernel−1]` is
1412/// the current position. The ring holds `in[t−1] … in[t−(kernel−1)]` at
1413/// slots `0 … kernel−2`.
1414fn short_conv_step(
1415    bcx: &[f32],
1416    conv: &[f32],
1417    cfg: &ShortConvCfg,
1418    ring_state: &mut [f32],
1419    y: &mut [f32],
1420) {
1421    let (h, k) = (cfg.hidden_size, cfg.kernel);
1422    let ring = k - 1;
1423    let (bg, cg, xg) = (&bcx[0..h], &bcx[h..2 * h], &bcx[2 * h..3 * h]);
1424    for c in 0..h {
1425        let bx = bg[c] * xg[c];
1426        let wc = &conv[c * k..(c + 1) * k];
1427        // Current tap, then the past taps read from the channel's ring.
1428        let mut acc = wc[k - 1] * bx;
1429        let rc = &mut ring_state[c * ring..c * ring + ring];
1430        for s in 0..ring {
1431            acc += wc[k - 2 - s] * rc[s];
1432        }
1433        y[c] = cg[c] * acc;
1434        // Shift newest-in-front: slot 0 becomes the just-seen input.
1435        for s in (1..ring).rev() {
1436            rc[s] = rc[s - 1];
1437        }
1438        if ring > 0 {
1439            rc[0] = bx;
1440        }
1441    }
1442}
1443
1444/// Forward one position through a short-conv layer, advancing `state`.
1445pub fn short_conv_forward(
1446    x: &[f32],
1447    w: &ShortConvWeights,
1448    cfg: &ShortConvCfg,
1449    state: &mut Vec<f32>,
1450    pool: Option<&Pool>,
1451) -> Vec<f32> {
1452    if state.len() != cfg.state_len() {
1453        *state = vec![0f32; cfg.state_len()];
1454    }
1455    let h = cfg.hidden_size;
1456    let mut bcx = vec![0.0f32; 3 * h];
1457    w.in_proj.matvec(x, &mut bcx, pool);
1458    let mut y = vec![0.0f32; h];
1459    short_conv_step(&bcx, &w.conv, cfg, state, &mut y);
1460    let mut out = vec![0.0f32; h];
1461    w.out_proj.matvec(&y, &mut out, pool);
1462    out
1463}
1464
1465/// Batched short-conv forward (prefill-GEMM): in_proj/out_proj are matmat
1466/// over the chunk (a weight row streamed once), the conv walks the
1467/// positions in order — the chunk is contiguous, so the ring state is
1468/// exactly the sequential path's and the math is elementwise identical.
1469pub fn short_conv_forward_batch(
1470    xs: &[f32],
1471    b: usize,
1472    w: &ShortConvWeights,
1473    cfg: &ShortConvCfg,
1474    state: &mut Vec<f32>,
1475    pool: Option<&Pool>,
1476) -> Vec<f32> {
1477    if state.len() != cfg.state_len() {
1478        *state = vec![0f32; cfg.state_len()];
1479    }
1480    let h = cfg.hidden_size;
1481    let mut bcx = vec![0.0f32; b * 3 * h];
1482    w.in_proj.matmat(xs, b, &mut bcx, pool);
1483    let mut y = vec![0.0f32; b * h];
1484    for bi in 0..b {
1485        short_conv_step(
1486            &bcx[bi * 3 * h..(bi + 1) * 3 * h],
1487            &w.conv,
1488            cfg,
1489            state,
1490            &mut y[bi * h..(bi + 1) * h],
1491        );
1492    }
1493    let mut out = vec![0.0f32; b * h];
1494    w.out_proj.matmat(&y, b, &mut out, pool);
1495    out
1496}
1497
1498/// Fused two-position forward (speculative verify). Lane 1 commits into
1499/// `state`; lane 2's tentative ring goes into `scratch` — swapped in on
1500/// draft acceptance, dropped on rejection. LFM2 ships no MTP head, so this
1501/// is exercised only by the pair-fusion micro-benchmark; kept correct.
1502#[allow(clippy::too_many_arguments)]
1503pub fn short_conv_pair(
1504    x1: &[f32],
1505    x2: &[f32],
1506    w: &ShortConvWeights,
1507    cfg: &ShortConvCfg,
1508    state: &mut Vec<f32>,
1509    scratch: &mut Vec<f32>,
1510    pool: Option<&Pool>,
1511) -> (Vec<f32>, Vec<f32>) {
1512    if state.len() != cfg.state_len() {
1513        *state = vec![0f32; cfg.state_len()];
1514    }
1515    let h = cfg.hidden_size;
1516    let mut bcx1 = vec![0.0f32; 3 * h];
1517    let mut bcx2 = vec![0.0f32; 3 * h];
1518    w.in_proj.matvec2(x1, x2, &mut bcx1, &mut bcx2, pool);
1519
1520    let mut y1 = vec![0.0f32; h];
1521    short_conv_step(&bcx1, &w.conv, cfg, state, &mut y1);
1522    scratch.clear();
1523    scratch.extend_from_slice(state);
1524    let mut y2 = vec![0.0f32; h];
1525    short_conv_step(&bcx2, &w.conv, cfg, scratch, &mut y2);
1526
1527    let mut out1 = vec![0.0f32; h];
1528    let mut out2 = vec![0.0f32; h];
1529    w.out_proj.matvec2(&y1, &y2, &mut out1, &mut out2, pool);
1530    (out1, out2)
1531}
1532
1533// ─── Kimi Delta Attention (KDA) ─────────────────────────────────────────
1534//
1535// Kimi Linear / Kimi-K3 linear mixer (reference: FLA naive_recurrent_kda
1536// + moonshotai modeling_kimi.py). Differences from GatedDeltaNet above:
1537// separate q/k/v projections each behind its OWN causal depthwise short
1538// convolution; the delta-rule decay is a PER-CHANNEL vector (diagonal)
1539// instead of a per-head scalar; the decay pre-activation comes from a
1540// low-rank projection f_b(f_a(x)); and the output gate norm uses
1541// sigmoid, not SiLU.
1542
1543pub struct KdaWeights {
1544    /// [nh·dk, hidden]
1545    pub q_proj: QTensor,
1546    /// [nh·dk, hidden]
1547    pub k_proj: QTensor,
1548    /// [nh·dv, hidden]
1549    pub v_proj: QTensor,
1550    /// [nh·dk × kk] — depthwise taps, oldest→newest (see GdnWeights.conv1d)
1551    pub conv_q: Vec<f32>,
1552    pub conv_k: Vec<f32>,
1553    /// [nh·dv × kk]
1554    pub conv_v: Vec<f32>,
1555    /// [rank, hidden] — low-rank decay projection, stage 1
1556    pub f_a: QTensor,
1557    /// [nh·dk, rank] — stage 2
1558    pub f_b: QTensor,
1559    /// [nh·dk]
1560    pub dt_bias: Vec<f32>,
1561    /// [nh] per-head (Kimi-Linear-48B) | [dk] per-dim (Kimi-K3) |
1562    /// [nh·dk] full — broadcast resolved by length.
1563    pub a_log: Vec<f32>,
1564    /// [nh, hidden] — β = σ(b_proj·x) per head
1565    pub b_proj: QTensor,
1566    /// Output gate: full-rank g_proj (K3) or low-rank g_b(g_a(x)) (48B).
1567    pub gate: KdaOutGate,
1568    /// [dv] — gated RMSNorm weight (per head over head_v_dim)
1569    pub o_norm: Vec<f32>,
1570    /// [hidden, nh·dv]
1571    pub o_proj: QTensor,
1572    /// Some(lb): log-decay = lb·σ(exp(A)·(f+bias)) (K3, lb=−5);
1573    /// None: −exp(A)·softplus(f+bias) (Kimi-Linear-48B).
1574    pub gate_lower_bound: Option<f32>,
1575}
1576
1577pub enum KdaOutGate {
1578    /// [nh·dv, hidden]
1579    Full(QTensor),
1580    /// g_a [rank, hidden], g_b [nh·dv, rank]
1581    LowRank(QTensor, QTensor),
1582}
1583
1584#[derive(Clone, Copy)]
1585pub struct KdaCfg {
1586    pub num_heads: usize,
1587    pub head_k_dim: usize,
1588    pub head_v_dim: usize,
1589    pub conv_kernel: usize,
1590    pub hidden_size: usize,
1591    pub rms_eps: f64,
1592}
1593
1594impl KdaCfg {
1595    /// Packed state: [q ring | k ring | v ring | S nh·dk·dv], one Vec —
1596    /// same single-buffer convention as GdnCfg::state_len.
1597    pub fn state_len(&self) -> usize {
1598        let (nh, dk, dv, kk) = (
1599            self.num_heads,
1600            self.head_k_dim,
1601            self.head_v_dim,
1602            self.conv_kernel,
1603        );
1604        (kk - 1) * (2 * nh * dk + nh * dv) + nh * dk * dv
1605    }
1606}
1607
1608/// Depthwise causal conv over [ring…, current] + SiLU, then ring shift.
1609/// Taps oldest→newest, tap kk−1 multiplies the current position.
1610fn kda_conv(raw: &[f32], taps: &[f32], ring: &mut [f32], kk: usize, out: &mut [f32]) {
1611    let c_dim = raw.len();
1612    for c in 0..c_dim {
1613        let t = &taps[c * kk..(c + 1) * kk];
1614        let mut acc = raw[c] as f64 * t[kk - 1] as f64;
1615        for j in 0..kk - 1 {
1616            acc += ring[j * c_dim + c] as f64 * t[j] as f64;
1617        }
1618        out[c] = silu(acc) as f32;
1619    }
1620    if kk > 1 {
1621        ring.copy_within(c_dim.., 0);
1622        let tail = (kk - 2) * c_dim;
1623        ring[tail..tail + c_dim].copy_from_slice(raw);
1624    }
1625}
1626
1627/// Per-channel log-decay for head-channel (h, d): resolves the A_log
1628/// broadcast by length and applies the configured gate formula.
1629#[inline]
1630fn kda_log_decay(w: &KdaWeights, cfg: &KdaCfg, h: usize, d: usize, f: f32) -> f64 {
1631    let (nh, dk) = (cfg.num_heads, cfg.head_k_dim);
1632    let a = if w.a_log.len() == nh {
1633        w.a_log[h] as f64
1634    } else if w.a_log.len() == dk {
1635        w.a_log[d] as f64
1636    } else {
1637        w.a_log[h * dk + d] as f64
1638    };
1639    let raw = f as f64 + w.dt_bias[h * dk + d] as f64;
1640    match w.gate_lower_bound {
1641        Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1642        None => -a.exp() * softplus(raw),
1643    }
1644}
1645
1646/// One recurrent step given this position's raw (pre-conv) projections.
1647/// Advances the packed state and writes the gated per-head output into
1648/// `of` [nh·dv]. Recurrence (FLA naive_recurrent_kda):
1649///   S ← Diag(exp(g))·S;  S += β·k ⊗ (v − kᵀS);  o = qᵀS
1650/// with q,k L2-normalized per head and q additionally scaled by 1/√dk —
1651/// regrouped into two S passes like gdn_step (per-channel decay folds
1652/// into the k readout of the first pass).
1653#[allow(clippy::too_many_arguments)]
1654fn kda_step(
1655    xq: &[f32],
1656    xk: &[f32],
1657    xv: &[f32],
1658    f: &[f32],
1659    b: &[f32],
1660    gate_out: &[f32],
1661    w: &KdaWeights,
1662    cfg: &KdaCfg,
1663    state: &mut [f32],
1664    of: &mut [f32],
1665    pool: Option<&Pool>,
1666) {
1667    let (nh, dk, dv, kk) = (
1668        cfg.num_heads,
1669        cfg.head_k_dim,
1670        cfg.head_v_dim,
1671        cfg.conv_kernel,
1672    );
1673    let (kd, vd) = (nh * dk, nh * dv);
1674    let ring_q_len = (kk - 1) * kd;
1675    let ring_v_len = (kk - 1) * vd;
1676    let (ring_q, rest) = state.split_at_mut(ring_q_len);
1677    let (ring_k, rest) = rest.split_at_mut(ring_q_len);
1678    let (ring_v, s_all) = rest.split_at_mut(ring_v_len);
1679
1680    let mut cq = vec![0f32; kd];
1681    let mut ck = vec![0f32; kd];
1682    let mut cv = vec![0f32; vd];
1683    kda_conv(xq, &w.conv_q, ring_q, kk, &mut cq);
1684    kda_conv(xk, &w.conv_k, ring_k, kk, &mut ck);
1685    kda_conv(xv, &w.conv_v, ring_v, kk, &mut cv);
1686
1687    let (cq, ck, cv) = (&cq, &ck, &cv);
1688    let s_ptr = SendMutF32(s_all.as_mut_ptr());
1689    let of_ptr = SendMutF32(of.as_mut_ptr());
1690    let head_range = |h0: usize, h1: usize| {
1691        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
1692        let mut kv = crate::attention::take_buf(dv);
1693        let mut delta = crate::attention::take_buf(dv);
1694        let mut o = crate::attention::take_buf(dv);
1695        let mut kf = crate::attention::take_buf(dk);
1696        let mut qf = crate::attention::take_buf(dk);
1697        let mut gd = crate::attention::take_buf(dk);
1698        for h in h0..h1 {
1699            let qs = h * dk;
1700            // l2-normalize q and k; q additionally scaled by 1/√dk.
1701            let (mut nq, mut nkn) = (0f64, 0f64);
1702            for d in 0..dk {
1703                nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
1704                nkn += (ck[qs + d] as f64) * (ck[qs + d] as f64);
1705            }
1706            let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
1707            let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
1708            for d in 0..dk {
1709                qf[d] = cq[qs + d] * invq;
1710                kf[d] = ck[qs + d] * invk;
1711                gd[d] = kda_log_decay(w, cfg, h, d, f[qs + d]).exp() as f32;
1712            }
1713            let beta = sigmoid(b[h] as f64) as f32;
1714
1715            // SAFETY: disjoint per-head S and output slices per worker.
1716            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
1717            let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
1718            let vt = &cv[h * dv..(h + 1) * dv];
1719
1720            // Pass 1: kv = kᵀ(Diag(gd)·S_old) — decay folded into k.
1721            kv[..dv].fill(0.0);
1722            for di in 0..dk {
1723                let kg = kf[di] * gd[di];
1724                let row = &s[di * dv..(di + 1) * dv];
1725                for dj in 0..dv {
1726                    kv[dj] += row[dj] * kg;
1727                }
1728            }
1729            for dj in 0..dv {
1730                delta[dj] = (vt[dj] - kv[dj]) * beta;
1731            }
1732            // Pass 2: S[di,:] = gd[di]·row + k[di]·delta;  o += q[di]·row.
1733            o[..dv].fill(0.0);
1734            for di in 0..dk {
1735                let (kfd, qfd, gdd) = (kf[di], qf[di], gd[di]);
1736                let row = &mut s[di * dv..(di + 1) * dv];
1737                for dj in 0..dv {
1738                    let cell = gdd * row[dj] + kfd * delta[dj];
1739                    row[dj] = cell;
1740                    o[dj] += qfd * cell;
1741                }
1742            }
1743            // Gated RMSNorm per head: x̂·w·σ(gate) — sigmoid, not SiLU.
1744            let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
1745            let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1746            for dj in 0..dv {
1747                oh[dj] = ((o[dj] as f64 * inv)
1748                    * w.o_norm[dj] as f64
1749                    * sigmoid(gate_out[h * dv + dj] as f64)) as f32;
1750            }
1751        }
1752        crate::attention::recycle_buf(&mut kv);
1753        crate::attention::recycle_buf(&mut delta);
1754        crate::attention::recycle_buf(&mut o);
1755        crate::attention::recycle_buf(&mut kf);
1756        crate::attention::recycle_buf(&mut qf);
1757        crate::attention::recycle_buf(&mut gd);
1758    };
1759    match pool {
1760        Some(pool) if nh >= 4 => pool.run(&|widx, n| {
1761            let chunk = nh.div_ceil(n);
1762            let h0 = (widx * chunk).min(nh);
1763            let h1 = (h0 + chunk).min(nh);
1764            if h0 < h1 {
1765                head_range(h0, h1);
1766            }
1767        }),
1768        _ => head_range(0, nh),
1769    }
1770}
1771
1772/// Project one position's raw q/k/v/f/β/gate inputs (shared by the
1773/// single and batched forwards; `bi` selects the row when batched).
1774fn kda_gate_out(w: &KdaWeights, x: &[f32], vd: usize, pool: Option<&Pool>) -> Vec<f32> {
1775    let mut g = vec![0.0f32; vd];
1776    match &w.gate {
1777        KdaOutGate::Full(gp) => gp.matvec(x, &mut g, pool),
1778        KdaOutGate::LowRank(ga, gb) => {
1779            let mut low = vec![0.0f32; ga.rows()];
1780            ga.matvec(x, &mut low, pool);
1781            gb.matvec(&low, &mut g, pool);
1782        }
1783    }
1784    g
1785}
1786
1787/// Forward one position through a KDA layer, advancing `state`.
1788pub fn kda_forward(
1789    x: &[f32],
1790    w: &KdaWeights,
1791    cfg: &KdaCfg,
1792    state: &mut Vec<f32>,
1793    pool: Option<&Pool>,
1794) -> Vec<f32> {
1795    if state.len() != cfg.state_len() {
1796        *state = vec![0f32; cfg.state_len()];
1797    }
1798    let (nh, dk, dv) = (cfg.num_heads, cfg.head_k_dim, cfg.head_v_dim);
1799    let (kd, vd) = (nh * dk, nh * dv);
1800
1801    let mut xq = vec![0.0f32; kd];
1802    let mut xk = vec![0.0f32; kd];
1803    let mut xv = vec![0.0f32; vd];
1804    let mut fl = vec![0.0f32; w.f_a.rows()];
1805    let mut b = vec![0.0f32; nh];
1806    // KDA's beta projection uses the same hidden input as q/k/v/f_a.  When
1807    // it shares their Q4TP layout, include it in the virtual row space so
1808    // the projection group pays one pool dispatch, not five.  The explicit
1809    // fallback keeps mixed-dtype checkpoints on their established kernels.
1810    let q4_group = [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a, &w.b_proj];
1811    let q4_uniform = q4_group.iter().all(|t| {
1812        matches!(
1813            t,
1814            QTensor::Mapped {
1815                dtype: TensorDtype::Q4TiledP,
1816                ..
1817            }
1818        )
1819    });
1820    if q4_uniform {
1821        QTensor::matvec_many(
1822            q4_group,
1823            x,
1824            [
1825                xq.as_mut_slice(),
1826                xk.as_mut_slice(),
1827                xv.as_mut_slice(),
1828                fl.as_mut_slice(),
1829                b.as_mut_slice(),
1830            ],
1831            pool,
1832        );
1833    } else {
1834        QTensor::matvec_many(
1835            [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a],
1836            x,
1837            [
1838                xq.as_mut_slice(),
1839                xk.as_mut_slice(),
1840                xv.as_mut_slice(),
1841                fl.as_mut_slice(),
1842            ],
1843            pool,
1844        );
1845        w.b_proj.matvec(x, &mut b, pool);
1846    }
1847    let mut f = vec![0.0f32; kd];
1848    w.f_b.matvec(&fl, &mut f, pool);
1849    let gate_out = kda_gate_out(w, x, vd, pool);
1850
1851    let mut of = vec![0.0f32; vd];
1852    kda_step(
1853        &xq, &xk, &xv, &f, &b, &gate_out, w, cfg, state, &mut of, pool,
1854    );
1855
1856    let mut out = vec![0.0f32; cfg.hidden_size];
1857    w.o_proj.matvec(&of, &mut out, pool);
1858    out
1859}
1860
1861/// Batched KDA forward (prefill-GEMM): projections as matmat over the
1862/// chunk, the recurrence sequential per position — elementwise identical
1863/// to the single-position path.
1864pub fn kda_forward_batch(
1865    xs: &[f32],
1866    bsz: usize,
1867    w: &KdaWeights,
1868    cfg: &KdaCfg,
1869    state: &mut Vec<f32>,
1870    pool: Option<&Pool>,
1871) -> Vec<f32> {
1872    if state.len() != cfg.state_len() {
1873        *state = vec![0f32; cfg.state_len()];
1874    }
1875    let (nh, dk, dv, hs) = (
1876        cfg.num_heads,
1877        cfg.head_k_dim,
1878        cfg.head_v_dim,
1879        cfg.hidden_size,
1880    );
1881    let (kd, vd) = (nh * dk, nh * dv);
1882
1883    let mut xq = vec![0.0f32; bsz * kd];
1884    w.q_proj.matmat(xs, bsz, &mut xq, pool);
1885    let mut xk = vec![0.0f32; bsz * kd];
1886    w.k_proj.matmat(xs, bsz, &mut xk, pool);
1887    let mut xv = vec![0.0f32; bsz * vd];
1888    w.v_proj.matmat(xs, bsz, &mut xv, pool);
1889    let rank = w.f_a.rows();
1890    let mut fl = vec![0.0f32; bsz * rank];
1891    w.f_a.matmat(xs, bsz, &mut fl, pool);
1892    let mut f = vec![0.0f32; bsz * kd];
1893    w.f_b.matmat(&fl, bsz, &mut f, pool);
1894    let mut b = vec![0.0f32; bsz * nh];
1895    w.b_proj.matmat(xs, bsz, &mut b, pool);
1896    let mut gate_out = vec![0.0f32; bsz * vd];
1897    match &w.gate {
1898        KdaOutGate::Full(gp) => gp.matmat(xs, bsz, &mut gate_out, pool),
1899        KdaOutGate::LowRank(ga, gb) => {
1900            let mut low = vec![0.0f32; bsz * ga.rows()];
1901            ga.matmat(xs, bsz, &mut low, pool);
1902            gb.matmat(&low, bsz, &mut gate_out, pool);
1903        }
1904    }
1905
1906    let mut of = vec![0.0f32; bsz * vd];
1907    for bi in 0..bsz {
1908        let mut oh = vec![0.0f32; vd];
1909        kda_step(
1910            &xq[bi * kd..(bi + 1) * kd],
1911            &xk[bi * kd..(bi + 1) * kd],
1912            &xv[bi * vd..(bi + 1) * vd],
1913            &f[bi * kd..(bi + 1) * kd],
1914            &b[bi * nh..(bi + 1) * nh],
1915            &gate_out[bi * vd..(bi + 1) * vd],
1916            w,
1917            cfg,
1918            state,
1919            &mut oh,
1920            pool,
1921        );
1922        of[bi * vd..(bi + 1) * vd].copy_from_slice(&oh);
1923    }
1924
1925    let mut out = vec![0.0f32; bsz * hs];
1926    w.o_proj.matmat(&of, bsz, &mut out, pool);
1927    out
1928}
1929
1930#[cfg(test)]
1931mod tests {
1932    #[test]
1933    fn kda_forward_matches_naive_reference() {
1934        // Small deterministic KDA layer; the oracle is a literal port of
1935        // FLA naive_recurrent_kda + naive_kda_gate + the modeling glue
1936        // (conv→silu, low-rank decay, sigmoid-gated output norm), coded
1937        // straight from the reference — a different shape from the fused
1938        // two-pass production kernel.
1939        let (nh, dk, dv, kk, hs, rank) = (2usize, 4usize, 4usize, 3usize, 6usize, 3usize);
1940        let synth = |rows: usize, cols: usize, salt: usize| -> QTensor {
1941            QTensor::from_f32(
1942                (0..rows * cols)
1943                    .map(|i| (((i * 31 + salt * 17) % 101) as f32 / 101.0 - 0.5) * 0.6)
1944                    .collect(),
1945                rows,
1946                cols,
1947            )
1948        };
1949        let vecf = |n: usize, salt: usize| -> Vec<f32> {
1950            (0..n)
1951                .map(|i| (((i * 13 + salt * 7) % 89) as f32 / 89.0 - 0.5) * 0.8)
1952                .collect()
1953        };
1954        for (label, a_log, lb) in [
1955            ("per-head standard", vecf(nh, 40), None),
1956            ("per-dim lower-bound", vecf(dk, 41), Some(-5.0f32)),
1957        ] {
1958            let w = KdaWeights {
1959                q_proj: synth(nh * dk, hs, 1),
1960                k_proj: synth(nh * dk, hs, 2),
1961                v_proj: synth(nh * dv, hs, 3),
1962                conv_q: vecf(nh * dk * kk, 4),
1963                conv_k: vecf(nh * dk * kk, 5),
1964                conv_v: vecf(nh * dv * kk, 6),
1965                f_a: synth(rank, hs, 7),
1966                f_b: synth(nh * dk, rank, 8),
1967                dt_bias: vecf(nh * dk, 9),
1968                a_log: a_log.clone(),
1969                b_proj: synth(nh, hs, 10),
1970                gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1971                o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1972                o_proj: synth(hs, nh * dv, 13),
1973                gate_lower_bound: lb,
1974            };
1975            let cfg = KdaCfg {
1976                num_heads: nh,
1977                head_k_dim: dk,
1978                head_v_dim: dv,
1979                conv_kernel: kk,
1980                hidden_size: hs,
1981                rms_eps: 1e-6,
1982            };
1983            let xs: Vec<Vec<f32>> = (0..6)
1984                .map(|t| {
1985                    (0..hs)
1986                        .map(|i| ((t * hs + i) as f32 * 0.37).sin() * 0.5)
1987                        .collect()
1988                })
1989                .collect();
1990
1991            // Production path.
1992            let mut state = Vec::new();
1993            let got: Vec<Vec<f32>> = xs
1994                .iter()
1995                .map(|x| kda_forward(x, &w, &cfg, &mut state, None))
1996                .collect();
1997
1998            // Oracle.
1999            let mv = |t: &QTensor, x: &[f32]| -> Vec<f32> {
2000                let mut o = vec![0.0f32; t.rows()];
2001                t.matvec(x, &mut o, None);
2002                o
2003            };
2004            let mut hist: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = Vec::new(); // raw xq/xk/xv
2005            let mut s_state = vec![0f64; nh * dk * dv];
2006            let mut want: Vec<Vec<f32>> = Vec::new();
2007            for x in &xs {
2008                let (xq, xk, xv) = (mv(&w.q_proj, x), mv(&w.k_proj, x), mv(&w.v_proj, x));
2009                hist.push((xq, xk, xv));
2010                // conv over the raw history, taps oldest→newest.
2011                let conv = |sel: fn(&(Vec<f32>, Vec<f32>, Vec<f32>)) -> &Vec<f32>,
2012                            taps: &[f32],
2013                            n: usize|
2014                 -> Vec<f32> {
2015                    (0..n)
2016                        .map(|c| {
2017                            let t = &taps[c * kk..(c + 1) * kk];
2018                            let mut acc = 0f64;
2019                            for j in 0..kk {
2020                                let idx = hist.len() as i64 - (kk as i64 - j as i64);
2021                                if idx >= 0 {
2022                                    acc += sel(&hist[idx as usize])[c] as f64 * t[j] as f64;
2023                                }
2024                            }
2025                            silu(acc)
2026                        })
2027                        .map(|v| v as f32)
2028                        .collect()
2029                };
2030                let cq = conv(|h| &h.0, &w.conv_q, nh * dk);
2031                let ck = conv(|h| &h.1, &w.conv_k, nh * dk);
2032                let cv = conv(|h| &h.2, &w.conv_v, nh * dv);
2033                let f = mv(&w.f_b, &mv(&w.f_a, x));
2034                let bb = mv(&w.b_proj, x);
2035                let gate_out = match &w.gate {
2036                    KdaOutGate::LowRank(ga, gb) => mv(gb, &mv(ga, x)),
2037                    KdaOutGate::Full(g) => mv(g, x),
2038                };
2039                let mut of = vec![0f32; nh * dv];
2040                for h in 0..nh {
2041                    // l2norm + scale.
2042                    let q: Vec<f64> = {
2043                        let sl = &cq[h * dk..(h + 1) * dk];
2044                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
2045                        let inv = 1.0 / ((n + 1e-6).sqrt() * (dk as f64).sqrt());
2046                        sl.iter().map(|&v| v as f64 * inv).collect()
2047                    };
2048                    let k: Vec<f64> = {
2049                        let sl = &ck[h * dk..(h + 1) * dk];
2050                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
2051                        let inv = 1.0 / (n + 1e-6).sqrt();
2052                        sl.iter().map(|&v| v as f64 * inv).collect()
2053                    };
2054                    let v: Vec<f64> = cv[h * dv..(h + 1) * dv].iter().map(|&v| v as f64).collect();
2055                    // gate: g = −exp(A)·softplus(f+bias) | lb·σ(exp(A)·(f+bias))
2056                    let g: Vec<f64> = (0..dk)
2057                        .map(|d| {
2058                            let a = if w.a_log.len() == nh {
2059                                w.a_log[h] as f64
2060                            } else {
2061                                w.a_log[d] as f64
2062                            };
2063                            let raw = f[h * dk + d] as f64 + w.dt_bias[h * dk + d] as f64;
2064                            match w.gate_lower_bound {
2065                                Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
2066                                None => -a.exp() * softplus(raw),
2067                            }
2068                        })
2069                        .collect();
2070                    let beta = sigmoid(bb[h] as f64);
2071                    let s = &mut s_state[h * dk * dv..(h + 1) * dk * dv];
2072                    // S = Diag(exp(g))·S
2073                    for di in 0..dk {
2074                        for dj in 0..dv {
2075                            s[di * dv + dj] *= g[di].exp();
2076                        }
2077                    }
2078                    // kv = kᵀS; S += β·k⊗(v−kv); o = qᵀS
2079                    let mut kv = vec![0f64; dv];
2080                    for di in 0..dk {
2081                        for dj in 0..dv {
2082                            kv[dj] += k[di] * s[di * dv + dj];
2083                        }
2084                    }
2085                    for di in 0..dk {
2086                        for dj in 0..dv {
2087                            s[di * dv + dj] += beta * k[di] * (v[dj] - kv[dj]);
2088                        }
2089                    }
2090                    let mut o = vec![0f64; dv];
2091                    for di in 0..dk {
2092                        for dj in 0..dv {
2093                            o[dj] += q[di] * s[di * dv + dj];
2094                        }
2095                    }
2096                    // sigmoid-gated RMSNorm
2097                    let ss: f64 = o.iter().map(|&v| v * v).sum();
2098                    let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
2099                    for dj in 0..dv {
2100                        of[h * dv + dj] = (o[dj]
2101                            * inv
2102                            * w.o_norm[dj] as f64
2103                            * sigmoid(gate_out[h * dv + dj] as f64))
2104                            as f32;
2105                    }
2106                }
2107                want.push(mv(&w.o_proj, &of));
2108            }
2109
2110            for (t, (g, e)) in got.iter().zip(&want).enumerate() {
2111                for (i, (a, b)) in g.iter().zip(e.iter()).enumerate() {
2112                    assert!((a - b).abs() < 2e-4, "{label}: t={t} i={i}: {a} vs {b}");
2113                }
2114            }
2115        }
2116
2117        // Batched prefill must equal the sequential singles bit-close.
2118        let w = KdaWeights {
2119            q_proj: synth(nh * dk, hs, 1),
2120            k_proj: synth(nh * dk, hs, 2),
2121            v_proj: synth(nh * dv, hs, 3),
2122            conv_q: vecf(nh * dk * kk, 4),
2123            conv_k: vecf(nh * dk * kk, 5),
2124            conv_v: vecf(nh * dv * kk, 6),
2125            f_a: synth(rank, hs, 7),
2126            f_b: synth(nh * dk, rank, 8),
2127            dt_bias: vecf(nh * dk, 9),
2128            a_log: vecf(nh, 40),
2129            b_proj: synth(nh, hs, 10),
2130            gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
2131            o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
2132            o_proj: synth(hs, nh * dv, 13),
2133            gate_lower_bound: None,
2134        };
2135        let cfg = KdaCfg {
2136            num_heads: nh,
2137            head_k_dim: dk,
2138            head_v_dim: dv,
2139            conv_kernel: kk,
2140            hidden_size: hs,
2141            rms_eps: 1e-6,
2142        };
2143        let xs: Vec<f32> = (0..5 * hs).map(|i| (i as f32 * 0.29).cos() * 0.4).collect();
2144        let mut st1 = Vec::new();
2145        let seq: Vec<f32> = (0..5)
2146            .flat_map(|t| kda_forward(&xs[t * hs..(t + 1) * hs], &w, &cfg, &mut st1, None))
2147            .collect();
2148        let mut st2 = Vec::new();
2149        let bat = kda_forward_batch(&xs, 5, &w, &cfg, &mut st2, None);
2150        for (i, (a, b)) in seq.iter().zip(&bat).enumerate() {
2151            assert!((a - b).abs() < 1e-5, "batch i={i}: {a} vs {b}");
2152        }
2153        assert_eq!(st1, st2, "state must match after the chunk");
2154    }
2155
2156    use super::*;
2157
2158    fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
2159        let cfg = VmfPhaseCfg {
2160            num_heads: 2,
2161            nphase: 3,
2162            value_head_dim: 4,
2163            hidden_size: 8,
2164            phase_mass: 0.0,
2165        };
2166        let synth = |rows: usize, cols: usize, salt: usize| {
2167            QTensor::from_f32(
2168                (0..rows * cols)
2169                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
2170                    .collect(),
2171                rows,
2172                cols,
2173            )
2174        };
2175        let w = VmfPhaseWeights {
2176            thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
2177            thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
2178            v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
2179            out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
2180            decay: (0..cfg.num_heads * 2 * cfg.nphase)
2181                .map(|i| 0.9 + 0.005 * (i % 10) as f64)
2182                .collect(),
2183            conv: None,
2184            k_gate: None,
2185            phase_delta: false,
2186        };
2187        (w, cfg)
2188    }
2189
2190    #[test]
2191    fn state_persists_and_changes_output() {
2192        let (w, cfg) = tiny();
2193        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2194        let mut state = Vec::new();
2195        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2196        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2197        // Same input, evolved state → different output.
2198        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
2199        assert_eq!(state.len(), cfg.state_len());
2200    }
2201
2202    #[test]
2203    fn phase_delta_matches_independent_token_oracle_and_ignores_phase_mass() {
2204        // One head, two phase angles, one value channel.  The projections are
2205        // intentionally simple so this test computes the token oracle without
2206        // calling any implementation helper: q=[x0,x1], k=[x1,x0], v=x0,
2207        // and the first output row is qᵀS.
2208        let cfg = VmfPhaseCfg {
2209            num_heads: 1,
2210            nphase: 2,
2211            value_head_dim: 1,
2212            hidden_size: 2,
2213            // A nonzero legacy knob must not alter a v1 operator.
2214            phase_mass: 17.0,
2215        };
2216        let w = VmfPhaseWeights {
2217            thq: QTensor::from_f32(vec![1.0, 0.0, 0.0, 1.0], 2, 2),
2218            thk: QTensor::from_f32(vec![0.0, 1.0, 1.0, 0.0], 2, 2),
2219            v_proj: QTensor::from_f32(vec![1.0, 0.0], 1, 2),
2220            out_proj: QTensor::from_f32(vec![1.0, 0.0], 2, 1),
2221            decay: vec![0.9, 0.8, 0.7, 0.6],
2222            conv: None,
2223            k_gate: None, // beta=1
2224            phase_delta: true,
2225        };
2226        let xs = [[0.3f32, 0.7f32], [-0.2, 0.4], [0.8, -0.6]];
2227        let c = 1.0f64 / 2.0f64.sqrt();
2228        let mut oracle_state = [0.0f64; 4];
2229        let mut state = Vec::new();
2230        for x in xs {
2231            let q = [
2232                c * (x[0] as f64).cos(),
2233                c * (x[1] as f64).cos(),
2234                c * (x[0] as f64).sin(),
2235                c * (x[1] as f64).sin(),
2236            ];
2237            let k = [
2238                c * (x[1] as f64).cos(),
2239                c * (x[0] as f64).cos(),
2240                c * (x[1] as f64).sin(),
2241                c * (x[0] as f64).sin(),
2242            ];
2243            let value = x[0] as f64;
2244            let mut read = 0.0;
2245            for f in 0..4 {
2246                oracle_state[f] *= w.decay[f];
2247                read += k[f] * oracle_state[f];
2248            }
2249            for f in 0..4 {
2250                oracle_state[f] += k[f] * (value - read);
2251            }
2252            let want: f32 = q
2253                .iter()
2254                .zip(oracle_state)
2255                .map(|(qf, sf)| (qf * sf) as f32)
2256                .sum();
2257            let got = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
2258            assert!(
2259                (got[0] - want).abs() < 2e-6,
2260                "oracle {want} vs runtime {}",
2261                got[0]
2262            );
2263            assert!(got[1].abs() < 2e-7);
2264        }
2265        assert_eq!(state.len(), cfg.state_len());
2266        for (got, want) in state.iter().zip(oracle_state) {
2267            assert!((*got as f64 - want).abs() < 2e-6);
2268        }
2269    }
2270
2271    #[test]
2272    fn phase_delta_fused_readout_is_bit_exact_over_long_continuation() {
2273        // Independent three-pass reference, with nonzero state/output and a
2274        // sentinel convolution tail. Exercise odd widths and the real shape.
2275        for (nh, nph, dv) in [(1, 1, 1), (3, 5, 7), (8, 32, 128)] {
2276            let cfg = VmfPhaseCfg {
2277                num_heads: nh,
2278                nphase: nph,
2279                value_head_dim: dv,
2280                hidden_size: nh * dv,
2281                phase_mass: 19.0,
2282            };
2283            let p2 = 2 * nph;
2284            let scale = 1.0f64 / (nph as f64).sqrt();
2285            let mut state: Vec<f32> = (0..cfg.state_len() + 11)
2286                .map(|i| (i as f32 * 0.37).sin())
2287                .collect();
2288            let mut reference = state.clone();
2289            let tail = state[cfg.state_len()..].to_vec();
2290            let decay: Vec<f64> = (0..nh * p2).map(|i| [0.0, 0.8, 0.99, 1.0][i % 4]).collect();
2291            for step in 0..128 {
2292                let q: Vec<f32> = (0..nh * nph)
2293                    .map(|i| ((i + step * 3) as f32 * 0.13).sin() * 4.0)
2294                    .collect();
2295                let k: Vec<f32> = (0..nh * nph)
2296                    .map(|i| ((i + step * 7) as f32 * 0.29).cos() * 4.0)
2297                    .collect();
2298                let v: Vec<f32> = (0..nh * dv)
2299                    .map(|i| ((i + step) as f32 * 0.43).sin())
2300                    .collect();
2301                let gates: Vec<f32> = (0..nh).map(|h| [0.0, 0.3, 1.0][(step + h) % 3]).collect();
2302                let kap = (step % 4 != 0).then_some(gates.as_slice());
2303                let mut out = vec![0.125f32; nh * dv];
2304                let mut want = out.clone();
2305                for h in 0..nh {
2306                    let feature = |theta: &[f32], f: usize| {
2307                        let angle = theta[h * nph + f % nph] as f64;
2308                        scale * if f < nph { angle.cos() } else { angle.sin() }
2309                    };
2310                    let beta = kap.map_or(1.0f64, |g| g[h] as f64);
2311                    let mut read = vec![0.0f64; dv];
2312                    for f in 0..p2 {
2313                        for d in 0..dv {
2314                            let at = (h * p2 + f) * dv + d;
2315                            read[d] += feature(&k, f) * (decay[h * p2 + f] * reference[at] as f64);
2316                        }
2317                    }
2318                    for f in 0..p2 {
2319                        for d in 0..dv {
2320                            let at = (h * p2 + f) * dv + d;
2321                            reference[at] = (decay[h * p2 + f] * reference[at] as f64
2322                                + beta * feature(&k, f) * (v[h * dv + d] as f64 - read[d]))
2323                                as f32;
2324                        }
2325                    }
2326                    for f in 0..p2 {
2327                        for d in 0..dv {
2328                            want[h * dv + d] +=
2329                                (feature(&q, f) * reference[(h * p2 + f) * dv + d] as f64) as f32;
2330                        }
2331                    }
2332                }
2333                phase_delta_step_f32(&q, &k, &v, &decay, kap, &cfg, &mut state, &mut out);
2334                assert_eq!(out, want, "shape {nh}/{nph}/{dv}, step {step}");
2335                assert_eq!(state, reference, "state at step {step}");
2336                assert_eq!(&state[cfg.state_len()..], tail);
2337            }
2338        }
2339    }
2340
2341    #[test]
2342    fn phase_delta_pair_reset_and_legacy_paths_are_distinct() {
2343        let (mut delta, mut cfg) = tiny();
2344        delta.phase_delta = true;
2345        // The phase normalization is observable for nphase=3, while the
2346        // legacy core remains unchanged and still uses raw cos/sin features.
2347        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2348        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2349        let mut seq_state = Vec::new();
2350        let d1 = vmf_phase_forward(&x1, &delta, &cfg, &mut seq_state, None);
2351        let d2 = vmf_phase_forward(&x2, &delta, &cfg, &mut seq_state, None);
2352
2353        let mut pair_state = Vec::new();
2354        let mut scratch = Vec::new();
2355        let (p1, p2) = vmf_phase_pair(&x1, &x2, &delta, &cfg, &mut pair_state, &mut scratch, None);
2356        assert_eq!(d1, p1);
2357        assert_eq!(d2, p2);
2358        std::mem::swap(&mut pair_state, &mut scratch);
2359        assert_eq!(seq_state, pair_state);
2360
2361        // Empty state is the runtime reset contract; it reproduces the first
2362        // token exactly and does not inherit the previous sequence.
2363        let mut reset = seq_state;
2364        reset.clear();
2365        let after_reset = vmf_phase_forward(&x1, &delta, &cfg, &mut reset, None);
2366        assert_eq!(after_reset, d1);
2367
2368        cfg.phase_mass = 0.0;
2369        let mut legacy = delta;
2370        legacy.phase_delta = false;
2371        let mut legacy_state = Vec::new();
2372        let old = vmf_phase_forward(&x1, &legacy, &cfg, &mut legacy_state, None);
2373        assert!(
2374            old.iter().zip(&d1).any(|(a, b)| (a - b).abs() > 1e-5),
2375            "legacy additive and normalized Phase-Delta paths must differ"
2376        );
2377    }
2378
2379    /// Phase-mass correction: mass=0 is bit-identical to the unscaled kernel; mass>0
2380    /// changes the output (phase narrowed → kernel widened). Guards the
2381    /// no-op default and that the knob is actually wired.
2382    #[test]
2383    fn phase_mass_zero_is_noop_and_positive_shifts() {
2384        let (w, cfg0) = tiny();
2385        let mut cfg_m = cfg0;
2386        cfg_m.phase_mass = 1.0;
2387        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
2388
2389        let mut s0 = Vec::new();
2390        let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
2391        // Re-run with mass=0 → must be bit-identical.
2392        let mut s0b = Vec::new();
2393        let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
2394        assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
2395        // mass=1 → output differs (θ halved before cos/sin).
2396        let mut sm = Vec::new();
2397        let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
2398        assert!(
2399            base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
2400            "mass>0 must change the output"
2401        );
2402        assert!(massed.iter().all(|v| v.is_finite()));
2403    }
2404
2405    /// κ write gate (hybrid_k): saturated-open gate (bias ≫ 0 → κ→1)
2406    /// matches the gateless kernel within fp tolerance; a closed gate
2407    /// (bias ≪ 0 → κ→0) writes nothing — the state stays zero and the
2408    /// output collapses to the empty-state readout.
2409    #[test]
2410    fn kappa_gate_open_matches_none_and_closed_writes_nothing() {
2411        let (mut w, cfg) = tiny();
2412        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2413
2414        let mut s_none = Vec::new();
2415        let base1 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
2416        let base2 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
2417
2418        // Open gate: W=0, bias=+20 → κ = σ(20) ≈ 1 − 2e−9.
2419        w.k_gate = Some((
2420            QTensor::from_f32(
2421                vec![0.0; cfg.num_heads * cfg.hidden_size],
2422                cfg.num_heads,
2423                cfg.hidden_size,
2424            ),
2425            vec![20.0; cfg.num_heads],
2426        ));
2427        let mut s_open = Vec::new();
2428        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
2429        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
2430        for (a, b) in base1.iter().zip(&o1).chain(base2.iter().zip(&o2)) {
2431            assert!(
2432                (a - b).abs() < 1e-5,
2433                "open κ must match gateless: {a} vs {b}"
2434            );
2435        }
2436
2437        // Closed gate: bias=−20 → κ ≈ 0 → nothing is written.
2438        w.k_gate = Some((
2439            QTensor::from_f32(
2440                vec![0.0; cfg.num_heads * cfg.hidden_size],
2441                cfg.num_heads,
2442                cfg.hidden_size,
2443            ),
2444            vec![-20.0; cfg.num_heads],
2445        ));
2446        let mut s_closed = Vec::new();
2447        let oc = vmf_phase_forward(&x, &w, &cfg, &mut s_closed, None);
2448        assert!(
2449            s_closed.iter().all(|&v| v.abs() < 1e-7),
2450            "closed κ: state must stay empty"
2451        );
2452        assert!(
2453            oc.iter().all(|&v| v.abs() < 1e-6),
2454            "closed κ: empty-state readout"
2455        );
2456    }
2457
2458    #[test]
2459    fn pair_matches_two_singles_bitexact() {
2460        let (w, cfg) = tiny();
2461        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2462        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2463
2464        // Reference: two sequential singles.
2465        let mut s_ref = Vec::new();
2466        let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
2467        let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
2468
2469        // Pair: lane1 commits, lane2 tentative in scratch.
2470        let mut s = Vec::new();
2471        let mut scratch = Vec::new();
2472        let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2473        assert_eq!(r1, p1, "lane 1 must be bit-identical");
2474        assert_eq!(r2, p2, "lane 2 must be bit-identical");
2475        // Accepting the draft = swapping scratch in → equals s_ref.
2476        std::mem::swap(&mut s, &mut scratch);
2477        assert_eq!(s, s_ref, "accepted state must equal sequential state");
2478    }
2479
2480    #[test]
2481    fn rejected_draft_leaves_state_at_lane1() {
2482        let (w, cfg) = tiny();
2483        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
2484        let x2 = vec![0.5f32; 8];
2485
2486        let mut s_ref = Vec::new();
2487        let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
2488
2489        let mut s = Vec::new();
2490        let mut scratch = Vec::new();
2491        let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2492        // Reject: state must be exactly the post-lane1 state.
2493        assert_eq!(s, s_ref);
2494    }
2495
2496    // ───────────── GatedDeltaNet ─────────────
2497
2498    fn tiny_gdn() -> (GdnWeights, GdnCfg) {
2499        let cfg = GdnCfg {
2500            num_v_heads: 4,
2501            num_k_heads: 2,
2502            key_head_dim: 3,
2503            value_head_dim: 5,
2504            conv_kernel: 4,
2505            hidden_size: 8,
2506            rms_eps: 1e-6,
2507            output_gate_sigmoid: false,
2508        };
2509        let c_dim = cfg.conv_dim();
2510        let vd = cfg.num_v_heads * cfg.value_head_dim;
2511        let synth = |rows: usize, cols: usize, salt: usize| {
2512            QTensor::from_f32(
2513                (0..rows * cols)
2514                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
2515                    .collect(),
2516                rows,
2517                cols,
2518            )
2519        };
2520        let vecf = |n: usize, salt: usize| -> Vec<f32> {
2521            (0..n)
2522                .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
2523                .collect()
2524        };
2525        let w = GdnWeights {
2526            in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
2527            in_proj_z: synth(vd, cfg.hidden_size, 2),
2528            in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
2529            in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
2530            conv1d: vecf(c_dim * cfg.conv_kernel, 5),
2531            a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
2532            dt_bias: vecf(cfg.num_v_heads, 6),
2533            norm: vec![1.0; cfg.value_head_dim],
2534            out_proj: synth(cfg.hidden_size, vd, 7),
2535        };
2536        (w, cfg)
2537    }
2538
2539    /// The chunked scan equals `gdn_step` position by position, outputs and
2540    /// state bit for bit — with a warm ring, a chunk shorter than the ring,
2541    /// and both gate forms.
2542    #[test]
2543    fn gdn_scan_matches_steps_bitexact() {
2544        for sigmoid_gate in [false, true] {
2545            let (w, mut cfg) = tiny_gdn();
2546            cfg.output_gate_sigmoid = sigmoid_gate;
2547            let (c_dim, vd, nv) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim, cfg.num_v_heads);
2548            let row = |n: usize, t: usize, salt: usize| -> Vec<f32> {
2549                (0..n).map(|i| (((i * 7 + t * 31 + salt * 3) % 83) as f32 / 83.0 - 0.5) * 1.3).collect()
2550            };
2551            let mut s_ref = vec![0f32; cfg.state_len()];
2552            let mut s_scan = s_ref.clone();
2553            for (chunk, b) in [(0usize, 2usize), (1, 7), (2, 1), (3, 5)] {
2554                let qkv: Vec<f32> = (0..b).flat_map(|t| row(c_dim, t + 10 * chunk, 1)).collect();
2555                let z: Vec<f32> = (0..b).flat_map(|t| row(vd, t + 10 * chunk, 2)).collect();
2556                let a: Vec<f32> = (0..b).flat_map(|t| row(nv, t + 10 * chunk, 3)).collect();
2557                let bb: Vec<f32> = (0..b).flat_map(|t| row(nv, t + 10 * chunk, 4)).collect();
2558                let mut of_ref = vec![0f32; b * vd];
2559                for t in 0..b {
2560                    gdn_step(
2561                        &qkv[t * c_dim..(t + 1) * c_dim],
2562                        &z[t * vd..(t + 1) * vd],
2563                        &a[t * nv..(t + 1) * nv],
2564                        &bb[t * nv..(t + 1) * nv],
2565                        &w,
2566                        &cfg,
2567                        &mut s_ref,
2568                        &mut of_ref[t * vd..(t + 1) * vd],
2569                        None,
2570                    );
2571                }
2572                let mut of_scan = vec![0f32; b * vd];
2573                gdn_scan_batch(&qkv, &z, &a, &bb, b, &w, &cfg, &mut s_scan, &mut of_scan, None);
2574                assert_eq!(of_scan, of_ref, "outputs, chunk {chunk} (b = {b})");
2575                assert_eq!(s_scan, s_ref, "state, chunk {chunk} (b = {b})");
2576            }
2577        }
2578    }
2579
2580    #[test]
2581    fn gdn_state_persists_and_changes_output() {
2582        let (w, cfg) = tiny_gdn();
2583        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
2584        let mut state = Vec::new();
2585        let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
2586        let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
2587        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
2588        assert_eq!(state.len(), cfg.state_len());
2589    }
2590
2591    #[test]
2592    fn gdn_pair_matches_two_singles_bitexact() {
2593        let (w, cfg) = tiny_gdn();
2594        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
2595        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
2596
2597        let mut s_ref = Vec::new();
2598        let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
2599        let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
2600
2601        let mut s = Vec::new();
2602        let mut scratch = Vec::new();
2603        let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2604        assert_eq!(r1, p1, "lane 1 must be bit-identical");
2605        assert_eq!(r2, p2, "lane 2 must be bit-identical");
2606        std::mem::swap(&mut s, &mut scratch);
2607        assert_eq!(s, s_ref, "accepted state must equal sequential state");
2608    }
2609
2610    #[test]
2611    fn gdn_rejected_draft_leaves_state_at_lane1() {
2612        let (w, cfg) = tiny_gdn();
2613        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
2614        let x2 = vec![0.5f32; 8];
2615
2616        let mut s_ref = Vec::new();
2617        let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
2618
2619        let mut s = Vec::new();
2620        let mut scratch = Vec::new();
2621        let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
2622        assert_eq!(s, s_ref);
2623    }
2624
2625    /// The conv ring must give the same result as an explicit causal
2626    /// conv over the whole sequence (oracle semantics: zero left-pad,
2627    /// tap kk−1 on the current position).
2628    #[test]
2629    fn gdn_conv_ring_matches_explicit_causal_conv() {
2630        let (w, cfg) = tiny_gdn();
2631        let seq: Vec<Vec<f32>> = (0..6)
2632            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
2633            .collect();
2634
2635        // Reference: recompute position t from scratch each time with a
2636        // fresh state built by replaying the prefix.
2637        let mut s_inc = Vec::new();
2638        for (t, x) in seq.iter().enumerate() {
2639            let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
2640            let mut s_replay = Vec::new();
2641            let mut replay = Vec::new();
2642            for xr in &seq[..=t] {
2643                replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
2644            }
2645            assert_eq!(inc, replay, "position {t}: ring must equal replay");
2646        }
2647    }
2648
2649    fn tiny_short_conv() -> (ShortConvWeights, ShortConvCfg) {
2650        let cfg = ShortConvCfg {
2651            hidden_size: 8,
2652            kernel: 3,
2653        };
2654        let synth = |rows: usize, cols: usize, salt: usize| {
2655            QTensor::from_f32(
2656                (0..rows * cols)
2657                    .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.5)
2658                    .collect(),
2659                rows,
2660                cols,
2661            )
2662        };
2663        let w = ShortConvWeights {
2664            in_proj: synth(3 * cfg.hidden_size, cfg.hidden_size, 1),
2665            conv: (0..cfg.hidden_size * cfg.kernel)
2666                .map(|i| ((i * 7 % 13) as f32 / 13.0 - 0.5) * 0.8)
2667                .collect(),
2668            out_proj: synth(cfg.hidden_size, cfg.hidden_size, 2),
2669        };
2670        (w, cfg)
2671    }
2672
2673    /// The incremental conv ring must equal a from-scratch causal replay
2674    /// of the prefix at every position — the decode/prefill contract.
2675    #[test]
2676    fn short_conv_ring_matches_explicit_causal_conv() {
2677        let (w, cfg) = tiny_short_conv();
2678        let seq: Vec<Vec<f32>> = (0..6)
2679            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.19).cos()).collect())
2680            .collect();
2681        let mut s_inc = Vec::new();
2682        for (t, x) in seq.iter().enumerate() {
2683            let inc = short_conv_forward(x, &w, &cfg, &mut s_inc, None);
2684            let mut s_replay = Vec::new();
2685            let mut replay = Vec::new();
2686            for xr in &seq[..=t] {
2687                replay = short_conv_forward(xr, &w, &cfg, &mut s_replay, None);
2688            }
2689            assert_eq!(inc, replay, "position {t}: ring must equal replay");
2690            assert_eq!(s_inc.len(), cfg.state_len());
2691        }
2692    }
2693
2694    /// The batched prefill path (matmat + sequential conv over the chunk)
2695    /// must reproduce the position-by-position decode path exactly.
2696    #[test]
2697    fn short_conv_batch_matches_sequential() {
2698        let (w, cfg) = tiny_short_conv();
2699        let b = 5;
2700        let xs: Vec<f32> = (0..b * cfg.hidden_size)
2701            .map(|i| (i as f32 * 0.13).sin() * 0.6)
2702            .collect();
2703
2704        let mut s_seq = Vec::new();
2705        let mut seq_out = vec![0.0f32; b * cfg.hidden_size];
2706        for bi in 0..b {
2707            let o = short_conv_forward(
2708                &xs[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size],
2709                &w,
2710                &cfg,
2711                &mut s_seq,
2712                None,
2713            );
2714            seq_out[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size].copy_from_slice(&o);
2715        }
2716
2717        let mut s_batch = Vec::new();
2718        let batch_out = short_conv_forward_batch(&xs, b, &w, &cfg, &mut s_batch, None);
2719        assert_eq!(
2720            seq_out, batch_out,
2721            "batch conv must match sequential decode"
2722        );
2723        assert_eq!(s_seq, s_batch, "ring state must match after the chunk");
2724    }
2725}