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 canonical 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//! Both cores implement the same contract: `*_forward` (one position,
21//! advances the state) and `*_pair` (fused two positions; lane 1
22//! commits, lane 2 is tentative in `scratch` for speculative verify).
23//! State lives in the layer's `linear_state: Vec<f32>` and is resized
24//! lazily by the core itself.
25
26use crate::pool::Pool;
27use crate::qtensor::QTensor;
28
29/// Weights of one vmf_phase layer (`model.layers.{i}.vmf_attn.*`).
30pub struct VmfPhaseWeights {
31    /// [nh·nphase, hidden] — query phase projection
32    pub thq: QTensor,
33    /// [nh·nphase, hidden] — key phase projection
34    pub thk: QTensor,
35    /// [nh·dv, hidden]
36    pub v_proj: QTensor,
37    /// [hidden, nh·dv]
38    pub out_proj: QTensor,
39    /// Per-component decay exp(−exp(A_log)), len nh·2·nphase (precomputed).
40    pub decay: Vec<f64>,
41    /// Short causal depthwise conv before the projections (embryo genomes
42    /// born with `--conv-k`): flat `[hidden·k]` taps, identity-initialised
43    /// at training start. The projections were TRAINED on the conv output —
44    /// running without it scrambles the layer (measured: train-val ppl 60
45    /// exported to runtime ppl 93 539). None = pre-conv genomes, untouched.
46    pub conv: Option<Vec<f32>>,
47    /// Selective-write input gate κ (hybrid_k core, stage 71): weight
48    /// [nh, hidden] + bias [nh]; κ_h = σ(W_k·x + b)_h multiplies the
49    /// state WRITE (S = decay·S + κ·φk⊗v). None = classic phase core,
50    /// bit-identical to the pre-κ kernel. Measured at mechanism level:
51    /// knee ×2–6 earlier, restores correlated-noise robustness, LM
52    /// crossover vs softmax at SEQ 512 (experiments/lc_final_merged.json).
53    pub k_gate: Option<(QTensor, Vec<f32>)>,
54}
55
56#[derive(Clone, Copy)]
57pub struct VmfPhaseCfg {
58    pub num_heads: usize,
59    pub nphase: usize,
60    pub value_head_dim: usize,
61    pub hidden_size: usize,
62    /// Phase-mass correction: scales the phase toward zero —
63    /// θ_eff = θ/(1+mass) — which widens the phase kernel.
64    /// Measured (experiments/vmf_native_core*.py) to restore noise
65    /// robustness when the phase projection is FIXED (exactly CMF's
66    /// fold-before-heal regime: thq/thk are init, not trained) — recall
67    /// 3%→91% at moderate noise; redundant once the projection is
68    /// healed. 0.0 = massless Goldstone (bit-identical to prior kernel).
69    /// Set via CMF_PHASE_MASS. Validated at mechanism level, not yet LM.
70    pub phase_mass: f32,
71}
72
73impl VmfPhaseCfg {
74    pub fn state_len(&self) -> usize {
75        self.num_heads * 2 * self.nphase * self.value_head_dim
76    }
77}
78
79/// One recurrent step for one head-set given projected phases/values.
80/// `state` is S[nh][p2, dv] stored f32 (per-element math in f64 — the
81/// storage halves, each step's arithmetic keeps the old precision).
82fn phase_step(
83    thq: &[f32],
84    thk: &[f32],
85    v: &[f32],
86    decay: &[f64],
87    kap: Option<&[f32]>,
88    cfg: &VmfPhaseCfg,
89    state: &mut [f32],
90    out: &mut [f32],
91) {
92    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
93    // Phase-mass correction: θ_eff = θ/(1+mass). mass=0 → factor 1 → no-op.
94    let mscale = 1.0f64 / (1.0 + cfg.phase_mass as f64);
95    let p2 = 2 * nph;
96    for h in 0..nh {
97        let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
98        let thk_h = &thk[h * nph..(h + 1) * nph];
99        let thq_h = &thq[h * nph..(h + 1) * nph];
100        let vt = &v[h * dv..(h + 1) * dv];
101        let ot = &mut out[h * dv..(h + 1) * dv];
102        let dec = &decay[h * p2..(h + 1) * p2];
103        // Selective write (hybrid_k): κ scales what enters the recurrent state.
104        let kh = kap.map_or(1.0f64, |k| k[h] as f64);
105        for f in 0..p2 {
106            // φ(θ) = [cos·nph, sin·nph], θ scaled by the correction factor.
107            let (fk, fq) = if f < nph {
108                (
109                    (thk_h[f] as f64 * mscale).cos(),
110                    (thq_h[f] as f64 * mscale).cos(),
111                )
112            } else {
113                (
114                    (thk_h[f - nph] as f64 * mscale).sin(),
115                    (thq_h[f - nph] as f64 * mscale).sin(),
116                )
117            };
118            let fkw = fk * kh;
119            let row = &mut s[f * dv..(f + 1) * dv];
120            let dcf = dec[f];
121            for d in 0..dv {
122                // S = decay·S + κ·φk⊗v (f64 math, f32 cell)
123                let cell = dcf * row[d] as f64 + fkw * vt[d] as f64;
124                row[d] = cell as f32;
125                ot[d] += (fq * cell) as f32; // o = Σ φq·S
126            }
127        }
128    }
129}
130
131/// κ_h = σ(W_k·x + b)_h — the per-head write gate (None when the layer
132/// has no k_gate tensors: classic phase core).
133fn kappa_of(x: &[f32], w: &VmfPhaseWeights, nh: usize, pool: Option<&Pool>) -> Option<Vec<f32>> {
134    let (kw, kb) = w.k_gate.as_ref()?;
135    let mut k = vec![0.0f32; nh];
136    kw.matvec(x, &mut k, pool);
137    for (v, b) in k.iter_mut().zip(kb) {
138        *v = 1.0 / (1.0 + (-(*v + b)).exp());
139    }
140    Some(k)
141}
142
143/// Causal depthwise conv over the mixer input, with the last k−1 inputs
144/// ringed at the TAIL of the layer state (oldest first). Returns the
145/// convolved input; a layer without conv taps passes through untouched.
146fn conv_in(
147    x: &[f32],
148    w: &VmfPhaseWeights,
149    cfg: &VmfPhaseCfg,
150    state: &mut Vec<f32>,
151) -> Option<Vec<f32>> {
152    let taps = w.conv.as_ref()?;
153    let h = cfg.hidden_size;
154    let k = taps.len() / h.max(1);
155    if k < 2 || taps.len() != h * k {
156        return None;
157    }
158    let ring = (k - 1) * h;
159    let base = cfg.state_len();
160    if state.len() != base + ring {
161        // The phase part resets alongside — a fresh sequence either way.
162        let mut ns = vec![0f32; base + ring];
163        let n = state.len().min(base);
164        ns[..n].copy_from_slice(&state[..n]);
165        *state = ns;
166    }
167    let mut y = vec![0.0f32; h];
168    for c in 0..h {
169        // taps j = 0..k−2 read the ring (oldest first), tap k−1 reads x.
170        let mut acc = taps[c * k + k - 1] * x[c];
171        for j in 0..k - 1 {
172            acc += taps[c * k + j] * state[base + j * h + c];
173        }
174        y[c] = acc;
175    }
176    conv_ring_push(x, h, base, state);
177    Some(y)
178}
179
180/// Rotate the conv ring at `base`: drop the oldest input, append `x`.
181fn conv_ring_push(x: &[f32], h: usize, base: usize, state: &mut [f32]) {
182    let ring = state.len() - base;
183    state.copy_within(base + h.., base);
184    let at = base + ring - h;
185    state[at..at + h].copy_from_slice(&x[..h]);
186}
187
188/// Forward one position through a vmf_phase layer, advancing `state`.
189pub fn vmf_phase_forward(
190    x: &[f32],
191    w: &VmfPhaseWeights,
192    cfg: &VmfPhaseCfg,
193    state: &mut Vec<f32>,
194    pool: Option<&Pool>,
195) -> Vec<f32> {
196    if w.conv.is_none() && state.len() != cfg.state_len() {
197        *state = vec![0f32; cfg.state_len()];
198    }
199    let xc = conv_in(x, w, cfg, state);
200    let x = xc.as_deref().unwrap_or(x);
201    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
202
203    let mut thq = vec![0.0f32; nh * nph];
204    w.thq.matvec(x, &mut thq, pool);
205    let mut thk = vec![0.0f32; nh * nph];
206    w.thk.matvec(x, &mut thk, pool);
207    let mut v = vec![0.0f32; nh * dv];
208    w.v_proj.matvec(x, &mut v, pool);
209
210    let kap = kappa_of(x, w, nh, pool);
211    let mut o = vec![0.0f32; nh * dv];
212    phase_step(&thq, &thk, &v, &w.decay, kap.as_deref(), cfg, state, &mut o);
213
214    let mut out = vec![0.0f32; cfg.hidden_size];
215    w.out_proj.matvec(&o, &mut out, pool);
216    out
217}
218
219/// Fused two-position forward (speculative verify). Lane 1 commits into
220/// `state` (its token is always committed); lane 2's tentative state
221/// goes into `scratch` — the caller swaps it in on draft acceptance and
222/// simply drops it on rejection.
223#[allow(clippy::too_many_arguments)]
224pub fn vmf_phase_pair(
225    x1: &[f32],
226    x2: &[f32],
227    w: &VmfPhaseWeights,
228    cfg: &VmfPhaseCfg,
229    state: &mut Vec<f32>,
230    scratch: &mut Vec<f32>,
231    pool: Option<&Pool>,
232) -> (Vec<f32>, Vec<f32>) {
233    if w.conv.is_none() && state.len() != cfg.state_len() {
234        *state = vec![0f32; cfg.state_len()];
235    }
236    // Lane 1 commits its ring advance into the real state; lane 2 works on
237    // the tentative copy exactly like the phase state itself.
238    let xc1 = conv_in(x1, w, cfg, state);
239    let x1 = xc1.as_deref().unwrap_or(x1);
240    let (xc2, x2raw) = if w.conv.is_some() {
241        let mut tmp = state.clone();
242        (conv_in(x2, w, cfg, &mut tmp), Some(x2))
243    } else {
244        (None, None)
245    };
246    let x2 = xc2.as_deref().unwrap_or(x2);
247    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
248
249    let mut thq1 = vec![0.0f32; nh * nph];
250    let mut thq2 = vec![0.0f32; nh * nph];
251    w.thq.matvec2(x1, x2, &mut thq1, &mut thq2, pool);
252    let mut thk1 = vec![0.0f32; nh * nph];
253    let mut thk2 = vec![0.0f32; nh * nph];
254    w.thk.matvec2(x1, x2, &mut thk1, &mut thk2, pool);
255    let mut v1 = vec![0.0f32; nh * dv];
256    let mut v2 = vec![0.0f32; nh * dv];
257    w.v_proj.matvec2(x1, x2, &mut v1, &mut v2, pool);
258
259    // Lane 1 commits into the real state.
260    let kap1 = kappa_of(x1, w, nh, pool);
261    let mut o1 = vec![0.0f32; nh * dv];
262    phase_step(
263        &thq1,
264        &thk1,
265        &v1,
266        &w.decay,
267        kap1.as_deref(),
268        cfg,
269        state,
270        &mut o1,
271    );
272
273    // Lane 2 runs on a copy — tentative until the draft is verified.
274    let kap2 = kappa_of(x2, w, nh, pool);
275    scratch.clear();
276    scratch.extend_from_slice(state);
277    let mut o2 = vec![0.0f32; nh * dv];
278    phase_step(
279        &thq2,
280        &thk2,
281        &v2,
282        &w.decay,
283        kap2.as_deref(),
284        cfg,
285        scratch,
286        &mut o2,
287    );
288    // The scratch copy above re-took state's ring (advanced only through
289    // x1); commit x2's advance so an accepted draft leaves a correct ring.
290    if let Some(xr) = x2raw {
291        conv_ring_push(xr, cfg.hidden_size, cfg.state_len(), scratch);
292    }
293
294    let mut out1 = vec![0.0f32; cfg.hidden_size];
295    let mut out2 = vec![0.0f32; cfg.hidden_size];
296    w.out_proj.matvec2(&o1, &o2, &mut out1, &mut out2, pool);
297    (out1, out2)
298}
299
300// ───────────────────────── GatedDeltaNet (faithful vendor operator) ─────────────────────────
301
302/// Weights of one GatedDeltaNet layer (`model.layers.{i}.linear_attn.*`,
303/// names 1:1 with the source model — no fold, no training).
304pub struct GdnWeights {
305    /// [2·nk·dk + nv·dv, hidden] — fused q/k/v projection
306    pub in_proj_qkv: QTensor,
307    /// [nv·dv, hidden] — output-gate projection z
308    pub in_proj_z: QTensor,
309    /// [nv, hidden] — decay modulation a
310    pub in_proj_a: QTensor,
311    /// [nv, hidden] — write-strength b (β = σ(b))
312    pub in_proj_b: QTensor,
313    /// [c_dim · kk] — depthwise causal conv taps, flattened [c][tap]
314    pub conv1d: Vec<f32>,
315    /// [nv]
316    pub a_log: Vec<f32>,
317    /// [nv]
318    pub dt_bias: Vec<f32>,
319    /// [dv] — gated RMSNorm weight (plain x̂·w, validated by the oracle)
320    pub norm: Vec<f32>,
321    /// [hidden, nv·dv]
322    pub out_proj: QTensor,
323}
324
325#[derive(Clone, Copy)]
326pub struct GdnCfg {
327    pub num_v_heads: usize,
328    pub num_k_heads: usize,
329    pub key_head_dim: usize,
330    pub value_head_dim: usize,
331    pub conv_kernel: usize,
332    pub hidden_size: usize,
333    pub rms_eps: f64,
334    /// Qwen4-exp trains the output gate as sigmoid(z); older GDN families
335    /// use SiLU(z). This is part of the operator, not a sampling option.
336    pub output_gate_sigmoid: bool,
337}
338
339impl GdnCfg {
340    pub fn conv_dim(&self) -> usize {
341        2 * self.num_k_heads * self.key_head_dim + self.num_v_heads * self.value_head_dim
342    }
343
344    /// Packed state: [conv ring (kk−1)·c_dim | S nv·dk·dv], one Vec<f64>
345    /// so the speculative scratch-swap moves ring and recurrent state together.
346    pub fn state_len(&self) -> usize {
347        (self.conv_kernel - 1) * self.conv_dim()
348            + self.num_v_heads * self.key_head_dim * self.value_head_dim
349    }
350}
351
352fn softplus(x: f64) -> f64 {
353    if x > 20.0 { x } else { x.exp().ln_1p() }
354}
355
356fn sigmoid(x: f64) -> f64 {
357    1.0 / (1.0 + (-x).exp())
358}
359
360fn silu(x: f64) -> f64 {
361    x / (1.0 + (-x).exp())
362}
363
364/// `*mut f32` that may cross worker threads; safety comes from the
365/// disjoint (head, element) ranges each worker writes.
366#[derive(Clone, Copy)]
367struct SendMutF32(*mut f32);
368unsafe impl Send for SendMutF32 {}
369unsafe impl Sync for SendMutF32 {}
370
371/// One recurrent step given the raw (pre-conv) projections of this
372/// position. Advances the packed state (conv ring + S) and writes the
373/// gated per-head output into `of` [nv·dv].
374///
375/// The recurrent-state math runs in f32 (the vendor operator's own dtype —
376/// `mamba_ssm_dtype: float32` in the source configs; the old f64 was
377/// over-precision at 4× the traffic and no SIMD). The two S passes are
378/// element-wise in `dj` with no cross-lane reduction, so LLVM
379/// auto-vectorizes them (fmla on NEON, FMA on AVX2). Heads are
380/// independent given the conv output and run across the pool — on a
381/// Qwen3.5-27B this loop is 48 heads × 128×128 × 48 layers per token,
382/// the single biggest serial block in the hybrid's decode.
383#[allow(clippy::too_many_arguments)]
384fn gdn_step(
385    qkv: &[f32],
386    z: &[f32],
387    a: &[f32],
388    b: &[f32],
389    w: &GdnWeights,
390    cfg: &GdnCfg,
391    state: &mut [f32],
392    of: &mut [f32],
393    pool: Option<&Pool>,
394) {
395    let (nv, nk, dk, dv, kk) = (
396        cfg.num_v_heads,
397        cfg.num_k_heads,
398        cfg.key_head_dim,
399        cfg.value_head_dim,
400        cfg.conv_kernel,
401    );
402    let c_dim = cfg.conv_dim();
403    let (kd, rep) = (nk * dk, nv / nk);
404    let (ring, s_all) = state.split_at_mut((kk - 1) * c_dim);
405
406    // Depthwise causal conv over [ring…, current] + SiLU. Taps are
407    // ordered oldest→newest; tap kk−1 multiplies the current position.
408    // (Tiny: c_dim × kk — f64 accumulation kept.)
409    let mut cq = vec![0f32; c_dim];
410    for c in 0..c_dim {
411        let taps = &w.conv1d[c * kk..(c + 1) * kk];
412        let mut acc = qkv[c] as f64 * taps[kk - 1] as f64;
413        for j in 0..kk - 1 {
414            acc += ring[j * c_dim + c] as f64 * taps[j] as f64;
415        }
416        cq[c] = silu(acc) as f32;
417    }
418    // Ring shift: drop the oldest position, append the raw current one.
419    if kk > 1 {
420        ring.copy_within(c_dim.., 0);
421        let tail = (kk - 2) * c_dim;
422        ring[tail..tail + c_dim].copy_from_slice(&qkv[..c_dim]);
423    }
424
425    let cq = &cq;
426    let s_ptr = SendMutF32(s_all.as_mut_ptr());
427    let of_ptr = SendMutF32(of.as_mut_ptr());
428    let head_range = |h0: usize, h1: usize| {
429        // Rebind the Sync wrappers whole — edition-2021 disjoint capture
430        // would otherwise grab the raw `.0` fields and lose Send/Sync.
431        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
432        // Per-worker scratch, recycled across calls (thread-local freelists).
433        let mut kv = crate::attention::take_buf(dv);
434        let mut delta = crate::attention::take_buf(dv);
435        let mut o = crate::attention::take_buf(dv);
436        let mut kf = crate::attention::take_buf(dk);
437        let mut qf = crate::attention::take_buf(dk);
438        for h in h0..h1 {
439            let ko = h / rep; // source q/k head (GQA)
440            let (qs, ks) = (ko * dk, kd + ko * dk);
441            // l2-normalize q and k; q additionally scaled by 1/√dk.
442            let (mut nq, mut nkn) = (0f64, 0f64);
443            for d in 0..dk {
444                nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
445                nkn += (cq[ks + d] as f64) * (cq[ks + d] as f64);
446            }
447            let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
448            let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
449            for d in 0..dk {
450                qf[d] = cq[qs + d] * invq;
451                kf[d] = cq[ks + d] * invk;
452            }
453
454            let g = (-(w.a_log[h] as f64).exp() * softplus(a[h] as f64 + w.dt_bias[h] as f64)).exp()
455                as f32;
456            let beta = sigmoid(b[h] as f64) as f32;
457
458            // SAFETY: disjoint per-head S and output slices per worker.
459            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
460            let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
461            let vt = &cq[2 * kd + h * dv..2 * kd + (h + 1) * dv];
462
463            // S ← g·S;  kv = kᵀS;  S += k ⊗ β(v − kv);  o = qᵀS —
464            // algebraically regrouped so S is READ twice and WRITTEN
465            // once: kv over S_old (then ×g), one fused update+query pass.
466            kv[..dv].fill(0.0);
467            for di in 0..dk {
468                let kfd = kf[di];
469                let row = &s[di * dv..(di + 1) * dv];
470                for dj in 0..dv {
471                    kv[dj] += row[dj] * kfd; // elementwise in dj → SIMD
472                }
473            }
474            for dj in 0..dv {
475                delta[dj] = (vt[dj] - g * kv[dj]) * beta;
476            }
477            o[..dv].fill(0.0);
478            for di in 0..dk {
479                let kfd = kf[di];
480                let qfd = qf[di];
481                let row = &mut s[di * dv..(di + 1) * dv];
482                for dj in 0..dv {
483                    let cell = g * row[dj] + kfd * delta[dj];
484                    row[dj] = cell;
485                    o[dj] += qfd * cell; // elementwise in dj → SIMD
486                }
487            }
488            // Gated RMSNorm per head: x̂·w·silu(z) (oracle-validated form).
489            let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
490            let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
491            for dj in 0..dv {
492                let gate = if cfg.output_gate_sigmoid {
493                    sigmoid(z[h * dv + dj] as f64)
494                } else {
495                    silu(z[h * dv + dj] as f64)
496                };
497                oh[dj] = ((o[dj] as f64 * inv) * w.norm[dj] as f64 * gate) as f32;
498            }
499        }
500        crate::attention::recycle_buf(&mut kv);
501        crate::attention::recycle_buf(&mut delta);
502        crate::attention::recycle_buf(&mut o);
503        crate::attention::recycle_buf(&mut kf);
504        crate::attention::recycle_buf(&mut qf);
505    };
506    match pool {
507        Some(pool) if nv >= 4 => pool.run(&|widx, n| {
508            let chunk = nv.div_ceil(n);
509            let h0 = (widx * chunk).min(nv);
510            let h1 = (h0 + chunk).min(nv);
511            if h0 < h1 {
512                head_range(h0, h1);
513            }
514        }),
515        _ => head_range(0, nv),
516    }
517}
518
519/// Forward one position through a GatedDeltaNet layer, advancing `state`.
520pub fn gdn_forward(
521    x: &[f32],
522    w: &GdnWeights,
523    cfg: &GdnCfg,
524    state: &mut Vec<f32>,
525    pool: Option<&Pool>,
526) -> Vec<f32> {
527    if state.len() != cfg.state_len() {
528        *state = vec![0f32; cfg.state_len()];
529    }
530    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
531
532    let mut qkv = vec![0.0f32; c_dim];
533    let mut z = vec![0.0f32; vd];
534    let mut a = vec![0.0f32; cfg.num_v_heads];
535    let mut b = vec![0.0f32; cfg.num_v_heads];
536    // D5: two heavy projections (the GDN mixer is ~half a hybrid layer's
537    // bytes) — one GPU submission; a/b are tiny and stay on CPU. The
538    // Batch probe arbitrates GPU vs the fused-CPU dispatch per machine.
539    let cpu_projs = |qkv: &mut Vec<f32>, z: &mut Vec<f32>, a: &mut Vec<f32>, b: &mut Vec<f32>| {
540        QTensor::matvec_many(
541            [&w.in_proj_qkv, &w.in_proj_z, &w.in_proj_a, &w.in_proj_b],
542            x,
543            [
544                qkv.as_mut_slice(),
545                z.as_mut_slice(),
546                a.as_mut_slice(),
547                b.as_mut_slice(),
548            ],
549            pool,
550        );
551    };
552    let mut done = false;
553    if crate::gpu::enabled_here() && gdn_projs_eligible(w) {
554        match crate::gpu::probe_arm(crate::gpu::OpClass::Batch) {
555            crate::gpu::ProbeArm::Gpu => {
556                let t0 = std::time::Instant::now();
557                if gdn_projs_gpu(w, x, &mut qkv, &mut z) {
558                    crate::gpu::probe_record(crate::gpu::OpClass::Batch, true, t0.elapsed());
559                    w.in_proj_a.matvec(x, &mut a, pool);
560                    w.in_proj_b.matvec(x, &mut b, pool);
561                    done = true;
562                } else {
563                    crate::gpu::probe_note_decline(crate::gpu::OpClass::Batch);
564                }
565            }
566            crate::gpu::ProbeArm::CpuTimed => {
567                let t0 = std::time::Instant::now();
568                crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
569                crate::gpu::probe_record(crate::gpu::OpClass::Batch, false, t0.elapsed());
570                done = true;
571            }
572            crate::gpu::ProbeArm::Cpu => {
573                crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
574                done = true;
575            }
576        }
577    }
578    if !done {
579        cpu_projs(&mut qkv, &mut z, &mut a, &mut b);
580    }
581
582    let mut of = vec![0.0f32; vd];
583    gdn_step(&qkv, &z, &a, &b, w, cfg, state, &mut of, pool);
584
585    let mut out = vec![0.0f32; cfg.hidden_size];
586    w.out_proj.matvec(&of, &mut out, pool);
587    out
588}
589
590/// Batched GDN forward (prefill-GEMM): the qkv/z/a/b and out_proj
591/// projections are matmat over the batch (a weight row once per chunk),
592/// the gdn_step recurrence runs sequentially over positions (state is the
593/// same as the sequential path; the math is elementwise identical).
594pub fn gdn_forward_batch(
595    xs: &[f32],
596    b: usize,
597    w: &GdnWeights,
598    cfg: &GdnCfg,
599    state: &mut Vec<f32>,
600    pool: Option<&Pool>,
601) -> Vec<f32> {
602    if state.len() != cfg.state_len() {
603        *state = vec![0f32; cfg.state_len()];
604    }
605    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
606    let nv = cfg.num_v_heads;
607
608    let mut qkv = vec![0.0f32; b * c_dim];
609    w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
610    let mut z = vec![0.0f32; b * vd];
611    w.in_proj_z.matmat(xs, b, &mut z, pool);
612    let mut a = vec![0.0f32; b * nv];
613    w.in_proj_a.matmat(xs, b, &mut a, pool);
614    let mut bb = vec![0.0f32; b * nv];
615    w.in_proj_b.matmat(xs, b, &mut bb, pool);
616
617    let mut of = vec![0.0f32; b * vd];
618    for bi in 0..b {
619        gdn_step(
620            &qkv[bi * c_dim..(bi + 1) * c_dim],
621            &z[bi * vd..(bi + 1) * vd],
622            &a[bi * nv..(bi + 1) * nv],
623            &bb[bi * nv..(bi + 1) * nv],
624            w,
625            cfg,
626            state,
627            &mut of[bi * vd..(bi + 1) * vd],
628            pool,
629        );
630    }
631    let mut out = vec![0.0f32; b * cfg.hidden_size];
632    w.out_proj.matmat(&of, b, &mut out, pool);
633    if std::env::var("CMF_GDN_TRACE").is_ok() {
634        let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
635        eprintln!(
636            "gdn-batch b={b}: |x0|={:.5} |qkv0|={:.5} |z0|={:.5} |a0|={:.5} |b0|={:.5} |of0|={:.5} |out0|={:.5} |state|={:.5}",
637            n(&xs[..cfg.hidden_size]),
638            n(&qkv[..c_dim]),
639            n(&z[..vd]),
640            n(&a[..nv]),
641            n(&bb[..nv]),
642            n(&of[..vd]),
643            n(&out[..cfg.hidden_size]),
644            n(state)
645        );
646    }
647    out
648}
649
650/// GDN qkv+z GPU eligibility: q1 mixers offload by default (the CPU q1
651/// kernel is compute-bound); q8 stays opt-in via CMF_GPU_GDN=1 (measured
652/// neutral). The probe in `gdn_forward` still arbitrates either way.
653fn gdn_projs_eligible(w: &GdnWeights) -> bool {
654    // Any tile-embedded-scale layout, not just q1: the batched matvec now has
655    // kernels for all of them, and the probe arbitrates whether it pays.
656    // `CMF_GPU_GDN=0` forces the CPU projections back — the switch exists so
657    // the two arms can be compared inside ONE run: this machine throttles far
658    // enough that measurements minutes apart are not comparable.
659    if std::env::var("CMF_GPU_GDN")
660        .map(|v| v == "0")
661        .unwrap_or(false)
662    {
663        return false;
664    }
665    w.in_proj_qkv.is_q1()
666        || w.in_proj_qkv.q4t_parts().is_some()
667        || w.in_proj_qkv.q4tp_parts().is_some()
668        || std::env::var("CMF_GPU_GDN")
669            .map(|v| v == "1")
670            .unwrap_or(false)
671}
672
673/// GDN qkv+z on GPU in a single submission (independent matvecs of one input).
674fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
675    use crate::gpu::matvec_batch;
676    use crate::qtensor::QTensor;
677    if !crate::gpu::enabled_here() {
678        return false;
679    }
680    fn part<'a>(
681        t: &'a QTensor,
682        x: &[f32],
683    ) -> Option<(
684        std::sync::Arc<cortiq_core::CmfModel>,
685        crate::gpu::BatchJob<'a>,
686    )> {
687        use crate::gpu::BatchJob;
688        use crate::qtensor::prescale;
689        use cortiq_core::TensorDtype;
690        match t {
691            QTensor::Mapped {
692                model,
693                idx,
694                dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
695                rows,
696                cols,
697                row_scale,
698                col_field,
699                ..
700            } => Some((
701                model.clone(),
702                BatchJob {
703                    idx: *idx,
704                    rows: *rows,
705                    cols: *cols,
706                    row_scale,
707                    xs: prescale(x, col_field, *dt).into_owned(),
708                    layout: crate::gpu::BatchLayout::Q8,
709                },
710            )),
711            QTensor::Mapped {
712                model,
713                idx,
714                dtype: TensorDtype::Q1,
715                rows,
716                cols,
717                ..
718            } => Some((
719                model.clone(),
720                BatchJob {
721                    idx: *idx,
722                    rows: *rows,
723                    cols: *cols,
724                    row_scale: &[],
725                    xs: x.to_vec(),
726                    layout: crate::gpu::BatchLayout::Q1,
727                },
728            )),
729            // q4t/q4tp: same tile-embedded-scale contract as q1, different
730            // stride. Missing here, a hybrid model's GDN projections fell to
731            // the CPU on every layer while its experts rode the device.
732            QTensor::Mapped {
733                model,
734                idx,
735                dtype: dt @ (TensorDtype::Q4Tiled | TensorDtype::Q4TiledP),
736                rows,
737                cols,
738                ..
739            } => Some((
740                model.clone(),
741                BatchJob {
742                    idx: *idx,
743                    rows: *rows,
744                    cols: *cols,
745                    row_scale: &[],
746                    xs: x.to_vec(),
747                    layout: if *dt == TensorDtype::Q4TiledP {
748                        crate::gpu::BatchLayout::Q4tp
749                    } else {
750                        crate::gpu::BatchLayout::Q4t
751                    },
752                },
753            )),
754            _ => None,
755        }
756    }
757    let Some((model, jq)) = part(&w.in_proj_qkv, x) else {
758        return false;
759    };
760    let Some((_, jz)) = part(&w.in_proj_z, x) else {
761        return false;
762    };
763    matvec_batch(&model, &[jq, jz], &mut [qkv, z])
764}
765
766/// Fused two-position forward (speculative verify): lane 1 commits into
767/// `state`, lane 2 is tentative in `scratch` (ring + S move together).
768#[allow(clippy::too_many_arguments)]
769pub fn gdn_pair(
770    x1: &[f32],
771    x2: &[f32],
772    w: &GdnWeights,
773    cfg: &GdnCfg,
774    state: &mut Vec<f32>,
775    scratch: &mut Vec<f32>,
776    pool: Option<&Pool>,
777) -> (Vec<f32>, Vec<f32>) {
778    if state.len() != cfg.state_len() {
779        *state = vec![0f32; cfg.state_len()];
780    }
781    let (c_dim, vd, nv) = (
782        cfg.conv_dim(),
783        cfg.num_v_heads * cfg.value_head_dim,
784        cfg.num_v_heads,
785    );
786
787    let mut qkv1 = vec![0.0f32; c_dim];
788    let mut qkv2 = vec![0.0f32; c_dim];
789    w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
790    let mut z1 = vec![0.0f32; vd];
791    let mut z2 = vec![0.0f32; vd];
792    w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
793    let mut a1 = vec![0.0f32; nv];
794    let mut a2 = vec![0.0f32; nv];
795    w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
796    let mut b1 = vec![0.0f32; nv];
797    let mut b2 = vec![0.0f32; nv];
798    w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
799
800    let mut of1 = vec![0.0f32; vd];
801    gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1, pool);
802
803    scratch.clear();
804    scratch.extend_from_slice(state);
805    let mut of2 = vec![0.0f32; vd];
806    gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2, pool);
807
808    let mut out1 = vec![0.0f32; cfg.hidden_size];
809    let mut out2 = vec![0.0f32; cfg.hidden_size];
810    w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
811    (out1, out2)
812}
813
814// ───────────────────────── ShortConv (LFM2 gated short convolution) ─────────────────────────
815
816/// Weights of one LFM2 short-convolution mixer
817/// (`model.layers.{i}.short_conv.*`, renamed from the vendor `conv.*` at
818/// convert time). No recurrent mixer state — the only state is the causal
819/// conv ring (the last `kernel−1` gated inputs per channel).
820pub struct ShortConvWeights {
821    /// [3·hidden, hidden] — fused (B, C, x) projection.
822    pub in_proj: QTensor,
823    /// [hidden · kernel] depthwise conv taps, flattened `[channel][tap]`
824    /// (the source `[hidden, 1, kernel]` with the singleton group axis
825    /// dropped). Tap `kernel−1` multiplies the current position.
826    pub conv: Vec<f32>,
827    /// [hidden, hidden] — output projection.
828    pub out_proj: QTensor,
829}
830
831#[derive(Clone, Copy)]
832pub struct ShortConvCfg {
833    pub hidden_size: usize,
834    /// Conv kernel width `L` (`conv_L_cache`; LFM2 uses 3).
835    pub kernel: usize,
836}
837
838impl ShortConvCfg {
839    /// Conv ring: the last `kernel−1` gated inputs per channel.
840    pub fn state_len(&self) -> usize {
841        (self.kernel - 1) * self.hidden_size
842    }
843}
844
845/// One position through the gated conv, given the fused projection
846/// `bcx = in_proj·x` [3·hidden] = [B | C | x]. Advances the conv ring and
847/// writes the gated conv output `y = C ⊙ conv(B ⊙ x)` [hidden] into `y`.
848///
849/// The conv is PyTorch's causal depthwise `Conv1d(padding=kernel−1)`
850/// truncated to the current length: for tap `k`, weight `w[c][k]` pairs
851/// with the input `kernel−1−k` steps in the past, so `w[c][kernel−1]` is
852/// the current position. The ring holds `in[t−1] … in[t−(kernel−1)]` at
853/// slots `0 … kernel−2`.
854fn short_conv_step(
855    bcx: &[f32],
856    conv: &[f32],
857    cfg: &ShortConvCfg,
858    ring_state: &mut [f32],
859    y: &mut [f32],
860) {
861    let (h, k) = (cfg.hidden_size, cfg.kernel);
862    let ring = k - 1;
863    let (bg, cg, xg) = (&bcx[0..h], &bcx[h..2 * h], &bcx[2 * h..3 * h]);
864    for c in 0..h {
865        let bx = bg[c] * xg[c];
866        let wc = &conv[c * k..(c + 1) * k];
867        // Current tap, then the past taps read from the channel's ring.
868        let mut acc = wc[k - 1] * bx;
869        let rc = &mut ring_state[c * ring..c * ring + ring];
870        for s in 0..ring {
871            acc += wc[k - 2 - s] * rc[s];
872        }
873        y[c] = cg[c] * acc;
874        // Shift newest-in-front: slot 0 becomes the just-seen input.
875        for s in (1..ring).rev() {
876            rc[s] = rc[s - 1];
877        }
878        if ring > 0 {
879            rc[0] = bx;
880        }
881    }
882}
883
884/// Forward one position through a short-conv layer, advancing `state`.
885pub fn short_conv_forward(
886    x: &[f32],
887    w: &ShortConvWeights,
888    cfg: &ShortConvCfg,
889    state: &mut Vec<f32>,
890    pool: Option<&Pool>,
891) -> Vec<f32> {
892    if state.len() != cfg.state_len() {
893        *state = vec![0f32; cfg.state_len()];
894    }
895    let h = cfg.hidden_size;
896    let mut bcx = vec![0.0f32; 3 * h];
897    w.in_proj.matvec(x, &mut bcx, pool);
898    let mut y = vec![0.0f32; h];
899    short_conv_step(&bcx, &w.conv, cfg, state, &mut y);
900    let mut out = vec![0.0f32; h];
901    w.out_proj.matvec(&y, &mut out, pool);
902    out
903}
904
905/// Batched short-conv forward (prefill-GEMM): in_proj/out_proj are matmat
906/// over the chunk (a weight row streamed once), the conv walks the
907/// positions in order — the chunk is contiguous, so the ring state is
908/// exactly the sequential path's and the math is elementwise identical.
909pub fn short_conv_forward_batch(
910    xs: &[f32],
911    b: usize,
912    w: &ShortConvWeights,
913    cfg: &ShortConvCfg,
914    state: &mut Vec<f32>,
915    pool: Option<&Pool>,
916) -> Vec<f32> {
917    if state.len() != cfg.state_len() {
918        *state = vec![0f32; cfg.state_len()];
919    }
920    let h = cfg.hidden_size;
921    let mut bcx = vec![0.0f32; b * 3 * h];
922    w.in_proj.matmat(xs, b, &mut bcx, pool);
923    let mut y = vec![0.0f32; b * h];
924    for bi in 0..b {
925        short_conv_step(
926            &bcx[bi * 3 * h..(bi + 1) * 3 * h],
927            &w.conv,
928            cfg,
929            state,
930            &mut y[bi * h..(bi + 1) * h],
931        );
932    }
933    let mut out = vec![0.0f32; b * h];
934    w.out_proj.matmat(&y, b, &mut out, pool);
935    out
936}
937
938/// Fused two-position forward (speculative verify). Lane 1 commits into
939/// `state`; lane 2's tentative ring goes into `scratch` — swapped in on
940/// draft acceptance, dropped on rejection. LFM2 ships no MTP head, so this
941/// is exercised only by the pair-fusion micro-benchmark; kept correct.
942#[allow(clippy::too_many_arguments)]
943pub fn short_conv_pair(
944    x1: &[f32],
945    x2: &[f32],
946    w: &ShortConvWeights,
947    cfg: &ShortConvCfg,
948    state: &mut Vec<f32>,
949    scratch: &mut Vec<f32>,
950    pool: Option<&Pool>,
951) -> (Vec<f32>, Vec<f32>) {
952    if state.len() != cfg.state_len() {
953        *state = vec![0f32; cfg.state_len()];
954    }
955    let h = cfg.hidden_size;
956    let mut bcx1 = vec![0.0f32; 3 * h];
957    let mut bcx2 = vec![0.0f32; 3 * h];
958    w.in_proj.matvec2(x1, x2, &mut bcx1, &mut bcx2, pool);
959
960    let mut y1 = vec![0.0f32; h];
961    short_conv_step(&bcx1, &w.conv, cfg, state, &mut y1);
962    scratch.clear();
963    scratch.extend_from_slice(state);
964    let mut y2 = vec![0.0f32; h];
965    short_conv_step(&bcx2, &w.conv, cfg, scratch, &mut y2);
966
967    let mut out1 = vec![0.0f32; h];
968    let mut out2 = vec![0.0f32; h];
969    w.out_proj.matvec2(&y1, &y2, &mut out1, &mut out2, pool);
970    (out1, out2)
971}
972
973// ─── Kimi Delta Attention (KDA) ─────────────────────────────────────────
974//
975// Kimi Linear / Kimi-K3 linear mixer (reference: FLA naive_recurrent_kda
976// + moonshotai modeling_kimi.py). Differences from GatedDeltaNet above:
977// separate q/k/v projections each behind its OWN causal depthwise short
978// convolution; the delta-rule decay is a PER-CHANNEL vector (diagonal)
979// instead of a per-head scalar; the decay pre-activation comes from a
980// low-rank projection f_b(f_a(x)); and the output gate norm uses
981// sigmoid, not SiLU.
982
983pub struct KdaWeights {
984    /// [nh·dk, hidden]
985    pub q_proj: QTensor,
986    /// [nh·dk, hidden]
987    pub k_proj: QTensor,
988    /// [nh·dv, hidden]
989    pub v_proj: QTensor,
990    /// [nh·dk × kk] — depthwise taps, oldest→newest (see GdnWeights.conv1d)
991    pub conv_q: Vec<f32>,
992    pub conv_k: Vec<f32>,
993    /// [nh·dv × kk]
994    pub conv_v: Vec<f32>,
995    /// [rank, hidden] — low-rank decay projection, stage 1
996    pub f_a: QTensor,
997    /// [nh·dk, rank] — stage 2
998    pub f_b: QTensor,
999    /// [nh·dk]
1000    pub dt_bias: Vec<f32>,
1001    /// [nh] per-head (Kimi-Linear-48B) | [dk] per-dim (Kimi-K3) |
1002    /// [nh·dk] full — broadcast resolved by length.
1003    pub a_log: Vec<f32>,
1004    /// [nh, hidden] — β = σ(b_proj·x) per head
1005    pub b_proj: QTensor,
1006    /// Output gate: full-rank g_proj (K3) or low-rank g_b(g_a(x)) (48B).
1007    pub gate: KdaOutGate,
1008    /// [dv] — gated RMSNorm weight (per head over head_v_dim)
1009    pub o_norm: Vec<f32>,
1010    /// [hidden, nh·dv]
1011    pub o_proj: QTensor,
1012    /// Some(lb): log-decay = lb·σ(exp(A)·(f+bias)) (K3, lb=−5);
1013    /// None: −exp(A)·softplus(f+bias) (Kimi-Linear-48B).
1014    pub gate_lower_bound: Option<f32>,
1015}
1016
1017pub enum KdaOutGate {
1018    /// [nh·dv, hidden]
1019    Full(QTensor),
1020    /// g_a [rank, hidden], g_b [nh·dv, rank]
1021    LowRank(QTensor, QTensor),
1022}
1023
1024#[derive(Clone, Copy)]
1025pub struct KdaCfg {
1026    pub num_heads: usize,
1027    pub head_k_dim: usize,
1028    pub head_v_dim: usize,
1029    pub conv_kernel: usize,
1030    pub hidden_size: usize,
1031    pub rms_eps: f64,
1032}
1033
1034impl KdaCfg {
1035    /// Packed state: [q ring | k ring | v ring | S nh·dk·dv], one Vec —
1036    /// same single-buffer convention as GdnCfg::state_len.
1037    pub fn state_len(&self) -> usize {
1038        let (nh, dk, dv, kk) = (
1039            self.num_heads,
1040            self.head_k_dim,
1041            self.head_v_dim,
1042            self.conv_kernel,
1043        );
1044        (kk - 1) * (2 * nh * dk + nh * dv) + nh * dk * dv
1045    }
1046}
1047
1048/// Depthwise causal conv over [ring…, current] + SiLU, then ring shift.
1049/// Taps oldest→newest, tap kk−1 multiplies the current position.
1050fn kda_conv(raw: &[f32], taps: &[f32], ring: &mut [f32], kk: usize, out: &mut [f32]) {
1051    let c_dim = raw.len();
1052    for c in 0..c_dim {
1053        let t = &taps[c * kk..(c + 1) * kk];
1054        let mut acc = raw[c] as f64 * t[kk - 1] as f64;
1055        for j in 0..kk - 1 {
1056            acc += ring[j * c_dim + c] as f64 * t[j] as f64;
1057        }
1058        out[c] = silu(acc) as f32;
1059    }
1060    if kk > 1 {
1061        ring.copy_within(c_dim.., 0);
1062        let tail = (kk - 2) * c_dim;
1063        ring[tail..tail + c_dim].copy_from_slice(raw);
1064    }
1065}
1066
1067/// Per-channel log-decay for head-channel (h, d): resolves the A_log
1068/// broadcast by length and applies the configured gate formula.
1069#[inline]
1070fn kda_log_decay(w: &KdaWeights, cfg: &KdaCfg, h: usize, d: usize, f: f32) -> f64 {
1071    let (nh, dk) = (cfg.num_heads, cfg.head_k_dim);
1072    let a = if w.a_log.len() == nh {
1073        w.a_log[h] as f64
1074    } else if w.a_log.len() == dk {
1075        w.a_log[d] as f64
1076    } else {
1077        w.a_log[h * dk + d] as f64
1078    };
1079    let raw = f as f64 + w.dt_bias[h * dk + d] as f64;
1080    match w.gate_lower_bound {
1081        Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1082        None => -a.exp() * softplus(raw),
1083    }
1084}
1085
1086/// One recurrent step given this position's raw (pre-conv) projections.
1087/// Advances the packed state and writes the gated per-head output into
1088/// `of` [nh·dv]. Recurrence (FLA naive_recurrent_kda):
1089///   S ← Diag(exp(g))·S;  S += β·k ⊗ (v − kᵀS);  o = qᵀS
1090/// with q,k L2-normalized per head and q additionally scaled by 1/√dk —
1091/// regrouped into two S passes like gdn_step (per-channel decay folds
1092/// into the k readout of the first pass).
1093#[allow(clippy::too_many_arguments)]
1094fn kda_step(
1095    xq: &[f32],
1096    xk: &[f32],
1097    xv: &[f32],
1098    f: &[f32],
1099    b: &[f32],
1100    gate_out: &[f32],
1101    w: &KdaWeights,
1102    cfg: &KdaCfg,
1103    state: &mut [f32],
1104    of: &mut [f32],
1105    pool: Option<&Pool>,
1106) {
1107    let (nh, dk, dv, kk) = (
1108        cfg.num_heads,
1109        cfg.head_k_dim,
1110        cfg.head_v_dim,
1111        cfg.conv_kernel,
1112    );
1113    let (kd, vd) = (nh * dk, nh * dv);
1114    let ring_q_len = (kk - 1) * kd;
1115    let ring_v_len = (kk - 1) * vd;
1116    let (ring_q, rest) = state.split_at_mut(ring_q_len);
1117    let (ring_k, rest) = rest.split_at_mut(ring_q_len);
1118    let (ring_v, s_all) = rest.split_at_mut(ring_v_len);
1119
1120    let mut cq = vec![0f32; kd];
1121    let mut ck = vec![0f32; kd];
1122    let mut cv = vec![0f32; vd];
1123    kda_conv(xq, &w.conv_q, ring_q, kk, &mut cq);
1124    kda_conv(xk, &w.conv_k, ring_k, kk, &mut ck);
1125    kda_conv(xv, &w.conv_v, ring_v, kk, &mut cv);
1126
1127    let (cq, ck, cv) = (&cq, &ck, &cv);
1128    let s_ptr = SendMutF32(s_all.as_mut_ptr());
1129    let of_ptr = SendMutF32(of.as_mut_ptr());
1130    let head_range = |h0: usize, h1: usize| {
1131        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
1132        let mut kv = crate::attention::take_buf(dv);
1133        let mut delta = crate::attention::take_buf(dv);
1134        let mut o = crate::attention::take_buf(dv);
1135        let mut kf = crate::attention::take_buf(dk);
1136        let mut qf = crate::attention::take_buf(dk);
1137        let mut gd = crate::attention::take_buf(dk);
1138        for h in h0..h1 {
1139            let qs = h * dk;
1140            // l2-normalize q and k; q additionally scaled by 1/√dk.
1141            let (mut nq, mut nkn) = (0f64, 0f64);
1142            for d in 0..dk {
1143                nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
1144                nkn += (ck[qs + d] as f64) * (ck[qs + d] as f64);
1145            }
1146            let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
1147            let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
1148            for d in 0..dk {
1149                qf[d] = cq[qs + d] * invq;
1150                kf[d] = ck[qs + d] * invk;
1151                gd[d] = kda_log_decay(w, cfg, h, d, f[qs + d]).exp() as f32;
1152            }
1153            let beta = sigmoid(b[h] as f64) as f32;
1154
1155            // SAFETY: disjoint per-head S and output slices per worker.
1156            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
1157            let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
1158            let vt = &cv[h * dv..(h + 1) * dv];
1159
1160            // Pass 1: kv = kᵀ(Diag(gd)·S_old) — decay folded into k.
1161            kv[..dv].fill(0.0);
1162            for di in 0..dk {
1163                let kg = kf[di] * gd[di];
1164                let row = &s[di * dv..(di + 1) * dv];
1165                for dj in 0..dv {
1166                    kv[dj] += row[dj] * kg;
1167                }
1168            }
1169            for dj in 0..dv {
1170                delta[dj] = (vt[dj] - kv[dj]) * beta;
1171            }
1172            // Pass 2: S[di,:] = gd[di]·row + k[di]·delta;  o += q[di]·row.
1173            o[..dv].fill(0.0);
1174            for di in 0..dk {
1175                let (kfd, qfd, gdd) = (kf[di], qf[di], gd[di]);
1176                let row = &mut s[di * dv..(di + 1) * dv];
1177                for dj in 0..dv {
1178                    let cell = gdd * row[dj] + kfd * delta[dj];
1179                    row[dj] = cell;
1180                    o[dj] += qfd * cell;
1181                }
1182            }
1183            // Gated RMSNorm per head: x̂·w·σ(gate) — sigmoid, not SiLU.
1184            let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
1185            let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1186            for dj in 0..dv {
1187                oh[dj] = ((o[dj] as f64 * inv)
1188                    * w.o_norm[dj] as f64
1189                    * sigmoid(gate_out[h * dv + dj] as f64)) as f32;
1190            }
1191        }
1192        crate::attention::recycle_buf(&mut kv);
1193        crate::attention::recycle_buf(&mut delta);
1194        crate::attention::recycle_buf(&mut o);
1195        crate::attention::recycle_buf(&mut kf);
1196        crate::attention::recycle_buf(&mut qf);
1197        crate::attention::recycle_buf(&mut gd);
1198    };
1199    match pool {
1200        Some(pool) if nh >= 4 => pool.run(&|widx, n| {
1201            let chunk = nh.div_ceil(n);
1202            let h0 = (widx * chunk).min(nh);
1203            let h1 = (h0 + chunk).min(nh);
1204            if h0 < h1 {
1205                head_range(h0, h1);
1206            }
1207        }),
1208        _ => head_range(0, nh),
1209    }
1210}
1211
1212/// Project one position's raw q/k/v/f/β/gate inputs (shared by the
1213/// single and batched forwards; `bi` selects the row when batched).
1214fn kda_gate_out(w: &KdaWeights, x: &[f32], vd: usize, pool: Option<&Pool>) -> Vec<f32> {
1215    let mut g = vec![0.0f32; vd];
1216    match &w.gate {
1217        KdaOutGate::Full(gp) => gp.matvec(x, &mut g, pool),
1218        KdaOutGate::LowRank(ga, gb) => {
1219            let mut low = vec![0.0f32; ga.rows()];
1220            ga.matvec(x, &mut low, pool);
1221            gb.matvec(&low, &mut g, pool);
1222        }
1223    }
1224    g
1225}
1226
1227/// Forward one position through a KDA layer, advancing `state`.
1228pub fn kda_forward(
1229    x: &[f32],
1230    w: &KdaWeights,
1231    cfg: &KdaCfg,
1232    state: &mut Vec<f32>,
1233    pool: Option<&Pool>,
1234) -> Vec<f32> {
1235    if state.len() != cfg.state_len() {
1236        *state = vec![0f32; cfg.state_len()];
1237    }
1238    let (nh, dk, dv) = (cfg.num_heads, cfg.head_k_dim, cfg.head_v_dim);
1239    let (kd, vd) = (nh * dk, nh * dv);
1240
1241    let mut xq = vec![0.0f32; kd];
1242    let mut xk = vec![0.0f32; kd];
1243    let mut xv = vec![0.0f32; vd];
1244    let mut fl = vec![0.0f32; w.f_a.rows()];
1245    let mut b = vec![0.0f32; nh];
1246    QTensor::matvec_many(
1247        [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a],
1248        x,
1249        [
1250            xq.as_mut_slice(),
1251            xk.as_mut_slice(),
1252            xv.as_mut_slice(),
1253            fl.as_mut_slice(),
1254        ],
1255        pool,
1256    );
1257    w.b_proj.matvec(x, &mut b, pool);
1258    let mut f = vec![0.0f32; kd];
1259    w.f_b.matvec(&fl, &mut f, pool);
1260    let gate_out = kda_gate_out(w, x, vd, pool);
1261
1262    let mut of = vec![0.0f32; vd];
1263    kda_step(
1264        &xq, &xk, &xv, &f, &b, &gate_out, w, cfg, state, &mut of, pool,
1265    );
1266
1267    let mut out = vec![0.0f32; cfg.hidden_size];
1268    w.o_proj.matvec(&of, &mut out, pool);
1269    out
1270}
1271
1272/// Batched KDA forward (prefill-GEMM): projections as matmat over the
1273/// chunk, the recurrence sequential per position — elementwise identical
1274/// to the single-position path.
1275pub fn kda_forward_batch(
1276    xs: &[f32],
1277    bsz: usize,
1278    w: &KdaWeights,
1279    cfg: &KdaCfg,
1280    state: &mut Vec<f32>,
1281    pool: Option<&Pool>,
1282) -> Vec<f32> {
1283    if state.len() != cfg.state_len() {
1284        *state = vec![0f32; cfg.state_len()];
1285    }
1286    let (nh, dk, dv, hs) = (
1287        cfg.num_heads,
1288        cfg.head_k_dim,
1289        cfg.head_v_dim,
1290        cfg.hidden_size,
1291    );
1292    let (kd, vd) = (nh * dk, nh * dv);
1293
1294    let mut xq = vec![0.0f32; bsz * kd];
1295    w.q_proj.matmat(xs, bsz, &mut xq, pool);
1296    let mut xk = vec![0.0f32; bsz * kd];
1297    w.k_proj.matmat(xs, bsz, &mut xk, pool);
1298    let mut xv = vec![0.0f32; bsz * vd];
1299    w.v_proj.matmat(xs, bsz, &mut xv, pool);
1300    let rank = w.f_a.rows();
1301    let mut fl = vec![0.0f32; bsz * rank];
1302    w.f_a.matmat(xs, bsz, &mut fl, pool);
1303    let mut f = vec![0.0f32; bsz * kd];
1304    w.f_b.matmat(&fl, bsz, &mut f, pool);
1305    let mut b = vec![0.0f32; bsz * nh];
1306    w.b_proj.matmat(xs, bsz, &mut b, pool);
1307    let mut gate_out = vec![0.0f32; bsz * vd];
1308    match &w.gate {
1309        KdaOutGate::Full(gp) => gp.matmat(xs, bsz, &mut gate_out, pool),
1310        KdaOutGate::LowRank(ga, gb) => {
1311            let mut low = vec![0.0f32; bsz * ga.rows()];
1312            ga.matmat(xs, bsz, &mut low, pool);
1313            gb.matmat(&low, bsz, &mut gate_out, pool);
1314        }
1315    }
1316
1317    let mut of = vec![0.0f32; bsz * vd];
1318    for bi in 0..bsz {
1319        let mut oh = vec![0.0f32; vd];
1320        kda_step(
1321            &xq[bi * kd..(bi + 1) * kd],
1322            &xk[bi * kd..(bi + 1) * kd],
1323            &xv[bi * vd..(bi + 1) * vd],
1324            &f[bi * kd..(bi + 1) * kd],
1325            &b[bi * nh..(bi + 1) * nh],
1326            &gate_out[bi * vd..(bi + 1) * vd],
1327            w,
1328            cfg,
1329            state,
1330            &mut oh,
1331            pool,
1332        );
1333        of[bi * vd..(bi + 1) * vd].copy_from_slice(&oh);
1334    }
1335
1336    let mut out = vec![0.0f32; bsz * hs];
1337    w.o_proj.matmat(&of, bsz, &mut out, pool);
1338    out
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343    #[test]
1344    fn kda_forward_matches_naive_reference() {
1345        // Small deterministic KDA layer; the oracle is a literal port of
1346        // FLA naive_recurrent_kda + naive_kda_gate + the modeling glue
1347        // (conv→silu, low-rank decay, sigmoid-gated output norm), coded
1348        // straight from the reference — a different shape from the fused
1349        // two-pass production kernel.
1350        let (nh, dk, dv, kk, hs, rank) = (2usize, 4usize, 4usize, 3usize, 6usize, 3usize);
1351        let synth = |rows: usize, cols: usize, salt: usize| -> QTensor {
1352            QTensor::from_f32(
1353                (0..rows * cols)
1354                    .map(|i| (((i * 31 + salt * 17) % 101) as f32 / 101.0 - 0.5) * 0.6)
1355                    .collect(),
1356                rows,
1357                cols,
1358            )
1359        };
1360        let vecf = |n: usize, salt: usize| -> Vec<f32> {
1361            (0..n)
1362                .map(|i| (((i * 13 + salt * 7) % 89) as f32 / 89.0 - 0.5) * 0.8)
1363                .collect()
1364        };
1365        for (label, a_log, lb) in [
1366            ("per-head standard", vecf(nh, 40), None),
1367            ("per-dim lower-bound", vecf(dk, 41), Some(-5.0f32)),
1368        ] {
1369            let w = KdaWeights {
1370                q_proj: synth(nh * dk, hs, 1),
1371                k_proj: synth(nh * dk, hs, 2),
1372                v_proj: synth(nh * dv, hs, 3),
1373                conv_q: vecf(nh * dk * kk, 4),
1374                conv_k: vecf(nh * dk * kk, 5),
1375                conv_v: vecf(nh * dv * kk, 6),
1376                f_a: synth(rank, hs, 7),
1377                f_b: synth(nh * dk, rank, 8),
1378                dt_bias: vecf(nh * dk, 9),
1379                a_log: a_log.clone(),
1380                b_proj: synth(nh, hs, 10),
1381                gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1382                o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1383                o_proj: synth(hs, nh * dv, 13),
1384                gate_lower_bound: lb,
1385            };
1386            let cfg = KdaCfg {
1387                num_heads: nh,
1388                head_k_dim: dk,
1389                head_v_dim: dv,
1390                conv_kernel: kk,
1391                hidden_size: hs,
1392                rms_eps: 1e-6,
1393            };
1394            let xs: Vec<Vec<f32>> = (0..6)
1395                .map(|t| {
1396                    (0..hs)
1397                        .map(|i| ((t * hs + i) as f32 * 0.37).sin() * 0.5)
1398                        .collect()
1399                })
1400                .collect();
1401
1402            // Production path.
1403            let mut state = Vec::new();
1404            let got: Vec<Vec<f32>> = xs
1405                .iter()
1406                .map(|x| kda_forward(x, &w, &cfg, &mut state, None))
1407                .collect();
1408
1409            // Oracle.
1410            let mv = |t: &QTensor, x: &[f32]| -> Vec<f32> {
1411                let mut o = vec![0.0f32; t.rows()];
1412                t.matvec(x, &mut o, None);
1413                o
1414            };
1415            let mut hist: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = Vec::new(); // raw xq/xk/xv
1416            let mut s_state = vec![0f64; nh * dk * dv];
1417            let mut want: Vec<Vec<f32>> = Vec::new();
1418            for x in &xs {
1419                let (xq, xk, xv) = (mv(&w.q_proj, x), mv(&w.k_proj, x), mv(&w.v_proj, x));
1420                hist.push((xq, xk, xv));
1421                // conv over the raw history, taps oldest→newest.
1422                let conv = |sel: fn(&(Vec<f32>, Vec<f32>, Vec<f32>)) -> &Vec<f32>,
1423                            taps: &[f32],
1424                            n: usize|
1425                 -> Vec<f32> {
1426                    (0..n)
1427                        .map(|c| {
1428                            let t = &taps[c * kk..(c + 1) * kk];
1429                            let mut acc = 0f64;
1430                            for j in 0..kk {
1431                                let idx = hist.len() as i64 - (kk as i64 - j as i64);
1432                                if idx >= 0 {
1433                                    acc += sel(&hist[idx as usize])[c] as f64 * t[j] as f64;
1434                                }
1435                            }
1436                            silu(acc)
1437                        })
1438                        .map(|v| v as f32)
1439                        .collect()
1440                };
1441                let cq = conv(|h| &h.0, &w.conv_q, nh * dk);
1442                let ck = conv(|h| &h.1, &w.conv_k, nh * dk);
1443                let cv = conv(|h| &h.2, &w.conv_v, nh * dv);
1444                let f = mv(&w.f_b, &mv(&w.f_a, x));
1445                let bb = mv(&w.b_proj, x);
1446                let gate_out = match &w.gate {
1447                    KdaOutGate::LowRank(ga, gb) => mv(gb, &mv(ga, x)),
1448                    KdaOutGate::Full(g) => mv(g, x),
1449                };
1450                let mut of = vec![0f32; nh * dv];
1451                for h in 0..nh {
1452                    // l2norm + scale.
1453                    let q: Vec<f64> = {
1454                        let sl = &cq[h * dk..(h + 1) * dk];
1455                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
1456                        let inv = 1.0 / ((n + 1e-6).sqrt() * (dk as f64).sqrt());
1457                        sl.iter().map(|&v| v as f64 * inv).collect()
1458                    };
1459                    let k: Vec<f64> = {
1460                        let sl = &ck[h * dk..(h + 1) * dk];
1461                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
1462                        let inv = 1.0 / (n + 1e-6).sqrt();
1463                        sl.iter().map(|&v| v as f64 * inv).collect()
1464                    };
1465                    let v: Vec<f64> = cv[h * dv..(h + 1) * dv].iter().map(|&v| v as f64).collect();
1466                    // gate: g = −exp(A)·softplus(f+bias) | lb·σ(exp(A)·(f+bias))
1467                    let g: Vec<f64> = (0..dk)
1468                        .map(|d| {
1469                            let a = if w.a_log.len() == nh {
1470                                w.a_log[h] as f64
1471                            } else {
1472                                w.a_log[d] as f64
1473                            };
1474                            let raw = f[h * dk + d] as f64 + w.dt_bias[h * dk + d] as f64;
1475                            match w.gate_lower_bound {
1476                                Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1477                                None => -a.exp() * softplus(raw),
1478                            }
1479                        })
1480                        .collect();
1481                    let beta = sigmoid(bb[h] as f64);
1482                    let s = &mut s_state[h * dk * dv..(h + 1) * dk * dv];
1483                    // S = Diag(exp(g))·S
1484                    for di in 0..dk {
1485                        for dj in 0..dv {
1486                            s[di * dv + dj] *= g[di].exp();
1487                        }
1488                    }
1489                    // kv = kᵀS; S += β·k⊗(v−kv); o = qᵀS
1490                    let mut kv = vec![0f64; dv];
1491                    for di in 0..dk {
1492                        for dj in 0..dv {
1493                            kv[dj] += k[di] * s[di * dv + dj];
1494                        }
1495                    }
1496                    for di in 0..dk {
1497                        for dj in 0..dv {
1498                            s[di * dv + dj] += beta * k[di] * (v[dj] - kv[dj]);
1499                        }
1500                    }
1501                    let mut o = vec![0f64; dv];
1502                    for di in 0..dk {
1503                        for dj in 0..dv {
1504                            o[dj] += q[di] * s[di * dv + dj];
1505                        }
1506                    }
1507                    // sigmoid-gated RMSNorm
1508                    let ss: f64 = o.iter().map(|&v| v * v).sum();
1509                    let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1510                    for dj in 0..dv {
1511                        of[h * dv + dj] = (o[dj]
1512                            * inv
1513                            * w.o_norm[dj] as f64
1514                            * sigmoid(gate_out[h * dv + dj] as f64))
1515                            as f32;
1516                    }
1517                }
1518                want.push(mv(&w.o_proj, &of));
1519            }
1520
1521            for (t, (g, e)) in got.iter().zip(&want).enumerate() {
1522                for (i, (a, b)) in g.iter().zip(e.iter()).enumerate() {
1523                    assert!((a - b).abs() < 2e-4, "{label}: t={t} i={i}: {a} vs {b}");
1524                }
1525            }
1526        }
1527
1528        // Batched prefill must equal the sequential singles bit-close.
1529        let w = KdaWeights {
1530            q_proj: synth(nh * dk, hs, 1),
1531            k_proj: synth(nh * dk, hs, 2),
1532            v_proj: synth(nh * dv, hs, 3),
1533            conv_q: vecf(nh * dk * kk, 4),
1534            conv_k: vecf(nh * dk * kk, 5),
1535            conv_v: vecf(nh * dv * kk, 6),
1536            f_a: synth(rank, hs, 7),
1537            f_b: synth(nh * dk, rank, 8),
1538            dt_bias: vecf(nh * dk, 9),
1539            a_log: vecf(nh, 40),
1540            b_proj: synth(nh, hs, 10),
1541            gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1542            o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1543            o_proj: synth(hs, nh * dv, 13),
1544            gate_lower_bound: None,
1545        };
1546        let cfg = KdaCfg {
1547            num_heads: nh,
1548            head_k_dim: dk,
1549            head_v_dim: dv,
1550            conv_kernel: kk,
1551            hidden_size: hs,
1552            rms_eps: 1e-6,
1553        };
1554        let xs: Vec<f32> = (0..5 * hs).map(|i| (i as f32 * 0.29).cos() * 0.4).collect();
1555        let mut st1 = Vec::new();
1556        let seq: Vec<f32> = (0..5)
1557            .flat_map(|t| kda_forward(&xs[t * hs..(t + 1) * hs], &w, &cfg, &mut st1, None))
1558            .collect();
1559        let mut st2 = Vec::new();
1560        let bat = kda_forward_batch(&xs, 5, &w, &cfg, &mut st2, None);
1561        for (i, (a, b)) in seq.iter().zip(&bat).enumerate() {
1562            assert!((a - b).abs() < 1e-5, "batch i={i}: {a} vs {b}");
1563        }
1564        assert_eq!(st1, st2, "state must match after the chunk");
1565    }
1566
1567    use super::*;
1568
1569    fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
1570        let cfg = VmfPhaseCfg {
1571            num_heads: 2,
1572            nphase: 3,
1573            value_head_dim: 4,
1574            hidden_size: 8,
1575            phase_mass: 0.0,
1576        };
1577        let synth = |rows: usize, cols: usize, salt: usize| {
1578            QTensor::from_f32(
1579                (0..rows * cols)
1580                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
1581                    .collect(),
1582                rows,
1583                cols,
1584            )
1585        };
1586        let w = VmfPhaseWeights {
1587            thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
1588            thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
1589            v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
1590            out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
1591            decay: (0..cfg.num_heads * 2 * cfg.nphase)
1592                .map(|i| 0.9 + 0.005 * (i % 10) as f64)
1593                .collect(),
1594            conv: None,
1595            k_gate: None,
1596        };
1597        (w, cfg)
1598    }
1599
1600    #[test]
1601    fn state_persists_and_changes_output() {
1602        let (w, cfg) = tiny();
1603        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
1604        let mut state = Vec::new();
1605        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
1606        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
1607        // Same input, evolved state → different output.
1608        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
1609        assert_eq!(state.len(), cfg.state_len());
1610    }
1611
1612    /// Phase-mass correction: mass=0 is bit-identical to the unscaled kernel; mass>0
1613    /// changes the output (phase narrowed → kernel widened). Guards the
1614    /// no-op default and that the knob is actually wired.
1615    #[test]
1616    fn phase_mass_zero_is_noop_and_positive_shifts() {
1617        let (w, cfg0) = tiny();
1618        let mut cfg_m = cfg0;
1619        cfg_m.phase_mass = 1.0;
1620        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
1621
1622        let mut s0 = Vec::new();
1623        let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
1624        // Re-run with mass=0 → must be bit-identical.
1625        let mut s0b = Vec::new();
1626        let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
1627        assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
1628        // mass=1 → output differs (θ halved before cos/sin).
1629        let mut sm = Vec::new();
1630        let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
1631        assert!(
1632            base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
1633            "mass>0 must change the output"
1634        );
1635        assert!(massed.iter().all(|v| v.is_finite()));
1636    }
1637
1638    /// κ write gate (hybrid_k): saturated-open gate (bias ≫ 0 → κ→1)
1639    /// matches the gateless kernel within fp tolerance; a closed gate
1640    /// (bias ≪ 0 → κ→0) writes nothing — the state stays zero and the
1641    /// output collapses to the empty-state readout.
1642    #[test]
1643    fn kappa_gate_open_matches_none_and_closed_writes_nothing() {
1644        let (mut w, cfg) = tiny();
1645        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
1646
1647        let mut s_none = Vec::new();
1648        let base1 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
1649        let base2 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
1650
1651        // Open gate: W=0, bias=+20 → κ = σ(20) ≈ 1 − 2e−9.
1652        w.k_gate = Some((
1653            QTensor::from_f32(
1654                vec![0.0; cfg.num_heads * cfg.hidden_size],
1655                cfg.num_heads,
1656                cfg.hidden_size,
1657            ),
1658            vec![20.0; cfg.num_heads],
1659        ));
1660        let mut s_open = Vec::new();
1661        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
1662        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
1663        for (a, b) in base1.iter().zip(&o1).chain(base2.iter().zip(&o2)) {
1664            assert!(
1665                (a - b).abs() < 1e-5,
1666                "open κ must match gateless: {a} vs {b}"
1667            );
1668        }
1669
1670        // Closed gate: bias=−20 → κ ≈ 0 → nothing is written.
1671        w.k_gate = Some((
1672            QTensor::from_f32(
1673                vec![0.0; cfg.num_heads * cfg.hidden_size],
1674                cfg.num_heads,
1675                cfg.hidden_size,
1676            ),
1677            vec![-20.0; cfg.num_heads],
1678        ));
1679        let mut s_closed = Vec::new();
1680        let oc = vmf_phase_forward(&x, &w, &cfg, &mut s_closed, None);
1681        assert!(
1682            s_closed.iter().all(|&v| v.abs() < 1e-7),
1683            "closed κ: state must stay empty"
1684        );
1685        assert!(
1686            oc.iter().all(|&v| v.abs() < 1e-6),
1687            "closed κ: empty-state readout"
1688        );
1689    }
1690
1691    #[test]
1692    fn pair_matches_two_singles_bitexact() {
1693        let (w, cfg) = tiny();
1694        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
1695        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
1696
1697        // Reference: two sequential singles.
1698        let mut s_ref = Vec::new();
1699        let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
1700        let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
1701
1702        // Pair: lane1 commits, lane2 tentative in scratch.
1703        let mut s = Vec::new();
1704        let mut scratch = Vec::new();
1705        let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1706        assert_eq!(r1, p1, "lane 1 must be bit-identical");
1707        assert_eq!(r2, p2, "lane 2 must be bit-identical");
1708        // Accepting the draft = swapping scratch in → equals s_ref.
1709        std::mem::swap(&mut s, &mut scratch);
1710        assert_eq!(s, s_ref, "accepted state must equal sequential state");
1711    }
1712
1713    #[test]
1714    fn rejected_draft_leaves_state_at_lane1() {
1715        let (w, cfg) = tiny();
1716        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
1717        let x2 = vec![0.5f32; 8];
1718
1719        let mut s_ref = Vec::new();
1720        let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
1721
1722        let mut s = Vec::new();
1723        let mut scratch = Vec::new();
1724        let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1725        // Reject: state must be exactly the post-lane1 state.
1726        assert_eq!(s, s_ref);
1727    }
1728
1729    // ───────────── GatedDeltaNet ─────────────
1730
1731    fn tiny_gdn() -> (GdnWeights, GdnCfg) {
1732        let cfg = GdnCfg {
1733            num_v_heads: 4,
1734            num_k_heads: 2,
1735            key_head_dim: 3,
1736            value_head_dim: 5,
1737            conv_kernel: 4,
1738            hidden_size: 8,
1739            rms_eps: 1e-6,
1740            output_gate_sigmoid: false,
1741        };
1742        let c_dim = cfg.conv_dim();
1743        let vd = cfg.num_v_heads * cfg.value_head_dim;
1744        let synth = |rows: usize, cols: usize, salt: usize| {
1745            QTensor::from_f32(
1746                (0..rows * cols)
1747                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
1748                    .collect(),
1749                rows,
1750                cols,
1751            )
1752        };
1753        let vecf = |n: usize, salt: usize| -> Vec<f32> {
1754            (0..n)
1755                .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
1756                .collect()
1757        };
1758        let w = GdnWeights {
1759            in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
1760            in_proj_z: synth(vd, cfg.hidden_size, 2),
1761            in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
1762            in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
1763            conv1d: vecf(c_dim * cfg.conv_kernel, 5),
1764            a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
1765            dt_bias: vecf(cfg.num_v_heads, 6),
1766            norm: vec![1.0; cfg.value_head_dim],
1767            out_proj: synth(cfg.hidden_size, vd, 7),
1768        };
1769        (w, cfg)
1770    }
1771
1772    #[test]
1773    fn gdn_state_persists_and_changes_output() {
1774        let (w, cfg) = tiny_gdn();
1775        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
1776        let mut state = Vec::new();
1777        let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
1778        let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
1779        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
1780        assert_eq!(state.len(), cfg.state_len());
1781    }
1782
1783    #[test]
1784    fn gdn_pair_matches_two_singles_bitexact() {
1785        let (w, cfg) = tiny_gdn();
1786        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
1787        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
1788
1789        let mut s_ref = Vec::new();
1790        let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
1791        let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
1792
1793        let mut s = Vec::new();
1794        let mut scratch = Vec::new();
1795        let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1796        assert_eq!(r1, p1, "lane 1 must be bit-identical");
1797        assert_eq!(r2, p2, "lane 2 must be bit-identical");
1798        std::mem::swap(&mut s, &mut scratch);
1799        assert_eq!(s, s_ref, "accepted state must equal sequential state");
1800    }
1801
1802    #[test]
1803    fn gdn_rejected_draft_leaves_state_at_lane1() {
1804        let (w, cfg) = tiny_gdn();
1805        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
1806        let x2 = vec![0.5f32; 8];
1807
1808        let mut s_ref = Vec::new();
1809        let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
1810
1811        let mut s = Vec::new();
1812        let mut scratch = Vec::new();
1813        let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1814        assert_eq!(s, s_ref);
1815    }
1816
1817    /// The conv ring must give the same result as an explicit causal
1818    /// conv over the whole sequence (oracle semantics: zero left-pad,
1819    /// tap kk−1 on the current position).
1820    #[test]
1821    fn gdn_conv_ring_matches_explicit_causal_conv() {
1822        let (w, cfg) = tiny_gdn();
1823        let seq: Vec<Vec<f32>> = (0..6)
1824            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
1825            .collect();
1826
1827        // Reference: recompute position t from scratch each time with a
1828        // fresh state built by replaying the prefix.
1829        let mut s_inc = Vec::new();
1830        for (t, x) in seq.iter().enumerate() {
1831            let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
1832            let mut s_replay = Vec::new();
1833            let mut replay = Vec::new();
1834            for xr in &seq[..=t] {
1835                replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
1836            }
1837            assert_eq!(inc, replay, "position {t}: ring must equal replay");
1838        }
1839    }
1840
1841    fn tiny_short_conv() -> (ShortConvWeights, ShortConvCfg) {
1842        let cfg = ShortConvCfg {
1843            hidden_size: 8,
1844            kernel: 3,
1845        };
1846        let synth = |rows: usize, cols: usize, salt: usize| {
1847            QTensor::from_f32(
1848                (0..rows * cols)
1849                    .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.5)
1850                    .collect(),
1851                rows,
1852                cols,
1853            )
1854        };
1855        let w = ShortConvWeights {
1856            in_proj: synth(3 * cfg.hidden_size, cfg.hidden_size, 1),
1857            conv: (0..cfg.hidden_size * cfg.kernel)
1858                .map(|i| ((i * 7 % 13) as f32 / 13.0 - 0.5) * 0.8)
1859                .collect(),
1860            out_proj: synth(cfg.hidden_size, cfg.hidden_size, 2),
1861        };
1862        (w, cfg)
1863    }
1864
1865    /// The incremental conv ring must equal a from-scratch causal replay
1866    /// of the prefix at every position — the decode/prefill contract.
1867    #[test]
1868    fn short_conv_ring_matches_explicit_causal_conv() {
1869        let (w, cfg) = tiny_short_conv();
1870        let seq: Vec<Vec<f32>> = (0..6)
1871            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.19).cos()).collect())
1872            .collect();
1873        let mut s_inc = Vec::new();
1874        for (t, x) in seq.iter().enumerate() {
1875            let inc = short_conv_forward(x, &w, &cfg, &mut s_inc, None);
1876            let mut s_replay = Vec::new();
1877            let mut replay = Vec::new();
1878            for xr in &seq[..=t] {
1879                replay = short_conv_forward(xr, &w, &cfg, &mut s_replay, None);
1880            }
1881            assert_eq!(inc, replay, "position {t}: ring must equal replay");
1882            assert_eq!(s_inc.len(), cfg.state_len());
1883        }
1884    }
1885
1886    /// The batched prefill path (matmat + sequential conv over the chunk)
1887    /// must reproduce the position-by-position decode path exactly.
1888    #[test]
1889    fn short_conv_batch_matches_sequential() {
1890        let (w, cfg) = tiny_short_conv();
1891        let b = 5;
1892        let xs: Vec<f32> = (0..b * cfg.hidden_size)
1893            .map(|i| (i as f32 * 0.13).sin() * 0.6)
1894            .collect();
1895
1896        let mut s_seq = Vec::new();
1897        let mut seq_out = vec![0.0f32; b * cfg.hidden_size];
1898        for bi in 0..b {
1899            let o = short_conv_forward(
1900                &xs[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size],
1901                &w,
1902                &cfg,
1903                &mut s_seq,
1904                None,
1905            );
1906            seq_out[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size].copy_from_slice(&o);
1907        }
1908
1909        let mut s_batch = Vec::new();
1910        let batch_out = short_conv_forward_batch(&xs, b, &w, &cfg, &mut s_batch, None);
1911        assert_eq!(
1912            seq_out, batch_out,
1913            "batch conv must match sequential decode"
1914        );
1915        assert_eq!(s_seq, s_batch, "ring state must match after the chunk");
1916    }
1917}