Skip to main content

katgpt_types/
math.rs

1//! SIMD-accelerated math primitives.
2
3use super::*;
4
5// ---------------------------------------------------------------------------
6// Math utilities — SIMD-accelerated
7// ---------------------------------------------------------------------------
8
9/// In-place softmax. Handles empty slices gracefully.
10/// Three-pass: find max → shift+exp+sum (fused) → normalize.
11#[inline(always)]
12pub fn softmax(x: &mut [f32]) {
13    if x.is_empty() {
14        return;
15    }
16
17    // Pass 1: find max for numerical stability (SIMD-accelerated)
18    let max_val = crate::simd::simd_max_f32(x);
19
20    // Pass 2: subtract max (SIMD-accelerated)
21    crate::simd::simd_add_scalar_inplace(x, -max_val);
22
23    // Pass 3: SIMD exp + sum (fused — saves one full buffer traversal vs separate exp+sum)
24    let sum: f32 = crate::simd::simd_exp_sum_inplace(x);
25    let inv_sum = 1.0 / sum;
26    crate::simd::simd_scale_inplace(x, inv_sum);
27}
28
29/// In-place softmax with temperature scaling: `softmax(x / temperature)`.
30///
31/// Fuses the temperature division into the exp computation, saving one full pass
32/// vs separate `for p /= temp; softmax(x)`.
33///
34/// `inv_temp` should be `1.0 / temperature` — compute once, pass to every call.
35#[inline(always)]
36pub fn softmax_scaled(x: &mut [f32], inv_temp: f32) {
37    if x.is_empty() {
38        return;
39    }
40
41    // Pass 1: find max for numerical stability (SIMD-accelerated)
42    let max_val = crate::simd::simd_max_f32(x);
43
44    // Pass 2: shift and apply temperature in one fused SIMD pass
45    crate::simd::simd_fused_sub_scale_inplace(x, max_val, inv_temp);
46
47    // Pass 3: SIMD exp + sum (fused — saves one full buffer traversal vs separate exp+sum)
48    let sum: f32 = crate::simd::simd_exp_sum_inplace(x);
49    let inv_sum = 1.0 / sum;
50    crate::simd::simd_scale_inplace(x, inv_sum);
51}
52
53/// In-place RMSNorm (no learnable gain).
54/// Two-pass: compute sum-of-squares, then scale.
55#[inline(always)]
56pub fn rmsnorm(x: &mut [f32]) {
57    if x.is_empty() {
58        return;
59    }
60
61    // Pass 1: sum of squares (SIMD-accelerated)
62    let sum_sq = crate::simd::simd_sum_sq(x, x.len());
63
64    // Pass 2: scale — stay f32 throughout to avoid f64 round-trip
65    let inv_rms = 1.0 / (sum_sq / x.len() as f32 + 1e-5f32).sqrt();
66    crate::simd::simd_scale_inplace(x, inv_rms);
67}
68
69/// GeGLU activation: hidden = gelu(gate) * up (elementwise).
70/// Uses approximate GELU: gelu(x) ≈ x * sigmoid(1.702 * x).
71/// `gate` and `up` are `[mlp_hidden]`, output goes to `hidden`.
72///
73/// SIMD-accelerated: exp() computed via `simd_exp_inplace` on stack buffers.
74#[inline(always)]
75pub fn gegelu(hidden: &mut [f32], gate: &[f32], up: &[f32]) {
76    const CHUNK: usize = 64;
77    let mut buf = [0.0f32; CHUNK];
78
79    let mut i = 0;
80    while i + CHUNK <= hidden.len() {
81        // buf[j] = -1.702 * gate[j] via fused SIMD copy+scale (single pass)
82        crate::simd::simd_fused_decay_write(&mut buf, 0.0, &gate[i..i + CHUNK], -1.702);
83        // buf[j] = exp(-1.702 * gate[j]) via SIMD
84        crate::simd::simd_exp_inplace(&mut buf);
85        // hidden[j] = gate[j] * sigmoid(1.702 * gate[j]) * up[j]
86        // SIMD: buf = 1 + buf, then buf = 1/buf (sigmoid), then fused gate*sigmoid*up
87        crate::simd::simd_add_scalar_inplace(&mut buf, 1.0);
88        // Vectorized reciprocal: buf = sigmoid = 1/(1+exp(-1.702*gate))
89        crate::simd::simd_reciprocal_inplace(&mut buf);
90        // Fused: hidden = gate * up, then scale-multiply by sigmoid
91        for j in 0..CHUNK {
92            hidden[i + j] = gate[i + j] * up[i + j];
93        }
94        crate::simd::simd_scale_mul_inplace(&mut hidden[i..i + CHUNK], &buf, 1.0);
95        i += CHUNK;
96    }
97    // Scalar remainder
98    for i in i..hidden.len() {
99        let g = gate[i];
100        let sigmoid = 1.0 / (1.0 + (-1.702 * g).exp());
101        hidden[i] = g * sigmoid * up[i];
102    }
103}
104
105/// GeGLU with tanh GELU approximation (Gemma 2 activation).
106/// tanh GELU: 0.5 * x * (1 + tanh(sqrt(2/π) * (x + 0.044715 * x³)))
107/// `hidden[i] = gelu_tanh(gate[i]) * up[i]`
108///
109/// SIMD-accelerated: exp() for tanh approximation computed via `simd_exp_inplace`.
110#[inline(always)]
111pub fn gegelu_tanh(hidden: &mut [f32], gate: &[f32], up: &[f32]) {
112    const CHUNK: usize = 64;
113    const SQRT_2_OVER_PI: f32 = 0.797_884_6; // (2.0f32 / π).sqrt()
114    const SCALE_2: f32 = 1.595_769_2; // 2.0 * SQRT_2_OVER_PI
115    let mut buf = [0.0f32; CHUNK];
116    let mut buf2 = [0.0f32; CHUNK];
117
118    let mut i = 0;
119    while i + CHUNK <= hidden.len() {
120        // buf[j] = 0.044715 * g via fused SIMD copy+scale (single pass)
121        crate::simd::simd_fused_decay_write(&mut buf, 0.0, &gate[i..i + CHUNK], 0.044715);
122        crate::simd::simd_scale_mul_inplace(&mut buf, &gate[i..i + CHUNK], 1.0); // buf = 0.044715 * g²
123        // Finish cubic via SIMD: buf = 1 + 0.044715*g², then buf = scale_2 * g * (1 + 0.044715*g²)
124        crate::simd::simd_add_scalar_inplace(&mut buf, 1.0);
125        crate::simd::simd_scale_mul_inplace(&mut buf, &gate[i..i + CHUNK], SCALE_2);
126        // buf[j] = exp(2*inner[j]) via SIMD
127        crate::simd::simd_exp_inplace(&mut buf);
128        // hidden[j] = g * exp(2x) / (exp(2x) + 1) * up[j]
129        // Compute denominator (exp + 1) via SIMD, then SIMD tanh + fused mul
130        buf2[..CHUNK].copy_from_slice(&buf);
131        crate::simd::simd_add_scalar_inplace(&mut buf2, 1.0); // buf2 = exp + 1
132        // hidden = gate * up, then hidden *= tanh(inner)
133        for j in 0..CHUNK {
134            // Branch-free tanh: exp(2x) / (exp(2x) + 1) via division
135            buf[j] /= buf2[j];
136            hidden[i + j] = gate[i + j] * up[i + j];
137        }
138        crate::simd::simd_scale_mul_inplace(&mut hidden[i..i + CHUNK], &buf, 1.0);
139        i += CHUNK;
140    }
141    // Scalar remainder
142    for i in i..hidden.len() {
143        let g = gate[i];
144        let inner = SQRT_2_OVER_PI * (g + 0.044715 * g * g * g);
145        let gelu_val = 0.5 * g * (1.0 + crate::simd::fast_tanh(inner));
146        hidden[i] = gelu_val * up[i];
147    }
148}
149
150/// SiLU (Sigmoid Linear Unit) activation: x * sigmoid(x).
151/// Used in LLaMA, Mistral, and other LLaMA-family models for SwiGLU MLP.
152///
153/// SIMD-accelerated: exp() computed via `simd_exp_inplace` on stack buffers.
154#[inline(always)]
155pub fn silu(x: &mut [f32]) {
156    const CHUNK: usize = 64;
157    let mut buf = [0.0f32; CHUNK];
158
159    let mut i = 0;
160    while i + CHUNK <= x.len() {
161        // buf[j] = -x[j] via fused SIMD copy+scale (single pass)
162        crate::simd::simd_fused_decay_write(&mut buf, 0.0, &x[i..i + CHUNK], -1.0);
163        // buf[j] = exp(-x[j]) via SIMD
164        crate::simd::simd_exp_inplace(&mut buf);
165        // x[j] = x[j] / (1 + exp(-x[j]))
166        // SIMD: buf = 1 + exp(-x), then buf = 1/buf, then x *= buf elementwise
167        crate::simd::simd_add_scalar_inplace(&mut buf, 1.0);
168        crate::simd::simd_reciprocal_inplace(&mut buf);
169        crate::simd::simd_scale_mul_inplace(&mut x[i..i + CHUNK], &buf, 1.0);
170        i += CHUNK;
171    }
172    // Scalar remainder
173    for v in x[i..].iter_mut() {
174        *v = *v / (1.0 + (-*v).exp());
175    }
176}
177
178/// SwiGLU activation: SiLU(gate) * up.
179/// Used in LLaMA-family models (gate_proj and up_proj are separate weights).
180/// Result stored in `hidden`: `hidden[i] = silu(gate[i]) * up[i]`
181///
182/// SIMD-accelerated: exp() computed via `simd_exp_inplace` on stack buffers.
183#[inline(always)]
184pub fn swiglu(hidden: &mut [f32], gate: &[f32], up: &[f32]) {
185    const CHUNK: usize = 64;
186    let mut buf = [0.0f32; CHUNK];
187
188    let mut i = 0;
189    while i + CHUNK <= hidden.len() {
190        // buf[j] = -gate[j] via fused SIMD copy+scale (single pass)
191        crate::simd::simd_fused_decay_write(&mut buf, 0.0, &gate[i..i + CHUNK], -1.0);
192        // buf[j] = exp(-gate[j]) via SIMD
193        crate::simd::simd_exp_inplace(&mut buf);
194        // hidden[j] = gate[j] / (1 + exp(-gate[j])) * up[j]
195        // SIMD: buf = 1 + exp(-gate), then vectorized reciprocal + gate*up
196        crate::simd::simd_add_scalar_inplace(&mut buf, 1.0);
197        // Vectorized reciprocal: buf = sigmoid = 1/(1+exp(-gate))
198        crate::simd::simd_reciprocal_inplace(&mut buf);
199        // Fused: hidden = gate * up, then scale-multiply by sigmoid
200        for j in 0..CHUNK {
201            hidden[i + j] = gate[i + j] * up[i + j];
202        }
203        crate::simd::simd_scale_mul_inplace(&mut hidden[i..i + CHUNK], &buf, 1.0);
204        i += CHUNK;
205    }
206    // Scalar remainder
207    for i in i..hidden.len() {
208        let g = gate[i];
209        hidden[i] = g / (1.0 + (-g).exp()) * up[i];
210    }
211}
212
213/// SwiGLU in-place: `hidden[j] = silu(hidden[j]) * up[j]`.
214///
215/// The [`swiglu`] implementation copies `gate` into a local SIMD buffer before
216/// writing to `hidden`, so it is safe for `hidden` to alias the gate input.
217/// This wrapper provides that aliasing without `unsafe` at call sites.
218#[inline(always)]
219pub fn swiglu_inplace(hidden: &mut [f32], up: &[f32]) {
220    let gate = unsafe { core::slice::from_raw_parts(hidden.as_ptr(), hidden.len()) };
221    swiglu(hidden, gate, up);
222}
223
224/// RMSNorm with learnable gamma (gain) vector.
225/// Gemma 2 stores gamma as (gamma-1), so +1 is added during load.
226/// `x` is normalized in-place then scaled by `gamma[i]`:
227///   `x[i] = gamma[i] * x[i] / sqrt(mean_sq + eps)`
228#[inline(always)]
229pub fn rmsnorm_with_gamma(x: &mut [f32], gamma: &[f32]) {
230    rmsnorm_with_gamma_eps(x, gamma, 1e-5)
231}
232
233/// RMSNorm with learnable gamma and configurable epsilon.
234#[inline(always)]
235pub fn rmsnorm_with_gamma_eps(x: &mut [f32], gamma: &[f32], eps: f64) {
236    let n = x.len();
237    if n == 0 {
238        return;
239    }
240    let sum_sq = crate::simd::simd_sum_sq(x, n);
241    // Cast eps to f32 once — the f64 param is kept for API compat
242    let inv_rms = 1.0 / (sum_sq / n as f32 + eps as f32).sqrt();
243    crate::simd::simd_scale_mul_inplace(x, gamma, inv_rms);
244}
245
246/// Matrix-vector multiply: output = weight @ input.
247/// Weight layout: [rows, cols] row-major.
248#[inline(always)]
249pub fn matmul(output: &mut [f32], weight: &[f32], input: &[f32], rows: usize, cols: usize) {
250    crate::simd::simd_matmul_rows(output, weight, input, rows, cols);
251}
252
253/// Row-parallel matrix-vector multiply for large weight matrices (Plan 096).
254///
255/// Splits output rows across rayon threads. Use for large matmuls where
256/// row count >> core count (e.g., `down_proj` 2304×9216, `lm_head` 256K×2304).
257/// Falls back to sequential [`matmul`] for small matrices (rows < 512).
258#[inline(always)]
259pub fn matmul_parallel(
260    output: &mut [f32],
261    weight: &[f32],
262    input: &[f32],
263    rows: usize,
264    cols: usize,
265) {
266    crate::simd::simd_matmul_rows_parallel(output, weight, input, rows, cols);
267}
268
269/// Fused matrix-vector multiply + ReLU: output = max(0, weight @ input).
270/// Saves one full buffer scan vs separate matmul + ReLU.
271/// Used for MLP hidden layer where activation immediately follows projection.
272#[inline(always)]
273pub fn matmul_relu(output: &mut [f32], weight: &[f32], input: &[f32], rows: usize, cols: usize) {
274    crate::simd::simd_matmul_relu_rows(output, weight, input, rows, cols);
275}
276
277/// Matrix-vector multiply with f16 weights: output = f16_weight @ f32_input.
278/// Weight layout: [rows, cols] row-major, stored as `half::f16`.
279///
280/// Converts f16 weights to f32 on-the-fly during dot product.
281/// Halves memory bandwidth for weight reads vs f32 storage.
282#[inline(always)]
283pub fn matmul_f16(
284    output: &mut [f32],
285    weight: &[half::f16],
286    input: &[f32],
287    rows: usize,
288    cols: usize,
289) {
290    crate::simd::simd_matmul_f16_f32_rows(output, weight, input, rows, cols);
291}
292
293/// Row-parallel f16×f32 matrix-vector multiply for large weight matrices (Plan 096).
294///
295/// Splits output rows across rayon threads. Use for large f16 matmuls where
296/// row count >> core count (e.g., `down_proj` 2304×9216, `lm_head` 256K×2304).
297/// Falls back to sequential [`matmul_f16`] for small matrices (rows < 512).
298#[inline(always)]
299pub fn matmul_f16_parallel(
300    output: &mut [f32],
301    weight: &[half::f16],
302    input: &[f32],
303    rows: usize,
304    cols: usize,
305) {
306    crate::simd::simd_matmul_f16_f32_rows_parallel(output, weight, input, rows, cols);
307}
308
309/// Matrix-vector multiply with f16 weights and f16 activations (Issue 201).
310///
311/// Output is f32. Uses the ARMv8.2-A widening FMA (`fmlalb`/`fmlalt`) which
312/// does f16×f16→f32 in a single instruction — no explicit FCVT on the critical
313/// path. Halves bandwidth for BOTH weight AND activation reads vs f32×f32
314/// (genuine 50% reduction, vs the 25% that doomed weight-only f16 in Issue 200).
315#[inline(always)]
316pub fn matmul_f16_f16(
317    output: &mut [f32],
318    weight: &[half::f16],
319    input: &[half::f16],
320    rows: usize,
321    cols: usize,
322) {
323    crate::simd::simd_matmul_f16_f16_rows(output, weight, input, rows, cols);
324}
325
326/// Row-parallel f16×f16 matrix-vector multiply (Issue 201).
327///
328/// Splits output rows across rayon threads. Falls back to sequential
329/// [`matmul_f16_f16`] for small matrices (rows < 512).
330#[inline(always)]
331pub fn matmul_f16_f16_parallel(
332    output: &mut [f32],
333    weight: &[half::f16],
334    input: &[half::f16],
335    rows: usize,
336    cols: usize,
337) {
338    crate::simd::simd_matmul_f16_f16_rows_parallel(output, weight, input, rows, cols);
339}
340
341/// Sparse matrix-vector multiply for ReLU-activated inputs (TwELL-inspired).
342///
343/// Only processes columns where `input[c] > 0.0`, skipping dead neurons entirely.
344/// Exploits the natural sparsity of ReLU activations in MLP layers where 95-99%
345/// of hidden neurons are exactly zero after training with L1 regularization.
346///
347/// Distilled from "Sparser, Faster, Lighter Transformer Language Models"
348/// (arXiv:2603.23198) by Sakana AI & NVIDIA.
349///
350/// Two-phase execution:
351/// 1. Dynamic Packing: scan input, store non-zero indices & values into pre-allocated buffers
352/// 2. Sparse Multiply: only iterate weights at alive column indices
353///
354/// Returns the number of alive (non-zero) neurons for diagnostics/threshold checks.
355/// Buffers `active_indices` and `active_values` must be pre-allocated to at least `cols` capacity.
356#[cfg(feature = "sparse_mlp")]
357#[inline(always)]
358pub fn sparse_matmul(
359    output: &mut [f32],
360    weight: &[f32],
361    input: &[f32],
362    rows: usize,
363    cols: usize,
364    active_indices: &mut [usize],
365    active_values: &mut [f32],
366) -> usize {
367    // Phase 1: Pack alive neurons (software TwELL formulation)
368    // Branch-predicted: with 95-99% sparsity, the branch is predicted correctly
369    // most of the time (~1 cycle), and we avoid the wasted store to active_values
370    // for dead neurons that the branch-free version always performed.
371    let mut alive = 0;
372    for c in 0..cols {
373        let val = unsafe { *input.get_unchecked(c) };
374        if val > 0.0 {
375            unsafe {
376                *active_indices.get_unchecked_mut(alive) = c;
377                *active_values.get_unchecked_mut(alive) = val;
378            }
379            alive += 1;
380        }
381    }
382
383    // Phase 2: Sparse multiply — SIMD-accelerated (Plan 060 T5)
384    // NEON gathers 4 elements/iter, AVX2 gathers 8 elements/iter via hardware gather.
385    // Scalar fallback for alive ≤ 4 (gather overhead exceeds benefit).
386    crate::simd::simd_sparse_matmul_rows(
387        output,
388        weight,
389        active_indices,
390        active_values,
391        rows,
392        cols,
393        alive,
394    );
395
396    alive
397}
398
399/// Sample a token index from a probability distribution.
400///
401/// Builds a prefix-sum (CDF) then uses binary search for O(log V) lookup
402/// instead of the O(V/2) average of a linear scan.
403///
404/// **Allocates a CDF buffer on every call.** For the hot decode loop, prefer
405/// [`sample_token_into`] which reuses a pre-allocated buffer.
406#[deprecated(
407    since = "0.1.0",
408    note = "allocates a vocab-sized Vec per call; use `sample_token_into` on hot paths"
409)]
410pub fn sample_token(probs: &[f32], rng: &mut Rng) -> usize {
411    // Redraw on exactly 0.0: `rng.uniform()` can return 0.0 (notably the first draw
412    // for low-entropy seeds), which deterministically maps to the first nonzero-mass
413    // token via the left boundary of the inverse-CDF map. `partition_point(c <= r)`
414    // below already implements the strict comparison, so this guard only fixes the
415    // degenerate-zero draw without changing the comparison semantics.
416    let mut r = rng.uniform();
417    while r == 0.0 {
418        r = rng.uniform();
419    }
420    let n = probs.len();
421    if n == 0 {
422        return 0;
423    }
424
425    // Build cumulative sum array — pre-allocated, direct write avoids per-push bounds check
426    let mut cdf = vec![0.0f32; n];
427    let mut sum = 0.0f32;
428    for (i, &p) in probs.iter().enumerate() {
429        sum += p;
430        // SAFETY: cdf has length n, i < n by enumeration
431        unsafe {
432            *cdf.get_unchecked_mut(i) = sum;
433        }
434    }
435
436    // partition_point: first index where cdf[i] > r — monotonically increasing
437    let idx = cdf[..n].partition_point(|&c| c <= r);
438    idx.min(n - 1)
439}
440
441/// Zero-alloc variant of [`sample_token`] that reuses a pre-allocated CDF buffer.
442///
443/// Pass a `cdf` buffer (e.g. `ForwardContext::cdf`) to avoid a ~vocab_size allocation
444/// on every token decode. The buffer is cleared and refilled each call.
445pub fn sample_token_into(probs: &[f32], rng: &mut Rng, cdf: &mut Vec<f32>) -> usize {
446    // See `sample_token`: redraw on exactly 0.0 to avoid the degenerate left-boundary draw.
447    let mut r = rng.uniform();
448    while r == 0.0 {
449        r = rng.uniform();
450    }
451    let n = probs.len();
452    if n == 0 {
453        return 0;
454    }
455    cdf.resize(n, 0.0);
456    let mut sum = 0.0f32;
457    for (i, &p) in probs.iter().enumerate() {
458        sum += p;
459        unsafe {
460            *cdf.get_unchecked_mut(i) = sum;
461        }
462    }
463    // partition_point: first index where cdf[i] > r — monotonically increasing
464    // so this is equivalent to binary_search_by with Less/Greater but avoids
465    // closure overhead and is branch-predictor friendly.
466    let idx = cdf[..n].partition_point(|&c| c <= r);
467    idx.min(n - 1)
468}