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                }
487            }
488            crate::gpu::ProbeArm::CpuTimed => {
489                let t0 = std::time::Instant::now();
490                crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
491                crate::gpu::probe_record(crate::gpu::OpClass::Batch, false, t0.elapsed());
492                done = true;
493            }
494            crate::gpu::ProbeArm::Cpu => {
495                crate::gpu::cpu_scope(|| cpu_projs(&mut qkv, &mut z, &mut a, &mut b));
496                done = true;
497            }
498        }
499    }
500    if !done {
501        cpu_projs(&mut qkv, &mut z, &mut a, &mut b);
502    }
503
504    let mut of = vec![0.0f32; vd];
505    gdn_step(&qkv, &z, &a, &b, w, cfg, state, &mut of, pool);
506
507    let mut out = vec![0.0f32; cfg.hidden_size];
508    w.out_proj.matvec(&of, &mut out, pool);
509    out
510}
511
512/// Batched GDN forward (prefill-GEMM): the qkv/z/a/b and out_proj
513/// projections are matmat over the batch (a weight row once per chunk),
514/// the gdn_step recurrence runs sequentially over positions (state is the
515/// same as the sequential path; the math is elementwise identical).
516pub fn gdn_forward_batch(
517    xs: &[f32],
518    b: usize,
519    w: &GdnWeights,
520    cfg: &GdnCfg,
521    state: &mut Vec<f32>,
522    pool: Option<&Pool>,
523) -> Vec<f32> {
524    if state.len() != cfg.state_len() {
525        *state = vec![0f32; cfg.state_len()];
526    }
527    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
528    let nv = cfg.num_v_heads;
529
530    let mut qkv = vec![0.0f32; b * c_dim];
531    w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
532    let mut z = vec![0.0f32; b * vd];
533    w.in_proj_z.matmat(xs, b, &mut z, pool);
534    let mut a = vec![0.0f32; b * nv];
535    w.in_proj_a.matmat(xs, b, &mut a, pool);
536    let mut bb = vec![0.0f32; b * nv];
537    w.in_proj_b.matmat(xs, b, &mut bb, pool);
538
539    let mut of = vec![0.0f32; b * vd];
540    for bi in 0..b {
541        gdn_step(
542            &qkv[bi * c_dim..(bi + 1) * c_dim],
543            &z[bi * vd..(bi + 1) * vd],
544            &a[bi * nv..(bi + 1) * nv],
545            &bb[bi * nv..(bi + 1) * nv],
546            w,
547            cfg,
548            state,
549            &mut of[bi * vd..(bi + 1) * vd],
550            pool,
551        );
552    }
553    let mut out = vec![0.0f32; b * cfg.hidden_size];
554    w.out_proj.matmat(&of, b, &mut out, pool);
555    if std::env::var("CMF_GDN_TRACE").is_ok() {
556        let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
557        eprintln!(
558            "gdn-batch b={b}: |x0|={:.5} |qkv0|={:.5} |z0|={:.5} |a0|={:.5} |b0|={:.5} |of0|={:.5} |out0|={:.5} |state|={:.5}",
559            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)
560        );
561    }
562    out
563}
564
565/// GDN qkv+z GPU eligibility: q1 mixers offload by default (the CPU q1
566/// kernel is compute-bound); q8 stays opt-in via CMF_GPU_GDN=1 (measured
567/// neutral). The probe in `gdn_forward` still arbitrates either way.
568fn gdn_projs_eligible(w: &GdnWeights) -> bool {
569    // Any tile-embedded-scale layout, not just q1: the batched matvec now has
570    // kernels for all of them, and the probe arbitrates whether it pays.
571    // `CMF_GPU_GDN=0` forces the CPU projections back — the switch exists so
572    // the two arms can be compared inside ONE run: this machine throttles far
573    // enough that measurements minutes apart are not comparable.
574    if std::env::var("CMF_GPU_GDN")
575        .map(|v| v == "0")
576        .unwrap_or(false)
577    {
578        return false;
579    }
580    w.in_proj_qkv.is_q1()
581        || w.in_proj_qkv.q4t_parts().is_some()
582        || w.in_proj_qkv.q4tp_parts().is_some()
583        || std::env::var("CMF_GPU_GDN")
584            .map(|v| v == "1")
585            .unwrap_or(false)
586}
587
588/// GDN qkv+z on GPU in a single submission (independent matvecs of one input).
589fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
590    use crate::gpu::matvec_batch;
591    use crate::qtensor::QTensor;
592    if !crate::gpu::enabled_here() {
593        return false;
594    }
595    fn part<'a>(
596        t: &'a QTensor,
597        x: &[f32],
598    ) -> Option<(
599        std::sync::Arc<cortiq_core::CmfModel>,
600        crate::gpu::BatchJob<'a>,
601    )> {
602        use crate::gpu::BatchJob;
603        use crate::qtensor::prescale;
604        use cortiq_core::TensorDtype;
605        match t {
606            QTensor::Mapped {
607                model,
608                idx,
609                dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
610                rows,
611                cols,
612                row_scale,
613                col_field,
614                ..
615            } => Some((
616                model.clone(),
617                BatchJob {
618                    idx: *idx,
619                    rows: *rows,
620                    cols: *cols,
621                    row_scale,
622                    xs: prescale(x, col_field, *dt).into_owned(),
623                    layout: crate::gpu::BatchLayout::Q8,
624                },
625            )),
626            QTensor::Mapped {
627                model,
628                idx,
629                dtype: TensorDtype::Q1,
630                rows,
631                cols,
632                ..
633            } => Some((
634                model.clone(),
635                BatchJob {
636                    idx: *idx,
637                    rows: *rows,
638                    cols: *cols,
639                    row_scale: &[],
640                    xs: x.to_vec(),
641                    layout: crate::gpu::BatchLayout::Q1,
642                },
643            )),
644            // q4t/q4tp: same tile-embedded-scale contract as q1, different
645            // stride. Missing here, a hybrid model's GDN projections fell to
646            // the CPU on every layer while its experts rode the device.
647            QTensor::Mapped {
648                model,
649                idx,
650                dtype: dt @ (TensorDtype::Q4Tiled | TensorDtype::Q4TiledP),
651                rows,
652                cols,
653                ..
654            } => Some((
655                model.clone(),
656                BatchJob {
657                    idx: *idx,
658                    rows: *rows,
659                    cols: *cols,
660                    row_scale: &[],
661                    xs: x.to_vec(),
662                    layout: if *dt == TensorDtype::Q4TiledP {
663                        crate::gpu::BatchLayout::Q4tp
664                    } else {
665                        crate::gpu::BatchLayout::Q4t
666                    },
667                },
668            )),
669            _ => None,
670        }
671    }
672    let Some((model, jq)) = part(&w.in_proj_qkv, x) else {
673        return false;
674    };
675    let Some((_, jz)) = part(&w.in_proj_z, x) else {
676        return false;
677    };
678    matvec_batch(&model, &[jq, jz], &mut [qkv, z])
679}
680
681/// Fused two-position forward (speculative verify): lane 1 commits into
682/// `state`, lane 2 is tentative in `scratch` (ring + S move together).
683#[allow(clippy::too_many_arguments)]
684pub fn gdn_pair(
685    x1: &[f32],
686    x2: &[f32],
687    w: &GdnWeights,
688    cfg: &GdnCfg,
689    state: &mut Vec<f32>,
690    scratch: &mut Vec<f32>,
691    pool: Option<&Pool>,
692) -> (Vec<f32>, Vec<f32>) {
693    if state.len() != cfg.state_len() {
694        *state = vec![0f32; cfg.state_len()];
695    }
696    let (c_dim, vd, nv) = (
697        cfg.conv_dim(),
698        cfg.num_v_heads * cfg.value_head_dim,
699        cfg.num_v_heads,
700    );
701
702    let mut qkv1 = vec![0.0f32; c_dim];
703    let mut qkv2 = vec![0.0f32; c_dim];
704    w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
705    let mut z1 = vec![0.0f32; vd];
706    let mut z2 = vec![0.0f32; vd];
707    w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
708    let mut a1 = vec![0.0f32; nv];
709    let mut a2 = vec![0.0f32; nv];
710    w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
711    let mut b1 = vec![0.0f32; nv];
712    let mut b2 = vec![0.0f32; nv];
713    w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
714
715    let mut of1 = vec![0.0f32; vd];
716    gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1, pool);
717
718    scratch.clear();
719    scratch.extend_from_slice(state);
720    let mut of2 = vec![0.0f32; vd];
721    gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2, pool);
722
723    let mut out1 = vec![0.0f32; cfg.hidden_size];
724    let mut out2 = vec![0.0f32; cfg.hidden_size];
725    w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
726    (out1, out2)
727}
728
729// ───────────────────────── ShortConv (LFM2 gated short convolution) ─────────────────────────
730
731/// Weights of one LFM2 short-convolution mixer
732/// (`model.layers.{i}.short_conv.*`, renamed from the vendor `conv.*` at
733/// convert time). No recurrent condensate — the only state is the causal
734/// conv ring (the last `kernel−1` gated inputs per channel).
735pub struct ShortConvWeights {
736    /// [3·hidden, hidden] — fused (B, C, x) projection.
737    pub in_proj: QTensor,
738    /// [hidden · kernel] depthwise conv taps, flattened `[channel][tap]`
739    /// (the source `[hidden, 1, kernel]` with the singleton group axis
740    /// dropped). Tap `kernel−1` multiplies the current position.
741    pub conv: Vec<f32>,
742    /// [hidden, hidden] — output projection.
743    pub out_proj: QTensor,
744}
745
746#[derive(Clone, Copy)]
747pub struct ShortConvCfg {
748    pub hidden_size: usize,
749    /// Conv kernel width `L` (`conv_L_cache`; LFM2 uses 3).
750    pub kernel: usize,
751}
752
753impl ShortConvCfg {
754    /// Conv ring: the last `kernel−1` gated inputs per channel.
755    pub fn state_len(&self) -> usize {
756        (self.kernel - 1) * self.hidden_size
757    }
758}
759
760/// One position through the gated conv, given the fused projection
761/// `bcx = in_proj·x` [3·hidden] = [B | C | x]. Advances the conv ring and
762/// writes the gated conv output `y = C ⊙ conv(B ⊙ x)` [hidden] into `y`.
763///
764/// The conv is PyTorch's causal depthwise `Conv1d(padding=kernel−1)`
765/// truncated to the current length: for tap `k`, weight `w[c][k]` pairs
766/// with the input `kernel−1−k` steps in the past, so `w[c][kernel−1]` is
767/// the current position. The ring holds `in[t−1] … in[t−(kernel−1)]` at
768/// slots `0 … kernel−2`.
769fn short_conv_step(
770    bcx: &[f32],
771    conv: &[f32],
772    cfg: &ShortConvCfg,
773    ring_state: &mut [f32],
774    y: &mut [f32],
775) {
776    let (h, k) = (cfg.hidden_size, cfg.kernel);
777    let ring = k - 1;
778    let (bg, cg, xg) = (&bcx[0..h], &bcx[h..2 * h], &bcx[2 * h..3 * h]);
779    for c in 0..h {
780        let bx = bg[c] * xg[c];
781        let wc = &conv[c * k..(c + 1) * k];
782        // Current tap, then the past taps read from the channel's ring.
783        let mut acc = wc[k - 1] * bx;
784        let rc = &mut ring_state[c * ring..c * ring + ring];
785        for s in 0..ring {
786            acc += wc[k - 2 - s] * rc[s];
787        }
788        y[c] = cg[c] * acc;
789        // Shift newest-in-front: slot 0 becomes the just-seen input.
790        for s in (1..ring).rev() {
791            rc[s] = rc[s - 1];
792        }
793        if ring > 0 {
794            rc[0] = bx;
795        }
796    }
797}
798
799/// Forward one position through a short-conv layer, advancing `state`.
800pub fn short_conv_forward(
801    x: &[f32],
802    w: &ShortConvWeights,
803    cfg: &ShortConvCfg,
804    state: &mut Vec<f32>,
805    pool: Option<&Pool>,
806) -> Vec<f32> {
807    if state.len() != cfg.state_len() {
808        *state = vec![0f32; cfg.state_len()];
809    }
810    let h = cfg.hidden_size;
811    let mut bcx = vec![0.0f32; 3 * h];
812    w.in_proj.matvec(x, &mut bcx, pool);
813    let mut y = vec![0.0f32; h];
814    short_conv_step(&bcx, &w.conv, cfg, state, &mut y);
815    let mut out = vec![0.0f32; h];
816    w.out_proj.matvec(&y, &mut out, pool);
817    out
818}
819
820/// Batched short-conv forward (prefill-GEMM): in_proj/out_proj are matmat
821/// over the chunk (a weight row streamed once), the conv walks the
822/// positions in order — the chunk is contiguous, so the ring state is
823/// exactly the sequential path's and the math is elementwise identical.
824pub fn short_conv_forward_batch(
825    xs: &[f32],
826    b: usize,
827    w: &ShortConvWeights,
828    cfg: &ShortConvCfg,
829    state: &mut Vec<f32>,
830    pool: Option<&Pool>,
831) -> Vec<f32> {
832    if state.len() != cfg.state_len() {
833        *state = vec![0f32; cfg.state_len()];
834    }
835    let h = cfg.hidden_size;
836    let mut bcx = vec![0.0f32; b * 3 * h];
837    w.in_proj.matmat(xs, b, &mut bcx, pool);
838    let mut y = vec![0.0f32; b * h];
839    for bi in 0..b {
840        short_conv_step(
841            &bcx[bi * 3 * h..(bi + 1) * 3 * h],
842            &w.conv,
843            cfg,
844            state,
845            &mut y[bi * h..(bi + 1) * h],
846        );
847    }
848    let mut out = vec![0.0f32; b * h];
849    w.out_proj.matmat(&y, b, &mut out, pool);
850    out
851}
852
853/// Fused two-position forward (speculative verify). Lane 1 commits into
854/// `state`; lane 2's tentative ring goes into `scratch` — swapped in on
855/// draft acceptance, dropped on rejection. LFM2 ships no MTP head, so this
856/// is exercised only by the pair-fusion micro-benchmark; kept correct.
857#[allow(clippy::too_many_arguments)]
858pub fn short_conv_pair(
859    x1: &[f32],
860    x2: &[f32],
861    w: &ShortConvWeights,
862    cfg: &ShortConvCfg,
863    state: &mut Vec<f32>,
864    scratch: &mut Vec<f32>,
865    pool: Option<&Pool>,
866) -> (Vec<f32>, Vec<f32>) {
867    if state.len() != cfg.state_len() {
868        *state = vec![0f32; cfg.state_len()];
869    }
870    let h = cfg.hidden_size;
871    let mut bcx1 = vec![0.0f32; 3 * h];
872    let mut bcx2 = vec![0.0f32; 3 * h];
873    w.in_proj.matvec2(x1, x2, &mut bcx1, &mut bcx2, pool);
874
875    let mut y1 = vec![0.0f32; h];
876    short_conv_step(&bcx1, &w.conv, cfg, state, &mut y1);
877    scratch.clear();
878    scratch.extend_from_slice(state);
879    let mut y2 = vec![0.0f32; h];
880    short_conv_step(&bcx2, &w.conv, cfg, scratch, &mut y2);
881
882    let mut out1 = vec![0.0f32; h];
883    let mut out2 = vec![0.0f32; h];
884    w.out_proj.matvec2(&y1, &y2, &mut out1, &mut out2, pool);
885    (out1, out2)
886}
887
888// ─── Kimi Delta Attention (KDA) ─────────────────────────────────────────
889//
890// Kimi Linear / Kimi-K3 linear mixer (reference: FLA naive_recurrent_kda
891// + moonshotai modeling_kimi.py). Differences from GatedDeltaNet above:
892// separate q/k/v projections each behind its OWN causal depthwise short
893// convolution; the delta-rule decay is a PER-CHANNEL vector (diagonal)
894// instead of a per-head scalar; the decay pre-activation comes from a
895// low-rank projection f_b(f_a(x)); and the output gate norm uses
896// sigmoid, not SiLU.
897
898pub struct KdaWeights {
899    /// [nh·dk, hidden]
900    pub q_proj: QTensor,
901    /// [nh·dk, hidden]
902    pub k_proj: QTensor,
903    /// [nh·dv, hidden]
904    pub v_proj: QTensor,
905    /// [nh·dk × kk] — depthwise taps, oldest→newest (see GdnWeights.conv1d)
906    pub conv_q: Vec<f32>,
907    pub conv_k: Vec<f32>,
908    /// [nh·dv × kk]
909    pub conv_v: Vec<f32>,
910    /// [rank, hidden] — low-rank decay projection, stage 1
911    pub f_a: QTensor,
912    /// [nh·dk, rank] — stage 2
913    pub f_b: QTensor,
914    /// [nh·dk]
915    pub dt_bias: Vec<f32>,
916    /// [nh] per-head (Kimi-Linear-48B) | [dk] per-dim (Kimi-K3) |
917    /// [nh·dk] full — broadcast resolved by length.
918    pub a_log: Vec<f32>,
919    /// [nh, hidden] — β = σ(b_proj·x) per head
920    pub b_proj: QTensor,
921    /// Output gate: full-rank g_proj (K3) or low-rank g_b(g_a(x)) (48B).
922    pub gate: KdaOutGate,
923    /// [dv] — gated RMSNorm weight (per head over head_v_dim)
924    pub o_norm: Vec<f32>,
925    /// [hidden, nh·dv]
926    pub o_proj: QTensor,
927    /// Some(lb): log-decay = lb·σ(exp(A)·(f+bias)) (K3, lb=−5);
928    /// None: −exp(A)·softplus(f+bias) (Kimi-Linear-48B).
929    pub gate_lower_bound: Option<f32>,
930}
931
932pub enum KdaOutGate {
933    /// [nh·dv, hidden]
934    Full(QTensor),
935    /// g_a [rank, hidden], g_b [nh·dv, rank]
936    LowRank(QTensor, QTensor),
937}
938
939#[derive(Clone, Copy)]
940pub struct KdaCfg {
941    pub num_heads: usize,
942    pub head_k_dim: usize,
943    pub head_v_dim: usize,
944    pub conv_kernel: usize,
945    pub hidden_size: usize,
946    pub rms_eps: f64,
947}
948
949impl KdaCfg {
950    /// Packed state: [q ring | k ring | v ring | S nh·dk·dv], one Vec —
951    /// same single-buffer convention as GdnCfg::state_len.
952    pub fn state_len(&self) -> usize {
953        let (nh, dk, dv, kk) = (
954            self.num_heads,
955            self.head_k_dim,
956            self.head_v_dim,
957            self.conv_kernel,
958        );
959        (kk - 1) * (2 * nh * dk + nh * dv) + nh * dk * dv
960    }
961}
962
963/// Depthwise causal conv over [ring…, current] + SiLU, then ring shift.
964/// Taps oldest→newest, tap kk−1 multiplies the current position.
965fn kda_conv(raw: &[f32], taps: &[f32], ring: &mut [f32], kk: usize, out: &mut [f32]) {
966    let c_dim = raw.len();
967    for c in 0..c_dim {
968        let t = &taps[c * kk..(c + 1) * kk];
969        let mut acc = raw[c] as f64 * t[kk - 1] as f64;
970        for j in 0..kk - 1 {
971            acc += ring[j * c_dim + c] as f64 * t[j] as f64;
972        }
973        out[c] = silu(acc) as f32;
974    }
975    if kk > 1 {
976        ring.copy_within(c_dim.., 0);
977        let tail = (kk - 2) * c_dim;
978        ring[tail..tail + c_dim].copy_from_slice(raw);
979    }
980}
981
982/// Per-channel log-decay for head-channel (h, d): resolves the A_log
983/// broadcast by length and applies the configured gate formula.
984#[inline]
985fn kda_log_decay(w: &KdaWeights, cfg: &KdaCfg, h: usize, d: usize, f: f32) -> f64 {
986    let (nh, dk) = (cfg.num_heads, cfg.head_k_dim);
987    let a = if w.a_log.len() == nh {
988        w.a_log[h] as f64
989    } else if w.a_log.len() == dk {
990        w.a_log[d] as f64
991    } else {
992        w.a_log[h * dk + d] as f64
993    };
994    let raw = f as f64 + w.dt_bias[h * dk + d] as f64;
995    match w.gate_lower_bound {
996        Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
997        None => -a.exp() * softplus(raw),
998    }
999}
1000
1001/// One recurrent step given this position's raw (pre-conv) projections.
1002/// Advances the packed state and writes the gated per-head output into
1003/// `of` [nh·dv]. Recurrence (FLA naive_recurrent_kda):
1004///   S ← Diag(exp(g))·S;  S += β·k ⊗ (v − kᵀS);  o = qᵀS
1005/// with q,k L2-normalized per head and q additionally scaled by 1/√dk —
1006/// regrouped into two S passes like gdn_step (per-channel decay folds
1007/// into the k readout of the first pass).
1008#[allow(clippy::too_many_arguments)]
1009fn kda_step(
1010    xq: &[f32],
1011    xk: &[f32],
1012    xv: &[f32],
1013    f: &[f32],
1014    b: &[f32],
1015    gate_out: &[f32],
1016    w: &KdaWeights,
1017    cfg: &KdaCfg,
1018    state: &mut [f32],
1019    of: &mut [f32],
1020    pool: Option<&Pool>,
1021) {
1022    let (nh, dk, dv, kk) = (
1023        cfg.num_heads,
1024        cfg.head_k_dim,
1025        cfg.head_v_dim,
1026        cfg.conv_kernel,
1027    );
1028    let (kd, vd) = (nh * dk, nh * dv);
1029    let ring_q_len = (kk - 1) * kd;
1030    let ring_v_len = (kk - 1) * vd;
1031    let (ring_q, rest) = state.split_at_mut(ring_q_len);
1032    let (ring_k, rest) = rest.split_at_mut(ring_q_len);
1033    let (ring_v, s_all) = rest.split_at_mut(ring_v_len);
1034
1035    let mut cq = vec![0f32; kd];
1036    let mut ck = vec![0f32; kd];
1037    let mut cv = vec![0f32; vd];
1038    kda_conv(xq, &w.conv_q, ring_q, kk, &mut cq);
1039    kda_conv(xk, &w.conv_k, ring_k, kk, &mut ck);
1040    kda_conv(xv, &w.conv_v, ring_v, kk, &mut cv);
1041
1042    let (cq, ck, cv) = (&cq, &ck, &cv);
1043    let s_ptr = SendMutF32(s_all.as_mut_ptr());
1044    let of_ptr = SendMutF32(of.as_mut_ptr());
1045    let head_range = |h0: usize, h1: usize| {
1046        let (s_ptr, of_ptr) = (s_ptr, of_ptr);
1047        let mut kv = crate::attention::take_buf(dv);
1048        let mut delta = crate::attention::take_buf(dv);
1049        let mut o = crate::attention::take_buf(dv);
1050        let mut kf = crate::attention::take_buf(dk);
1051        let mut qf = crate::attention::take_buf(dk);
1052        let mut gd = crate::attention::take_buf(dk);
1053        for h in h0..h1 {
1054            let qs = h * dk;
1055            // l2-normalize q and k; q additionally scaled by 1/√dk.
1056            let (mut nq, mut nkn) = (0f64, 0f64);
1057            for d in 0..dk {
1058                nq += (cq[qs + d] as f64) * (cq[qs + d] as f64);
1059                nkn += (ck[qs + d] as f64) * (ck[qs + d] as f64);
1060            }
1061            let invq = (1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt())) as f32;
1062            let invk = (1.0 / (nkn + 1e-6).sqrt()) as f32;
1063            for d in 0..dk {
1064                qf[d] = cq[qs + d] * invq;
1065                kf[d] = ck[qs + d] * invk;
1066                gd[d] = kda_log_decay(w, cfg, h, d, f[qs + d]).exp() as f32;
1067            }
1068            let beta = sigmoid(b[h] as f64) as f32;
1069
1070            // SAFETY: disjoint per-head S and output slices per worker.
1071            let s = unsafe { std::slice::from_raw_parts_mut(s_ptr.0.add(h * dk * dv), dk * dv) };
1072            let oh = unsafe { std::slice::from_raw_parts_mut(of_ptr.0.add(h * dv), dv) };
1073            let vt = &cv[h * dv..(h + 1) * dv];
1074
1075            // Pass 1: kv = kᵀ(Diag(gd)·S_old) — decay folded into k.
1076            kv[..dv].fill(0.0);
1077            for di in 0..dk {
1078                let kg = kf[di] * gd[di];
1079                let row = &s[di * dv..(di + 1) * dv];
1080                for dj in 0..dv {
1081                    kv[dj] += row[dj] * kg;
1082                }
1083            }
1084            for dj in 0..dv {
1085                delta[dj] = (vt[dj] - kv[dj]) * beta;
1086            }
1087            // Pass 2: S[di,:] = gd[di]·row + k[di]·delta;  o += q[di]·row.
1088            o[..dv].fill(0.0);
1089            for di in 0..dk {
1090                let (kfd, qfd, gdd) = (kf[di], qf[di], gd[di]);
1091                let row = &mut s[di * dv..(di + 1) * dv];
1092                for dj in 0..dv {
1093                    let cell = gdd * row[dj] + kfd * delta[dj];
1094                    row[dj] = cell;
1095                    o[dj] += qfd * cell;
1096                }
1097            }
1098            // Gated RMSNorm per head: x̂·w·σ(gate) — sigmoid, not SiLU.
1099            let ss: f64 = o[..dv].iter().map(|&v| (v as f64) * (v as f64)).sum();
1100            let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1101            for dj in 0..dv {
1102                oh[dj] = ((o[dj] as f64 * inv)
1103                    * w.o_norm[dj] as f64
1104                    * sigmoid(gate_out[h * dv + dj] as f64)) as f32;
1105            }
1106        }
1107        crate::attention::recycle_buf(&mut kv);
1108        crate::attention::recycle_buf(&mut delta);
1109        crate::attention::recycle_buf(&mut o);
1110        crate::attention::recycle_buf(&mut kf);
1111        crate::attention::recycle_buf(&mut qf);
1112        crate::attention::recycle_buf(&mut gd);
1113    };
1114    match pool {
1115        Some(pool) if nh >= 4 => pool.run(&|widx, n| {
1116            let chunk = nh.div_ceil(n);
1117            let h0 = (widx * chunk).min(nh);
1118            let h1 = (h0 + chunk).min(nh);
1119            if h0 < h1 {
1120                head_range(h0, h1);
1121            }
1122        }),
1123        _ => head_range(0, nh),
1124    }
1125}
1126
1127/// Project one position's raw q/k/v/f/β/gate inputs (shared by the
1128/// single and batched forwards; `bi` selects the row when batched).
1129fn kda_gate_out(w: &KdaWeights, x: &[f32], vd: usize, pool: Option<&Pool>) -> Vec<f32> {
1130    let mut g = vec![0.0f32; vd];
1131    match &w.gate {
1132        KdaOutGate::Full(gp) => gp.matvec(x, &mut g, pool),
1133        KdaOutGate::LowRank(ga, gb) => {
1134            let mut low = vec![0.0f32; ga.rows()];
1135            ga.matvec(x, &mut low, pool);
1136            gb.matvec(&low, &mut g, pool);
1137        }
1138    }
1139    g
1140}
1141
1142/// Forward one position through a KDA layer, advancing `state`.
1143pub fn kda_forward(
1144    x: &[f32],
1145    w: &KdaWeights,
1146    cfg: &KdaCfg,
1147    state: &mut Vec<f32>,
1148    pool: Option<&Pool>,
1149) -> Vec<f32> {
1150    if state.len() != cfg.state_len() {
1151        *state = vec![0f32; cfg.state_len()];
1152    }
1153    let (nh, dk, dv) = (cfg.num_heads, cfg.head_k_dim, cfg.head_v_dim);
1154    let (kd, vd) = (nh * dk, nh * dv);
1155
1156    let mut xq = vec![0.0f32; kd];
1157    let mut xk = vec![0.0f32; kd];
1158    let mut xv = vec![0.0f32; vd];
1159    let mut fl = vec![0.0f32; w.f_a.rows()];
1160    let mut b = vec![0.0f32; nh];
1161    QTensor::matvec_many(
1162        [&w.q_proj, &w.k_proj, &w.v_proj, &w.f_a],
1163        x,
1164        [
1165            xq.as_mut_slice(),
1166            xk.as_mut_slice(),
1167            xv.as_mut_slice(),
1168            fl.as_mut_slice(),
1169        ],
1170        pool,
1171    );
1172    w.b_proj.matvec(x, &mut b, pool);
1173    let mut f = vec![0.0f32; kd];
1174    w.f_b.matvec(&fl, &mut f, pool);
1175    let gate_out = kda_gate_out(w, x, vd, pool);
1176
1177    let mut of = vec![0.0f32; vd];
1178    kda_step(
1179        &xq, &xk, &xv, &f, &b, &gate_out, w, cfg, state, &mut of, pool,
1180    );
1181
1182    let mut out = vec![0.0f32; cfg.hidden_size];
1183    w.o_proj.matvec(&of, &mut out, pool);
1184    out
1185}
1186
1187/// Batched KDA forward (prefill-GEMM): projections as matmat over the
1188/// chunk, the recurrence sequential per position — elementwise identical
1189/// to the single-position path.
1190pub fn kda_forward_batch(
1191    xs: &[f32],
1192    bsz: usize,
1193    w: &KdaWeights,
1194    cfg: &KdaCfg,
1195    state: &mut Vec<f32>,
1196    pool: Option<&Pool>,
1197) -> Vec<f32> {
1198    if state.len() != cfg.state_len() {
1199        *state = vec![0f32; cfg.state_len()];
1200    }
1201    let (nh, dk, dv, hs) = (
1202        cfg.num_heads,
1203        cfg.head_k_dim,
1204        cfg.head_v_dim,
1205        cfg.hidden_size,
1206    );
1207    let (kd, vd) = (nh * dk, nh * dv);
1208
1209    let mut xq = vec![0.0f32; bsz * kd];
1210    w.q_proj.matmat(xs, bsz, &mut xq, pool);
1211    let mut xk = vec![0.0f32; bsz * kd];
1212    w.k_proj.matmat(xs, bsz, &mut xk, pool);
1213    let mut xv = vec![0.0f32; bsz * vd];
1214    w.v_proj.matmat(xs, bsz, &mut xv, pool);
1215    let rank = w.f_a.rows();
1216    let mut fl = vec![0.0f32; bsz * rank];
1217    w.f_a.matmat(xs, bsz, &mut fl, pool);
1218    let mut f = vec![0.0f32; bsz * kd];
1219    w.f_b.matmat(&fl, bsz, &mut f, pool);
1220    let mut b = vec![0.0f32; bsz * nh];
1221    w.b_proj.matmat(xs, bsz, &mut b, pool);
1222    let mut gate_out = vec![0.0f32; bsz * vd];
1223    match &w.gate {
1224        KdaOutGate::Full(gp) => gp.matmat(xs, bsz, &mut gate_out, pool),
1225        KdaOutGate::LowRank(ga, gb) => {
1226            let mut low = vec![0.0f32; bsz * ga.rows()];
1227            ga.matmat(xs, bsz, &mut low, pool);
1228            gb.matmat(&low, bsz, &mut gate_out, pool);
1229        }
1230    }
1231
1232    let mut of = vec![0.0f32; bsz * vd];
1233    for bi in 0..bsz {
1234        let mut oh = vec![0.0f32; vd];
1235        kda_step(
1236            &xq[bi * kd..(bi + 1) * kd],
1237            &xk[bi * kd..(bi + 1) * kd],
1238            &xv[bi * vd..(bi + 1) * vd],
1239            &f[bi * kd..(bi + 1) * kd],
1240            &b[bi * nh..(bi + 1) * nh],
1241            &gate_out[bi * vd..(bi + 1) * vd],
1242            w,
1243            cfg,
1244            state,
1245            &mut oh,
1246            pool,
1247        );
1248        of[bi * vd..(bi + 1) * vd].copy_from_slice(&oh);
1249    }
1250
1251    let mut out = vec![0.0f32; bsz * hs];
1252    w.o_proj.matmat(&of, bsz, &mut out, pool);
1253    out
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    #[test]
1259    fn kda_forward_matches_naive_reference() {
1260        // Small deterministic KDA layer; the oracle is a literal port of
1261        // FLA naive_recurrent_kda + naive_kda_gate + the modeling glue
1262        // (conv→silu, low-rank decay, sigmoid-gated output norm), coded
1263        // straight from the reference — a different shape from the fused
1264        // two-pass production kernel.
1265        let (nh, dk, dv, kk, hs, rank) = (2usize, 4usize, 4usize, 3usize, 6usize, 3usize);
1266        let synth = |rows: usize, cols: usize, salt: usize| -> QTensor {
1267            QTensor::from_f32(
1268                (0..rows * cols)
1269                    .map(|i| (((i * 31 + salt * 17) % 101) as f32 / 101.0 - 0.5) * 0.6)
1270                    .collect(),
1271                rows,
1272                cols,
1273            )
1274        };
1275        let vecf = |n: usize, salt: usize| -> Vec<f32> {
1276            (0..n)
1277                .map(|i| (((i * 13 + salt * 7) % 89) as f32 / 89.0 - 0.5) * 0.8)
1278                .collect()
1279        };
1280        for (label, a_log, lb) in [
1281            ("per-head standard", vecf(nh, 40), None),
1282            ("per-dim lower-bound", vecf(dk, 41), Some(-5.0f32)),
1283        ] {
1284            let w = KdaWeights {
1285                q_proj: synth(nh * dk, hs, 1),
1286                k_proj: synth(nh * dk, hs, 2),
1287                v_proj: synth(nh * dv, hs, 3),
1288                conv_q: vecf(nh * dk * kk, 4),
1289                conv_k: vecf(nh * dk * kk, 5),
1290                conv_v: vecf(nh * dv * kk, 6),
1291                f_a: synth(rank, hs, 7),
1292                f_b: synth(nh * dk, rank, 8),
1293                dt_bias: vecf(nh * dk, 9),
1294                a_log: a_log.clone(),
1295                b_proj: synth(nh, hs, 10),
1296                gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1297                o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1298                o_proj: synth(hs, nh * dv, 13),
1299                gate_lower_bound: lb,
1300            };
1301            let cfg = KdaCfg {
1302                num_heads: nh,
1303                head_k_dim: dk,
1304                head_v_dim: dv,
1305                conv_kernel: kk,
1306                hidden_size: hs,
1307                rms_eps: 1e-6,
1308            };
1309            let xs: Vec<Vec<f32>> = (0..6)
1310                .map(|t| {
1311                    (0..hs)
1312                        .map(|i| ((t * hs + i) as f32 * 0.37).sin() * 0.5)
1313                        .collect()
1314                })
1315                .collect();
1316
1317            // Production path.
1318            let mut state = Vec::new();
1319            let got: Vec<Vec<f32>> = xs
1320                .iter()
1321                .map(|x| kda_forward(x, &w, &cfg, &mut state, None))
1322                .collect();
1323
1324            // Oracle.
1325            let mv = |t: &QTensor, x: &[f32]| -> Vec<f32> {
1326                let mut o = vec![0.0f32; t.rows()];
1327                t.matvec(x, &mut o, None);
1328                o
1329            };
1330            let mut hist: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = Vec::new(); // raw xq/xk/xv
1331            let mut s_state = vec![0f64; nh * dk * dv];
1332            let mut want: Vec<Vec<f32>> = Vec::new();
1333            for x in &xs {
1334                let (xq, xk, xv) = (mv(&w.q_proj, x), mv(&w.k_proj, x), mv(&w.v_proj, x));
1335                hist.push((xq, xk, xv));
1336                // conv over the raw history, taps oldest→newest.
1337                let conv = |sel: fn(&(Vec<f32>, Vec<f32>, Vec<f32>)) -> &Vec<f32>,
1338                            taps: &[f32],
1339                            n: usize|
1340                 -> Vec<f32> {
1341                    (0..n)
1342                        .map(|c| {
1343                            let t = &taps[c * kk..(c + 1) * kk];
1344                            let mut acc = 0f64;
1345                            for j in 0..kk {
1346                                let idx = hist.len() as i64 - (kk as i64 - j as i64);
1347                                if idx >= 0 {
1348                                    acc += sel(&hist[idx as usize])[c] as f64 * t[j] as f64;
1349                                }
1350                            }
1351                            silu(acc)
1352                        })
1353                        .map(|v| v as f32)
1354                        .collect()
1355                };
1356                let cq = conv(|h| &h.0, &w.conv_q, nh * dk);
1357                let ck = conv(|h| &h.1, &w.conv_k, nh * dk);
1358                let cv = conv(|h| &h.2, &w.conv_v, nh * dv);
1359                let f = mv(&w.f_b, &mv(&w.f_a, x));
1360                let bb = mv(&w.b_proj, x);
1361                let gate_out = match &w.gate {
1362                    KdaOutGate::LowRank(ga, gb) => mv(gb, &mv(ga, x)),
1363                    KdaOutGate::Full(g) => mv(g, x),
1364                };
1365                let mut of = vec![0f32; nh * dv];
1366                for h in 0..nh {
1367                    // l2norm + scale.
1368                    let q: Vec<f64> = {
1369                        let sl = &cq[h * dk..(h + 1) * dk];
1370                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
1371                        let inv = 1.0 / ((n + 1e-6).sqrt() * (dk as f64).sqrt());
1372                        sl.iter().map(|&v| v as f64 * inv).collect()
1373                    };
1374                    let k: Vec<f64> = {
1375                        let sl = &ck[h * dk..(h + 1) * dk];
1376                        let n: f64 = sl.iter().map(|&v| (v as f64) * (v as f64)).sum();
1377                        let inv = 1.0 / (n + 1e-6).sqrt();
1378                        sl.iter().map(|&v| v as f64 * inv).collect()
1379                    };
1380                    let v: Vec<f64> = cv[h * dv..(h + 1) * dv].iter().map(|&v| v as f64).collect();
1381                    // gate: g = −exp(A)·softplus(f+bias) | lb·σ(exp(A)·(f+bias))
1382                    let g: Vec<f64> = (0..dk)
1383                        .map(|d| {
1384                            let a = if w.a_log.len() == nh {
1385                                w.a_log[h] as f64
1386                            } else {
1387                                w.a_log[d] as f64
1388                            };
1389                            let raw = f[h * dk + d] as f64 + w.dt_bias[h * dk + d] as f64;
1390                            match w.gate_lower_bound {
1391                                Some(lb) => lb as f64 * sigmoid(a.exp() * raw),
1392                                None => -a.exp() * softplus(raw),
1393                            }
1394                        })
1395                        .collect();
1396                    let beta = sigmoid(bb[h] as f64);
1397                    let s = &mut s_state[h * dk * dv..(h + 1) * dk * dv];
1398                    // S = Diag(exp(g))·S
1399                    for di in 0..dk {
1400                        for dj in 0..dv {
1401                            s[di * dv + dj] *= g[di].exp();
1402                        }
1403                    }
1404                    // kv = kᵀS; S += β·k⊗(v−kv); o = qᵀS
1405                    let mut kv = vec![0f64; dv];
1406                    for di in 0..dk {
1407                        for dj in 0..dv {
1408                            kv[dj] += k[di] * s[di * dv + dj];
1409                        }
1410                    }
1411                    for di in 0..dk {
1412                        for dj in 0..dv {
1413                            s[di * dv + dj] += beta * k[di] * (v[dj] - kv[dj]);
1414                        }
1415                    }
1416                    let mut o = vec![0f64; dv];
1417                    for di in 0..dk {
1418                        for dj in 0..dv {
1419                            o[dj] += q[di] * s[di * dv + dj];
1420                        }
1421                    }
1422                    // sigmoid-gated RMSNorm
1423                    let ss: f64 = o.iter().map(|&v| v * v).sum();
1424                    let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
1425                    for dj in 0..dv {
1426                        of[h * dv + dj] = (o[dj]
1427                            * inv
1428                            * w.o_norm[dj] as f64
1429                            * sigmoid(gate_out[h * dv + dj] as f64))
1430                            as f32;
1431                    }
1432                }
1433                want.push(mv(&w.o_proj, &of));
1434            }
1435
1436            for (t, (g, e)) in got.iter().zip(&want).enumerate() {
1437                for (i, (a, b)) in g.iter().zip(e.iter()).enumerate() {
1438                    assert!((a - b).abs() < 2e-4, "{label}: t={t} i={i}: {a} vs {b}");
1439                }
1440            }
1441        }
1442
1443        // Batched prefill must equal the sequential singles bit-close.
1444        let w = KdaWeights {
1445            q_proj: synth(nh * dk, hs, 1),
1446            k_proj: synth(nh * dk, hs, 2),
1447            v_proj: synth(nh * dv, hs, 3),
1448            conv_q: vecf(nh * dk * kk, 4),
1449            conv_k: vecf(nh * dk * kk, 5),
1450            conv_v: vecf(nh * dv * kk, 6),
1451            f_a: synth(rank, hs, 7),
1452            f_b: synth(nh * dk, rank, 8),
1453            dt_bias: vecf(nh * dk, 9),
1454            a_log: vecf(nh, 40),
1455            b_proj: synth(nh, hs, 10),
1456            gate: KdaOutGate::LowRank(synth(rank, hs, 11), synth(nh * dv, rank, 12)),
1457            o_norm: (0..dv).map(|i| 1.0 + 0.1 * i as f32).collect(),
1458            o_proj: synth(hs, nh * dv, 13),
1459            gate_lower_bound: None,
1460        };
1461        let cfg = KdaCfg {
1462            num_heads: nh,
1463            head_k_dim: dk,
1464            head_v_dim: dv,
1465            conv_kernel: kk,
1466            hidden_size: hs,
1467            rms_eps: 1e-6,
1468        };
1469        let xs: Vec<f32> = (0..5 * hs).map(|i| (i as f32 * 0.29).cos() * 0.4).collect();
1470        let mut st1 = Vec::new();
1471        let seq: Vec<f32> = (0..5)
1472            .flat_map(|t| kda_forward(&xs[t * hs..(t + 1) * hs], &w, &cfg, &mut st1, None))
1473            .collect();
1474        let mut st2 = Vec::new();
1475        let bat = kda_forward_batch(&xs, 5, &w, &cfg, &mut st2, None);
1476        for (i, (a, b)) in seq.iter().zip(&bat).enumerate() {
1477            assert!((a - b).abs() < 1e-5, "batch i={i}: {a} vs {b}");
1478        }
1479        assert_eq!(st1, st2, "state must match after the chunk");
1480    }
1481
1482    use super::*;
1483
1484    fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
1485        let cfg = VmfPhaseCfg {
1486            num_heads: 2,
1487            nphase: 3,
1488            value_head_dim: 4,
1489            hidden_size: 8,
1490            phase_mass: 0.0,
1491        };
1492        let synth = |rows: usize, cols: usize, salt: usize| {
1493            QTensor::from_f32(
1494                (0..rows * cols)
1495                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
1496                    .collect(),
1497                rows,
1498                cols,
1499            )
1500        };
1501        let w = VmfPhaseWeights {
1502            thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
1503            thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
1504            v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
1505            out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
1506            decay: (0..cfg.num_heads * 2 * cfg.nphase)
1507                .map(|i| 0.9 + 0.005 * (i % 10) as f64)
1508                .collect(),
1509            k_gate: None,
1510        };
1511        (w, cfg)
1512    }
1513
1514    #[test]
1515    fn state_persists_and_changes_output() {
1516        let (w, cfg) = tiny();
1517        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
1518        let mut state = Vec::new();
1519        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
1520        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
1521        // Same input, evolved condensate → different output.
1522        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
1523        assert_eq!(state.len(), cfg.state_len());
1524    }
1525
1526    /// θ-mass (η′): mass=0 is bit-identical to the massless kernel; mass>0
1527    /// changes the output (phase narrowed → kernel widened). Guards the
1528    /// no-op default and that the knob is actually wired.
1529    #[test]
1530    fn phase_mass_zero_is_noop_and_positive_shifts() {
1531        let (w, cfg0) = tiny();
1532        let mut cfg_m = cfg0;
1533        cfg_m.phase_mass = 1.0;
1534        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
1535
1536        let mut s0 = Vec::new();
1537        let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
1538        // Re-run with mass=0 → must be bit-identical.
1539        let mut s0b = Vec::new();
1540        let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
1541        assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
1542        // mass=1 → output differs (θ halved before cos/sin).
1543        let mut sm = Vec::new();
1544        let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
1545        assert!(
1546            base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
1547            "mass>0 must change the output"
1548        );
1549        assert!(massed.iter().all(|v| v.is_finite()));
1550    }
1551
1552    /// κ write gate (hybrid_k): saturated-open gate (bias ≫ 0 → κ→1)
1553    /// matches the gateless kernel within fp tolerance; a closed gate
1554    /// (bias ≪ 0 → κ→0) writes nothing — the state stays zero and the
1555    /// output collapses to the empty-condensate readout.
1556    #[test]
1557    fn kappa_gate_open_matches_none_and_closed_writes_nothing() {
1558        let (mut w, cfg) = tiny();
1559        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
1560
1561        let mut s_none = Vec::new();
1562        let base1 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
1563        let base2 = vmf_phase_forward(&x, &w, &cfg, &mut s_none, None);
1564
1565        // Open gate: W=0, bias=+20 → κ = σ(20) ≈ 1 − 2e−9.
1566        w.k_gate = Some((
1567            QTensor::from_f32(
1568                vec![0.0; cfg.num_heads * cfg.hidden_size],
1569                cfg.num_heads,
1570                cfg.hidden_size,
1571            ),
1572            vec![20.0; cfg.num_heads],
1573        ));
1574        let mut s_open = Vec::new();
1575        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
1576        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut s_open, None);
1577        for (a, b) in base1.iter().zip(&o1).chain(base2.iter().zip(&o2)) {
1578            assert!(
1579                (a - b).abs() < 1e-5,
1580                "open κ must match gateless: {a} vs {b}"
1581            );
1582        }
1583
1584        // Closed gate: bias=−20 → κ ≈ 0 → nothing is written.
1585        w.k_gate = Some((
1586            QTensor::from_f32(
1587                vec![0.0; cfg.num_heads * cfg.hidden_size],
1588                cfg.num_heads,
1589                cfg.hidden_size,
1590            ),
1591            vec![-20.0; cfg.num_heads],
1592        ));
1593        let mut s_closed = Vec::new();
1594        let oc = vmf_phase_forward(&x, &w, &cfg, &mut s_closed, None);
1595        assert!(
1596            s_closed.iter().all(|&v| v.abs() < 1e-7),
1597            "closed κ: state must stay empty"
1598        );
1599        assert!(
1600            oc.iter().all(|&v| v.abs() < 1e-6),
1601            "closed κ: empty-condensate readout"
1602        );
1603    }
1604
1605    #[test]
1606    fn pair_matches_two_singles_bitexact() {
1607        let (w, cfg) = tiny();
1608        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
1609        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
1610
1611        // Reference: two sequential singles.
1612        let mut s_ref = Vec::new();
1613        let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
1614        let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
1615
1616        // Pair: lane1 commits, lane2 tentative in scratch.
1617        let mut s = Vec::new();
1618        let mut scratch = Vec::new();
1619        let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1620        assert_eq!(r1, p1, "lane 1 must be bit-identical");
1621        assert_eq!(r2, p2, "lane 2 must be bit-identical");
1622        // Accepting the draft = swapping scratch in → equals s_ref.
1623        std::mem::swap(&mut s, &mut scratch);
1624        assert_eq!(s, s_ref, "accepted state must equal sequential state");
1625    }
1626
1627    #[test]
1628    fn rejected_draft_leaves_state_at_lane1() {
1629        let (w, cfg) = tiny();
1630        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
1631        let x2 = vec![0.5f32; 8];
1632
1633        let mut s_ref = Vec::new();
1634        let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
1635
1636        let mut s = Vec::new();
1637        let mut scratch = Vec::new();
1638        let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1639        // Reject: state must be exactly the post-lane1 state.
1640        assert_eq!(s, s_ref);
1641    }
1642
1643    // ───────────── GatedDeltaNet ─────────────
1644
1645    fn tiny_gdn() -> (GdnWeights, GdnCfg) {
1646        let cfg = GdnCfg {
1647            num_v_heads: 4,
1648            num_k_heads: 2,
1649            key_head_dim: 3,
1650            value_head_dim: 5,
1651            conv_kernel: 4,
1652            hidden_size: 8,
1653            rms_eps: 1e-6,
1654        };
1655        let c_dim = cfg.conv_dim();
1656        let vd = cfg.num_v_heads * cfg.value_head_dim;
1657        let synth = |rows: usize, cols: usize, salt: usize| {
1658            QTensor::from_f32(
1659                (0..rows * cols)
1660                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
1661                    .collect(),
1662                rows,
1663                cols,
1664            )
1665        };
1666        let vecf = |n: usize, salt: usize| -> Vec<f32> {
1667            (0..n)
1668                .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
1669                .collect()
1670        };
1671        let w = GdnWeights {
1672            in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
1673            in_proj_z: synth(vd, cfg.hidden_size, 2),
1674            in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
1675            in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
1676            conv1d: vecf(c_dim * cfg.conv_kernel, 5),
1677            a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
1678            dt_bias: vecf(cfg.num_v_heads, 6),
1679            norm: vec![1.0; cfg.value_head_dim],
1680            out_proj: synth(cfg.hidden_size, vd, 7),
1681        };
1682        (w, cfg)
1683    }
1684
1685    #[test]
1686    fn gdn_state_persists_and_changes_output() {
1687        let (w, cfg) = tiny_gdn();
1688        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
1689        let mut state = Vec::new();
1690        let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
1691        let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
1692        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
1693        assert_eq!(state.len(), cfg.state_len());
1694    }
1695
1696    #[test]
1697    fn gdn_pair_matches_two_singles_bitexact() {
1698        let (w, cfg) = tiny_gdn();
1699        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
1700        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
1701
1702        let mut s_ref = Vec::new();
1703        let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
1704        let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
1705
1706        let mut s = Vec::new();
1707        let mut scratch = Vec::new();
1708        let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1709        assert_eq!(r1, p1, "lane 1 must be bit-identical");
1710        assert_eq!(r2, p2, "lane 2 must be bit-identical");
1711        std::mem::swap(&mut s, &mut scratch);
1712        assert_eq!(s, s_ref, "accepted state must equal sequential state");
1713    }
1714
1715    #[test]
1716    fn gdn_rejected_draft_leaves_state_at_lane1() {
1717        let (w, cfg) = tiny_gdn();
1718        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
1719        let x2 = vec![0.5f32; 8];
1720
1721        let mut s_ref = Vec::new();
1722        let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
1723
1724        let mut s = Vec::new();
1725        let mut scratch = Vec::new();
1726        let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
1727        assert_eq!(s, s_ref);
1728    }
1729
1730    /// The conv ring must give the same result as an explicit causal
1731    /// conv over the whole sequence (oracle semantics: zero left-pad,
1732    /// tap kk−1 on the current position).
1733    #[test]
1734    fn gdn_conv_ring_matches_explicit_causal_conv() {
1735        let (w, cfg) = tiny_gdn();
1736        let seq: Vec<Vec<f32>> = (0..6)
1737            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
1738            .collect();
1739
1740        // Reference: recompute position t from scratch each time with a
1741        // fresh state built by replaying the prefix.
1742        let mut s_inc = Vec::new();
1743        for (t, x) in seq.iter().enumerate() {
1744            let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
1745            let mut s_replay = Vec::new();
1746            let mut replay = Vec::new();
1747            for xr in &seq[..=t] {
1748                replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
1749            }
1750            assert_eq!(inc, replay, "position {t}: ring must equal replay");
1751        }
1752    }
1753
1754    fn tiny_short_conv() -> (ShortConvWeights, ShortConvCfg) {
1755        let cfg = ShortConvCfg {
1756            hidden_size: 8,
1757            kernel: 3,
1758        };
1759        let synth = |rows: usize, cols: usize, salt: usize| {
1760            QTensor::from_f32(
1761                (0..rows * cols)
1762                    .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.5)
1763                    .collect(),
1764                rows,
1765                cols,
1766            )
1767        };
1768        let w = ShortConvWeights {
1769            in_proj: synth(3 * cfg.hidden_size, cfg.hidden_size, 1),
1770            conv: (0..cfg.hidden_size * cfg.kernel)
1771                .map(|i| ((i * 7 % 13) as f32 / 13.0 - 0.5) * 0.8)
1772                .collect(),
1773            out_proj: synth(cfg.hidden_size, cfg.hidden_size, 2),
1774        };
1775        (w, cfg)
1776    }
1777
1778    /// The incremental conv ring must equal a from-scratch causal replay
1779    /// of the prefix at every position — the decode/prefill contract.
1780    #[test]
1781    fn short_conv_ring_matches_explicit_causal_conv() {
1782        let (w, cfg) = tiny_short_conv();
1783        let seq: Vec<Vec<f32>> = (0..6)
1784            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.19).cos()).collect())
1785            .collect();
1786        let mut s_inc = Vec::new();
1787        for (t, x) in seq.iter().enumerate() {
1788            let inc = short_conv_forward(x, &w, &cfg, &mut s_inc, None);
1789            let mut s_replay = Vec::new();
1790            let mut replay = Vec::new();
1791            for xr in &seq[..=t] {
1792                replay = short_conv_forward(xr, &w, &cfg, &mut s_replay, None);
1793            }
1794            assert_eq!(inc, replay, "position {t}: ring must equal replay");
1795            assert_eq!(s_inc.len(), cfg.state_len());
1796        }
1797    }
1798
1799    /// The batched prefill path (matmat + sequential conv over the chunk)
1800    /// must reproduce the position-by-position decode path exactly.
1801    #[test]
1802    fn short_conv_batch_matches_sequential() {
1803        let (w, cfg) = tiny_short_conv();
1804        let b = 5;
1805        let xs: Vec<f32> = (0..b * cfg.hidden_size)
1806            .map(|i| (i as f32 * 0.13).sin() * 0.6)
1807            .collect();
1808
1809        let mut s_seq = Vec::new();
1810        let mut seq_out = vec![0.0f32; b * cfg.hidden_size];
1811        for bi in 0..b {
1812            let o = short_conv_forward(
1813                &xs[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size],
1814                &w,
1815                &cfg,
1816                &mut s_seq,
1817                None,
1818            );
1819            seq_out[bi * cfg.hidden_size..(bi + 1) * cfg.hidden_size].copy_from_slice(&o);
1820        }
1821
1822        let mut s_batch = Vec::new();
1823        let batch_out = short_conv_forward_batch(&xs, b, &w, &cfg, &mut s_batch, None);
1824        assert_eq!(
1825            seq_out, batch_out,
1826            "batch conv must match sequential decode"
1827        );
1828        assert_eq!(s_seq, s_batch, "ring state must match after the chunk");
1829    }
1830}