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}