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