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