katgpt_types/simd/mod.rs
1//! SIMD-accelerated linear algebra kernels for inference.
2//!
3//! Provides NEON (aarch64), AVX2 (x86_64), and scalar backends for the hot-path
4//! operations used throughout the crate:
5//! - Dot products, outer-product accumulator, matvec, and matmul variants
6//! - Sparse dot / sparse matmul (gather-based, for active-tokens-only matmul)
7//! - Elementwise ops (scale / add / sum / max / fused-decay / scale-mul)
8//! - Activations (exp / sigmoid / tanh-clamp / reciprocal / fast_sigmoid)
9//! — backed by a 6th-order Cephes polynomial for `exp` (~1 ULP, |x| < 88)
10//! - Argmax (single-pass `(usize, f32)`)
11//! - MaxSim late-interaction scoring
12//! - Ternary bit-plane matvec (multiplication-free, `plasma_path` feature)
13//! - Research primitives (sigmoid margin, retrieval margin, Gram, entropy,
14//! coincidence, sum_sq / sum_abs / dist_sq / fused_sub_acc / fused_scale_acc)
15//!
16//! # Dispatch
17//!
18//! Runtime detection picks the best backend: NEON is mandatory on `aarch64`;
19//! AVX2+FMA is detected via `cpuid` on `x86_64` (cached in an `AtomicBool`);
20//! everything else falls back to the 4-accumulator scalar form.
21//!
22//! # Stability
23//!
24//! Uses `core::arch` intrinsics directly — stable on both `aarch64` and
25//! `x86_64`. No nightly features, no external SIMD crates.
26//!
27//! # Module layout
28//!
29//! This file is the dispatcher surface. Backends live in sibling files:
30//! - `dot` — dot products, outer-product, matvec, matmul
31//! - `sparse` — sparse dot / sparse matmul
32//! - `elementwise` — scale / add / sum / max / fused ops
33//! - `activations` — exp / sigmoid / tanh-clamp / reciprocal / fast_sigmoid
34//! - `argmax` — single-pass argmax
35//! - `maxsim` — ColBERT-style late-interaction scoring
36//! - `ternary` — bit-plane ternary matvec (`plasma_path`)
37//! - `research` — sigmoid margin, retrieval margin, Gram, entropy, norms
38//! - `horizontal` — shared AVX2 horizontal reducers (`pub(super)`)
39
40// Submodule backends. Each file owns its NEON/AVX2/scalar impls verbatim;
41// only `is_avx2_fma_available` (below) and `horizontal::*` are shared.
42mod activations;
43mod argmax;
44/// Binary matvec kernels (`binary_plasma` feature, Issue 145).
45#[cfg(feature = "binary_plasma")]
46pub mod binary;
47mod dot;
48mod elementwise;
49mod horizontal;
50mod maxsim;
51mod research;
52mod sparse;
53mod ternary;
54
55#[cfg(test)]
56mod tests;
57#[cfg(test)]
58mod tests_sense;
59
60// Test-only imports: bring the scalar reference implementations (each
61// `pub(super)` in its backend submodule) into `simd::` scope so the test
62// modules can call them via bare names through `use super::*`. Only the
63// scalars actually referenced by `tests.rs` are imported.
64//
65// NOTE: this block exists because `simd.rs` was recently split into a
66// directory (`simd/`); the test file still references the scalar helpers
67// that used to be in the same module. This is a minimal fix to unblock test
68// compilation; a fuller refactor would move the scalar helpers into a
69// dedicated `simd/scalar_ref.rs` submodule.
70#[cfg(test)]
71use dot::{scalar_dot_f32, scalar_outer_product_acc};
72#[cfg(test)]
73use elementwise::{
74 scalar_add_inplace, scalar_add_into, scalar_add_scalar_inplace, scalar_fused_decay_write,
75 scalar_max_f32, scalar_scale_inplace, scalar_sum_f32,
76};
77#[cfg(test)]
78use research::scalar_l_inf_distance_f32;
79#[cfg(test)]
80use research::scalar_sum_sq_quartic;
81#[cfg(test)]
82use sparse::scalar_sparse_dot_f32;
83
84// Re-export the entire public surface so `crate::simd::*` paths are unchanged
85// after the file → folder split. Existing call sites (e.g. `simd::simd_dot_f32`,
86// `simd::SimdLevel`) continue to resolve without modification.
87pub use activations::{
88 cephes_exp_scalar, fast_exp, fast_sigmoid, fast_tanh, simd_exp_inplace, simd_exp_sum_inplace,
89 simd_reciprocal_inplace, simd_sigmoid_inplace, simd_sigmoid_tanh_clamp_inplace,
90 simd_tanh_inplace,
91};
92pub use argmax::simd_argmax_f32;
93pub use dot::{
94 simd_dot_f16_f16, simd_dot_f16_f32, simd_dot_f32, simd_fma_row, simd_matmul_f16_f16_rows,
95 simd_matmul_f16_f16_rows_parallel, simd_matmul_f16_f32_rows, simd_matmul_f16_f32_rows_parallel,
96 simd_matmul_relu_rows, simd_matmul_rows, simd_matmul_rows_parallel, simd_matvec,
97 simd_outer_product_acc, simd_outer_product_acc_scaled,
98};
99pub use elementwise::{
100 simd_add_inplace, simd_add_into, simd_add_scalar_inplace, simd_fused_decay_write,
101 simd_fused_sub_scale_inplace, simd_masked_sum_count_f32, simd_max_f32, simd_scale_inplace,
102 simd_scale_mul_inplace, simd_sum_f32,
103};
104// Feature-gated re-exports — mirror the `#[cfg(feature = "...")]` gates on
105// the underlying items so `cargo check --no-default-features` stays green.
106#[cfg(feature = "binary_plasma")]
107pub use binary::{binary_matvec_scalar, simd_binary_matmul_batch, simd_binary_matvec};
108#[cfg(feature = "maxsim")]
109pub use maxsim::{maxsim_score, maxsim_score_packed};
110pub use research::{
111 coincidence_score, entropy_f32, simd_dist_sq, simd_fused_scale_acc, simd_fused_scale_acc_f16,
112 simd_fused_sub_acc, simd_gram_f32, simd_l_inf_distance_f32, simd_sum_abs_f32, simd_sum_sq,
113 simd_sum_sq_quartic,
114};
115#[cfg(feature = "sigmoid_margin")]
116pub use research::{compute_retrieval_margin, dim_sufficiency_bound, sigmoid_margin_loss};
117pub use sparse::{simd_sparse_dot_f32, simd_sparse_matmul_rows};
118pub use ternary::simd_ternary_dot_f32;
119#[cfg(feature = "plasma_path")]
120pub use ternary::{
121 project_ternary_simd, project_ternary_simd_scalar, simd_ternary_matmul_batch,
122 simd_ternary_matvec, ternary_matvec_scalar,
123};
124// WASM SIMD128 SWAR kernel — only available on `wasm32 +simd128`. Exported so
125// callers can invoke the specialized path directly (e.g. benches that want to
126// measure the SIMD speedup vs the scalar reference). On other targets this
127// symbol does not exist and the re-export is omitted.
128#[cfg(all(
129 feature = "plasma_path",
130 target_arch = "wasm32",
131 target_feature = "simd128"
132))]
133pub use ternary::project_ternary_simd_wasm32;
134
135/// SIMD capability level detected at runtime.
136#[repr(u8)]
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum SimdLevel {
139 /// No SIMD — scalar fallback.
140 Scalar,
141 /// ARM NEON (4× f32 per operation).
142 Neon,
143 /// x86 AVX2+FMA (8× f32 per operation).
144 Avx2,
145 /// WASM SIMD128 (4× f32 per operation) — compile-time gated by `target_feature = "simd128"`.
146 WasmSimd128,
147}
148
149/// Detect the best available SIMD level for the current CPU.
150///
151/// On `aarch64`: always returns [`SimdLevel::Neon`] (mandatory on ARMv8+).
152/// On `x86_64`: returns [`SimdLevel::Avx2`] if CPU supports AVX2+FMA, else [`SimdLevel::Scalar`].
153/// On `wasm32` with `+simd128`: returns [`SimdLevel::WasmSimd128`] (compile-time feature gate).
154/// On other architectures: returns [`SimdLevel::Scalar`].
155#[inline]
156pub fn simd_level() -> SimdLevel {
157 #[cfg(target_arch = "aarch64")]
158 {
159 SimdLevel::Neon
160 }
161 #[cfg(target_arch = "x86_64")]
162 {
163 if is_avx2_fma_available() {
164 SimdLevel::Avx2
165 } else {
166 SimdLevel::Scalar
167 }
168 }
169 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
170 {
171 SimdLevel::WasmSimd128
172 }
173 #[cfg(not(any(
174 target_arch = "aarch64",
175 target_arch = "x86_64",
176 all(target_arch = "wasm32", target_feature = "simd128")
177 )))]
178 {
179 SimdLevel::Scalar
180 }
181}
182
183// ── x86_64 Runtime Detection ─────────────────────────────────
184
185/// Detect AVX2+FMA support on x86_64. Cached after first call.
186///
187/// `pub(super)` — every dispatcher in `dot`/`elementwise`/`activations`/etc.
188/// calls this to pick between the AVX2 and scalar paths.
189#[cfg(target_arch = "x86_64")]
190pub(super) fn is_avx2_fma_available() -> bool {
191 #[cfg(target_feature = "avx2")]
192 {
193 true
194 }
195 #[cfg(not(target_feature = "avx2"))]
196 {
197 use std::sync::atomic::{AtomicBool, Ordering};
198 static CACHED: AtomicBool = AtomicBool::new(false);
199 static INIT: std::sync::Once = std::sync::Once::new();
200 // `__cpuid` became safe in Rust 1.93 (returns a Copy struct, no memory
201 // dereference). The `unsafe` wrappers remain for older Rust; this
202 // block-level allow silences the unnecessary-unsafe-block lint under
203 // `-D warnings` on 1.93+ without affecting the inner statements.
204 #[allow(unused_unsafe)]
205 INIT.call_once(|| {
206 let cpuid1 = unsafe { core::arch::x86_64::__cpuid(1) };
207 let has_avx = (cpuid1.ecx & (1 << 28)) != 0;
208 let has_fma = (cpuid1.ecx & (1 << 12)) != 0;
209 let cpuid7 = unsafe { core::arch::x86_64::__cpuid(7) };
210 let has_avx2 = (cpuid7.ebx & (1 << 5)) != 0;
211 CACHED.store(has_avx && has_fma && has_avx2, Ordering::Relaxed);
212 });
213 CACHED.load(Ordering::Relaxed)
214 }
215}