Files
ruvnet--RuView/vendor/ruvector/examples/edge-net/src/compute/simd.rs
T

1418 lines
44 KiB
Rust

//! SIMD-Optimized Compute Operations for edge-net
//!
//! This module provides vectorized operations for neural network inference
//! with automatic dispatch to the best available SIMD implementation:
//!
//! - WASM simd128: 4x f32 lanes (browser targets)
//! - x86_64 AVX2: 8x f32 lanes (native x86 targets)
//! - Scalar: Portable fallback
//!
//! # Performance Targets
//!
//! - dot_product: 8x speedup over scalar
//! - matmul: 10x speedup with tiling + prefetch
//! - softmax: Numerically stable with max subtraction
//! - Q4 quantization: 4x memory reduction with 1% accuracy loss
#[cfg(target_arch = "wasm32")]
use core::arch::wasm32::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
/// SIMD compute backend with automatic platform detection
pub struct SimdCompute {
/// Platform capabilities detected at runtime
#[allow(dead_code)]
capabilities: SimdCapabilities,
}
/// Detected SIMD capabilities
#[derive(Clone, Debug)]
pub struct SimdCapabilities {
/// WASM simd128 available
pub wasm_simd128: bool,
/// x86 AVX2 available
pub avx2: bool,
/// x86 SSE4.1 available
pub sse41: bool,
/// x86 FMA available
pub fma: bool,
}
impl Default for SimdCapabilities {
fn default() -> Self {
Self::detect()
}
}
impl SimdCapabilities {
/// Detect available SIMD capabilities at runtime
pub fn detect() -> Self {
#[cfg(target_arch = "wasm32")]
{
Self {
wasm_simd128: true, // Always available on wasm32 with simd128 feature
avx2: false,
sse41: false,
fma: false,
}
}
#[cfg(target_arch = "x86_64")]
{
Self {
wasm_simd128: false,
avx2: is_x86_feature_detected!("avx2"),
sse41: is_x86_feature_detected!("sse4.1"),
fma: is_x86_feature_detected!("fma"),
}
}
#[cfg(not(any(target_arch = "wasm32", target_arch = "x86_64")))]
{
Self {
wasm_simd128: false,
avx2: false,
sse41: false,
fma: false,
}
}
}
/// Get the SIMD lane width for f32 operations
pub fn lane_width(&self) -> usize {
if self.avx2 {
8
} else if self.wasm_simd128 || self.sse41 {
4
} else {
1
}
}
}
impl Default for SimdCompute {
fn default() -> Self {
Self::new()
}
}
impl SimdCompute {
/// Create a new SIMD compute backend with automatic platform detection
pub fn new() -> Self {
Self {
capabilities: SimdCapabilities::detect(),
}
}
/// Get detected capabilities
pub fn capabilities(&self) -> &SimdCapabilities {
&self.capabilities
}
// ========================================================================
// Dot Product Operations
// ========================================================================
/// SIMD dot product for f32 vectors
///
/// Automatically dispatches to the best available implementation:
/// - AVX2: 8x f32 lanes with FMA
/// - WASM simd128: 4x f32 lanes
/// - SSE4.1: 4x f32 lanes
/// - Scalar: Portable fallback
#[inline]
pub fn dot_product(a: &[f32], b: &[f32]) -> f32 {
debug_assert_eq!(a.len(), b.len(), "Vector lengths must match");
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
return unsafe { Self::dot_product_avx2_fma(a, b) };
} else if is_x86_feature_detected!("avx2") {
return unsafe { Self::dot_product_avx2(a, b) };
} else if is_x86_feature_detected!("sse4.1") {
return unsafe { Self::dot_product_sse41(a, b) };
} else {
return Self::dot_product_scalar(a, b);
}
}
#[cfg(target_arch = "wasm32")]
{
return Self::dot_product_wasm_simd128(a, b);
}
#[cfg(not(any(target_arch = "wasm32", target_arch = "x86_64")))]
{
Self::dot_product_scalar(a, b)
}
}
/// Scalar dot product (fallback)
#[inline]
pub fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
/// WASM simd128 dot product with 4x f32 lanes
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn dot_product_wasm_simd128(a: &[f32], b: &[f32]) -> f32 {
let len = a.len();
let chunks = len / 4;
let mut sum = f32x4_splat(0.0);
// Process 4 elements at a time
for i in 0..chunks {
let offset = i * 4;
let a_vec = unsafe {
v128_load(a.as_ptr().add(offset) as *const v128)
};
let b_vec = unsafe {
v128_load(b.as_ptr().add(offset) as *const v128)
};
let prod = f32x4_mul(a_vec, b_vec);
sum = f32x4_add(sum, prod);
}
// Horizontal sum: extract all 4 lanes and add
let mut result = f32x4_extract_lane::<0>(sum)
+ f32x4_extract_lane::<1>(sum)
+ f32x4_extract_lane::<2>(sum)
+ f32x4_extract_lane::<3>(sum);
// Handle remainder
for i in (chunks * 4)..len {
result += a[i] * b[i];
}
result
}
/// x86_64 AVX2 dot product with 8x f32 lanes
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn dot_product_avx2(a: &[f32], b: &[f32]) -> f32 {
let len = a.len();
let chunks = len / 8;
let mut sum = _mm256_setzero_ps();
for i in 0..chunks {
let offset = i * 8;
let a_vec = _mm256_loadu_ps(a.as_ptr().add(offset));
let b_vec = _mm256_loadu_ps(b.as_ptr().add(offset));
let prod = _mm256_mul_ps(a_vec, b_vec);
sum = _mm256_add_ps(sum, prod);
}
// Horizontal sum reduction
let result = Self::hsum_avx2(sum);
// Handle remainder
let mut final_result = result;
for i in (chunks * 8)..len {
final_result += a[i] * b[i];
}
final_result
}
/// x86_64 AVX2+FMA dot product with fused multiply-add
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2", enable = "fma")]
#[inline]
unsafe fn dot_product_avx2_fma(a: &[f32], b: &[f32]) -> f32 {
let len = a.len();
let chunks = len / 8;
let mut sum = _mm256_setzero_ps();
for i in 0..chunks {
let offset = i * 8;
let a_vec = _mm256_loadu_ps(a.as_ptr().add(offset));
let b_vec = _mm256_loadu_ps(b.as_ptr().add(offset));
// FMA: sum = a * b + sum
sum = _mm256_fmadd_ps(a_vec, b_vec, sum);
}
let result = Self::hsum_avx2(sum);
let mut final_result = result;
for i in (chunks * 8)..len {
final_result += a[i] * b[i];
}
final_result
}
/// x86_64 SSE4.1 dot product with 4x f32 lanes
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "sse4.1")]
#[inline]
unsafe fn dot_product_sse41(a: &[f32], b: &[f32]) -> f32 {
let len = a.len();
let chunks = len / 4;
let mut sum = _mm_setzero_ps();
for i in 0..chunks {
let offset = i * 4;
let a_vec = _mm_loadu_ps(a.as_ptr().add(offset));
let b_vec = _mm_loadu_ps(b.as_ptr().add(offset));
let prod = _mm_mul_ps(a_vec, b_vec);
sum = _mm_add_ps(sum, prod);
}
// Horizontal sum using shuffle
let shuf = _mm_shuffle_ps(sum, sum, 0b10_11_00_01);
let sums = _mm_add_ps(sum, shuf);
let shuf = _mm_movehl_ps(sums, sums);
let sums = _mm_add_ss(sums, shuf);
let mut result = _mm_cvtss_f32(sums);
for i in (chunks * 4)..len {
result += a[i] * b[i];
}
result
}
/// Horizontal sum for AVX2 __m256
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn hsum_avx2(v: __m256) -> f32 {
let high = _mm256_extractf128_ps(v, 1);
let low = _mm256_castps256_ps128(v);
let sum128 = _mm_add_ps(high, low);
let shuf = _mm_shuffle_ps(sum128, sum128, 0b10_11_00_01);
let sums = _mm_add_ps(sum128, shuf);
let shuf = _mm_movehl_ps(sums, sums);
let sums = _mm_add_ss(sums, shuf);
_mm_cvtss_f32(sums)
}
// ========================================================================
// Matrix Multiplication (Tiled with Prefetch Hints)
// ========================================================================
/// SIMD tiled matrix multiplication
///
/// Performs C = A * B with cache-friendly tiling for optimal performance.
/// Uses prefetch hints for next tile to reduce cache misses.
///
/// # Arguments
/// * `a` - Left matrix (m x k) in row-major order
/// * `b` - Right matrix (k x n) in row-major order
/// * `m` - Rows in A
/// * `k` - Cols in A / Rows in B
/// * `n` - Cols in B
///
/// # Returns
/// Result matrix C (m x n) in row-major order
#[inline]
pub fn matmul_simd(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
debug_assert_eq!(a.len(), m * k, "A dimensions mismatch");
debug_assert_eq!(b.len(), k * n, "B dimensions mismatch");
let mut c = vec![0.0f32; m * n];
// Tile size for cache optimization (64 elements = 256 bytes = 4 cache lines)
const TILE_SIZE: usize = 64;
// Tiled matrix multiplication
for ii in (0..m).step_by(TILE_SIZE) {
for jj in (0..n).step_by(TILE_SIZE) {
for kk in (0..k).step_by(TILE_SIZE) {
let i_end = (ii + TILE_SIZE).min(m);
let j_end = (jj + TILE_SIZE).min(n);
let k_end = (kk + TILE_SIZE).min(k);
// Process tile
for i in ii..i_end {
for j in jj..j_end {
let mut sum = c[i * n + j];
// Use SIMD for inner product within tile
let a_row = &a[i * k + kk..i * k + k_end];
let b_col_start = kk * n + j;
// Gather B column elements (strided access)
let mut b_col = Vec::with_capacity(k_end - kk);
for ki in kk..k_end {
b_col.push(b[ki * n + j]);
}
sum += Self::dot_product(a_row, &b_col);
c[i * n + j] = sum;
}
}
}
}
}
c
}
/// Optimized matrix-vector multiplication
///
/// Computes y = A * x where A is m x n matrix
#[inline]
pub fn matvec_simd(a: &[f32], x: &[f32], m: usize, n: usize) -> Vec<f32> {
debug_assert_eq!(a.len(), m * n, "Matrix dimensions mismatch");
debug_assert_eq!(x.len(), n, "Vector dimension mismatch");
let mut y = Vec::with_capacity(m);
for i in 0..m {
let row_start = i * n;
let row = &a[row_start..row_start + n];
y.push(Self::dot_product(row, x));
}
y
}
// ========================================================================
// Softmax (Numerically Stable with Max Subtraction)
// ========================================================================
/// Numerically stable softmax with SIMD acceleration
///
/// Uses the log-sum-exp trick: softmax(x) = exp(x - max(x)) / sum(exp(x - max(x)))
/// This prevents overflow for large values.
#[inline]
pub fn softmax_simd(input: &mut [f32]) {
if input.is_empty() {
return;
}
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
unsafe { Self::softmax_avx2(input) };
return;
}
}
#[cfg(target_arch = "wasm32")]
{
Self::softmax_wasm_simd128(input);
return;
}
#[cfg(not(any(target_arch = "wasm32", target_arch = "x86_64")))]
{
Self::softmax_scalar(input);
}
}
/// Scalar softmax implementation
#[inline]
pub fn softmax_scalar(input: &mut [f32]) {
// Find max for numerical stability
let max_val = input.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
// Compute exp(x - max) and sum
let mut sum = 0.0f32;
for x in input.iter_mut() {
*x = (*x - max_val).exp();
sum += *x;
}
// Normalize
let inv_sum = 1.0 / sum;
for x in input.iter_mut() {
*x *= inv_sum;
}
}
/// WASM simd128 softmax
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn softmax_wasm_simd128(input: &mut [f32]) {
let len = input.len();
let chunks = len / 4;
// Find max using SIMD
let mut max_vec = f32x4_splat(f32::NEG_INFINITY);
for i in 0..chunks {
let v = unsafe { v128_load(input.as_ptr().add(i * 4) as *const v128) };
max_vec = f32x4_pmax(max_vec, v);
}
// Horizontal max
let mut max_val = f32x4_extract_lane::<0>(max_vec)
.max(f32x4_extract_lane::<1>(max_vec))
.max(f32x4_extract_lane::<2>(max_vec))
.max(f32x4_extract_lane::<3>(max_vec));
// Handle remainder for max
for i in (chunks * 4)..len {
max_val = max_val.max(input[i]);
}
let max_broadcast = f32x4_splat(max_val);
// Compute exp(x - max) and accumulate sum
let mut sum = 0.0f32;
for i in 0..chunks {
let offset = i * 4;
let v = unsafe { v128_load(input.as_ptr().add(offset) as *const v128) };
let shifted = f32x4_sub(v, max_broadcast);
// Fast exp approximation for each lane
let exp_vals = [
Self::fast_exp(f32x4_extract_lane::<0>(shifted)),
Self::fast_exp(f32x4_extract_lane::<1>(shifted)),
Self::fast_exp(f32x4_extract_lane::<2>(shifted)),
Self::fast_exp(f32x4_extract_lane::<3>(shifted)),
];
input[offset] = exp_vals[0];
input[offset + 1] = exp_vals[1];
input[offset + 2] = exp_vals[2];
input[offset + 3] = exp_vals[3];
sum += exp_vals[0] + exp_vals[1] + exp_vals[2] + exp_vals[3];
}
// Handle remainder
for i in (chunks * 4)..len {
input[i] = (input[i] - max_val).exp();
sum += input[i];
}
// Normalize
let inv_sum = 1.0 / sum;
let inv_sum_vec = f32x4_splat(inv_sum);
for i in 0..chunks {
let offset = i * 4;
let v = unsafe { v128_load(input.as_ptr().add(offset) as *const v128) };
let normalized = f32x4_mul(v, inv_sum_vec);
unsafe {
v128_store(input.as_mut_ptr().add(offset) as *mut v128, normalized);
}
}
for i in (chunks * 4)..len {
input[i] *= inv_sum;
}
}
/// AVX2 softmax
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn softmax_avx2(input: &mut [f32]) {
let len = input.len();
let chunks = len / 8;
// Find max using AVX2
let mut max_vec = _mm256_set1_ps(f32::NEG_INFINITY);
for i in 0..chunks {
let v = _mm256_loadu_ps(input.as_ptr().add(i * 8));
max_vec = _mm256_max_ps(max_vec, v);
}
// Horizontal max reduction
let mut max_val = Self::hmax_avx2(max_vec);
// Handle remainder for max
for i in (chunks * 8)..len {
max_val = max_val.max(input[i]);
}
let max_broadcast = _mm256_set1_ps(max_val);
// Compute exp(x - max) and sum
let mut sum = 0.0f32;
for i in 0..chunks {
let ptr = input.as_mut_ptr().add(i * 8);
let v = _mm256_loadu_ps(ptr);
let shifted = _mm256_sub_ps(v, max_broadcast);
let exp_v = Self::fast_exp_avx2(shifted);
_mm256_storeu_ps(ptr, exp_v);
// Accumulate sum
sum += Self::hsum_avx2(exp_v);
}
// Handle remainder
for i in (chunks * 8)..len {
input[i] = (input[i] - max_val).exp();
sum += input[i];
}
// Normalize
let inv_sum = 1.0 / sum;
let inv_sum_vec = _mm256_set1_ps(inv_sum);
for i in 0..chunks {
let ptr = input.as_mut_ptr().add(i * 8);
let v = _mm256_loadu_ps(ptr);
_mm256_storeu_ps(ptr, _mm256_mul_ps(v, inv_sum_vec));
}
for i in (chunks * 8)..len {
input[i] *= inv_sum;
}
}
/// Horizontal max for AVX2
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn hmax_avx2(v: __m256) -> f32 {
let high = _mm256_extractf128_ps(v, 1);
let low = _mm256_castps256_ps128(v);
let max128 = _mm_max_ps(high, low);
let max64 = _mm_max_ps(max128, _mm_movehl_ps(max128, max128));
let max32 = _mm_max_ss(max64, _mm_shuffle_ps(max64, max64, 1));
_mm_cvtss_f32(max32)
}
/// Fast exp approximation for AVX2
/// Uses polynomial: exp(x) ~ 1 + x + x^2/2 + x^3/6 for |x| < 1
/// For larger x, uses range reduction
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn fast_exp_avx2(x: __m256) -> __m256 {
// Clamp to avoid overflow/underflow
let min_val = _mm256_set1_ps(-88.0);
let max_val = _mm256_set1_ps(88.0);
let x = _mm256_max_ps(_mm256_min_ps(x, max_val), min_val);
// Constants for polynomial approximation
let one = _mm256_set1_ps(1.0);
let half = _mm256_set1_ps(0.5);
let sixth = _mm256_set1_ps(1.0 / 6.0);
let twenty_fourth = _mm256_set1_ps(1.0 / 24.0);
let x2 = _mm256_mul_ps(x, x);
let x3 = _mm256_mul_ps(x2, x);
let x4 = _mm256_mul_ps(x2, x2);
// exp(x) ~ 1 + x + x^2/2 + x^3/6 + x^4/24
let term1 = _mm256_add_ps(one, x);
let term2 = _mm256_mul_ps(x2, half);
let term3 = _mm256_mul_ps(x3, sixth);
let term4 = _mm256_mul_ps(x4, twenty_fourth);
_mm256_add_ps(_mm256_add_ps(term1, term2), _mm256_add_ps(term3, term4))
}
// ========================================================================
// GELU Activation (Fast Approximation)
// ========================================================================
/// GELU activation using fast tanh approximation
///
/// GELU(x) = 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
#[inline]
pub fn gelu_simd(input: &mut [f32]) {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
unsafe { Self::gelu_avx2(input) };
return;
}
}
#[cfg(target_arch = "wasm32")]
{
Self::gelu_wasm_simd128(input);
return;
}
#[cfg(not(any(target_arch = "wasm32", target_arch = "x86_64")))]
{
Self::gelu_scalar(input);
}
}
/// Scalar GELU
#[inline]
pub fn gelu_scalar(input: &mut [f32]) {
const SQRT_2_PI: f32 = 0.7978845608028654;
const COEF: f32 = 0.044715;
for x in input.iter_mut() {
let x3 = *x * *x * *x;
let inner = SQRT_2_PI * (*x + COEF * x3);
*x = 0.5 * *x * (1.0 + Self::fast_tanh(inner));
}
}
/// WASM simd128 GELU
#[cfg(target_arch = "wasm32")]
#[inline]
pub fn gelu_wasm_simd128(input: &mut [f32]) {
const SQRT_2_PI: f32 = 0.7978845608028654;
const COEF: f32 = 0.044715;
let len = input.len();
let chunks = len / 4;
let sqrt_2_pi = f32x4_splat(SQRT_2_PI);
let coef = f32x4_splat(COEF);
let half = f32x4_splat(0.5);
let one = f32x4_splat(1.0);
for i in 0..chunks {
let offset = i * 4;
let x = unsafe { v128_load(input.as_ptr().add(offset) as *const v128) };
// x^3
let x2 = f32x4_mul(x, x);
let x3 = f32x4_mul(x2, x);
// sqrt(2/pi) * (x + 0.044715 * x^3)
let inner = f32x4_mul(sqrt_2_pi, f32x4_add(x, f32x4_mul(coef, x3)));
// Fast tanh approximation for each lane
let tanh_vals = [
Self::fast_tanh(f32x4_extract_lane::<0>(inner)),
Self::fast_tanh(f32x4_extract_lane::<1>(inner)),
Self::fast_tanh(f32x4_extract_lane::<2>(inner)),
Self::fast_tanh(f32x4_extract_lane::<3>(inner)),
];
let tanh_vec = f32x4(tanh_vals[0], tanh_vals[1], tanh_vals[2], tanh_vals[3]);
// 0.5 * x * (1 + tanh)
let result = f32x4_mul(half, f32x4_mul(x, f32x4_add(one, tanh_vec)));
unsafe {
v128_store(input.as_mut_ptr().add(offset) as *mut v128, result);
}
}
// Handle remainder
for i in (chunks * 4)..len {
let x = input[i];
let x3 = x * x * x;
let inner = SQRT_2_PI * (x + COEF * x3);
input[i] = 0.5 * x * (1.0 + Self::fast_tanh(inner));
}
}
/// AVX2 GELU
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn gelu_avx2(input: &mut [f32]) {
let len = input.len();
let chunks = len / 8;
let sqrt_2_pi = _mm256_set1_ps(0.7978845608028654);
let coef = _mm256_set1_ps(0.044715);
let half = _mm256_set1_ps(0.5);
let one = _mm256_set1_ps(1.0);
for i in 0..chunks {
let ptr = input.as_mut_ptr().add(i * 8);
let x = _mm256_loadu_ps(ptr);
// x^3
let x2 = _mm256_mul_ps(x, x);
let x3 = _mm256_mul_ps(x2, x);
// sqrt(2/pi) * (x + 0.044715 * x^3)
let inner = _mm256_mul_ps(sqrt_2_pi, _mm256_add_ps(x, _mm256_mul_ps(coef, x3)));
// Fast tanh approximation
let tanh = Self::fast_tanh_avx2(inner);
// 0.5 * x * (1 + tanh)
let result = _mm256_mul_ps(half, _mm256_mul_ps(x, _mm256_add_ps(one, tanh)));
_mm256_storeu_ps(ptr, result);
}
// Handle remainder
const SQRT_2_PI: f32 = 0.7978845608028654;
const COEF: f32 = 0.044715;
for i in (chunks * 8)..len {
let x = input[i];
let x3 = x * x * x;
let inner = SQRT_2_PI * (x + COEF * x3);
input[i] = 0.5 * x * (1.0 + Self::fast_tanh(inner));
}
}
/// Fast tanh approximation for AVX2
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn fast_tanh_avx2(x: __m256) -> __m256 {
// tanh(x) ~ x * (27 + x^2) / (27 + 9*x^2) for |x| < 3
// This is Pade approximation
let x2 = _mm256_mul_ps(x, x);
let c27 = _mm256_set1_ps(27.0);
let c9 = _mm256_set1_ps(9.0);
let num = _mm256_mul_ps(x, _mm256_add_ps(c27, x2));
let den = _mm256_add_ps(c27, _mm256_mul_ps(c9, x2));
// Clamp result to [-1, 1]
let result = _mm256_div_ps(num, den);
let one = _mm256_set1_ps(1.0);
let neg_one = _mm256_set1_ps(-1.0);
_mm256_max_ps(_mm256_min_ps(result, one), neg_one)
}
/// Fast scalar tanh approximation
#[inline]
fn fast_tanh(x: f32) -> f32 {
// Pade approximation: tanh(x) ~ x * (27 + x^2) / (27 + 9*x^2)
let x2 = x * x;
let result = x * (27.0 + x2) / (27.0 + 9.0 * x2);
result.clamp(-1.0, 1.0)
}
/// Fast scalar exp approximation
#[inline]
fn fast_exp(x: f32) -> f32 {
// Clamp to avoid overflow/underflow
let x = x.clamp(-88.0, 88.0);
// Polynomial approximation
let x2 = x * x;
let x3 = x2 * x;
let x4 = x2 * x2;
1.0 + x + x2 * 0.5 + x3 / 6.0 + x4 / 24.0
}
// ========================================================================
// Layer Normalization (Welford Algorithm for Numerical Stability)
// ========================================================================
/// Layer normalization using Welford's online algorithm
///
/// Uses running mean/variance computation for numerical stability
/// with large numbers or values with large variance.
///
/// # Arguments
/// * `input` - Input tensor
/// * `weight` - Learned scale parameters (gamma)
/// * `bias` - Learned shift parameters (beta), optional
/// * `eps` - Small constant for numerical stability (typically 1e-5)
#[inline]
pub fn layer_norm_simd(
input: &[f32],
weight: &[f32],
bias: Option<&[f32]>,
eps: f32,
) -> Vec<f32> {
debug_assert_eq!(input.len(), weight.len(), "Dimension mismatch");
if let Some(b) = bias {
debug_assert_eq!(input.len(), b.len(), "Bias dimension mismatch");
}
// Welford's algorithm for computing mean and variance in one pass
let (mean, var) = Self::welford_mean_var(input);
let inv_std = 1.0 / (var + eps).sqrt();
let mut output = Vec::with_capacity(input.len());
match bias {
Some(b) => {
for i in 0..input.len() {
let normalized = (input[i] - mean) * inv_std;
output.push(normalized * weight[i] + b[i]);
}
}
None => {
for i in 0..input.len() {
let normalized = (input[i] - mean) * inv_std;
output.push(normalized * weight[i]);
}
}
}
output
}
/// RMS normalization (used in modern transformers like LLaMA)
///
/// RMSNorm(x) = x * weight / sqrt(mean(x^2) + eps)
#[inline]
pub fn rms_norm_simd(input: &[f32], weight: &[f32], eps: f32) -> Vec<f32> {
debug_assert_eq!(input.len(), weight.len(), "Dimension mismatch");
// Compute mean of squared values using SIMD
let sum_sq = Self::dot_product(input, input);
let rms = (sum_sq / input.len() as f32 + eps).sqrt();
let inv_rms = 1.0 / rms;
let mut output = Vec::with_capacity(input.len());
for i in 0..input.len() {
output.push(input[i] * inv_rms * weight[i]);
}
output
}
/// Welford's online algorithm for mean and variance
///
/// Numerically stable single-pass algorithm
#[inline]
fn welford_mean_var(data: &[f32]) -> (f32, f32) {
if data.is_empty() {
return (0.0, 0.0);
}
let mut count = 0.0f64;
let mut mean = 0.0f64;
let mut m2 = 0.0f64;
for &x in data {
count += 1.0;
let delta = x as f64 - mean;
mean += delta / count;
let delta2 = x as f64 - mean;
m2 += delta * delta2;
}
let variance = if count > 1.0 { m2 / count } else { 0.0 };
(mean as f32, variance as f32)
}
// ========================================================================
// Quantization Operations (Q4/Q8)
// ========================================================================
/// Q4 block size (number of elements per scale factor)
pub const Q4_BLOCK_SIZE: usize = 32;
/// Q8 block size
pub const Q8_BLOCK_SIZE: usize = 32;
/// Quantize f32 array to Q4 format (4-bit quantization)
///
/// Uses block-wise quantization with per-block scale factors.
/// Achieves ~4x memory reduction with ~1% accuracy loss.
///
/// # Returns
/// Tuple of (quantized_data, scales) where:
/// - quantized_data: Packed 4-bit values (2 values per byte)
/// - scales: Per-block scale factors
#[inline]
pub fn quantize_simd_q4(input: &[f32]) -> (Vec<u8>, Vec<f32>) {
let num_blocks = (input.len() + Self::Q4_BLOCK_SIZE - 1) / Self::Q4_BLOCK_SIZE;
let mut data = Vec::with_capacity(input.len() / 2);
let mut scales = Vec::with_capacity(num_blocks);
for block in input.chunks(Self::Q4_BLOCK_SIZE) {
// Find max absolute value for scale
let max_abs = block.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
let scale = max_abs / 7.0; // Q4 range is -8 to 7
scales.push(scale);
// Quantize with zero-centered mapping
let inv_scale = if scale > 1e-10 { 1.0 / scale } else { 0.0 };
for pair in block.chunks(2) {
let q0 = ((pair[0] * inv_scale).round() as i8).clamp(-8, 7) as u8 & 0x0F;
let q1 = if pair.len() > 1 {
((pair[1] * inv_scale).round() as i8).clamp(-8, 7) as u8 & 0x0F
} else {
0
};
data.push((q1 << 4) | q0);
}
}
(data, scales)
}
/// Dequantize Q4 data back to f32
#[inline]
pub fn dequantize_simd_q4(
data: &[u8],
scales: &[f32],
output_len: usize,
) -> Vec<f32> {
let mut output = Vec::with_capacity(output_len);
for (block_idx, scale) in scales.iter().enumerate() {
let block_start = block_idx * Self::Q4_BLOCK_SIZE / 2;
let block_end = ((block_idx + 1) * Self::Q4_BLOCK_SIZE / 2).min(data.len());
for byte_idx in block_start..block_end {
if output.len() >= output_len {
break;
}
let byte = data[byte_idx];
// Low nibble
let q0 = (byte & 0x0F) as i8;
let q0 = if q0 > 7 { q0 - 16 } else { q0 };
output.push(q0 as f32 * scale);
if output.len() >= output_len {
break;
}
// High nibble
let q1 = ((byte >> 4) & 0x0F) as i8;
let q1 = if q1 > 7 { q1 - 16 } else { q1 };
output.push(q1 as f32 * scale);
}
}
output
}
/// Quantize f32 array to Q8 format (8-bit quantization)
///
/// Uses block-wise quantization with per-block scale factors.
/// Achieves ~4x memory reduction with minimal accuracy loss.
#[inline]
pub fn quantize_simd_q8(input: &[f32]) -> (Vec<i8>, Vec<f32>) {
let num_blocks = (input.len() + Self::Q8_BLOCK_SIZE - 1) / Self::Q8_BLOCK_SIZE;
let mut data = Vec::with_capacity(input.len());
let mut scales = Vec::with_capacity(num_blocks);
for block in input.chunks(Self::Q8_BLOCK_SIZE) {
// Find max absolute value for scale
let max_abs = block.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
let scale = max_abs / 127.0; // Q8 range is -128 to 127
scales.push(scale);
// Quantize
let inv_scale = if scale > 1e-10 { 1.0 / scale } else { 0.0 };
for &x in block {
let q = (x * inv_scale).round() as i8;
data.push(q);
}
}
(data, scales)
}
/// Dequantize Q8 data back to f32
#[inline]
pub fn dequantize_simd_q8(data: &[i8], scales: &[f32], output_len: usize) -> Vec<f32> {
let mut output = Vec::with_capacity(output_len);
for (block_idx, scale) in scales.iter().enumerate() {
let block_start = block_idx * Self::Q8_BLOCK_SIZE;
let block_end = ((block_idx + 1) * Self::Q8_BLOCK_SIZE).min(data.len());
for idx in block_start..block_end {
if output.len() >= output_len {
break;
}
output.push(data[idx] as f32 * scale);
}
}
output
}
/// Quantized matrix-vector multiplication (Q4 * f32 -> f32)
///
/// Efficient implementation that dequantizes on-the-fly without
/// allocating full dequantized matrix.
#[inline]
pub fn matvec_q4(
data: &[u8],
scales: &[f32],
x: &[f32],
m: usize,
n: usize,
) -> Vec<f32> {
let mut y = vec![0.0f32; m];
let total_elements = m * n;
let num_blocks = (total_elements + Self::Q4_BLOCK_SIZE - 1) / Self::Q4_BLOCK_SIZE;
for row in 0..m {
let mut sum = 0.0f32;
let row_offset = row * n;
for col in 0..n {
let idx = row_offset + col;
// Find which block this element belongs to
let block_idx = idx / Self::Q4_BLOCK_SIZE;
let scale = if block_idx < scales.len() {
scales[block_idx]
} else {
// Fallback for last partial block
scales.last().copied().unwrap_or(1.0)
};
let byte = data[idx / 2];
let q = if idx % 2 == 0 {
(byte & 0x0F) as i8
} else {
((byte >> 4) & 0x0F) as i8
};
let q = if q > 7 { q - 16 } else { q };
sum += q as f32 * scale * x[col];
}
y[row] = sum;
}
y
}
// ========================================================================
// Additional Activation Functions
// ========================================================================
/// SiLU (Swish) activation: x * sigmoid(x)
#[inline]
pub fn silu_simd(input: &mut [f32]) {
for x in input.iter_mut() {
*x = *x / (1.0 + (-*x).exp());
}
}
/// ReLU activation: max(0, x)
#[inline]
pub fn relu_simd(input: &mut [f32]) {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
unsafe { Self::relu_avx2(input) };
return;
}
}
#[cfg(target_arch = "wasm32")]
{
Self::relu_wasm_simd128(input);
return;
}
for x in input.iter_mut() {
*x = x.max(0.0);
}
}
#[cfg(target_arch = "wasm32")]
#[inline]
fn relu_wasm_simd128(input: &mut [f32]) {
let len = input.len();
let chunks = len / 4;
let zero = f32x4_splat(0.0);
for i in 0..chunks {
let offset = i * 4;
let v = unsafe { v128_load(input.as_ptr().add(offset) as *const v128) };
let result = f32x4_pmax(v, zero);
unsafe {
v128_store(input.as_mut_ptr().add(offset) as *mut v128, result);
}
}
for i in (chunks * 4)..len {
input[i] = input[i].max(0.0);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn relu_avx2(input: &mut [f32]) {
let len = input.len();
let chunks = len / 8;
let zero = _mm256_setzero_ps();
for i in 0..chunks {
let ptr = input.as_mut_ptr().add(i * 8);
let v = _mm256_loadu_ps(ptr);
let result = _mm256_max_ps(v, zero);
_mm256_storeu_ps(ptr, result);
}
for i in (chunks * 8)..len {
input[i] = input[i].max(0.0);
}
}
}
// ============================================================================
// Quantized Weight Storage
// ============================================================================
/// Q4 quantized weight matrix for memory-efficient inference
#[derive(Clone)]
pub struct Q4Weights {
/// Packed 4-bit quantized data
data: Vec<u8>,
/// Per-block scale factors
scales: Vec<f32>,
/// Matrix dimensions
rows: usize,
cols: usize,
}
impl Q4Weights {
/// Create Q4 weights from f32 matrix (row-major)
pub fn from_f32(weights: &[f32], rows: usize, cols: usize) -> Self {
debug_assert_eq!(weights.len(), rows * cols);
let (data, scales) = SimdCompute::quantize_simd_q4(weights);
Self {
data,
scales,
rows,
cols,
}
}
/// Matrix-vector multiplication with on-the-fly dequantization
pub fn matvec(&self, x: &[f32]) -> Vec<f32> {
debug_assert_eq!(x.len(), self.cols);
SimdCompute::matvec_q4(&self.data, &self.scales, x, self.rows, self.cols)
}
/// Get matrix dimensions
pub fn dims(&self) -> (usize, usize) {
(self.rows, self.cols)
}
/// Memory usage in bytes
pub fn memory_bytes(&self) -> usize {
self.data.len() + self.scales.len() * 4
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dot_product_scalar() {
let a = vec![1.0, 2.0, 3.0, 4.0];
let b = vec![1.0, 1.0, 1.0, 1.0];
let result = SimdCompute::dot_product_scalar(&a, &b);
assert!((result - 10.0).abs() < 1e-5);
}
#[test]
fn test_dot_product_simd() {
let a: Vec<f32> = (0..256).map(|i| i as f32 * 0.1).collect();
let b: Vec<f32> = (0..256).map(|i| (255 - i) as f32 * 0.1).collect();
let scalar_result = SimdCompute::dot_product_scalar(&a, &b);
let simd_result = SimdCompute::dot_product(&a, &b);
assert!(
(scalar_result - simd_result).abs() < 0.1,
"Scalar: {}, SIMD: {}",
scalar_result,
simd_result
);
}
#[test]
fn test_softmax_scalar() {
let mut values = vec![1.0, 2.0, 3.0];
SimdCompute::softmax_scalar(&mut values);
let sum: f32 = values.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
assert!(values[2] > values[1]);
assert!(values[1] > values[0]);
}
#[test]
fn test_softmax_numerical_stability() {
// Test with large values that would overflow without max subtraction
let mut values = vec![1000.0, 1001.0, 1002.0];
SimdCompute::softmax_simd(&mut values);
let sum: f32 = values.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
assert!(values.iter().all(|&x| x.is_finite()));
}
#[test]
fn test_gelu() {
let mut values = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
SimdCompute::gelu_scalar(&mut values);
// GELU(0) = 0
assert!(values[2].abs() < 1e-5);
// GELU(-2) is very small negative, GELU(-1) is also small negative
// For large negative inputs, GELU approaches 0 from below
// GELU(-2) ~ -0.045, GELU(-1) ~ -0.158
// So GELU(-2) > GELU(-1) (less negative)
// For x > 0, GELU is monotonically increasing and positive
assert!(values[1] < values[2]); // GELU(-1) < GELU(0)
assert!(values[2] < values[3]); // GELU(0) < GELU(1)
assert!(values[3] < values[4]); // GELU(1) < GELU(2)
// GELU(-2) > GELU(-1) because GELU(-2) is closer to 0
assert!(values[0] > values[1]); // GELU(-2) > GELU(-1)
}
#[test]
fn test_layer_norm() {
let input = vec![1.0, 2.0, 3.0, 4.0];
let weight = vec![1.0, 1.0, 1.0, 1.0];
let bias = vec![0.0, 0.0, 0.0, 0.0];
let output = SimdCompute::layer_norm_simd(&input, &weight, Some(&bias), 1e-5);
// Mean of output should be ~0
let mean: f32 = output.iter().sum::<f32>() / output.len() as f32;
assert!(mean.abs() < 1e-5);
// Variance should be ~1
let var: f32 = output.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / output.len() as f32;
assert!((var - 1.0).abs() < 0.1);
}
#[test]
fn test_rms_norm() {
let input = vec![1.0, 2.0, 3.0, 4.0];
let weight = vec![1.0, 1.0, 1.0, 1.0];
let output = SimdCompute::rms_norm_simd(&input, &weight, 1e-5);
assert_eq!(output.len(), input.len());
// RMS normalized values should be smaller for larger inputs
assert!(output[0].abs() < input[0].abs());
}
#[test]
fn test_q4_quantization() {
let input: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.1).collect();
let (data, scales) = SimdCompute::quantize_simd_q4(&input);
let output = SimdCompute::dequantize_simd_q4(&data, &scales, input.len());
assert_eq!(output.len(), input.len());
// Check that dequantized values are close to original
let max_error: f32 = input
.iter()
.zip(output.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
// Q4 should have reasonable accuracy (within 10% of range)
let range = 6.4; // -3.2 to 3.2
assert!(max_error < range * 0.15, "Max error: {}", max_error);
}
#[test]
fn test_q8_quantization() {
let input: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.1).collect();
let (data, scales) = SimdCompute::quantize_simd_q8(&input);
let output = SimdCompute::dequantize_simd_q8(&data, &scales, input.len());
assert_eq!(output.len(), input.len());
// Q8 should be more accurate than Q4
let max_error: f32 = input
.iter()
.zip(output.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
let range = 6.4;
assert!(max_error < range * 0.02, "Max error: {}", max_error);
}
#[test]
fn test_q4_weights() {
let weights: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.01).collect();
let q4 = Q4Weights::from_f32(&weights, 8, 8);
assert_eq!(q4.dims(), (8, 8));
// Test matvec
let x = vec![1.0; 8];
let y = q4.matvec(&x);
assert_eq!(y.len(), 8);
}
#[test]
fn test_matvec() {
// 2x3 matrix times 3-vector
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let x = vec![1.0, 1.0, 1.0];
let y = SimdCompute::matvec_simd(&a, &x, 2, 3);
assert_eq!(y.len(), 2);
assert!((y[0] - 6.0).abs() < 1e-5); // 1+2+3
assert!((y[1] - 15.0).abs() < 1e-5); // 4+5+6
}
#[test]
fn test_matmul() {
// 2x2 * 2x2
let a = vec![1.0, 2.0, 3.0, 4.0];
let b = vec![5.0, 6.0, 7.0, 8.0];
let c = SimdCompute::matmul_simd(&a, &b, 2, 2, 2);
assert_eq!(c.len(), 4);
// [[1,2],[3,4]] * [[5,6],[7,8]] = [[19,22],[43,50]]
assert!((c[0] - 19.0).abs() < 1e-4, "c[0]={}", c[0]);
assert!((c[1] - 22.0).abs() < 1e-4, "c[1]={}", c[1]);
assert!((c[2] - 43.0).abs() < 1e-4, "c[2]={}", c[2]);
assert!((c[3] - 50.0).abs() < 1e-4, "c[3]={}", c[3]);
}
#[test]
fn test_relu() {
let mut values = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
SimdCompute::relu_simd(&mut values);
assert_eq!(values, vec![0.0, 0.0, 0.0, 1.0, 2.0]);
}
#[test]
fn test_silu() {
let mut values = vec![0.0, 1.0, -1.0];
SimdCompute::silu_simd(&mut values);
// SiLU(0) = 0
assert!(values[0].abs() < 1e-5);
// SiLU(1) ~ 0.731
assert!((values[1] - 0.731).abs() < 0.01);
// SiLU(-1) ~ -0.269
assert!((values[2] + 0.269).abs() < 0.01);
}
#[test]
fn test_welford() {
let data = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let (mean, var) = SimdCompute::welford_mean_var(&data);
assert!((mean - 5.0).abs() < 1e-5);
assert!((var - 4.0).abs() < 1e-5);
}
#[test]
fn test_capabilities_detection() {
let caps = SimdCapabilities::detect();
#[cfg(target_arch = "wasm32")]
assert!(caps.wasm_simd128);
// lane_width should be at least 1
assert!(caps.lane_width() >= 1);
}
}