Skip to main content

ferrox_core/
matmul.rs

1use rayon::prelude::*;
2
3use crate::tensor::Tensor;
4
5/// Row-major matmul: `a` is [m, k], `b_t` is [n, k] (i.e. already
6/// transposed, which is how GGUF stores weight matrices for a `y = W x`
7/// projection). Output is [m, n]. Parallelized over output rows with
8/// rayon, matching the "each output row is independent" decomposition
9/// used across llama.cpp / ggml's matmul kernels -- except for the
10/// single-token decode case (`m == 1`, the common case for this path,
11/// since `WeightMatrix::F32` is only used for small tensors like
12/// embeddings/synthetic weights), where parallelizing over `m` would
13/// give exactly one chunk regardless of thread count, i.e. no
14/// parallelism at all no matter how large `n` is. That case instead
15/// parallelizes over `n` (output features) directly, since `out` is
16/// exactly `n` elements long when `m == 1` and needs no layout
17/// transpose to do so.
18pub fn matmul_f32(a: &Tensor, b_t: &Tensor) -> Tensor {
19    let m = a.rows();
20    let k = a.cols();
21    let n = b_t.rows();
22    assert_eq!(
23        b_t.cols(),
24        k,
25        "matmul shape mismatch: a is [{m},{k}], b_t is [{},{}]",
26        b_t.rows(),
27        b_t.cols()
28    );
29
30    let mut out = vec![0f32; m * n];
31    if m == 1 {
32        let a_row = a.row(0);
33        out.par_iter_mut().enumerate().for_each(|(col, out_val)| {
34            let b_row = b_t.row(col);
35            let mut acc = 0f32;
36            for i in 0..k {
37                acc += a_row[i] * b_row[i];
38            }
39            *out_val = acc;
40        });
41    } else {
42        out.par_chunks_mut(n)
43            .enumerate()
44            .for_each(|(row, out_row)| {
45                let a_row = a.row(row);
46                for (col, out_val) in out_row.iter_mut().enumerate() {
47                    let b_row = b_t.row(col);
48                    let mut acc = 0f32;
49                    for i in 0..k {
50                        acc += a_row[i] * b_row[i];
51                    }
52                    *out_val = acc;
53                }
54            });
55    }
56
57    Tensor::new(out, vec![m, n])
58}
59
60/// RMSNorm as used by LLaMA-family and DeepSeek/GLM/Kimi-family decoders:
61/// x_normalized = x / sqrt(mean(x^2) + eps) * weight
62pub fn rms_norm(x: &[f32], weight: &[f32], eps: f32) -> Vec<f32> {
63    assert_eq!(x.len(), weight.len());
64    let mean_sq = sum_sq(x) / x.len() as f32;
65    let scale = 1.0 / (mean_sq + eps).sqrt();
66    let mut out = vec![0f32; x.len()];
67    mul3_scale(x, weight, scale, &mut out);
68    out
69}
70
71/// Per-head RMSNorm (Qwen3 / Gemma3 `attn_q_norm` / `attn_k_norm`):
72/// `weight` has length `head_dim` and is reused for every head in
73/// `x` (layout `[n_heads, head_dim]` row-major).
74pub fn rms_norm_per_head(x: &[f32], weight: &[f32], head_dim: usize, eps: f32) -> Vec<f32> {
75    assert_eq!(weight.len(), head_dim);
76    assert_eq!(x.len() % head_dim, 0);
77    let mut out = vec![0f32; x.len()];
78    for (head, out_h) in x.chunks_exact(head_dim).zip(out.chunks_exact_mut(head_dim)) {
79        let mean_sq = sum_sq(head) / head_dim as f32;
80        let scale = 1.0 / (mean_sq + eps).sqrt();
81        mul3_scale(head, weight, scale, out_h);
82    }
83    out
84}
85
86#[inline]
87fn sum_sq(x: &[f32]) -> f32 {
88    #[cfg(target_arch = "aarch64")]
89    {
90        if std::arch::is_aarch64_feature_detected!("neon") {
91            return unsafe { sum_sq_neon(x) };
92        }
93    }
94    #[cfg(target_arch = "x86_64")]
95    {
96        if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
97            return unsafe { sum_sq_avx2(x) };
98        }
99    }
100    x.iter().map(|v| v * v).sum()
101}
102
103#[inline]
104fn mul3_scale(x: &[f32], w: &[f32], scale: f32, out: &mut [f32]) {
105    debug_assert_eq!(x.len(), w.len());
106    debug_assert_eq!(x.len(), out.len());
107    #[cfg(target_arch = "aarch64")]
108    {
109        if std::arch::is_aarch64_feature_detected!("neon") {
110            unsafe { mul3_scale_neon(x, w, scale, out) };
111            return;
112        }
113    }
114    #[cfg(target_arch = "x86_64")]
115    {
116        if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
117            unsafe { mul3_scale_avx2(x, w, scale, out) };
118            return;
119        }
120    }
121    for ((o, &xv), &wv) in out.iter_mut().zip(x).zip(w) {
122        *o = xv * scale * wv;
123    }
124}
125
126#[cfg(target_arch = "aarch64")]
127#[target_feature(enable = "neon")]
128unsafe fn sum_sq_neon(x: &[f32]) -> f32 {
129    use std::arch::aarch64::*;
130    let n = x.len();
131    let mut acc = vdupq_n_f32(0.0);
132    let mut i = 0;
133    while i + 4 <= n {
134        let v = vld1q_f32(x.as_ptr().add(i));
135        acc = vfmaq_f32(acc, v, v);
136        i += 4;
137    }
138    let mut sum = vaddvq_f32(acc);
139    while i < n {
140        sum += x[i] * x[i];
141        i += 1;
142    }
143    sum
144}
145
146#[cfg(target_arch = "aarch64")]
147#[target_feature(enable = "neon")]
148unsafe fn mul3_scale_neon(x: &[f32], w: &[f32], scale: f32, out: &mut [f32]) {
149    use std::arch::aarch64::*;
150    let n = x.len();
151    let vs = vdupq_n_f32(scale);
152    let mut i = 0;
153    while i + 4 <= n {
154        let xv = vld1q_f32(x.as_ptr().add(i));
155        let wv = vld1q_f32(w.as_ptr().add(i));
156        vst1q_f32(out.as_mut_ptr().add(i), vmulq_f32(vmulq_f32(xv, vs), wv));
157        i += 4;
158    }
159    while i < n {
160        out[i] = x[i] * scale * w[i];
161        i += 1;
162    }
163}
164
165#[cfg(target_arch = "x86_64")]
166#[target_feature(enable = "avx2,fma")]
167unsafe fn sum_sq_avx2(x: &[f32]) -> f32 {
168    use std::arch::x86_64::*;
169    let n = x.len();
170    let mut acc = _mm256_setzero_ps();
171    let mut i = 0;
172    while i + 8 <= n {
173        let v = _mm256_loadu_ps(x.as_ptr().add(i));
174        acc = _mm256_fmadd_ps(v, v, acc);
175        i += 8;
176    }
177    let lo = _mm256_castps256_ps128(acc);
178    let hi = _mm256_extractf128_ps(acc, 1);
179    let mut s128 = _mm_add_ps(lo, hi);
180    s128 = _mm_add_ps(s128, _mm_movehl_ps(s128, s128));
181    s128 = _mm_add_ss(s128, _mm_shuffle_ps(s128, s128, 1));
182    let mut sum = _mm_cvtss_f32(s128);
183    while i < n {
184        sum += x[i] * x[i];
185        i += 1;
186    }
187    sum
188}
189
190#[cfg(target_arch = "x86_64")]
191#[target_feature(enable = "avx2,fma")]
192unsafe fn mul3_scale_avx2(x: &[f32], w: &[f32], scale: f32, out: &mut [f32]) {
193    use std::arch::x86_64::*;
194    let n = x.len();
195    let vs = _mm256_set1_ps(scale);
196    let mut i = 0;
197    while i + 8 <= n {
198        let xv = _mm256_loadu_ps(x.as_ptr().add(i));
199        let wv = _mm256_loadu_ps(w.as_ptr().add(i));
200        _mm256_storeu_ps(
201            out.as_mut_ptr().add(i),
202            _mm256_mul_ps(_mm256_mul_ps(xv, vs), wv),
203        );
204        i += 8;
205    }
206    while i < n {
207        out[i] = x[i] * scale * w[i];
208        i += 1;
209    }
210}
211
212/// Soft-cap used by Gemma 2+ attention / final logits:
213/// `softcap * tanh(x / softcap)`.
214pub fn softcap_inplace(x: &mut [f32], softcap: f32) {
215    if softcap <= 0.0 {
216        return;
217    }
218    let inv = 1.0 / softcap;
219    for v in x.iter_mut() {
220        *v = softcap * (*v * inv).tanh();
221    }
222}
223
224/// GELU (tanh approximation) used by Gemma GeGLU FFNs.
225pub fn gelu(x: f32) -> f32 {
226    // HuggingFace / llama.cpp GELU tanh approx.
227    const K: f32 = 0.797_884_6; // sqrt(2/pi)
228    const C: f32 = 0.044_715;
229    0.5 * x * (1.0 + (K * (x + C * x * x * x)).tanh())
230}
231
232/// Elementwise gated FFN combine: gelu(gate) * up (Gemma GeGLU).
233///
234/// **The tanh is gone and the answers moved. Both are on purpose.**
235///
236/// `gelu` above spends a libm `tanhf` per element, which does not
237/// vectorize and does not inline: on Gemma-3-1B CPU `pp512` it was 10.7%
238/// of all non-idle samples in the process. The identity
239/// `0.5·x·(1 + tanh(u)) == x / (1 + exp(-2u))` turns that into one
240/// vectorized exponential, and [`expf_neon`] / [`expf_avx2`] do four or
241/// eight at a time.
242///
243/// The rewrite is also *more* accurate on the side that matters, which is
244/// why it clears the "no less accurate than what it replaces" bar rather
245/// than merely getting close to it. For `x` negative, `tanh(u) → -1`, so
246/// `1 + tanh(u)` is a difference of nearly equal numbers and loses most of
247/// its significant bits; `1 + exp(-2u)` is a sum of a large term and 1 and
248/// loses none. `geglu_is_no_less_accurate_than_the_tanh_form_it_replaces`
249/// measures both against an `f64` evaluation of the same formula and
250/// pins it.
251pub fn geglu(gate: &[f32], up: &[f32]) -> Vec<f32> {
252    assert_eq!(gate.len(), up.len());
253    par_gated_chunks(gate, up, gelu_mul)
254}
255
256/// Threshold above which the elementwise FFN activations fork to Rayon.
257/// Decode passes one row (`ffn_dim`, a few thousand elements) and would
258/// only pay fork-join; prefill passes `batch × ffn_dim`, which on
259/// Gemma-3-1B is 3.5 M elements per layer.
260const GATED_PAR_MIN: usize = 1 << 15;
261
262/// Run a gated-activation kernel over the whole pair, forked to Rayon
263/// past [`GATED_PAR_MIN`].
264///
265/// Prefill ran these serially on the calling thread while every other
266/// core sat in the FFN's fork-join: on Gemma-3-1B CPU `pp512`, `tanhf`
267/// under `geglu` was 10.7% of *all* non-idle samples in the process and
268/// 3682 of its 3688 samples were on the main thread alone.
269///
270/// `f` takes slices rather than one element, so the kernel can hold a
271/// vector register across a run instead of being called per lane.
272/// Chunking stays bit-exact for the same reason it always was — no
273/// reduction, each output element depends only on its own inputs — and
274/// the chunk length is a multiple of 16, so it never splits a 4- or
275/// 8-lane group and the vector arms cannot see a boundary either.
276#[inline]
277fn par_gated_chunks<F>(gate: &[f32], up: &[f32], f: F) -> Vec<f32>
278where
279    F: Fn(&[f32], &[f32], &mut [f32]) + Sync + Send,
280{
281    let n = gate.len();
282    let mut out = vec![0f32; n];
283    if n < GATED_PAR_MIN {
284        f(gate, up, &mut out);
285        return out;
286    }
287    // Cache-line-aligned chunks so no two tasks share a 64-byte line.
288    let chunk = (n.div_ceil(rayon::current_num_threads() * 4)).next_multiple_of(16);
289    out.par_chunks_mut(chunk)
290        .zip(gate.par_chunks(chunk))
291        .zip(up.par_chunks(chunk))
292        .for_each(|((o, g), u)| f(g, u, o));
293    out
294}
295
296/// `out[i] = gelu(gate[i]) * up[i]`, vectorized where there is a vector
297/// exponential and falling back to the scalar [`gelu`] where there is not.
298fn gelu_mul(gate: &[f32], up: &[f32], out: &mut [f32]) {
299    debug_assert_eq!(gate.len(), up.len());
300    debug_assert_eq!(gate.len(), out.len());
301    #[cfg(target_arch = "aarch64")]
302    {
303        if std::arch::is_aarch64_feature_detected!("neon") {
304            unsafe { gelu_mul_neon(gate, up, out) };
305            return;
306        }
307    }
308    #[cfg(target_arch = "x86_64")]
309    {
310        if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
311            unsafe { gelu_mul_avx2(gate, up, out) };
312            return;
313        }
314    }
315    for ((o, g), u) in out.iter_mut().zip(gate.iter()).zip(up.iter()) {
316        *o = gelu(*g) * *u;
317    }
318}
319
320/// `out[i] = silu(gate[i]) * up[i]`, same shape as [`gelu_mul`].
321fn silu_mul(gate: &[f32], up: &[f32], out: &mut [f32]) {
322    debug_assert_eq!(gate.len(), up.len());
323    debug_assert_eq!(gate.len(), out.len());
324    #[cfg(target_arch = "aarch64")]
325    {
326        if std::arch::is_aarch64_feature_detected!("neon") {
327            unsafe { silu_mul_neon(gate, up, out) };
328            return;
329        }
330    }
331    #[cfg(target_arch = "x86_64")]
332    {
333        if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
334            unsafe { silu_mul_avx2(gate, up, out) };
335            return;
336        }
337    }
338    for ((o, g), u) in out.iter_mut().zip(gate.iter()).zip(up.iter()) {
339        *o = silu(*g) * *u;
340    }
341}
342
343// The `ggml_v_expf` constants (`ggml/src/ggml-cpu/vec.h`), used only by
344// the vector arms below -- gated so a host with neither SIMD arm still
345// compiles clean under `-D warnings`.
346//
347// `crate::attention` carries its own private copy of this kernel for the
348// softmax. THEY ARE THE SAME ROUTINE AND SHOULD LIVE IN ONE MODULE; this
349// copy exists only because that one is private to `attention` and this
350// branch does not own that file. Whoever merges them: the two differ in
351// exactly one way, the clamp, and the difference is load-bearing --
352// see [`EXP_CLAMP`].
353#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
354mod exp_consts {
355    // The nine ggml_v_expf polynomial constants live in
356    // `ferrox_core::vexp`, shared with `attention`, which had a
357    // byte-identical copy. Only the clamp differs between the two, and
358    // it differs on purpose: see EXP_CLAMP.
359    pub use crate::vexp::*;
360
361    /// **Clamped on BOTH sides, unlike the softmax copy in `attention`,
362    /// and that is the whole difference between the two.**
363    ///
364    /// This kernel drops `ggml_v_expf`'s overflow branch, which is only
365    /// sound while `|n| <= 126`. A softmax argument is `score - row_max`,
366    /// hence never positive, so the softmax copy clamps from below alone.
367    /// A gated-activation argument is `-x` (SiLU) or `-2u` (GELU) and has
368    /// no sign at all, so an upper clamp is required or a large negative
369    /// input walks `bits(z) << 23` straight out of the exponent field and
370    /// returns garbage rather than infinity.
371    ///
372    /// `87` is where clamping stops costing anything *below*: `exp(-87)`
373    /// is already under `f32::MIN_POSITIVE`, and `87 · log2(e)` is
374    /// `125.5`, which keeps `n` inside the fast path's range with room.
375    ///
376    /// Above it the clamp alone is NOT harmless, and this is the trap:
377    /// the value appears as the denominator `x / (1 + exp(t))`, so a
378    /// saturated `exp` divides a numerator that has no bound of its own.
379    /// At `x = -1e30` the true `t` is infinite and the answer is `-0`,
380    /// while `x / (1 + exp(87))` is `-1.6e-8` -- a real number where there
381    /// should be nothing. So the vector arms do not merely clamp on the
382    /// high side, they *select*: `t >= EXP_CLAMP` yields zero. That is
383    /// exact to within `1.5e-37`, because `t >= 87` only happens where the
384    /// activation itself has collapsed (`x <= -10` for GELU, `x <= -87`
385    /// for SiLU) and it collapses far faster than `|x|` grows.
386    pub const EXP_CLAMP: f32 = 87.0;
387    /// `sqrt(2/pi)`, the GELU tanh approximation's outer constant.
388    pub const GELU_K: f32 = 0.797_884_6;
389    /// `-2 · GELU_K`: `gelu(x) = x / (1 + exp(x·(A + B·x²)))` with
390    /// `A = -2K` and `B = -2KC`, which is `0.5·x·(1 + tanh(K·(x + C·x³)))`
391    /// rearranged so no cancellation is left in it.
392    pub const GELU_A: f32 = -2.0 * GELU_K;
393    /// `-2 · GELU_K · 0.044715`.
394    pub const GELU_B: f32 = -2.0 * GELU_K * 0.044_715;
395}
396#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
397use exp_consts::*;
398
399/// `exp(x)` for four lanes: ARM optimized-routines' `expf`, the shape
400/// llama.cpp vendors as `ggml_v_expf`. Accurate to under an ulp.
401///
402/// `z = fma(x, log2(e), 0x1.8p23)` rounds `x·log2(e)` to an integer `n`
403/// and leaves it in the low mantissa bits of `z`, so `bits(z) << 23` is
404/// the exponent field of `2^n`. `b = x - n·ln2_hi - n·ln2_lo` is the
405/// reduced argument in `[-ln2/2, ln2/2]`, and the degree-5 polynomial
406/// evaluates `e^b - 1` there.
407///
408/// The input is clamped to `±EXP_CLAMP` first, which is what lets the
409/// overflow branch of the original be dropped; read [`EXP_CLAMP`] before
410/// widening it.
411#[cfg(target_arch = "aarch64")]
412#[target_feature(enable = "neon")]
413#[inline]
414unsafe fn expf_neon(x: std::arch::aarch64::float32x4_t) -> std::arch::aarch64::float32x4_t {
415    use std::arch::aarch64::*;
416    let x = vminq_f32(
417        vmaxq_f32(x, vdupq_n_f32(-EXP_CLAMP)),
418        vdupq_n_f32(EXP_CLAMP),
419    );
420    let r = vdupq_n_f32(EXP_SHIFT);
421    let z = vfmaq_f32(r, x, vdupq_n_f32(EXP_LOG2E));
422    let n = vsubq_f32(z, r);
423    // `b = x - n*ln2_hi - n*ln2_lo`; `vfmsq_f32(a, b, c) == a - b*c`.
424    let b = vfmsq_f32(
425        vfmsq_f32(x, n, vdupq_n_f32(EXP_LN2_HI)),
426        n,
427        vdupq_n_f32(EXP_LN2_LO),
428    );
429    let e = vshlq_n_u32::<23>(vreinterpretq_u32_f32(z));
430    let k = vreinterpretq_f32_u32(vaddq_u32(e, vreinterpretq_u32_f32(vdupq_n_f32(1.0))));
431    let u = vmulq_f32(b, b);
432    let j = vfmaq_f32(
433        vmulq_f32(vdupq_n_f32(EXP_C0), b),
434        vfmaq_f32(
435            vfmaq_f32(vdupq_n_f32(EXP_C1), vdupq_n_f32(EXP_C2), b),
436            vfmaq_f32(vdupq_n_f32(EXP_C3), vdupq_n_f32(EXP_C4), b),
437            u,
438        ),
439        u,
440    );
441    vfmaq_f32(k, j, k)
442}
443
444/// One-lane [`expf_neon`], op for op, so a kernel's scalar tail computes
445/// the same bits as its vector body.
446///
447/// A libm `expf` here instead would make the answer depend on where the
448/// slice happens to end, and prefill and decode do not end in the same
449/// place. `mul_add` is the scalar FMA the vector arms use lane-wise, and
450/// both round once, so this is equality and not approximation --
451/// `parallel_gated_activations_are_bit_identical_to_the_serial_form`
452/// asserts it by pushing every element through this path.
453#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
454#[inline]
455fn expf_scalar(x: f32) -> f32 {
456    let x = x.clamp(-EXP_CLAMP, EXP_CLAMP);
457    let z = x.mul_add(EXP_LOG2E, EXP_SHIFT);
458    let n = z - EXP_SHIFT;
459    let b = (-n).mul_add(EXP_LN2_LO, (-n).mul_add(EXP_LN2_HI, x));
460    let k = f32::from_bits((z.to_bits() << 23).wrapping_add(1.0f32.to_bits()));
461    let u = b * b;
462    let j = EXP_C4
463        .mul_add(b, EXP_C3)
464        .mul_add(u, EXP_C2.mul_add(b, EXP_C1))
465        .mul_add(u, EXP_C0 * b);
466    j.mul_add(k, k)
467}
468
469/// AVX2 sibling of [`expf_neon`]: same constants, same polynomial, same
470/// two-sided clamp, eight lanes.
471#[cfg(target_arch = "x86_64")]
472#[target_feature(enable = "avx2,fma")]
473#[inline]
474unsafe fn expf_avx2(x: std::arch::x86_64::__m256) -> std::arch::x86_64::__m256 {
475    use std::arch::x86_64::*;
476    let x = _mm256_min_ps(
477        _mm256_max_ps(x, _mm256_set1_ps(-EXP_CLAMP)),
478        _mm256_set1_ps(EXP_CLAMP),
479    );
480    let r = _mm256_set1_ps(EXP_SHIFT);
481    let z = _mm256_fmadd_ps(x, _mm256_set1_ps(EXP_LOG2E), r);
482    let n = _mm256_sub_ps(z, r);
483    let b = _mm256_fnmadd_ps(
484        n,
485        _mm256_set1_ps(EXP_LN2_LO),
486        _mm256_fnmadd_ps(n, _mm256_set1_ps(EXP_LN2_HI), x),
487    );
488    let e = _mm256_slli_epi32::<23>(_mm256_castps_si256(z));
489    let k = _mm256_castsi256_ps(_mm256_add_epi32(
490        e,
491        _mm256_castps_si256(_mm256_set1_ps(1.0)),
492    ));
493    let u = _mm256_mul_ps(b, b);
494    let j = _mm256_fmadd_ps(
495        _mm256_fmadd_ps(
496            _mm256_fmadd_ps(_mm256_set1_ps(EXP_C4), b, _mm256_set1_ps(EXP_C3)),
497            u,
498            _mm256_fmadd_ps(_mm256_set1_ps(EXP_C2), b, _mm256_set1_ps(EXP_C1)),
499        ),
500        u,
501        _mm256_mul_ps(_mm256_set1_ps(EXP_C0), b),
502    );
503    _mm256_fmadd_ps(j, k, k)
504}
505
506/// `x / (1 + exp(t))`, with the saturating branch [`EXP_CLAMP`] describes:
507/// once `t` reaches the clamp the true quotient is below `1.5e-37`, and
508/// returning it rather than dividing by a saturated exponential is what
509/// keeps a large negative input from producing a real number.
510#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
511#[inline]
512fn gate_by_exp_scalar(x: f32, t: f32) -> f32 {
513    if t >= EXP_CLAMP {
514        0.0
515    } else {
516        x / (1.0 + expf_scalar(t))
517    }
518}
519
520/// `t = -2·K·(g + C·g³)`, written as `g·(A + B·g²)` so it is one multiply
521/// and one FMA. GELU's exponent argument.
522#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
523#[inline]
524fn gelu_exp_arg(g: f32) -> f32 {
525    g * GELU_B.mul_add(g * g, GELU_A)
526}
527
528#[cfg(target_arch = "aarch64")]
529#[target_feature(enable = "neon")]
530unsafe fn gelu_mul_neon(gate: &[f32], up: &[f32], out: &mut [f32]) {
531    use std::arch::aarch64::*;
532    let n = out.len();
533    let nv = n & !3;
534    let one = vdupq_n_f32(1.0);
535    let zero = vdupq_n_f32(0.0);
536    let a = vdupq_n_f32(GELU_A);
537    let b = vdupq_n_f32(GELU_B);
538    let sat = vdupq_n_f32(EXP_CLAMP);
539    let mut i = 0;
540    while i < nv {
541        let g = vld1q_f32(gate.as_ptr().add(i));
542        // `t = g * (A + B*g^2)`, i.e. `-2*K*(g + C*g^3)`.
543        let t = vmulq_f32(g, vfmaq_f32(a, b, vmulq_f32(g, g)));
544        let y = vdivq_f32(g, vaddq_f32(one, expf_neon(t)));
545        let y = vbslq_f32(vcgeq_f32(t, sat), zero, y);
546        vst1q_f32(
547            out.as_mut_ptr().add(i),
548            vmulq_f32(y, vld1q_f32(up.as_ptr().add(i))),
549        );
550        i += 4;
551    }
552    for j in nv..n {
553        let g = *gate.get_unchecked(j);
554        *out.get_unchecked_mut(j) = gate_by_exp_scalar(g, gelu_exp_arg(g)) * *up.get_unchecked(j);
555    }
556}
557
558#[cfg(target_arch = "aarch64")]
559#[target_feature(enable = "neon")]
560unsafe fn silu_mul_neon(gate: &[f32], up: &[f32], out: &mut [f32]) {
561    use std::arch::aarch64::*;
562    let n = out.len();
563    let nv = n & !3;
564    let one = vdupq_n_f32(1.0);
565    let zero = vdupq_n_f32(0.0);
566    let sat = vdupq_n_f32(EXP_CLAMP);
567    let mut i = 0;
568    while i < nv {
569        let g = vld1q_f32(gate.as_ptr().add(i));
570        let t = vnegq_f32(g);
571        let y = vdivq_f32(g, vaddq_f32(one, expf_neon(t)));
572        let y = vbslq_f32(vcgeq_f32(t, sat), zero, y);
573        vst1q_f32(
574            out.as_mut_ptr().add(i),
575            vmulq_f32(y, vld1q_f32(up.as_ptr().add(i))),
576        );
577        i += 4;
578    }
579    for j in nv..n {
580        let g = *gate.get_unchecked(j);
581        *out.get_unchecked_mut(j) = gate_by_exp_scalar(g, -g) * *up.get_unchecked(j);
582    }
583}
584
585#[cfg(target_arch = "x86_64")]
586#[target_feature(enable = "avx2,fma")]
587unsafe fn gelu_mul_avx2(gate: &[f32], up: &[f32], out: &mut [f32]) {
588    use std::arch::x86_64::*;
589    let n = out.len();
590    let nv = n & !7;
591    let one = _mm256_set1_ps(1.0);
592    let zero = _mm256_setzero_ps();
593    let a = _mm256_set1_ps(GELU_A);
594    let b = _mm256_set1_ps(GELU_B);
595    let sat = _mm256_set1_ps(EXP_CLAMP);
596    let mut i = 0;
597    while i < nv {
598        let g = _mm256_loadu_ps(gate.as_ptr().add(i));
599        let t = _mm256_mul_ps(g, _mm256_fmadd_ps(b, _mm256_mul_ps(g, g), a));
600        let y = _mm256_div_ps(g, _mm256_add_ps(one, expf_avx2(t)));
601        let y = _mm256_blendv_ps(y, zero, _mm256_cmp_ps::<_CMP_GE_OQ>(t, sat));
602        _mm256_storeu_ps(
603            out.as_mut_ptr().add(i),
604            _mm256_mul_ps(y, _mm256_loadu_ps(up.as_ptr().add(i))),
605        );
606        i += 8;
607    }
608    for j in nv..n {
609        let g = *gate.get_unchecked(j);
610        *out.get_unchecked_mut(j) = gate_by_exp_scalar(g, gelu_exp_arg(g)) * *up.get_unchecked(j);
611    }
612}
613
614#[cfg(target_arch = "x86_64")]
615#[target_feature(enable = "avx2,fma")]
616unsafe fn silu_mul_avx2(gate: &[f32], up: &[f32], out: &mut [f32]) {
617    use std::arch::x86_64::*;
618    let n = out.len();
619    let nv = n & !7;
620    let one = _mm256_set1_ps(1.0);
621    let zero = _mm256_setzero_ps();
622    let neg = _mm256_set1_ps(-0.0);
623    let sat = _mm256_set1_ps(EXP_CLAMP);
624    let mut i = 0;
625    while i < nv {
626        let g = _mm256_loadu_ps(gate.as_ptr().add(i));
627        // `-g` as a sign flip, so `-0.0` negates to `0.0` exactly as
628        // NEON's `vnegq_f32` does and the two arms cannot disagree there.
629        let t = _mm256_xor_ps(g, neg);
630        let y = _mm256_div_ps(g, _mm256_add_ps(one, expf_avx2(t)));
631        let y = _mm256_blendv_ps(y, zero, _mm256_cmp_ps::<_CMP_GE_OQ>(t, sat));
632        _mm256_storeu_ps(
633            out.as_mut_ptr().add(i),
634            _mm256_mul_ps(y, _mm256_loadu_ps(up.as_ptr().add(i))),
635        );
636        i += 8;
637    }
638    for j in nv..n {
639        let g = *gate.get_unchecked(j);
640        *out.get_unchecked_mut(j) = gate_by_exp_scalar(g, -g) * *up.get_unchecked(j);
641    }
642}
643
644/// Plain (non-RMS) LayerNorm -- ggml's `LLM_NORM` (as opposed to
645/// `LLM_NORM_RMS`, what [`rms_norm`] implements): subtract the mean,
646/// divide by the standard deviation, then apply an elementwise
647/// affine `* weight + bias`. GLM-5.2's real DSA lightning indexer
648/// normalizes its compressed key through exactly this
649/// (`indexer_k_norm` carries both a `weight` *and* a `bias` GGUF
650/// tensor -- confirmed against llama.cpp PR #23346/#25407's real
651/// `create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight"|"bias", i),
652/// ...)` calls and the `build_norm(indexer_k, ..., LLM_NORM, il)` call
653/// site, `LLM_NORM` being ggml's plain-LayerNorm op, distinct from
654/// every other norm in this codebase so far, which are all RMSNorm).
655pub fn layer_norm(x: &[f32], weight: &[f32], bias: &[f32], eps: f32) -> Vec<f32> {
656    assert_eq!(x.len(), weight.len());
657    assert_eq!(x.len(), bias.len());
658    let n = x.len() as f32;
659    let mean = x.iter().sum::<f32>() / n;
660    let var = x.iter().map(|v| (v - mean).powi(2)).sum::<f32>() / n;
661    let inv_std = 1.0 / (var + eps).sqrt();
662    x.iter()
663        .zip(weight.iter())
664        .zip(bias.iter())
665        .map(|((v, w), b)| (v - mean) * inv_std * w + b)
666        .collect()
667}
668
669/// SiLU / swish activation: x * sigmoid(x). Used by the SwiGLU-style
670/// gated MLP and MoE expert feed-forward blocks in this family of models.
671pub fn silu(x: f32) -> f32 {
672    x / (1.0 + (-x).exp())
673}
674
675/// Elementwise gated FFN combine: silu(gate) * up, the standard SwiGLU
676/// pairing used inside both dense and MoE-expert feed-forward blocks.
677///
678/// Same rewrite as [`geglu`], and it reaches far more of the ledger:
679/// [`silu`] was already the cancellation-free `x / (1 + exp(-x))` form,
680/// so this changes only *which* exponential runs, from a scalar libm
681/// `expf` per element to four or eight lanes at a time. Every SwiGLU
682/// model on the CPU rows goes through here — TinyLlama, SmolLM2, Qwen,
683/// Mistral, Llama — not just the Gemma family that named the todo.
684pub fn swiglu(gate: &[f32], up: &[f32]) -> Vec<f32> {
685    assert_eq!(gate.len(), up.len());
686    par_gated_chunks(gate, up, silu_mul)
687}
688
689/// Kimi K3's `situ` activation (`hidden_act: "situ"` in its real
690/// `config.json`, registered as `ACT2FN["situ"] -> SituAndMul` in
691/// `modeling_kimi_linear.py`): `beta*tanh(gate/beta)*sigmoid(gate) *
692/// linear_beta*tanh(up/linear_beta)`. Not SiLU/SwiGLU -- a real,
693/// non-obvious fact confirmed by reading Kimi K3's actual reference
694/// source and config (`activation_situ_beta`=4.0,
695/// `activation_situ_linear_beta`=25.0) rather than assuming the more
696/// common SwiGLU convention every other model in this codebase uses.
697pub fn situ_and_mul(gate: &[f32], up: &[f32], beta: f32, linear_beta: f32) -> Vec<f32> {
698    assert_eq!(gate.len(), up.len());
699    gate.iter()
700        .zip(up.iter())
701        .map(|(g, u)| {
702            let situ_a = beta * (g / beta).tanh() * (1.0 / (1.0 + (-g).exp()));
703            let up_t = linear_beta * (u / linear_beta).tanh();
704            situ_a * up_t
705        })
706        .collect()
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    /// Decode and prefill must compute the same bits from the same
714    /// weights. Decode runs one row through the serial arm, prefill runs
715    /// `batch x ffn_dim` through the Rayon arm, and the vector kernel sees
716    /// a different tail split in each -- so the claim is not "chunking is
717    /// elementwise, therefore fine", it is that the kernel's own vector
718    /// and scalar-tail paths agree everywhere the split can fall.
719    ///
720    /// The reference is the kernel applied to one element at a time, which
721    /// puts every lane through the scalar tail. It is deliberately NOT
722    /// `silu(g) * u`: the scalar [`silu`] and [`gelu`] keep libm and are
723    /// still the reference for the CUDA and Metal ports and for `kda` /
724    /// `gdn`, while these two run the vector exponential. The distance
725    /// between them is what the accuracy test below measures.
726    #[test]
727    fn parallel_gated_activations_are_bit_identical_to_the_serial_form() {
728        // Straddle GATED_PAR_MIN so both arms are covered, and use a
729        // length that is not a multiple of the chunk size or of a vector
730        // width. Elementwise with no reduction, so "close enough" is not
731        // the bar: every bit must match, or prefill and decode disagree
732        // on the same model.
733        for n in [7usize, GATED_PAR_MIN - 1, GATED_PAR_MIN, 300_007] {
734            let gate: Vec<f32> = (0..n)
735                .map(|i| ((i as f32) * 0.0037 - 4.0).sin() * 6.0)
736                .collect();
737            let up: Vec<f32> = (0..n)
738                .map(|i| ((i as f32) * 0.0041 + 1.0).cos() * 2.5)
739                .collect();
740
741            let one_at_a_time = |f: fn(&[f32], &[f32], &mut [f32])| -> Vec<f32> {
742                let mut out = vec![0f32; n];
743                for i in 0..n {
744                    f(&gate[i..i + 1], &up[i..i + 1], &mut out[i..i + 1]);
745                }
746                out
747            };
748            assert_eq!(
749                swiglu(&gate, &up),
750                one_at_a_time(silu_mul),
751                "swiglu at n = {n}"
752            );
753            assert_eq!(
754                geglu(&gate, &up),
755                one_at_a_time(gelu_mul),
756                "geglu at n = {n}"
757            );
758        }
759    }
760
761    /// The bar for replacing a libm call in a hot loop is not "the output
762    /// did not change" -- it did change -- but "the output is no less
763    /// accurate than what it replaced", measured against a reference
764    /// neither side can flatter.
765    ///
766    /// **The obvious reference is the wrong one, and finding that out is
767    /// half of what this test is for.** Evaluating
768    /// `0.5·x·(1 + tanh(u))` in `f64` cancels for the same reason the
769    /// `f32` version does: `tanh(u) → -1` for `x` negative, so `1 + tanh(u)`
770    /// is a difference of nearly equal numbers in `f64` too, and past
771    /// `x ≈ -7` the "reference" is itself noise. It scored the vector form
772    /// at `1.97e22` relative error against a value that had already
773    /// collapsed. The reference here is therefore the algebraically
774    /// identical `x / (1 + exp(-2u))` in `f64`, which has no subtraction
775    /// in it at all.
776    ///
777    /// Two metrics, because they answer different questions and the
778    /// rewrite does not win both.
779    ///
780    /// 1. Scaled by `|x|` -- how much absolute error this contributes to
781    ///    the sum it feeds. GELU: `1.13e-7` vs `9.74e-8`, so the vector
782    ///    form is 1.16x behind, both at one to two ulp of `|x|`; the extra
783    ///    rounding is the divide. SiLU: identical to the last digit.
784    /// 2. Scaled by `|gelu(x)|` -- whether the returned number is itself
785    ///    right. Here the rewrite is not marginally better but categorically
786    ///    so: over 240001 samples the tanh form loses more than 0.1% of the
787    ///    value at 5047 of them, up to and including every significant bit,
788    ///    and the new form at none.
789    ///
790    /// So the assertions are: nobody may lose a value (metric 2), and the
791    /// absolute contribution may not slip by more than a quarter ulp
792    /// (metric 1). Both numbers above are what the code measures today, so
793    /// a regression in either direction fires.
794    #[test]
795    fn geglu_and_swiglu_are_no_less_accurate_than_the_libm_forms_they_replace() {
796        /// Worst error under both metrics, plus how many samples lost more
797        /// than 0.1% of the value.
798        ///
799        /// The `1e-30` floor on metric 2 is not a fudge: below it the
800        /// activation cannot change any `f32` sum it participates in, and
801        /// both forms flush to zero there anyway, which would otherwise
802        /// score as 100% error for both and hide the real difference.
803        fn sweep(
804            reference: fn(f64) -> f64,
805            vector: fn(f32) -> f32,
806            libm: fn(f32) -> f32,
807        ) -> (f64, f64, u32, u32) {
808            let (mut v_abs, mut l_abs) = (0f64, 0f64);
809            let (mut v_lost, mut l_lost) = (0u32, 0u32);
810            let mut i = -120_000i32;
811            while i <= 120_000 {
812                let x = i as f32 * 0.001;
813                let want = reference(x as f64);
814                let (v, l) = (vector(x) as f64 - want, libm(x) as f64 - want);
815                let scale = (x as f64).abs().max(1e-30);
816                v_abs = v_abs.max(v.abs() / scale);
817                l_abs = l_abs.max(l.abs() / scale);
818                if want.abs() >= 1e-30 {
819                    v_lost += u32::from(v.abs() / want.abs() > 1e-3);
820                    l_lost += u32::from(l.abs() / want.abs() > 1e-3);
821                }
822                i += 1;
823            }
824            (v_abs, l_abs, v_lost, l_lost)
825        }
826
827        /// The tanh-approximation GELU rearranged so it does not cancel,
828        /// with `sqrt(2/pi)` to full precision rather than the `f32`
829        /// constant either implementation rounds it to.
830        fn gelu_f64(x: f64) -> f64 {
831            const K: f64 = 0.797_884_560_802_865_4;
832            let u = K * (x + 0.044_715 * x * x * x);
833            x / (1.0 + (-2.0 * u).exp())
834        }
835        fn silu_f64(x: f64) -> f64 {
836            x / (1.0 + (-x).exp())
837        }
838        fn vec_gelu(x: f32) -> f32 {
839            let mut out = [0f32; 1];
840            gelu_mul(&[x], &[1.0], &mut out);
841            out[0]
842        }
843        fn vec_silu(x: f32) -> f32 {
844            let mut out = [0f32; 1];
845            silu_mul(&[x], &[1.0], &mut out);
846            out[0]
847        }
848
849        for (what, reference, vector, libm) in [
850            (
851                "GELU",
852                gelu_f64 as fn(f64) -> f64,
853                vec_gelu as fn(f32) -> f32,
854                gelu as fn(f32) -> f32,
855            ),
856            ("SiLU", silu_f64, vec_silu, silu),
857        ] {
858            let (v_abs, l_abs, v_lost, l_lost) = sweep(reference, vector, libm);
859            assert_eq!(
860                v_lost, 0,
861                "vector {what} lost more than 0.1% of the value at {v_lost} samples \
862                 (the form it replaces: {l_lost})"
863            );
864            assert!(
865                v_lost <= l_lost,
866                "vector {what} loses values the form it replaces kept: {v_lost} vs {l_lost}"
867            );
868            assert!(
869                v_abs <= l_abs * 1.25,
870                "vector {what} contributes more absolute error than the form it \
871                 replaces: {v_abs:e} vs {l_abs:e}"
872            );
873        }
874    }
875
876    /// The clamp is only free where [`EXP_CLAMP`] says it is, and the
877    /// saturating select is what makes the high side true.
878    ///
879    /// Without it a large negative input walks `bits(z) << 23` out of the
880    /// exponent field, or -- worse, because it looks like an answer --
881    /// divides an unbounded numerator by a saturated exponential and
882    /// returns `-1.6e-8` for `gelu(-1e30)`, which should be `-0`. The
883    /// tolerance is absolute rather than relative because everything here
884    /// is either exactly the input or smaller than any `f32` sum can
885    /// notice.
886    #[test]
887    fn the_two_sided_clamp_leaves_the_saturating_tails_correct() {
888        fn gelu_f64(x: f64) -> f64 {
889            const K: f64 = 0.797_884_560_802_865_4;
890            let u = K * (x + 0.044_715 * x * x * x);
891            x / (1.0 + (-2.0 * u).exp())
892        }
893        for x in [
894            -1e30f32, -1e10, -1000.0, -120.0, -88.0, -12.0, -10.0, 10.0, 12.0, 20.0, 120.0, 1000.0,
895            1e30,
896        ] {
897            let mut g = [0f32; 1];
898            gelu_mul(&[x], &[1.0], &mut g);
899            let mut s = [0f32; 1];
900            silu_mul(&[x], &[1.0], &mut s);
901            assert!(g[0].is_finite() || x.abs() > 1e20, "gelu({x}) = {}", g[0]);
902            assert!(s[0].is_finite() || x.abs() > 1e20, "silu({x}) = {}", s[0]);
903
904            // A `f64` reference that does not cancel; anything past the
905            // clamp has collapsed far below what an `f32` sum resolves.
906            let want_g = gelu_f64(x as f64);
907            let want_s = (x as f64) / (1.0 + (-(x as f64)).exp());
908            assert!(
909                (g[0] as f64 - want_g).abs() <= 1e-30 + 1e-6 * want_g.abs(),
910                "gelu({x}) = {} want {want_g:e}",
911                g[0]
912            );
913            assert!(
914                (s[0] as f64 - want_s).abs() <= 1e-30 + 1e-6 * want_s.abs(),
915                "silu({x}) = {} want {want_s:e}",
916                s[0]
917            );
918            // The two saturate to the identity at different places, and
919            // the gap is the point of the `-2·K·(x + C·x³)` argument:
920            // GELU's exponent runs away cubically, so it is already `x`
921            // by 10, while SiLU's is linear and still `9.9995` there.
922            if x >= 10.0 {
923                assert_eq!(g[0], x, "gelu should saturate to the identity at {x}");
924            }
925            if x >= 20.0 {
926                assert_eq!(s[0], x, "silu should saturate to the identity at {x}");
927            }
928        }
929    }
930
931    #[test]
932    fn layer_norm_zero_mean_unit_var_input_is_unchanged_by_weight_one_bias_zero() {
933        // A hand-picked vector with mean 0 and population variance 1:
934        // [-1, 1] has mean 0, var = ((1)+(1))/2 = 1.
935        let x = vec![-1.0, 1.0];
936        let weight = vec![1.0, 1.0];
937        let bias = vec![0.0, 0.0];
938        let out = layer_norm(&x, &weight, &bias, 0.0);
939        assert!((out[0] - (-1.0)).abs() < 1e-4);
940        assert!((out[1] - 1.0).abs() < 1e-4);
941    }
942
943    #[test]
944    fn layer_norm_applies_affine_weight_and_bias_after_normalizing() {
945        let x = vec![-1.0, 1.0];
946        let weight = vec![2.0, 3.0];
947        let bias = vec![10.0, -10.0];
948        let out = layer_norm(&x, &weight, &bias, 0.0);
949        // normalized = [-1, 1] (same as above), then * weight + bias:
950        assert!((out[0] - (-2.0 + 10.0)).abs() < 1e-4);
951        assert!((out[1] - (3.0 - 10.0)).abs() < 1e-4);
952    }
953
954    #[test]
955    fn layer_norm_constant_input_is_zero_before_bias() {
956        // Zero variance input: every normalized value must be exactly 0
957        // (mean-subtracted, so all zero) regardless of eps, then affine.
958        let x = vec![5.0, 5.0, 5.0];
959        let weight = vec![1.0, 1.0, 1.0];
960        let bias = vec![0.25, 0.25, 0.25];
961        let out = layer_norm(&x, &weight, &bias, 1e-5);
962        for v in out {
963            assert!((v - 0.25).abs() < 1e-4);
964        }
965    }
966
967    #[test]
968    fn matmul_identity_returns_input() {
969        // a = [[1,2],[3,4]], b_t = identity transposed = identity
970        let a = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]);
971        let identity = Tensor::new(vec![1.0, 0.0, 0.0, 1.0], vec![2, 2]);
972        let out = matmul_f32(&a, &identity);
973        assert_eq!(out.data, vec![1.0, 2.0, 3.0, 4.0]);
974    }
975
976    #[test]
977    fn matmul_known_values() {
978        // a = [1, 2, 3] (1x3), b_t = [[1,1,1]] (1x3) => dot = 6
979        let a = Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]);
980        let b_t = Tensor::new(vec![1.0, 1.0, 1.0], vec![1, 3]);
981        let out = matmul_f32(&a, &b_t);
982        assert_eq!(out.shape, vec![1, 1]);
983        assert_eq!(out.data[0], 6.0);
984    }
985
986    #[test]
987    fn matmul_single_row_batch_matches_sequential_dot_products() {
988        // m=1 exercises the dedicated single-token-decode path
989        // (parallelized over n, not m) -- check it against a plain
990        // sequential dot product per output column, not just m=1/n=1.
991        let a = Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]);
992        let b_t = Tensor::new(
993            vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 0.0, 0.0],
994            vec![4, 3],
995        );
996        let out = matmul_f32(&a, &b_t);
997        assert_eq!(out.shape, vec![1, 4]);
998        assert_eq!(out.data, vec![1.0, 2.0, 6.0, 2.0]);
999    }
1000
1001    #[test]
1002    fn rms_norm_unit_weight_preserves_direction() {
1003        let x = vec![3.0, 4.0];
1004        let w = vec![1.0, 1.0];
1005        let out = rms_norm(&x, &w, 1e-6);
1006        // ratio between components should be preserved
1007        assert!((out[0] / out[1] - 3.0 / 4.0).abs() < 1e-4);
1008    }
1009
1010    #[test]
1011    fn silu_is_zero_at_zero_and_monotonic_ish() {
1012        assert!((silu(0.0)).abs() < 1e-6);
1013        assert!(silu(5.0) > silu(1.0));
1014    }
1015
1016    // Golden values independently computed in Python from the same
1017    // formula transcribed from Kimi K3's real `SituAndMul` source
1018    // (`beta*tanh(gate/beta)*sigmoid(gate) * linear_beta*tanh(up/linear_beta)`),
1019    // using its real config values (`activation_situ_beta`=4.0,
1020    // `activation_situ_linear_beta`=25.0).
1021    #[test]
1022    fn situ_and_mul_matches_independent_python_reference() {
1023        let cases = [
1024            (0.0f32, 0.0f32, 0.0f32),
1025            (2.0, -3.0, -4.861_066_3),
1026            (-1.5, 10.0, -2.483_860_7),
1027        ];
1028        for (gate, up, expected) in cases {
1029            let got = situ_and_mul(&[gate], &[up], 4.0, 25.0)[0];
1030            assert!(
1031                (got - expected).abs() < 1e-4,
1032                "situ({gate},{up}): rust={got} python={expected}"
1033            );
1034        }
1035    }
1036}