Skip to main content

aria_kernel/
ops.rs

1use crate::{EngineError, SimdMode};
2use rayon::prelude::*;
3
4/// C = A @ B^T style? We use row-major: out[m,n] = sum_k a[m,k] * b[k,n]
5/// with `a`: [m,k], `b`: [k,n].
6pub fn matmul(
7    a: &[f32],
8    a_rows: usize,
9    a_cols: usize,
10    b: &[f32],
11    b_rows: usize,
12    b_cols: usize,
13    _mode: SimdMode,
14) -> Result<Vec<f32>, EngineError> {
15    if a_cols != b_rows {
16        return Err(EngineError::ShapeMismatch(format!(
17            "matmul inner dim {a_cols} != {b_rows}"
18        )));
19    }
20    if a.len() != a_rows * a_cols || b.len() != b_rows * b_cols {
21        return Err(EngineError::ShapeMismatch(
22            "matmul buffer length does not match shape".into(),
23        ));
24    }
25    let mut out = vec![0.0f32; a_rows * b_cols];
26    for i in 0..a_rows {
27        for j in 0..b_cols {
28            let mut s = 0.0f32;
29            for k in 0..a_cols {
30                s += a[i * a_cols + k] * b[k * b_cols + j];
31            }
32            out[i * b_cols + j] = s;
33        }
34    }
35    Ok(out)
36}
37
38/// y = x @ W^T where W is [out_features, in_features] row-major (GGUF-style).
39pub fn linear(x: &[f32], w: &[f32], out_f: usize, in_f: usize) -> Result<Vec<f32>, EngineError> {
40    if !x.len().is_multiple_of(in_f) {
41        return Err(EngineError::ShapeMismatch(format!(
42            "linear x len {} not divisible by in_f {in_f}",
43            x.len()
44        )));
45    }
46    if w.len() != out_f * in_f {
47        return Err(EngineError::ShapeMismatch(format!(
48            "linear weight length mismatch: got {} want out_f*in_f={out_f}*{in_f}={}",
49            w.len(),
50            out_f * in_f
51        )));
52    }
53    let batch = x.len() / in_f;
54    let mut out = vec![0.0f32; batch * out_f];
55    for b in 0..batch {
56        for o in 0..out_f {
57            let mut s = 0.0f32;
58            let wr = &w[o * in_f..(o + 1) * in_f];
59            let xr = &x[b * in_f..(b + 1) * in_f];
60            for i in 0..in_f {
61                s += xr[i] * wr[i];
62            }
63            out[b * out_f + o] = s;
64        }
65    }
66    Ok(out)
67}
68
69fn dot_f32(a: &[f32], b: &[f32]) -> f32 {
70    debug_assert_eq!(a.len(), b.len());
71    #[cfg(target_arch = "x86_64")]
72    {
73        if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
74            return unsafe { dot_avx2(a, b) };
75        }
76    }
77    #[cfg(target_arch = "aarch64")]
78    {
79        unsafe { dot_neon(a, b) }
80    }
81    #[cfg(not(target_arch = "aarch64"))]
82    {
83        let mut s = 0.0f32;
84        for i in 0..a.len() {
85            s += a[i] * b[i];
86        }
87        s
88    }
89}
90
91#[cfg(target_arch = "x86_64")]
92#[target_feature(enable = "avx2,fma")]
93unsafe fn dot_avx2(a: &[f32], b: &[f32]) -> f32 {
94    use std::arch::x86_64::*;
95    let n = a.len();
96    let mut i = 0usize;
97    let mut acc = _mm256_setzero_ps();
98    while i + 8 <= n {
99        let va = _mm256_loadu_ps(a.as_ptr().add(i));
100        let vb = _mm256_loadu_ps(b.as_ptr().add(i));
101        acc = _mm256_fmadd_ps(va, vb, acc);
102        i += 8;
103    }
104    let mut tmp = [0.0f32; 8];
105    _mm256_storeu_ps(tmp.as_mut_ptr(), acc);
106    let mut s = tmp.iter().sum::<f32>();
107    while i < n {
108        s += a[i] * b[i];
109        i += 1;
110    }
111    s
112}
113
114#[cfg(target_arch = "aarch64")]
115unsafe fn dot_neon(a: &[f32], b: &[f32]) -> f32 {
116    use std::arch::aarch64::*;
117    let n = a.len();
118    let mut i = 0usize;
119    let mut acc = vdupq_n_f32(0.0);
120    while i + 4 <= n {
121        let va = vld1q_f32(a.as_ptr().add(i));
122        let vb = vld1q_f32(b.as_ptr().add(i));
123        acc = vfmaq_f32(acc, va, vb);
124        i += 4;
125    }
126    let mut tmp = [0.0f32; 4];
127    vst1q_f32(tmp.as_mut_ptr(), acc);
128    let mut s = tmp[0] + tmp[1] + tmp[2] + tmp[3];
129    while i < n {
130        s += a[i] * b[i];
131        i += 1;
132    }
133    s
134}
135
136/// Multi-threaded `linear` (AVX2/FMA or NEON dots). Numerically close to [`linear`].
137pub fn linear_cpu(x: &[f32], w: &[f32], out_f: usize, in_f: usize) -> Result<Vec<f32>, EngineError> {
138    if !x.len().is_multiple_of(in_f) {
139        return Err(EngineError::ShapeMismatch(format!(
140            "linear x len {} not divisible by in_f {in_f}",
141            x.len()
142        )));
143    }
144    if w.len() != out_f * in_f {
145        return Err(EngineError::ShapeMismatch(format!(
146            "linear weight length mismatch: got {} want out_f*in_f={out_f}*{in_f}={}",
147            w.len(),
148            out_f * in_f
149        )));
150    }
151    let batch = x.len() / in_f;
152    let mut out = vec![0.0f32; batch * out_f];
153    if batch == 0 || out_f == 0 {
154        return Ok(out);
155    }
156    // Parallelize over every (batch, out_feature) pair so decode lm_head
157    // (batch=1, out_f≈vocab) and prefill FFN both scale across cores.
158    out.par_iter_mut().enumerate().for_each(|(idx, slot)| {
159        let b = idx / out_f;
160        let o = idx % out_f;
161        *slot = dot_f32(
162            &w[o * in_f..(o + 1) * in_f],
163            &x[b * in_f..(b + 1) * in_f],
164        );
165    });
166    Ok(out)
167}
168
169pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32) -> Result<Vec<f32>, EngineError> {
170    let n = weight.len();
171    if n == 0 || !x.len().is_multiple_of(n) {
172        return Err(EngineError::ShapeMismatch(
173            "rms_norm: x length must be multiple of weight len".into(),
174        ));
175    }
176    let batch = x.len() / n;
177    let mut out = vec![0.0f32; x.len()];
178    for b in 0..batch {
179        let base = b * n;
180        let mut ms = 0.0f32;
181        for i in 0..n {
182            ms += x[base + i] * x[base + i];
183        }
184        let scale = (ms / n as f32 + eps).sqrt().recip();
185        for i in 0..n {
186            out[base + i] = x[base + i] * scale * weight[i];
187        }
188    }
189    Ok(out)
190}
191
192pub fn softmax_inplace(logits: &mut [f32]) {
193    if logits.is_empty() {
194        return;
195    }
196    let m = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
197    let mut sum = 0.0f32;
198    for v in logits.iter_mut() {
199        *v = (*v - m).exp();
200        sum += *v;
201    }
202    let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
203    for v in logits.iter_mut() {
204        *v *= inv;
205    }
206}
207
208pub fn softmax(logits: &[f32]) -> Vec<f32> {
209    let mut o = logits.to_vec();
210    softmax_inplace(&mut o);
211    o
212}
213
214/// Apply RoPE to interleaved q/k pairs for one token (head_dim even).
215pub fn rope(x: &mut [f32], head_dim: usize, pos: usize, theta: f32) -> Result<(), EngineError> {
216    if head_dim == 0 || !head_dim.is_multiple_of(2) {
217        return Err(EngineError::ShapeMismatch(
218            "rope head_dim must be positive even".into(),
219        ));
220    }
221    if !x.len().is_multiple_of(head_dim) {
222        return Err(EngineError::ShapeMismatch(
223            "rope x len not divisible by head_dim".into(),
224        ));
225    }
226    let n_heads = x.len() / head_dim;
227    for h in 0..n_heads {
228        let base = h * head_dim;
229        for i in 0..(head_dim / 2) {
230            let freq = 1.0 / theta.powf((2 * i) as f32 / head_dim as f32);
231            let angle = pos as f32 * freq;
232            let (c, s) = (angle.cos(), angle.sin());
233            let u = x[base + 2 * i];
234            let v = x[base + 2 * i + 1];
235            x[base + 2 * i] = u * c - v * s;
236            x[base + 2 * i + 1] = u * s + v * c;
237        }
238    }
239    Ok(())
240}
241
242/// Causal attention for single query step against KV cache.
243/// q: [n_heads * head_dim], k_cache/v_cache: [seq, n_kv_heads * head_dim]
244pub fn attention(
245    q: &[f32],
246    k_cache: &[f32],
247    v_cache: &[f32],
248    n_heads: usize,
249    n_kv_heads: usize,
250    head_dim: usize,
251) -> Result<Vec<f32>, EngineError> {
252    let scale = 1.0 / (head_dim as f32).sqrt();
253    attention_with_scale(q, k_cache, v_cache, n_heads, n_kv_heads, head_dim, scale)
254}
255
256fn sliding_kv_start(seq: usize, window: Option<usize>) -> usize {
257    match window {
258        Some(w) if w > 0 => seq.saturating_sub(w),
259        _ => 0,
260    }
261}
262
263/// Restrict KV to the last `window` tokens (HF sliding-window). `None` is a no-op.
264pub fn kv_sliding_view<'a>(
265    k_cache: &'a [f32],
266    v_cache: &'a [f32],
267    kv_dim: usize,
268    window: Option<usize>,
269) -> Result<(&'a [f32], &'a [f32]), EngineError> {
270    if kv_dim == 0
271        || k_cache.len() != v_cache.len()
272        || !k_cache.len().is_multiple_of(kv_dim)
273    {
274        return Err(EngineError::ShapeMismatch(
275            "sliding kv view shape".into(),
276        ));
277    }
278    let seq = k_cache.len() / kv_dim;
279    let start = sliding_kv_start(seq, window);
280    Ok((
281        &k_cache[start * kv_dim..],
282        &v_cache[start * kv_dim..],
283    ))
284}
285
286/// Causal attention with an explicit softmax scale (Gemma-4 uses `1.0` after QK-norm).
287pub fn attention_with_scale(
288    q: &[f32],
289    k_cache: &[f32],
290    v_cache: &[f32],
291    n_heads: usize,
292    n_kv_heads: usize,
293    head_dim: usize,
294    scale: f32,
295) -> Result<Vec<f32>, EngineError> {
296    if n_heads == 0 || head_dim == 0 || n_kv_heads == 0 || !n_heads.is_multiple_of(n_kv_heads) {
297        return Err(EngineError::ShapeMismatch(
298            "attention invalid head configuration".into(),
299        ));
300    }
301    let kv_dim = n_kv_heads * head_dim;
302    if q.len() != n_heads * head_dim {
303        return Err(EngineError::ShapeMismatch("attention q shape".into()));
304    }
305    if k_cache.len() != v_cache.len() || !k_cache.len().is_multiple_of(kv_dim) {
306        return Err(EngineError::ShapeMismatch(
307            "attention kv cache shape".into(),
308        ));
309    }
310    let seq = k_cache.len() / kv_dim;
311    let rep = n_heads / n_kv_heads;
312    let mut out = vec![0.0f32; n_heads * head_dim];
313    for h in 0..n_heads {
314        let kv_h = h / rep;
315        let qh = &q[h * head_dim..(h + 1) * head_dim];
316        let mut scores = vec![0.0f32; seq];
317        for t in 0..seq {
318            let kh = &k_cache[t * kv_dim + kv_h * head_dim..t * kv_dim + (kv_h + 1) * head_dim];
319            let mut dot = 0.0f32;
320            for i in 0..head_dim {
321                dot += qh[i] * kh[i];
322            }
323            scores[t] = dot * scale;
324        }
325        softmax_inplace(&mut scores);
326        let oh = &mut out[h * head_dim..(h + 1) * head_dim];
327        for t in 0..seq {
328            let vh = &v_cache[t * kv_dim + kv_h * head_dim..t * kv_dim + (kv_h + 1) * head_dim];
329            for i in 0..head_dim {
330                oh[i] += scores[t] * vh[i];
331            }
332        }
333    }
334    Ok(out)
335}
336
337/// Causal attention for a batch of queries `[seq_q, n_heads*head_dim]`.
338/// When `seq_q == seq_kv`, query `t` attends to keys `0..=t` (prefill).
339/// When `seq_q == 1`, equivalent to [`attention`] (decode).
340pub fn attention_causal(
341    q: &[f32],
342    k_cache: &[f32],
343    v_cache: &[f32],
344    n_heads: usize,
345    n_kv_heads: usize,
346    head_dim: usize,
347) -> Result<Vec<f32>, EngineError> {
348    let q_dim = n_heads * head_dim;
349    if q_dim == 0 || !q.len().is_multiple_of(q_dim) {
350        return Err(EngineError::ShapeMismatch("attention_causal q shape".into()));
351    }
352    let seq_q = q.len() / q_dim;
353    if seq_q == 1 {
354        return attention(q, k_cache, v_cache, n_heads, n_kv_heads, head_dim);
355    }
356    let kv_dim = n_kv_heads * head_dim;
357    let seq_kv = k_cache.len() / kv_dim;
358    let causal = seq_q == seq_kv;
359    let mut out = vec![0.0f32; q.len()];
360    for tq in 0..seq_q {
361        let q_tok = &q[tq * q_dim..(tq + 1) * q_dim];
362        let k_end = if causal { tq + 1 } else { seq_kv };
363        let attn = attention(
364            q_tok,
365            &k_cache[..k_end * kv_dim],
366            &v_cache[..k_end * kv_dim],
367            n_heads,
368            n_kv_heads,
369            head_dim,
370        )?;
371        out[tq * q_dim..(tq + 1) * q_dim].copy_from_slice(&attn);
372    }
373    Ok(out)
374}
375
376/// Prefill causal attention with an explicit softmax scale.
377/// `window` is the sliding-window length (`None` = full causal prefix).
378#[allow(clippy::too_many_arguments)]
379pub fn attention_causal_with_scale(
380    q: &[f32],
381    k_cache: &[f32],
382    v_cache: &[f32],
383    n_heads: usize,
384    n_kv_heads: usize,
385    head_dim: usize,
386    scale: f32,
387    window: Option<usize>,
388) -> Result<Vec<f32>, EngineError> {
389    let q_dim = n_heads * head_dim;
390    if q_dim == 0 || !q.len().is_multiple_of(q_dim) {
391        return Err(EngineError::ShapeMismatch("attention_causal q shape".into()));
392    }
393    let seq_q = q.len() / q_dim;
394    let kv_dim = n_kv_heads * head_dim;
395    if seq_q == 1 {
396        let (k, v) = kv_sliding_view(k_cache, v_cache, kv_dim, window)?;
397        return attention_with_scale(q, k, v, n_heads, n_kv_heads, head_dim, scale);
398    }
399    let seq_kv = k_cache.len() / kv_dim;
400    let causal = seq_q == seq_kv;
401    let mut out = vec![0.0f32; q.len()];
402    for tq in 0..seq_q {
403        let q_tok = &q[tq * q_dim..(tq + 1) * q_dim];
404        let k_end = if causal { tq + 1 } else { seq_kv };
405        let (k, v) = kv_sliding_view(
406            &k_cache[..k_end * kv_dim],
407            &v_cache[..k_end * kv_dim],
408            kv_dim,
409            window,
410        )?;
411        let attn = attention_with_scale(q_tok, k, v, n_heads, n_kv_heads, head_dim, scale)?;
412        out[tq * q_dim..(tq + 1) * q_dim].copy_from_slice(&attn);
413    }
414    Ok(out)
415}
416
417pub fn swiglu(gate: &[f32], up: &[f32]) -> Result<Vec<f32>, EngineError> {
418    if gate.len() != up.len() {
419        return Err(EngineError::ShapeMismatch("swiglu length mismatch".into()));
420    }
421    Ok(gate
422        .iter()
423        .zip(up.iter())
424        .map(|(g, u)| {
425            let s = 1.0 / (1.0 + (-g).exp());
426            s * g * u
427        })
428        .collect())
429}
430
431/// Causal depthwise short-conv one-token step (LFM2).
432///
433/// `w` layout: `[hidden * kernel]`, channel-major, `w[c*kernel + 0]` taps the oldest
434/// sample. `state` is `[hidden * (kernel-1)]` (oldest→newest); updated in place.
435pub fn short_conv_step(
436    x: &[f32],
437    w: &[f32],
438    state: &mut [f32],
439    hidden: usize,
440    kernel: usize,
441) -> Result<Vec<f32>, EngineError> {
442    if hidden == 0 || kernel == 0 {
443        return Err(EngineError::ShapeMismatch(
444            "short_conv_step: hidden and kernel must be > 0".into(),
445        ));
446    }
447    if x.len() != hidden {
448        return Err(EngineError::ShapeMismatch(
449            "short_conv_step: x len != hidden".into(),
450        ));
451    }
452    if w.len() != hidden * kernel {
453        return Err(EngineError::ShapeMismatch(
454            "short_conv_step: weight len != hidden*kernel".into(),
455        ));
456    }
457    let hist = kernel.saturating_sub(1);
458    if state.len() != hidden * hist {
459        return Err(EngineError::ShapeMismatch(
460            "short_conv_step: state len != hidden*(kernel-1)".into(),
461        ));
462    }
463    let mut out = vec![0.0f32; hidden];
464    for c in 0..hidden {
465        let mut acc = 0.0f32;
466        let wbase = c * kernel;
467        let sbase = c * hist;
468        for k in 0..hist {
469            acc += w[wbase + k] * state[sbase + k];
470        }
471        acc += w[wbase + hist] * x[c];
472        out[c] = acc;
473        if hist > 0 {
474            for k in 0..(hist - 1) {
475                state[sbase + k] = state[sbase + k + 1];
476            }
477            state[sbase + hist - 1] = x[c];
478        }
479    }
480    Ok(out)
481}
482
483/// Softmax (or sigmoid) top-k MoE routing. Returns (expert_ids, normalized weights).
484pub fn moe_topk_route(
485    logits: &[f32],
486    top_k: usize,
487    use_sigmoid: bool,
488) -> Result<(Vec<usize>, Vec<f32>), EngineError> {
489    let n = logits.len();
490    if n == 0 || top_k == 0 {
491        return Err(EngineError::InvalidParam(
492            "moe_topk_route: num_experts and top_k must be > 0".into(),
493        ));
494    }
495    let k = top_k.min(n);
496    let scores: Vec<f32> = if use_sigmoid {
497        logits.iter().map(|x| 1.0 / (1.0 + (-x).exp())).collect()
498    } else {
499        let m = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
500        let mut exps: Vec<f32> = logits.iter().map(|x| (x - m).exp()).collect();
501        let s: f32 = exps.iter().sum();
502        if s > 0.0 {
503            for e in &mut exps {
504                *e /= s;
505            }
506        }
507        exps
508    };
509    let mut idx: Vec<usize> = (0..n).collect();
510    idx.sort_by(|&a, &b| {
511        scores[b]
512            .partial_cmp(&scores[a])
513            .unwrap_or(std::cmp::Ordering::Equal)
514    });
515    idx.truncate(k);
516    let mut weights: Vec<f32> = idx.iter().map(|&i| scores[i]).collect();
517    let sum: f32 = weights.iter().sum();
518    if sum > 0.0 {
519        for w in &mut weights {
520            *w /= sum;
521        }
522    }
523    Ok((idx, weights))
524}
525
526fn silu(x: f32) -> f32 {
527    x / (1.0 + (-x).exp())
528}
529
530/// Elementwise SiLU (DeltaNet conv activation).
531pub fn silu_vec(x: &mut [f32]) {
532    for v in x.iter_mut() {
533        *v = silu(*v);
534    }
535}
536
537/// Numerically stable softplus.
538pub fn softplus(x: f32) -> f32 {
539    if x > 20.0 {
540        x
541    } else {
542        (1.0 + x.exp()).ln()
543    }
544}
545
546fn l2_normalize_inplace(x: &mut [f32]) {
547    let mut ss = 0.0f32;
548    for v in x.iter() {
549        ss += *v * *v;
550    }
551    let n = (ss + 1e-6).sqrt();
552    if n > 0.0 {
553        for v in x.iter_mut() {
554            *v /= n;
555        }
556    }
557}
558
559/// Bundled args for [`gated_delta_step`] (avoids clippy `too_many_arguments`).
560pub struct GatedDeltaStep<'a> {
561    pub q: &'a [f32],
562    pub k: &'a [f32],
563    pub v: &'a [f32],
564    pub g: &'a [f32],
565    pub beta: &'a [f32],
566    pub state: &'a mut [f32],
567    pub n_heads: usize,
568    pub dk: usize,
569    pub dv: usize,
570}
571
572/// One-token Gated DeltaNet recurrence (Qwen3.5 / Bonsai linear attention).
573///
574/// `state` is `[n_heads * dk * dv]` (per-head `S[dk, dv]`). `q`/`k` are
575/// `[n_heads * dk]`, `v` `[n_heads * dv]`, `g`/`beta` `[n_heads]`.
576pub fn gated_delta_step(p: GatedDeltaStep<'_>) -> Result<Vec<f32>, EngineError> {
577    let GatedDeltaStep {
578        q,
579        k,
580        v,
581        g,
582        beta,
583        state,
584        n_heads,
585        dk,
586        dv,
587    } = p;
588    if n_heads == 0 || dk == 0 || dv == 0 {
589        return Err(EngineError::ShapeMismatch(
590            "gated_delta_step: heads/dk/dv must be > 0".into(),
591        ));
592    }
593    if q.len() != n_heads * dk
594        || k.len() != n_heads * dk
595        || v.len() != n_heads * dv
596        || g.len() != n_heads
597        || beta.len() != n_heads
598        || state.len() != n_heads * dk * dv
599    {
600        return Err(EngineError::ShapeMismatch(
601            "gated_delta_step: q/k/v/g/beta/state shape mismatch".into(),
602        ));
603    }
604    let mut qq = q.to_vec();
605    let mut kk = k.to_vec();
606    for h in 0..n_heads {
607        l2_normalize_inplace(&mut qq[h * dk..(h + 1) * dk]);
608        l2_normalize_inplace(&mut kk[h * dk..(h + 1) * dk]);
609    }
610    let scale = (dk as f32).sqrt().recip();
611    let mut out = vec![0.0f32; n_heads * dv];
612    for h in 0..n_heads {
613        let sbase = h * dk * dv;
614        let gh = g[h];
615        for i in 0..dk * dv {
616            state[sbase + i] *= gh;
617        }
618        let mut kv_mem = vec![0.0f32; dv];
619        for i in 0..dk {
620            let kv = kk[h * dk + i];
621            for j in 0..dv {
622                kv_mem[j] += state[sbase + i * dv + j] * kv;
623            }
624        }
625        for j in 0..dv {
626            let delta = (v[h * dv + j] - kv_mem[j]) * beta[h];
627            for i in 0..dk {
628                state[sbase + i * dv + j] += kk[h * dk + i] * delta;
629            }
630        }
631        for j in 0..dv {
632            let mut o = 0.0f32;
633            for i in 0..dk {
634                o += state[sbase + i * dv + j] * qq[h * dk + i];
635            }
636            out[h * dv + j] = o * scale;
637        }
638    }
639    Ok(out)
640}
641
642/// GeGLU: gelu(gate) * up (Gemma `gelu_pytorch_tanh` approximates with tanh form).
643pub fn geglu(gate: &[f32], up: &[f32]) -> Result<Vec<f32>, EngineError> {
644    if gate.len() != up.len() {
645        return Err(EngineError::ShapeMismatch("geglu length mismatch".into()));
646    }
647    Ok(gate
648        .iter()
649        .zip(up.iter())
650        .map(|(g, u)| gelu_pytorch_tanh(*g) * u)
651        .collect())
652}
653
654/// Match transformers `gelu_pytorch_tanh` (used by Gemma GeGLU).
655pub fn gelu_pytorch_tanh(x: f32) -> f32 {
656    // Match transformers gelu_pytorch_tanh.
657    const SQRT_2_OVER_PI: f32 = 0.797_884_6;
658    const COEFF: f32 = 0.044_715;
659    let inner = SQRT_2_OVER_PI * (x + COEFF * x * x * x);
660    0.5 * x * (1.0 + inner.tanh())
661}
662
663/// Gemma-style RMSNorm: x * rrms * (1 + weight).
664pub fn rms_norm_gemma(x: &[f32], weight: &[f32], eps: f32) -> Result<Vec<f32>, EngineError> {
665    let n = weight.len();
666    if n == 0 || !x.len().is_multiple_of(n) {
667        return Err(EngineError::ShapeMismatch(
668            "rms_norm_gemma: x length must be multiple of weight len".into(),
669        ));
670    }
671    let batch = x.len() / n;
672    let mut out = vec![0.0f32; x.len()];
673    for b in 0..batch {
674        let base = b * n;
675        let mut ms = 0.0f32;
676        for i in 0..n {
677            ms += x[base + i] * x[base + i];
678        }
679        let rrms = (ms / n as f32 + eps).sqrt().recip();
680        for i in 0..n {
681            out[base + i] = x[base + i] * rrms * (1.0 + weight[i]);
682        }
683    }
684    Ok(out)
685}
686
687/// HF Llama/Qwen/Gemma RoPE: rotate half of the head dims as a contiguous block.
688pub fn rope_half(
689    x: &mut [f32],
690    head_dim: usize,
691    pos: usize,
692    theta: f32,
693) -> Result<(), EngineError> {
694    if head_dim == 0 || !head_dim.is_multiple_of(2) {
695        return Err(EngineError::ShapeMismatch(
696            "rope_half head_dim must be positive even".into(),
697        ));
698    }
699    if !x.len().is_multiple_of(head_dim) {
700        return Err(EngineError::ShapeMismatch(
701            "rope_half x len not divisible by head_dim".into(),
702        ));
703    }
704    let half = head_dim / 2;
705    let n_heads = x.len() / head_dim;
706    for h in 0..n_heads {
707        let base = h * head_dim;
708        for i in 0..half {
709            let freq = 1.0 / theta.powf((2 * i) as f32 / head_dim as f32);
710            let angle = pos as f32 * freq;
711            let (c, s) = (angle.cos(), angle.sin());
712            let u = x[base + i];
713            let v = x[base + i + half];
714            x[base + i] = u * c - v * s;
715            x[base + i + half] = u * s + v * c;
716        }
717    }
718    Ok(())
719}
720
721/// Rotate only the first `rotary_dim` dims of each head (`partial_rotary_factor`).
722pub fn rope_half_partial(
723    x: &mut [f32],
724    head_dim: usize,
725    rotary_dim: usize,
726    pos: usize,
727    theta: f32,
728) -> Result<(), EngineError> {
729    if rotary_dim == 0 || rotary_dim > head_dim || !rotary_dim.is_multiple_of(2) {
730        return Err(EngineError::ShapeMismatch(
731            "rope_half_partial rotary_dim must be positive even and <= head_dim".into(),
732        ));
733    }
734    if rotary_dim == head_dim {
735        return rope_half(x, head_dim, pos, theta);
736    }
737    if !x.len().is_multiple_of(head_dim) {
738        return Err(EngineError::ShapeMismatch(
739            "rope_half_partial x len not divisible by head_dim".into(),
740        ));
741    }
742    let n_heads = x.len() / head_dim;
743    for h in 0..n_heads {
744        let sl = &mut x[h * head_dim..h * head_dim + rotary_dim];
745        rope_half(sl, rotary_dim, pos, theta)?;
746    }
747    Ok(())
748}
749
750/// Gemma-4 global (p-RoPE): rotate the first `factor * head_dim/2` pairs of
751/// `rotate_half` layout; remaining pairs stay identity. Frequencies use the
752/// full `head_dim` denominator (not the rotated subset).
753pub fn rope_half_proportional(
754    x: &mut [f32],
755    head_dim: usize,
756    factor: f32,
757    pos: usize,
758    theta: f32,
759) -> Result<(), EngineError> {
760    if !(0.0..=1.0).contains(&factor) {
761        return Err(EngineError::ShapeMismatch(
762            "rope_half_proportional factor must be in [0, 1]".into(),
763        ));
764    }
765    if (factor - 1.0).abs() < 1e-6 {
766        return rope_half(x, head_dim, pos, theta);
767    }
768    if head_dim == 0 || !head_dim.is_multiple_of(2) {
769        return Err(EngineError::ShapeMismatch(
770            "rope_half_proportional head_dim must be positive even".into(),
771        ));
772    }
773    if !x.len().is_multiple_of(head_dim) {
774        return Err(EngineError::ShapeMismatch(
775            "rope_half_proportional x len not divisible by head_dim".into(),
776        ));
777    }
778    let half = head_dim / 2;
779    let rope_angles = (factor * head_dim as f32 / 2.0) as usize;
780    if rope_angles == 0 {
781        return Ok(());
782    }
783    let n_heads = x.len() / head_dim;
784    for h in 0..n_heads {
785        let base = h * head_dim;
786        for i in 0..rope_angles.min(half) {
787            let freq = 1.0 / theta.powf((2 * i) as f32 / head_dim as f32);
788            let angle = pos as f32 * freq;
789            let (c, s) = (angle.cos(), angle.sin());
790            let u = x[base + i];
791            let v = x[base + i + half];
792            x[base + i] = u * c - v * s;
793            x[base + i + half] = u * s + v * c;
794        }
795    }
796    Ok(())
797}
798
799/// y = W_rot @ x followed by blocked unrotate on each out_f row (HDM fused path).
800///
801/// Equivalent to `linear(x, unrotate(W_rot))` for dense GEMM. **Not** valid for
802/// embedding row gather: axis-0 Hadamard mixes vocab rows, so `W_rot[token]` is
803/// not the token vector. Session reconstructs the full matrix at load instead.
804pub fn hdm_linear(
805    x: &[f32],
806    w_rot: &[f32],
807    out_f: usize,
808    in_f: usize,
809    hadamard_seed: Option<i64>,
810) -> Result<Vec<f32>, EngineError> {
811    let mut y = linear(x, w_rot, out_f, in_f)?;
812    if out_f == 0 || !y.len().is_multiple_of(out_f) {
813        return Err(EngineError::ShapeMismatch(
814            "hdm_linear output not divisible by out_f".into(),
815        ));
816    }
817    let batch = y.len() / out_f;
818    for b in 0..batch {
819        let sl = b * out_f..(b + 1) * out_f;
820        hadamard_blocked_vec(&mut y[sl], hadamard_seed, true)?;
821    }
822    Ok(y)
823}
824
825/// In-place orthogonal FWHT on length = power of two (scale 1/sqrt(n)).
826pub fn fwht(x: &mut [f32]) -> Result<(), EngineError> {
827    let n = x.len();
828    if n == 0 || !n.is_power_of_two() {
829        return Err(EngineError::ShapeMismatch(
830            "fwht length must be power of two".into(),
831        ));
832    }
833    if n == 1 {
834        return Ok(());
835    }
836    let mut h = 1usize;
837    while h < n {
838        for i in (0..n).step_by(h * 2) {
839            for j in i..(i + h) {
840                let a = x[j];
841                let b = x[j + h];
842                x[j] = a + b;
843                x[j + h] = a - b;
844            }
845        }
846        h *= 2;
847    }
848    let scale = 1.0 / (n as f32).sqrt();
849    for v in x.iter_mut() {
850        *v *= scale;
851    }
852    Ok(())
853}
854
855/// Greedy largest-pow2 tiling of row count (e.g. 10 → [8, 2]).
856pub fn pow2_tile_sizes(k: usize) -> Result<Vec<usize>, EngineError> {
857    if k == 0 {
858        return Err(EngineError::ShapeMismatch(
859            "pow2_tile_sizes expects k>=1".into(),
860        ));
861    }
862    let mut sizes = Vec::new();
863    let mut rem = k;
864    while rem > 0 {
865        let mut b = 1usize;
866        while (b << 1) <= rem {
867            b <<= 1;
868        }
869        sizes.push(b);
870        rem -= b;
871    }
872    Ok(sizes)
873}
874
875/// Portable ±1 signs matching Python `portable_block_signs`.
876pub fn portable_block_signs(seed: i64, start: usize, size: usize) -> Vec<f32> {
877    let mut signs = vec![0.0f32; size];
878    let mut state = (seed as u64) ^ ((start as u64).wrapping_mul(0x9E3779B97F4A7C15));
879    for s in signs.iter_mut() {
880        state = state.wrapping_add(0x9E3779B97F4A7C15);
881        let mut z = state;
882        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
883        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
884        z ^= z >> 31;
885        *s = if (z & 1) == 0 { 1.0 } else { -1.0 };
886    }
887    signs
888}
889
890/// Apply blocked Hadamard on rows of a row-major `[rows, cols]` matrix.
891/// `inverse=false` → per-block `H@S`; `inverse=true` → `S@H`.
892///
893/// Tile sizes default to greedy [`pow2_tile_sizes`] (Python `pow2_tile_blocks`).
894pub fn hadamard_blocked_rows(
895    data: &mut [f32],
896    rows: usize,
897    cols: usize,
898    seed: Option<i64>,
899    inverse: bool,
900) -> Result<(), EngineError> {
901    let sizes = pow2_tile_sizes(rows)?;
902    hadamard_blocked_rows_tiles(data, rows, cols, seed, inverse, &sizes)
903}
904
905/// Same as [`hadamard_blocked_rows`] but uses caller tile sizes (bundle `hadamard.blocks`).
906pub fn hadamard_blocked_rows_tiles(
907    data: &mut [f32],
908    rows: usize,
909    cols: usize,
910    seed: Option<i64>,
911    inverse: bool,
912    sizes: &[usize],
913) -> Result<(), EngineError> {
914    if rows == 0 || cols == 0 || data.len() != rows * cols {
915        return Err(EngineError::ShapeMismatch(
916            "hadamard_blocked_rows shape mismatch".into(),
917        ));
918    }
919    let covered: usize = sizes.iter().copied().sum();
920    if covered != rows || sizes.iter().any(|&s| s == 0) {
921        return Err(EngineError::ShapeMismatch(format!(
922            "hadamard tiles {:?} cover {covered} != rows {rows}",
923            sizes
924        )));
925    }
926    let mut start = 0usize;
927    for &sz in sizes {
928        let signs = seed.map(|s| portable_block_signs(s, start, sz));
929        // Process column-chunks to bound stack/temp if needed; here full width.
930        let mut work = vec![0.0f32; sz * cols];
931        for r in 0..sz {
932            let src = (start + r) * cols;
933            work[r * cols..(r + 1) * cols].copy_from_slice(&data[src..src + cols]);
934        }
935        let col_out: Result<Vec<Vec<f32>>, EngineError> = (0..cols)
936            .into_par_iter()
937            .map(|c| {
938                let mut colbuf = vec![0.0f32; sz];
939                for r in 0..sz {
940                    colbuf[r] = work[r * cols + c];
941                }
942                if let Some(ref sg) = signs {
943                    if !inverse {
944                        for r in 0..sz {
945                            colbuf[r] *= sg[r];
946                        }
947                        fwht(&mut colbuf)?;
948                    } else {
949                        fwht(&mut colbuf)?;
950                        for r in 0..sz {
951                            colbuf[r] *= sg[r];
952                        }
953                    }
954                } else if sz > 1 {
955                    fwht(&mut colbuf)?;
956                }
957                Ok(colbuf)
958            })
959            .collect();
960        let col_out = col_out?;
961        for c in 0..cols {
962            for r in 0..sz {
963                work[r * cols + c] = col_out[c][r];
964            }
965        }
966        for r in 0..sz {
967            let dst = (start + r) * cols;
968            data[dst..dst + cols].copy_from_slice(&work[r * cols..(r + 1) * cols]);
969        }
970        start += sz;
971    }
972    Ok(())
973}
974
975/// Blocked unrotate on a length-`rows` vector (treat as `[rows, 1]`).
976pub fn hadamard_blocked_vec(
977    x: &mut [f32],
978    seed: Option<i64>,
979    inverse: bool,
980) -> Result<(), EngineError> {
981    let rows = x.len();
982    hadamard_blocked_rows(x, rows, 1, seed, inverse)
983}
984
985/// Codebook lookup dequant (group share): indices [k_work, n], codebook [g, kc].
986pub fn dequant_lookup_group(
987    indices: &[u8],
988    codebook: &[f32],
989    num_groups: usize,
990    group_size: usize,
991    n: usize,
992    kc: usize,
993    k0: usize,
994) -> Result<Vec<f32>, EngineError> {
995    let k_work = num_groups * group_size;
996    if indices.len() != k_work * n {
997        return Err(EngineError::ShapeMismatch(
998            "dequant indices length mismatch".into(),
999        ));
1000    }
1001    if codebook.len() != num_groups * kc {
1002        return Err(EngineError::Quant(
1003            "dequant codebook length mismatch".into(),
1004        ));
1005    }
1006    let mut out = vec![0.0f32; k_work * n];
1007    for g in 0..num_groups {
1008        let cb = &codebook[g * kc..(g + 1) * kc];
1009        for r in 0..group_size {
1010            let row = g * group_size + r;
1011            for j in 0..n {
1012                let idx = indices[row * n + j] as usize;
1013                if idx >= kc {
1014                    return Err(EngineError::Quant(format!("index {idx} >= kc {kc}")));
1015                }
1016                out[row * n + j] = cb[idx];
1017            }
1018        }
1019    }
1020    out.truncate(k0 * n);
1021    Ok(out)
1022}
1023
1024/// Blocked matmul used as Neon / SIMD-friendly path (portable; aarch64 may specialize later).
1025pub fn matmul_blocked(
1026    a: &[f32],
1027    a_rows: usize,
1028    a_cols: usize,
1029    b: &[f32],
1030    b_rows: usize,
1031    b_cols: usize,
1032    block: usize,
1033) -> Result<Vec<f32>, EngineError> {
1034    if a_cols != b_rows {
1035        return Err(EngineError::ShapeMismatch(format!(
1036            "matmul inner dim {a_cols} != {b_rows}"
1037        )));
1038    }
1039    if a.len() != a_rows * a_cols || b.len() != b_rows * b_cols {
1040        return Err(EngineError::ShapeMismatch(
1041            "matmul buffer length does not match shape".into(),
1042        ));
1043    }
1044    let block = block.max(1);
1045    let mut out = vec![0.0f32; a_rows * b_cols];
1046    for i0 in (0..a_rows).step_by(block) {
1047        for j0 in (0..b_cols).step_by(block) {
1048            for k0 in (0..a_cols).step_by(block) {
1049                let i_max = (i0 + block).min(a_rows);
1050                let j_max = (j0 + block).min(b_cols);
1051                let k_max = (k0 + block).min(a_cols);
1052                for i in i0..i_max {
1053                    for j in j0..j_max {
1054                        let mut s = out[i * b_cols + j];
1055                        for k in k0..k_max {
1056                            s += a[i * a_cols + k] * b[k * b_cols + j];
1057                        }
1058                        out[i * b_cols + j] = s;
1059                    }
1060                }
1061            }
1062        }
1063    }
1064    Ok(out)
1065}
1066
1067/// Dispatch scalar vs Neon (blocked) paths. Neon is available on all targets for parity tests;
1068/// on `aarch64` this is the production SIMD entry (intrinsics may replace the body later).
1069pub fn matmul_dispatch(
1070    a: &[f32],
1071    a_rows: usize,
1072    a_cols: usize,
1073    b: &[f32],
1074    b_rows: usize,
1075    b_cols: usize,
1076    mode: SimdMode,
1077) -> Result<Vec<f32>, EngineError> {
1078    match mode {
1079        SimdMode::Scalar => matmul(a, a_rows, a_cols, b, b_rows, b_cols, SimdMode::Scalar),
1080        SimdMode::Neon | SimdMode::Avx2 => matmul_blocked(a, a_rows, a_cols, b, b_rows, b_cols, 8),
1081    }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use super::*;
1087
1088    #[test]
1089    fn matmul_ok() {
1090        let a = [1.0f32, 2.0, 3.0, 4.0]; // 2x2
1091        let b = [1.0f32, 0.0, 0.0, 1.0];
1092        let c = matmul(&a, 2, 2, &b, 2, 2, SimdMode::Scalar).unwrap();
1093        assert_eq!(c, vec![1.0, 2.0, 3.0, 4.0]);
1094    }
1095
1096    #[test]
1097    fn matmul_shape_err() {
1098        let err = matmul(&[1.0], 1, 1, &[1.0, 2.0], 2, 1, SimdMode::Scalar).unwrap_err();
1099        assert!(matches!(err, EngineError::ShapeMismatch(_)));
1100    }
1101
1102    #[test]
1103    fn rms_and_softmax() {
1104        let w = [1.0f32, 1.0];
1105        let y = rms_norm(&[3.0, 4.0], &w, 1e-6).unwrap();
1106        // rms = sqrt((9+16)/2) = sqrt(12.5); y0 = 3/rms
1107        let rms = (12.5f32).sqrt();
1108        assert!((y[0] - 3.0 / rms).abs() < 1e-4);
1109        let s = softmax(&[1.0, 2.0, 3.0]);
1110        let sum: f32 = s.iter().sum();
1111        assert!((sum - 1.0).abs() < 1e-5);
1112    }
1113
1114    #[test]
1115    fn fwht_roundtrip_ish() {
1116        let mut x = [1.0f32, 2.0, 3.0, 4.0];
1117        let orig = x;
1118        fwht(&mut x).unwrap();
1119        fwht(&mut x).unwrap();
1120        for (a, b) in x.iter().zip(orig.iter()) {
1121            assert!((a - b).abs() < 1e-4);
1122        }
1123    }
1124
1125    #[test]
1126    fn pow2_tiles() {
1127        assert_eq!(pow2_tile_sizes(10).unwrap(), vec![8, 2]);
1128        assert_eq!(pow2_tile_sizes(3072).unwrap(), vec![2048, 1024]);
1129        assert_eq!(pow2_tile_sizes(64).unwrap(), vec![64]);
1130        assert_eq!(pow2_tile_sizes(1).unwrap(), vec![1]);
1131        assert_eq!(pow2_tile_sizes(151936).unwrap()[0], 131072);
1132        assert!(matches!(
1133            pow2_tile_sizes(0),
1134            Err(EngineError::ShapeMismatch(_))
1135        ));
1136    }
1137
1138    #[test]
1139    fn blocked_roundtrip_non_pow2() {
1140        let rows = 10usize;
1141        let cols = 3usize;
1142        let mut w: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.1 - 0.5).collect();
1143        let orig = w.clone();
1144        hadamard_blocked_rows(&mut w, rows, cols, Some(7), false).unwrap();
1145        // Second forward is not the inverse.
1146        let mut twice = w.clone();
1147        hadamard_blocked_rows(&mut twice, rows, cols, Some(7), false).unwrap();
1148        let mut err_wrong = 0.0f32;
1149        for (a, b) in twice.iter().zip(orig.iter()) {
1150            err_wrong += (a - b).abs();
1151        }
1152        assert!(err_wrong > 1.0, "second forward unexpectedly near identity");
1153        hadamard_blocked_rows(&mut w, rows, cols, Some(7), true).unwrap();
1154        for (a, b) in w.iter().zip(orig.iter()) {
1155            assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1156        }
1157    }
1158
1159    #[test]
1160    fn blocked_roundtrip_unsigned_and_pow2() {
1161        let rows = 16usize;
1162        let cols = 5usize;
1163        let mut w: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.03).collect();
1164        let orig = w.clone();
1165        hadamard_blocked_rows(&mut w, rows, cols, None, false).unwrap();
1166        hadamard_blocked_rows(&mut w, rows, cols, None, true).unwrap();
1167        for (a, b) in w.iter().zip(orig.iter()) {
1168            assert!((a - b).abs() < 1e-4);
1169        }
1170    }
1171
1172    #[test]
1173    fn blocked_matches_python_golden() {
1174        // W[i,j] = i*3+j)*0.1 - 0.5; seed=7 — from model.common.hadamard
1175        let rows = 10usize;
1176        let cols = 3usize;
1177        let mut w: Vec<f32> = (0..rows * cols).map(|i| (i as f32) * 0.1 - 0.5).collect();
1178        hadamard_blocked_rows(&mut w, rows, cols, Some(7), false).unwrap();
1179        // Clippy-safe f32 literals (Python float32 rounded to representable precision).
1180        let golden: [f32; 30] = [
1181            0.919239,
1182            0.989949,
1183            1.060_66,
1184            0.919239,
1185            0.989950,
1186            1.060_66,
1187            -0.919239,
1188            -0.989949,
1189            -1.060_66,
1190            0.777817,
1191            std::f32::consts::FRAC_1_SQRT_2,
1192            0.636396,
1193            -0.919239,
1194            -0.989949,
1195            -1.060_66,
1196            -0.070711,
1197            -0.141421,
1198            -0.212132,
1199            1.343503,
1200            std::f32::consts::SQRT_2,
1201            1.484924,
1202            -0.636396,
1203            -0.848528,
1204            -1.060_66,
1205            -2.899138,
1206            -3.040559,
1207            -3.181_98,
1208            0.212132,
1209            0.212132,
1210            0.212132,
1211        ];
1212        assert_eq!(w.len(), golden.len());
1213        for (a, b) in w.iter().zip(golden.iter()) {
1214            assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1215        }
1216        assert_eq!(
1217            portable_block_signs(7, 0, 8),
1218            vec![-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0]
1219        );
1220        assert_eq!(portable_block_signs(7, 8, 2), vec![-1.0, -1.0]);
1221    }
1222
1223    #[test]
1224    fn portable_signs_stable() {
1225        let a = portable_block_signs(0, 0, 8);
1226        let b = portable_block_signs(0, 0, 8);
1227        assert_eq!(a, b);
1228        // Golden from model.common.hadamard.portable_block_signs(0, 0, 8)
1229        let golden = [-1.0f32, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0];
1230        assert_eq!(a, golden);
1231    }
1232
1233    #[test]
1234    fn hadamard_blocked_shape_errors() {
1235        let mut w = [1.0f32, 2.0];
1236        assert!(matches!(
1237            hadamard_blocked_rows(&mut w, 2, 2, None, false),
1238            Err(EngineError::ShapeMismatch(_))
1239        ));
1240        assert!(matches!(
1241            hadamard_blocked_rows(&mut [], 0, 1, None, false),
1242            Err(EngineError::ShapeMismatch(_))
1243        ));
1244    }
1245
1246    #[test]
1247    fn hadamard_blocked_vec_roundtrip() {
1248        let mut x: Vec<f32> = (0..10).map(|i| (i as f32) * 0.2 - 1.0).collect();
1249        let orig = x.clone();
1250        hadamard_blocked_vec(&mut x, Some(11), false).unwrap();
1251        hadamard_blocked_vec(&mut x, Some(11), true).unwrap();
1252        for (a, b) in x.iter().zip(orig.iter()) {
1253            assert!((a - b).abs() < 1e-4);
1254        }
1255    }
1256
1257    #[test]
1258    fn dequant_group() {
1259        // 1 group, gs=2, n=2, kc=2
1260        let indices = [0u8, 1, 1, 0];
1261        let codebook = [10.0f32, 20.0];
1262        let out = dequant_lookup_group(&indices, &codebook, 1, 2, 2, 2, 2).unwrap();
1263        assert_eq!(out, vec![10.0, 20.0, 20.0, 10.0]);
1264    }
1265
1266    #[test]
1267    fn neon_scalar_matmul_parity() {
1268        let a: Vec<f32> = (0..64).map(|i| (i as f32) * 0.01).collect();
1269        let b: Vec<f32> = (0..64).map(|i| (i as f32) * 0.02 - 0.5).collect();
1270        let s = matmul_dispatch(&a, 8, 8, &b, 8, 8, SimdMode::Scalar).unwrap();
1271        let n = matmul_dispatch(&a, 8, 8, &b, 8, 8, SimdMode::Neon).unwrap();
1272        assert_eq!(s.len(), n.len());
1273        for (x, y) in s.iter().zip(n.iter()) {
1274            assert!((x - y).abs() < 1e-5, "{x} vs {y}");
1275        }
1276    }
1277
1278    #[test]
1279    fn short_conv_step_and_moe_route() {
1280        let hidden = 2usize;
1281        let kernel = 3usize;
1282        // Channel-major: each channel w=[0,0,1] taps newest only.
1283        let w = vec![0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0];
1284        let mut state = vec![0.0f32; hidden * (kernel - 1)];
1285        let x = [3.0f32, 5.0];
1286        let y = short_conv_step(&x, &w, &mut state, hidden, kernel).unwrap();
1287        assert!((y[0] - 3.0).abs() < 1e-5);
1288        assert!((y[1] - 5.0).abs() < 1e-5);
1289        // After one step hist=[0, x]; w_old=[1,0,0] taps oldest → 0.
1290        let w_old = vec![1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0];
1291        let y2 = short_conv_step(&[1.0, 2.0], &w_old, &mut state, hidden, kernel).unwrap();
1292        assert!((y2[0] - 0.0).abs() < 1e-5);
1293        assert!((y2[1] - 0.0).abs() < 1e-5);
1294
1295        let (ids, ws) = moe_topk_route(&[0.1, 2.0, 0.5, -1.0], 2, false).unwrap();
1296        assert_eq!(ids.len(), 2);
1297        assert_eq!(ids[0], 1);
1298        assert!((ws.iter().sum::<f32>() - 1.0).abs() < 1e-5);
1299        let (ids_s, _) = moe_topk_route(&[0.0, 10.0, 0.0], 1, true).unwrap();
1300        assert_eq!(ids_s, vec![1]);
1301    }
1302
1303    #[test]
1304    fn gated_delta_step_updates_state() {
1305        let n_heads = 1usize;
1306        let dk = 2usize;
1307        let dv = 2usize;
1308        let q = [1.0f32, 0.0];
1309        let k = [1.0f32, 0.0];
1310        let v = [0.5f32, -0.25];
1311        let g = [0.9f32];
1312        let beta = [1.0f32];
1313        let mut s = vec![0.0f32; n_heads * dk * dv];
1314        let o1 = gated_delta_step(GatedDeltaStep {
1315            q: &q,
1316            k: &k,
1317            v: &v,
1318            g: &g,
1319            beta: &beta,
1320            state: &mut s,
1321            n_heads,
1322            dk,
1323            dv,
1324        })
1325        .unwrap();
1326        assert_eq!(o1.len(), dv);
1327        let s_after = s.clone();
1328        let o2 = gated_delta_step(GatedDeltaStep {
1329            q: &q,
1330            k: &k,
1331            v: &v,
1332            g: &g,
1333            beta: &beta,
1334            state: &mut s,
1335            n_heads,
1336            dk,
1337            dv,
1338        })
1339        .unwrap();
1340        assert!(s != s_after || (o2[0] - o1[0]).abs() > 0.0);
1341    }
1342
1343    #[test]
1344    fn geglu_and_rms_norm_gemma() {
1345        let gate = [0.5f32, -1.0];
1346        let up = [2.0f32, 3.0];
1347        let y = geglu(&gate, &up).unwrap();
1348        assert!((y[0] - gelu_pytorch_tanh(0.5) * 2.0).abs() < 1e-5);
1349        assert!((y[1] - gelu_pytorch_tanh(-1.0) * 3.0).abs() < 1e-5);
1350        let x = [1.0f32, -1.0, 2.0, 0.0];
1351        let w = [0.1f32, 0.2];
1352        let n = rms_norm_gemma(&x, &w, 1e-6).unwrap();
1353        assert_eq!(n.len(), 4);
1354        // (1+w) vs plain rms_norm weight multiply differs.
1355        let plain = rms_norm(&x, &w, 1e-6).unwrap();
1356        assert!((n[0] - plain[0]).abs() > 1e-4 || (n[1] - plain[1]).abs() > 1e-4);
1357    }
1358
1359    #[test]
1360    fn rope_half_layout() {
1361        let mut x = [1.0f32, 2.0, 3.0, 4.0];
1362        rope_half(&mut x, 4, 1, 10000.0).unwrap();
1363        // At pos=1, half=2: rotate (1,3) and (2,4) as pairs across half.
1364        let freq0 = 1.0 / 10000f32.powf(0.0);
1365        let (c0, s0) = (freq0.cos(), freq0.sin());
1366        let freq1 = 1.0 / 10000f32.powf(2.0 / 4.0);
1367        let (c1, s1) = (freq1.cos(), freq1.sin());
1368        assert!((x[0] - (1.0 * c0 - 3.0 * s0)).abs() < 1e-5);
1369        assert!((x[2] - (1.0 * s0 + 3.0 * c0)).abs() < 1e-5);
1370        assert!((x[1] - (2.0 * c1 - 4.0 * s1)).abs() < 1e-5);
1371        assert!((x[3] - (2.0 * s1 + 4.0 * c1)).abs() < 1e-5);
1372        assert!(matches!(
1373            rope_half(&mut [1.0, 2.0, 3.0], 3, 0, 10000.0),
1374            Err(EngineError::ShapeMismatch(_))
1375        ));
1376    }
1377
1378    #[test]
1379    fn rope_half_partial_full_matches_rope_half() {
1380        let mut a = [1.0f32, 2.0, 3.0, 4.0];
1381        let mut b = a;
1382        rope_half(&mut a, 4, 2, 10000.0).unwrap();
1383        rope_half_partial(&mut b, 4, 4, 2, 10000.0).unwrap();
1384        for (x, y) in a.iter().zip(b.iter()) {
1385            assert!((x - y).abs() < 1e-6);
1386        }
1387        let mut c = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1388        let tail = [5.0f32, 6.0, 7.0, 8.0];
1389        rope_half_partial(&mut c, 8, 4, 1, 10000.0).unwrap();
1390        assert_eq!(&c[4..], &tail);
1391    }
1392
1393    #[test]
1394    fn rope_half_proportional_identity_tail() {
1395        let mut full = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1396        let orig = full;
1397        rope_half(&mut full, 8, 3, 10000.0).unwrap();
1398        let mut prop = orig;
1399        rope_half_proportional(&mut prop, 8, 1.0, 3, 10000.0).unwrap();
1400        for (a, b) in full.iter().zip(prop.iter()) {
1401            assert!((a - b).abs() < 1e-6);
1402        }
1403        let mut half = orig;
1404        rope_half_proportional(&mut half, 8, 0.25, 3, 10000.0).unwrap();
1405        // rope_angles = int(0.25 * 8 / 2) = 1: only pair (x0, x4) rotates.
1406        assert!((half[0] - orig[0]).abs() > 1e-6);
1407        assert!((half[4] - orig[4]).abs() > 1e-6);
1408        assert_eq!(&half[1..4], &orig[1..4]);
1409        assert_eq!(&half[5..], &orig[5..]);
1410    }
1411
1412    #[test]
1413    fn hdm_linear_matches_unrotated_weight() {
1414        let out_f = 8usize;
1415        let in_f = 4usize;
1416        let seed = Some(7i64);
1417        let mut w_orig: Vec<f32> = (0..out_f * in_f).map(|i| (i as f32) * 0.05 - 0.2).collect();
1418        let x: Vec<f32> = (0..in_f).map(|i| (i as f32) * 0.1).collect();
1419        let y_ref = linear(&x, &w_orig, out_f, in_f).unwrap();
1420        hadamard_blocked_rows(&mut w_orig, out_f, in_f, seed, false).unwrap();
1421        let y = hdm_linear(&x, &w_orig, out_f, in_f, seed).unwrap();
1422        for (a, b) in y.iter().zip(y_ref.iter()) {
1423            assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1424        }
1425    }
1426
1427    #[test]
1428    fn linear_cpu_matches_scalar_linear() {
1429        let out_f = 7usize;
1430        let in_f = 5usize;
1431        let w: Vec<f32> = (0..out_f * in_f).map(|i| (i as f32) * 0.02 - 0.1).collect();
1432        let x: Vec<f32> = (0..in_f * 3).map(|i| (i as f32) * 0.03).collect();
1433        let a = linear(&x, &w, out_f, in_f).unwrap();
1434        let b = linear_cpu(&x, &w, out_f, in_f).unwrap();
1435        for (x, y) in a.iter().zip(b.iter()) {
1436            assert!((x - y).abs() < 1e-4, "{x} vs {y}");
1437        }
1438    }
1439
1440    #[test]
1441    fn attention_causal_matches_stepwise() {
1442        let n_heads = 2usize;
1443        let n_kv = 1usize;
1444        let head_dim = 4usize;
1445        let seq = 3usize;
1446        let q_dim = n_heads * head_dim;
1447        let kv_dim = n_kv * head_dim;
1448        let q: Vec<f32> = (0..seq * q_dim).map(|i| (i as f32) * 0.01).collect();
1449        let k: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.02).collect();
1450        let v: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.03).collect();
1451        let batched = attention_causal(&q, &k, &v, n_heads, n_kv, head_dim).unwrap();
1452        let mut step = Vec::new();
1453        for t in 0..seq {
1454            let qi = &q[t * q_dim..(t + 1) * q_dim];
1455            let a = attention(
1456                qi,
1457                &k[..(t + 1) * kv_dim],
1458                &v[..(t + 1) * kv_dim],
1459                n_heads,
1460                n_kv,
1461                head_dim,
1462            )
1463            .unwrap();
1464            step.extend_from_slice(&a);
1465        }
1466        for (a, b) in batched.iter().zip(step.iter()) {
1467            assert!((a - b).abs() < 1e-5, "{a} vs {b}");
1468        }
1469    }
1470
1471    #[test]
1472    fn sliding_window_matches_truncated_kv() {
1473        let n_heads = 2usize;
1474        let n_kv = 1usize;
1475        let head_dim = 4usize;
1476        let seq = 6usize;
1477        let window = 2usize;
1478        let q_dim = n_heads * head_dim;
1479        let kv_dim = n_kv * head_dim;
1480        let scale = 1.0 / (head_dim as f32).sqrt();
1481        let q: Vec<f32> = (0..seq * q_dim).map(|i| (i as f32) * 0.01).collect();
1482        let k: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.02).collect();
1483        let v: Vec<f32> = (0..seq * kv_dim).map(|i| (i as f32) * 0.03).collect();
1484
1485        let wide = attention_causal_with_scale(&q, &k, &v, n_heads, n_kv, head_dim, scale, None)
1486            .unwrap();
1487        let win = attention_causal_with_scale(
1488            &q,
1489            &k,
1490            &v,
1491            n_heads,
1492            n_kv,
1493            head_dim,
1494            scale,
1495            Some(window),
1496        )
1497        .unwrap();
1498        // Early tokens have prefix shorter than window → identical to causal.
1499        for i in 0..window * q_dim {
1500            assert!((wide[i] - win[i]).abs() < 1e-6, "prefix {i}");
1501        }
1502        assert!(
1503            wide.iter()
1504                .zip(win.iter())
1505                .any(|(a, b)| (a - b).abs() > 1e-5),
1506            "window must change scores once seq > window"
1507        );
1508
1509        let last_q = &q[(seq - 1) * q_dim..];
1510        let start = seq - window;
1511        let sliced = attention_with_scale(
1512            last_q,
1513            &k[start * kv_dim..],
1514            &v[start * kv_dim..],
1515            n_heads,
1516            n_kv,
1517            head_dim,
1518            scale,
1519        )
1520        .unwrap();
1521        let (k_win, v_win) = kv_sliding_view(&k, &v, kv_dim, Some(window)).unwrap();
1522        let via_window =
1523            attention_with_scale(last_q, k_win, v_win, n_heads, n_kv, head_dim, scale).unwrap();
1524        for (a, b) in sliced.iter().zip(via_window.iter()) {
1525            assert!((a - b).abs() < 1e-6, "{a} vs {b}");
1526        }
1527        let (k_noop, v_noop) = kv_sliding_view(&k, &v, kv_dim, Some(seq + 8)).unwrap();
1528        let noop =
1529            attention_with_scale(last_q, k_noop, v_noop, n_heads, n_kv, head_dim, scale).unwrap();
1530        let full = attention_with_scale(last_q, &k, &v, n_heads, n_kv, head_dim, scale).unwrap();
1531        for (a, b) in noop.iter().zip(full.iter()) {
1532            assert!((a - b).abs() < 1e-6);
1533        }
1534    }
1535
1536    #[test]
1537    fn embedding_row_gather_needs_full_matrix_unrotate() {
1538        let vocab = 8usize;
1539        let hidden = 4usize;
1540        let seed = Some(7i64);
1541        let tid = 3usize;
1542        let mut w: Vec<f32> = (0..vocab * hidden)
1543            .map(|i| (i as f32) * 0.05 - 0.2)
1544            .collect();
1545        let orig = w[tid * hidden..(tid + 1) * hidden].to_vec();
1546        hadamard_blocked_rows(&mut w, vocab, hidden, seed, false).unwrap();
1547        let rotated_row = &w[tid * hidden..(tid + 1) * hidden];
1548        let drift: f32 = orig
1549            .iter()
1550            .zip(rotated_row.iter())
1551            .map(|(a, b)| (a - b).abs())
1552            .sum();
1553        assert!(
1554            drift > 1e-3,
1555            "axis-0 Hadamard must mix vocab rows; gather of W_rot[token] is wrong"
1556        );
1557        hadamard_blocked_rows(&mut w, vocab, hidden, seed, true).unwrap();
1558        for (a, b) in orig.iter().zip(w[tid * hidden..(tid + 1) * hidden].iter()) {
1559            assert!((a - b).abs() < 1e-4, "{a} vs {b}");
1560        }
1561    }
1562
1563    #[test]
1564    fn vocab_table_non_pow2_row_gather_after_unrotate() {
1565        // Gemma-4 embed is [vocab, hidden]; PLE is [vocab, layers*256]. Vocab need
1566        // not be the only pow2 (e.g. Qwen); tiles must still unrotate then gather.
1567        let vocab = 10usize;
1568        let hidden = 6usize;
1569        let seed = Some(0i64);
1570        let mut w: Vec<f32> = (0..vocab * hidden)
1571            .map(|i| (i as f32) * 0.02 - 0.1)
1572            .collect();
1573        let orig = w.clone();
1574        hadamard_blocked_rows(&mut w, vocab, hidden, seed, false).unwrap();
1575        hadamard_blocked_rows(&mut w, vocab, hidden, seed, true).unwrap();
1576        for tid in [0usize, 2, 9] {
1577            let a = &orig[tid * hidden..(tid + 1) * hidden];
1578            let b = &w[tid * hidden..(tid + 1) * hidden];
1579            for (x, y) in a.iter().zip(b.iter()) {
1580                assert!((x - y).abs() < 1e-4, "tid={tid} {x} vs {y}");
1581            }
1582        }
1583        let tiles = pow2_tile_sizes(vocab).unwrap();
1584        assert_eq!(tiles, vec![8, 2]);
1585        let mut w2 = orig.clone();
1586        hadamard_blocked_rows_tiles(&mut w2, vocab, hidden, seed, false, &tiles).unwrap();
1587        hadamard_blocked_rows_tiles(&mut w2, vocab, hidden, seed, true, &tiles).unwrap();
1588        for (a, b) in orig.iter().zip(w2.iter()) {
1589            assert!((a - b).abs() < 1e-4);
1590        }
1591        assert!(hadamard_blocked_rows_tiles(
1592            &mut orig.clone(),
1593            vocab,
1594            hidden,
1595            seed,
1596            true,
1597            &[4, 4],
1598        )
1599        .is_err());
1600    }
1601
1602    #[test]
1603    fn shape_errors() {
1604        assert!(matches!(
1605            rms_norm(&[1.0, 2.0], &[1.0, 1.0, 1.0], 1e-6),
1606            Err(EngineError::ShapeMismatch(_))
1607        ));
1608        assert!(matches!(
1609            linear(&[1.0], &[1.0, 2.0], 1, 1),
1610            Err(EngineError::ShapeMismatch(_))
1611        ));
1612        assert!(matches!(
1613            rope(&mut [1.0, 2.0, 3.0], 3, 0, 10000.0),
1614            Err(EngineError::ShapeMismatch(_))
1615        ));
1616        assert!(matches!(
1617            attention(&[1.0], &[1.0], &[1.0], 1, 1, 2),
1618            Err(EngineError::ShapeMismatch(_))
1619        ));
1620        assert!(matches!(
1621            fwht(&mut [1.0, 2.0, 3.0]),
1622            Err(EngineError::ShapeMismatch(_))
1623        ));
1624    }
1625}