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