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<f64>` 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}
42
43#[derive(Clone, Copy)]
44pub struct VmfPhaseCfg {
45    pub num_heads: usize,
46    pub nphase: usize,
47    pub value_head_dim: usize,
48    pub hidden_size: usize,
49    /// θ-mass (η′ correction): a restoring potential pulling the phase
50    /// toward 0 — θ_eff = θ/(1+mass) — which WIDENS the phase kernel.
51    /// Measured (experiments/vmf_native_core*.py) to restore noise
52    /// robustness when the phase projection is FIXED (exactly CMF's
53    /// fold-before-heal regime: thq/thk are init, not trained) — recall
54    /// 3%→91% at moderate noise; redundant once the projection is
55    /// healed. 0.0 = massless Goldstone (bit-identical to prior kernel).
56    /// Set via CMF_PHASE_MASS. Validated at mechanism level, not yet LM.
57    pub phase_mass: f32,
58}
59
60impl VmfPhaseCfg {
61    pub fn state_len(&self) -> usize {
62        self.num_heads * 2 * self.nphase * self.value_head_dim
63    }
64}
65
66/// One recurrent step for one head-set given projected phases/values.
67/// `state` is S[nh][p2, dv] in f64 (condensate persists across tokens).
68fn phase_step(
69    thq: &[f32],
70    thk: &[f32],
71    v: &[f32],
72    decay: &[f64],
73    cfg: &VmfPhaseCfg,
74    state: &mut [f64],
75    out: &mut [f32],
76) {
77    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
78    // θ-mass (η′): θ_eff = θ/(1+mass). mass=0 → factor 1 → no-op.
79    let mscale = 1.0f64 / (1.0 + cfg.phase_mass as f64);
80    let p2 = 2 * nph;
81    for h in 0..nh {
82        let s = &mut state[h * p2 * dv..(h + 1) * p2 * dv];
83        let thk_h = &thk[h * nph..(h + 1) * nph];
84        let thq_h = &thq[h * nph..(h + 1) * nph];
85        let vt = &v[h * dv..(h + 1) * dv];
86        let ot = &mut out[h * dv..(h + 1) * dv];
87        let dec = &decay[h * p2..(h + 1) * p2];
88        for f in 0..p2 {
89            // φ(θ) = [cos·nph, sin·nph], θ scaled by the mass factor.
90            let (fk, fq) = if f < nph {
91                ((thk_h[f] as f64 * mscale).cos(), (thq_h[f] as f64 * mscale).cos())
92            } else {
93                (
94                    (thk_h[f - nph] as f64 * mscale).sin(),
95                    (thq_h[f - nph] as f64 * mscale).sin(),
96                )
97            };
98            let row = &mut s[f * dv..(f + 1) * dv];
99            let dcf = dec[f];
100            for d in 0..dv {
101                row[d] = dcf * row[d] + fk * vt[d] as f64; // S = decay·S + φk⊗v
102                ot[d] += (fq * row[d]) as f32; // o = Σ φq·S
103            }
104        }
105    }
106}
107
108/// Forward one position through a vmf_phase layer, advancing `state`.
109pub fn vmf_phase_forward(
110    x: &[f32],
111    w: &VmfPhaseWeights,
112    cfg: &VmfPhaseCfg,
113    state: &mut Vec<f64>,
114    pool: Option<&Pool>,
115) -> Vec<f32> {
116    if state.len() != cfg.state_len() {
117        *state = vec![0f64; cfg.state_len()];
118    }
119    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
120
121    let mut thq = vec![0.0f32; nh * nph];
122    w.thq.matvec(x, &mut thq, pool);
123    let mut thk = vec![0.0f32; nh * nph];
124    w.thk.matvec(x, &mut thk, pool);
125    let mut v = vec![0.0f32; nh * dv];
126    w.v_proj.matvec(x, &mut v, pool);
127
128    let mut o = vec![0.0f32; nh * dv];
129    phase_step(&thq, &thk, &v, &w.decay, cfg, state, &mut o);
130
131    let mut out = vec![0.0f32; cfg.hidden_size];
132    w.out_proj.matvec(&o, &mut out, pool);
133    out
134}
135
136/// Fused two-position forward (speculative verify). Lane 1 commits into
137/// `state` (its token is always committed); lane 2's tentative state
138/// goes into `scratch` — the caller swaps it in on draft acceptance and
139/// simply drops it on rejection.
140#[allow(clippy::too_many_arguments)]
141pub fn vmf_phase_pair(
142    x1: &[f32],
143    x2: &[f32],
144    w: &VmfPhaseWeights,
145    cfg: &VmfPhaseCfg,
146    state: &mut Vec<f64>,
147    scratch: &mut Vec<f64>,
148    pool: Option<&Pool>,
149) -> (Vec<f32>, Vec<f32>) {
150    if state.len() != cfg.state_len() {
151        *state = vec![0f64; cfg.state_len()];
152    }
153    let (nh, nph, dv) = (cfg.num_heads, cfg.nphase, cfg.value_head_dim);
154
155    let mut thq1 = vec![0.0f32; nh * nph];
156    let mut thq2 = vec![0.0f32; nh * nph];
157    w.thq.matvec2(x1, x2, &mut thq1, &mut thq2, pool);
158    let mut thk1 = vec![0.0f32; nh * nph];
159    let mut thk2 = vec![0.0f32; nh * nph];
160    w.thk.matvec2(x1, x2, &mut thk1, &mut thk2, pool);
161    let mut v1 = vec![0.0f32; nh * dv];
162    let mut v2 = vec![0.0f32; nh * dv];
163    w.v_proj.matvec2(x1, x2, &mut v1, &mut v2, pool);
164
165    // Lane 1 commits into the real state.
166    let mut o1 = vec![0.0f32; nh * dv];
167    phase_step(&thq1, &thk1, &v1, &w.decay, cfg, state, &mut o1);
168
169    // Lane 2 runs on a copy — tentative until the draft is verified.
170    scratch.clear();
171    scratch.extend_from_slice(state);
172    let mut o2 = vec![0.0f32; nh * dv];
173    phase_step(&thq2, &thk2, &v2, &w.decay, cfg, scratch, &mut o2);
174
175    let mut out1 = vec![0.0f32; cfg.hidden_size];
176    let mut out2 = vec![0.0f32; cfg.hidden_size];
177    w.out_proj.matvec2(&o1, &o2, &mut out1, &mut out2, pool);
178    (out1, out2)
179}
180
181// ───────────────────────── GatedDeltaNet (faithful vendor operator) ─────────────────────────
182
183/// Weights of one GatedDeltaNet layer (`model.layers.{i}.linear_attn.*`,
184/// names 1:1 with the source model — no fold, no training).
185pub struct GdnWeights {
186    /// [2·nk·dk + nv·dv, hidden] — fused q/k/v projection
187    pub in_proj_qkv: QTensor,
188    /// [nv·dv, hidden] — output-gate projection z
189    pub in_proj_z: QTensor,
190    /// [nv, hidden] — decay modulation a
191    pub in_proj_a: QTensor,
192    /// [nv, hidden] — write-strength b (β = σ(b))
193    pub in_proj_b: QTensor,
194    /// [c_dim · kk] — depthwise causal conv taps, flattened [c][tap]
195    pub conv1d: Vec<f32>,
196    /// [nv]
197    pub a_log: Vec<f32>,
198    /// [nv]
199    pub dt_bias: Vec<f32>,
200    /// [dv] — gated RMSNorm weight (plain x̂·w, validated by the oracle)
201    pub norm: Vec<f32>,
202    /// [hidden, nv·dv]
203    pub out_proj: QTensor,
204}
205
206#[derive(Clone, Copy)]
207pub struct GdnCfg {
208    pub num_v_heads: usize,
209    pub num_k_heads: usize,
210    pub key_head_dim: usize,
211    pub value_head_dim: usize,
212    pub conv_kernel: usize,
213    pub hidden_size: usize,
214    pub rms_eps: f64,
215}
216
217impl GdnCfg {
218    pub fn conv_dim(&self) -> usize {
219        2 * self.num_k_heads * self.key_head_dim + self.num_v_heads * self.value_head_dim
220    }
221
222    /// Packed state: [conv ring (kk−1)·c_dim | S nv·dk·dv], one Vec<f64>
223    /// so the speculative scratch-swap moves ring and condensate together.
224    pub fn state_len(&self) -> usize {
225        (self.conv_kernel - 1) * self.conv_dim()
226            + self.num_v_heads * self.key_head_dim * self.value_head_dim
227    }
228}
229
230fn softplus(x: f64) -> f64 {
231    if x > 20.0 {
232        x
233    } else {
234        x.exp().ln_1p()
235    }
236}
237
238fn sigmoid(x: f64) -> f64 {
239    1.0 / (1.0 + (-x).exp())
240}
241
242fn silu(x: f64) -> f64 {
243    x / (1.0 + (-x).exp())
244}
245
246/// One recurrent step given the raw (pre-conv) projections of this
247/// position. Advances the packed state (conv ring + S) and writes the
248/// gated per-head output into `of` [nv·dv].
249#[allow(clippy::too_many_arguments)]
250fn gdn_step(qkv: &[f32], z: &[f32], a: &[f32], b: &[f32], w: &GdnWeights, cfg: &GdnCfg, state: &mut [f64], of: &mut [f32]) {
251    let (nv, nk, dk, dv, kk) = (
252        cfg.num_v_heads,
253        cfg.num_k_heads,
254        cfg.key_head_dim,
255        cfg.value_head_dim,
256        cfg.conv_kernel,
257    );
258    let c_dim = cfg.conv_dim();
259    let (kd, rep) = (nk * dk, nv / nk);
260    let (ring, s_all) = state.split_at_mut((kk - 1) * c_dim);
261
262    // Depthwise causal conv over [ring…, current] + SiLU. Taps are
263    // ordered oldest→newest; tap kk−1 multiplies the current position.
264    let mut cq = vec![0f64; c_dim];
265    for c in 0..c_dim {
266        let taps = &w.conv1d[c * kk..(c + 1) * kk];
267        let mut acc = qkv[c] as f64 * taps[kk - 1] as f64;
268        for j in 0..kk - 1 {
269            acc += ring[j * c_dim + c] * taps[j] as f64;
270        }
271        cq[c] = silu(acc);
272    }
273    // Ring shift: drop the oldest position, append the raw current one.
274    if kk > 1 {
275        ring.copy_within(c_dim.., 0);
276        let tail = (kk - 2) * c_dim;
277        for c in 0..c_dim {
278            ring[tail + c] = qkv[c] as f64;
279        }
280    }
281
282    for h in 0..nv {
283        let ko = h / rep; // source q/k head (GQA)
284        let (qs, ks) = (ko * dk, kd + ko * dk);
285        // l2-normalize q and k; q additionally scaled by 1/√dk.
286        let (mut nq, mut nkn) = (0f64, 0f64);
287        for d in 0..dk {
288            nq += cq[qs + d] * cq[qs + d];
289            nkn += cq[ks + d] * cq[ks + d];
290        }
291        let invq = 1.0 / ((nq + 1e-6).sqrt() * (dk as f64).sqrt());
292        let invk = 1.0 / (nkn + 1e-6).sqrt();
293
294        let g = (-(w.a_log[h] as f64).exp() * softplus(a[h] as f64 + w.dt_bias[h] as f64)).exp();
295        let beta = sigmoid(b[h] as f64);
296
297        let s = &mut s_all[h * dk * dv..(h + 1) * dk * dv];
298        let vt = &cq[2 * kd + h * dv..2 * kd + (h + 1) * dv];
299        // S ← g·S;  kv = kᵀS;  S += k ⊗ β(v − kv);  o = qᵀS
300        let mut kv = vec![0f64; dv];
301        for di in 0..dk {
302            let kf = cq[ks + di] * invk;
303            let row = &mut s[di * dv..(di + 1) * dv];
304            for dj in 0..dv {
305                row[dj] *= g;
306                kv[dj] += row[dj] * kf;
307            }
308        }
309        let mut o = vec![0f64; dv];
310        for di in 0..dk {
311            let kf = cq[ks + di] * invk;
312            let qf = cq[qs + di] * invq;
313            let row = &mut s[di * dv..(di + 1) * dv];
314            for dj in 0..dv {
315                row[dj] += kf * (vt[dj] - kv[dj]) * beta;
316                o[dj] += qf * row[dj];
317            }
318        }
319        // Gated RMSNorm per head: x̂·w·silu(z) (oracle-validated form).
320        let ss: f64 = o.iter().map(|v| v * v).sum();
321        let inv = 1.0 / (ss / dv as f64 + cfg.rms_eps).sqrt();
322        for dj in 0..dv {
323            of[h * dv + dj] =
324                ((o[dj] * inv) * w.norm[dj] as f64 * silu(z[h * dv + dj] as f64)) as f32;
325        }
326    }
327}
328
329/// Forward one position through a GatedDeltaNet layer, advancing `state`.
330pub fn gdn_forward(
331    x: &[f32],
332    w: &GdnWeights,
333    cfg: &GdnCfg,
334    state: &mut Vec<f64>,
335    pool: Option<&Pool>,
336) -> Vec<f32> {
337    if state.len() != cfg.state_len() {
338        *state = vec![0f64; cfg.state_len()];
339    }
340    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
341
342    let mut qkv = vec![0.0f32; c_dim];
343    let mut z = vec![0.0f32; vd];
344    // D5: two heavy projections (qkv+z ≈ 24MB q8 per 35B layer, ×30 layers
345    // = half the token's bytes) — in a single GPU submission; a/b are tiny
346    // f16 and stay on CPU.
347    if !gdn_projs_gpu(w, x, &mut qkv, &mut z) {
348        w.in_proj_qkv.matvec(x, &mut qkv, pool);
349        w.in_proj_z.matvec(x, &mut z, pool);
350    }
351    let mut a = vec![0.0f32; cfg.num_v_heads];
352    w.in_proj_a.matvec(x, &mut a, pool);
353    let mut b = vec![0.0f32; cfg.num_v_heads];
354    w.in_proj_b.matvec(x, &mut b, pool);
355
356    let mut of = vec![0.0f32; vd];
357    gdn_step(&qkv, &z, &a, &b, w, cfg, state, &mut of);
358
359    let mut out = vec![0.0f32; cfg.hidden_size];
360    w.out_proj.matvec(&of, &mut out, pool);
361    out
362}
363
364/// Batched GDN forward (prefill-GEMM): the qkv/z/a/b and out_proj
365/// projections are matmat over the batch (a weight row once per chunk),
366/// the gdn_step recurrence runs sequentially over positions (state is the
367/// same as the sequential path; the math is elementwise identical).
368pub fn gdn_forward_batch(
369    xs: &[f32],
370    b: usize,
371    w: &GdnWeights,
372    cfg: &GdnCfg,
373    state: &mut Vec<f64>,
374    pool: Option<&Pool>,
375) -> Vec<f32> {
376    if state.len() != cfg.state_len() {
377        *state = vec![0f64; cfg.state_len()];
378    }
379    let (c_dim, vd) = (cfg.conv_dim(), cfg.num_v_heads * cfg.value_head_dim);
380    let nv = cfg.num_v_heads;
381
382    let mut qkv = vec![0.0f32; b * c_dim];
383    w.in_proj_qkv.matmat(xs, b, &mut qkv, pool);
384    let mut z = vec![0.0f32; b * vd];
385    w.in_proj_z.matmat(xs, b, &mut z, pool);
386    let mut a = vec![0.0f32; b * nv];
387    w.in_proj_a.matmat(xs, b, &mut a, pool);
388    let mut bb = vec![0.0f32; b * nv];
389    w.in_proj_b.matmat(xs, b, &mut bb, pool);
390
391    let mut of = vec![0.0f32; b * vd];
392    for bi in 0..b {
393        gdn_step(
394            &qkv[bi * c_dim..(bi + 1) * c_dim],
395            &z[bi * vd..(bi + 1) * vd],
396            &a[bi * nv..(bi + 1) * nv],
397            &bb[bi * nv..(bi + 1) * nv],
398            w,
399            cfg,
400            state,
401            &mut of[bi * vd..(bi + 1) * vd],
402        );
403    }
404    let mut out = vec![0.0f32; b * cfg.hidden_size];
405    w.out_proj.matmat(&of, b, &mut out, pool);
406    out
407}
408
409/// GDN qkv+z on GPU in a single submission (independent matvecs of one input).
410fn gdn_projs_gpu(w: &GdnWeights, x: &[f32], qkv: &mut [f32], z: &mut [f32]) -> bool {
411    use crate::gpu::matvec_batch;
412    use crate::qtensor::QTensor;
413    // Measured (35B, alternating A/B): 2 matvecs per submission do NOT
414    // amortize the sync cost — neutral within the noise. Opt-in until
415    // attention/norms move into this same submission.
416    if !crate::gpu::enabled_here()
417        || !std::env::var("CMF_GPU_GDN").map(|v| v == "1").unwrap_or(false)
418    {
419        return false;
420    }
421    fn part<'a>(
422        t: &'a QTensor,
423        x: &[f32],
424    ) -> Option<(std::sync::Arc<cortiq_core::CmfModel>, crate::gpu::BatchJob<'a>)> {
425        use crate::gpu::BatchJob;
426        use crate::qtensor::prescale;
427        use cortiq_core::TensorDtype;
428        match t {
429        QTensor::Mapped {
430            model,
431            idx,
432            dtype: dt @ (TensorDtype::Q8Row | TensorDtype::Q8_2f),
433            rows,
434            cols,
435            row_scale,
436            col_field,
437        } => Some((
438            model.clone(),
439            BatchJob {
440                idx: *idx,
441                rows: *rows,
442                cols: *cols,
443                row_scale,
444                xs: prescale(x, col_field, *dt),
445            },
446        )),
447        _ => None,
448        }
449    }
450    let Some((model, jq)) = part(&w.in_proj_qkv, x) else { return false };
451    let Some((_, jz)) = part(&w.in_proj_z, x) else { return false };
452    matvec_batch(&model, &[jq, jz], &mut [qkv, z])
453}
454
455/// Fused two-position forward (speculative verify): lane 1 commits into
456/// `state`, lane 2 is tentative in `scratch` (ring + S move together).
457#[allow(clippy::too_many_arguments)]
458pub fn gdn_pair(
459    x1: &[f32],
460    x2: &[f32],
461    w: &GdnWeights,
462    cfg: &GdnCfg,
463    state: &mut Vec<f64>,
464    scratch: &mut Vec<f64>,
465    pool: Option<&Pool>,
466) -> (Vec<f32>, Vec<f32>) {
467    if state.len() != cfg.state_len() {
468        *state = vec![0f64; cfg.state_len()];
469    }
470    let (c_dim, vd, nv) = (
471        cfg.conv_dim(),
472        cfg.num_v_heads * cfg.value_head_dim,
473        cfg.num_v_heads,
474    );
475
476    let mut qkv1 = vec![0.0f32; c_dim];
477    let mut qkv2 = vec![0.0f32; c_dim];
478    w.in_proj_qkv.matvec2(x1, x2, &mut qkv1, &mut qkv2, pool);
479    let mut z1 = vec![0.0f32; vd];
480    let mut z2 = vec![0.0f32; vd];
481    w.in_proj_z.matvec2(x1, x2, &mut z1, &mut z2, pool);
482    let mut a1 = vec![0.0f32; nv];
483    let mut a2 = vec![0.0f32; nv];
484    w.in_proj_a.matvec2(x1, x2, &mut a1, &mut a2, pool);
485    let mut b1 = vec![0.0f32; nv];
486    let mut b2 = vec![0.0f32; nv];
487    w.in_proj_b.matvec2(x1, x2, &mut b1, &mut b2, pool);
488
489    let mut of1 = vec![0.0f32; vd];
490    gdn_step(&qkv1, &z1, &a1, &b1, w, cfg, state, &mut of1);
491
492    scratch.clear();
493    scratch.extend_from_slice(state);
494    let mut of2 = vec![0.0f32; vd];
495    gdn_step(&qkv2, &z2, &a2, &b2, w, cfg, scratch, &mut of2);
496
497    let mut out1 = vec![0.0f32; cfg.hidden_size];
498    let mut out2 = vec![0.0f32; cfg.hidden_size];
499    w.out_proj.matvec2(&of1, &of2, &mut out1, &mut out2, pool);
500    (out1, out2)
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    fn tiny() -> (VmfPhaseWeights, VmfPhaseCfg) {
508        let cfg = VmfPhaseCfg {
509            num_heads: 2,
510            nphase: 3,
511            value_head_dim: 4,
512            hidden_size: 8,
513            phase_mass: 0.0,
514        };
515        let synth = |rows: usize, cols: usize, salt: usize| {
516            QTensor::from_f32(
517                (0..rows * cols)
518                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
519                    .collect(),
520                rows,
521                cols,
522            )
523        };
524        let w = VmfPhaseWeights {
525            thq: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 1),
526            thk: synth(cfg.num_heads * cfg.nphase, cfg.hidden_size, 2),
527            v_proj: synth(cfg.num_heads * cfg.value_head_dim, cfg.hidden_size, 3),
528            out_proj: synth(cfg.hidden_size, cfg.num_heads * cfg.value_head_dim, 4),
529            decay: (0..cfg.num_heads * 2 * cfg.nphase)
530                .map(|i| 0.9 + 0.005 * (i % 10) as f64)
531                .collect(),
532        };
533        (w, cfg)
534    }
535
536    #[test]
537    fn state_persists_and_changes_output() {
538        let (w, cfg) = tiny();
539        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
540        let mut state = Vec::new();
541        let o1 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
542        let o2 = vmf_phase_forward(&x, &w, &cfg, &mut state, None);
543        // Same input, evolved condensate → different output.
544        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
545        assert_eq!(state.len(), cfg.state_len());
546    }
547
548    /// θ-mass (η′): mass=0 is bit-identical to the massless kernel; mass>0
549    /// changes the output (phase narrowed → kernel widened). Guards the
550    /// no-op default and that the knob is actually wired.
551    #[test]
552    fn phase_mass_zero_is_noop_and_positive_shifts() {
553        let (w, cfg0) = tiny();
554        let mut cfg_m = cfg0.clone();
555        cfg_m.phase_mass = 1.0;
556        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.4).sin()).collect();
557
558        let mut s0 = Vec::new();
559        let base = vmf_phase_forward(&x, &w, &cfg0, &mut s0, None);
560        // Re-run with mass=0 → must be bit-identical.
561        let mut s0b = Vec::new();
562        let base2 = vmf_phase_forward(&x, &w, &cfg0, &mut s0b, None);
563        assert_eq!(base, base2, "mass=0 must be deterministic/no-op");
564        // mass=1 → output differs (θ halved before cos/sin).
565        let mut sm = Vec::new();
566        let massed = vmf_phase_forward(&x, &w, &cfg_m, &mut sm, None);
567        assert!(
568            base.iter().zip(&massed).any(|(a, b)| (a - b).abs() > 1e-5),
569            "mass>0 must change the output"
570        );
571        assert!(massed.iter().all(|v| v.is_finite()));
572    }
573
574    #[test]
575    fn pair_matches_two_singles_bitexact() {
576        let (w, cfg) = tiny();
577        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
578        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
579
580        // Reference: two sequential singles.
581        let mut s_ref = Vec::new();
582        let r1 = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
583        let r2 = vmf_phase_forward(&x2, &w, &cfg, &mut s_ref, None);
584
585        // Pair: lane1 commits, lane2 tentative in scratch.
586        let mut s = Vec::new();
587        let mut scratch = Vec::new();
588        let (p1, p2) = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
589        assert_eq!(r1, p1, "lane 1 must be bit-identical");
590        assert_eq!(r2, p2, "lane 2 must be bit-identical");
591        // Accepting the draft = swapping scratch in → equals s_ref.
592        std::mem::swap(&mut s, &mut scratch);
593        assert_eq!(s, s_ref, "accepted state must equal sequential state");
594    }
595
596    #[test]
597    fn rejected_draft_leaves_state_at_lane1() {
598        let (w, cfg) = tiny();
599        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
600        let x2 = vec![0.5f32; 8];
601
602        let mut s_ref = Vec::new();
603        let _ = vmf_phase_forward(&x1, &w, &cfg, &mut s_ref, None);
604
605        let mut s = Vec::new();
606        let mut scratch = Vec::new();
607        let _ = vmf_phase_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
608        // Reject: state must be exactly the post-lane1 state.
609        assert_eq!(s, s_ref);
610    }
611
612    // ───────────── GatedDeltaNet ─────────────
613
614    fn tiny_gdn() -> (GdnWeights, GdnCfg) {
615        let cfg = GdnCfg {
616            num_v_heads: 4,
617            num_k_heads: 2,
618            key_head_dim: 3,
619            value_head_dim: 5,
620            conv_kernel: 4,
621            hidden_size: 8,
622            rms_eps: 1e-6,
623        };
624        let c_dim = cfg.conv_dim();
625        let vd = cfg.num_v_heads * cfg.value_head_dim;
626        let synth = |rows: usize, cols: usize, salt: usize| {
627            QTensor::from_f32(
628                (0..rows * cols)
629                    .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
630                    .collect(),
631                rows,
632                cols,
633            )
634        };
635        let vecf = |n: usize, salt: usize| -> Vec<f32> {
636            (0..n)
637                .map(|i| (((i * 11 + salt * 5) % 89) as f32 / 89.0 - 0.5) * 0.6)
638                .collect()
639        };
640        let w = GdnWeights {
641            in_proj_qkv: synth(c_dim, cfg.hidden_size, 1),
642            in_proj_z: synth(vd, cfg.hidden_size, 2),
643            in_proj_a: synth(cfg.num_v_heads, cfg.hidden_size, 3),
644            in_proj_b: synth(cfg.num_v_heads, cfg.hidden_size, 4),
645            conv1d: vecf(c_dim * cfg.conv_kernel, 5),
646            a_log: (0..cfg.num_v_heads).map(|i| 0.2 + 0.3 * i as f32).collect(),
647            dt_bias: vecf(cfg.num_v_heads, 6),
648            norm: vec![1.0; cfg.value_head_dim],
649            out_proj: synth(cfg.hidden_size, vd, 7),
650        };
651        (w, cfg)
652    }
653
654    #[test]
655    fn gdn_state_persists_and_changes_output() {
656        let (w, cfg) = tiny_gdn();
657        let x: Vec<f32> = (0..8).map(|i| (i as f32 * 0.3).sin()).collect();
658        let mut state = Vec::new();
659        let o1 = gdn_forward(&x, &w, &cfg, &mut state, None);
660        let o2 = gdn_forward(&x, &w, &cfg, &mut state, None);
661        assert!(o1.iter().zip(&o2).any(|(a, b)| (a - b).abs() > 1e-6));
662        assert_eq!(state.len(), cfg.state_len());
663    }
664
665    #[test]
666    fn gdn_pair_matches_two_singles_bitexact() {
667        let (w, cfg) = tiny_gdn();
668        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.2).cos()).collect();
669        let x2: Vec<f32> = (0..8).map(|i| (i as f32 * 0.5).sin()).collect();
670
671        let mut s_ref = Vec::new();
672        let r1 = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
673        let r2 = gdn_forward(&x2, &w, &cfg, &mut s_ref, None);
674
675        let mut s = Vec::new();
676        let mut scratch = Vec::new();
677        let (p1, p2) = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
678        assert_eq!(r1, p1, "lane 1 must be bit-identical");
679        assert_eq!(r2, p2, "lane 2 must be bit-identical");
680        std::mem::swap(&mut s, &mut scratch);
681        assert_eq!(s, s_ref, "accepted state must equal sequential state");
682    }
683
684    #[test]
685    fn gdn_rejected_draft_leaves_state_at_lane1() {
686        let (w, cfg) = tiny_gdn();
687        let x1: Vec<f32> = (0..8).map(|i| (i as f32 * 0.7).sin()).collect();
688        let x2 = vec![0.5f32; 8];
689
690        let mut s_ref = Vec::new();
691        let _ = gdn_forward(&x1, &w, &cfg, &mut s_ref, None);
692
693        let mut s = Vec::new();
694        let mut scratch = Vec::new();
695        let _ = gdn_pair(&x1, &x2, &w, &cfg, &mut s, &mut scratch, None);
696        assert_eq!(s, s_ref);
697    }
698
699    /// The conv ring must give the same result as an explicit causal
700    /// conv over the whole sequence (oracle semantics: zero left-pad,
701    /// tap kk−1 on the current position).
702    #[test]
703    fn gdn_conv_ring_matches_explicit_causal_conv() {
704        let (w, cfg) = tiny_gdn();
705        let seq: Vec<Vec<f32>> = (0..6)
706            .map(|t| (0..8).map(|i| ((t * 8 + i) as f32 * 0.17).sin()).collect())
707            .collect();
708
709        // Reference: recompute position t from scratch each time with a
710        // fresh state built by replaying the prefix.
711        let mut s_inc = Vec::new();
712        for (t, x) in seq.iter().enumerate() {
713            let inc = gdn_forward(x, &w, &cfg, &mut s_inc, None);
714            let mut s_replay = Vec::new();
715            let mut replay = Vec::new();
716            for xr in &seq[..=t] {
717                replay = gdn_forward(xr, &w, &cfg, &mut s_replay, None);
718            }
719            assert_eq!(inc, replay, "position {t}: ring must equal replay");
720        }
721    }
722}