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