mirror of
https://github.com/ruvnet/RuView
synced 2026-08-06 19:51:43 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
//! CPU backend with portable SIMD optimizations
|
||||
|
||||
use super::Backend;
|
||||
use crate::config::ActivationType;
|
||||
use ndarray::Array2;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use std::arch::aarch64::*;
|
||||
|
||||
/// Cached SIMD feature detection for x86_64
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
static SIMD_FEATURES: OnceLock<SimdFeatures> = OnceLock::new();
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct SimdFeatures {
|
||||
has_avx2: bool,
|
||||
has_sse41: bool,
|
||||
has_fma: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn get_simd_features() -> SimdFeatures {
|
||||
*SIMD_FEATURES.get_or_init(|| SimdFeatures {
|
||||
has_avx2: is_x86_feature_detected!("avx2"),
|
||||
has_sse41: is_x86_feature_detected!("sse4.1"),
|
||||
has_fma: is_x86_feature_detected!("fma"),
|
||||
})
|
||||
}
|
||||
|
||||
/// CPU backend using portable SIMD
|
||||
pub struct CpuBackend;
|
||||
|
||||
impl Backend for CpuBackend {
|
||||
fn dot_product(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
debug_assert_eq!(a.len(), b.len());
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
let features = get_simd_features();
|
||||
if features.has_avx2 {
|
||||
return unsafe { dot_product_avx2(a, b) };
|
||||
} else if features.has_sse41 {
|
||||
return unsafe { dot_product_sse(a, b) };
|
||||
}
|
||||
return dot_product_scalar(a, b);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
return unsafe { dot_product_neon(a, b) };
|
||||
|
||||
// Fallback scalar
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
dot_product_scalar(a, b)
|
||||
}
|
||||
|
||||
fn sparse_matmul(&self, matrix: &Array2<f32>, input: &[f32], rows: &[usize]) -> Vec<f32> {
|
||||
let mut output = Vec::with_capacity(rows.len());
|
||||
|
||||
for &row_idx in rows {
|
||||
let row = matrix.row(row_idx);
|
||||
let dot = self.dot_product(row.as_slice().unwrap(), input);
|
||||
output.push(dot);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn sparse_matmul_accumulate(
|
||||
&self,
|
||||
matrix: &Array2<f32>,
|
||||
input: &[f32],
|
||||
cols: &[usize],
|
||||
output: &mut [f32],
|
||||
) {
|
||||
for (i, &col_idx) in cols.iter().enumerate() {
|
||||
let col = matrix.column(col_idx);
|
||||
let scalar = input[i];
|
||||
// Column view may not be contiguous, iterate element-by-element
|
||||
for (j, &val) in col.iter().enumerate() {
|
||||
output[j] += val * scalar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn activation(&self, data: &mut [f32], activation_type: ActivationType) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
let features = get_simd_features();
|
||||
|
||||
match activation_type {
|
||||
ActivationType::Relu => {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if features.has_avx2 {
|
||||
return unsafe { relu_avx2(data) };
|
||||
}
|
||||
relu_scalar(data);
|
||||
}
|
||||
ActivationType::Gelu => {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if features.has_avx2 {
|
||||
return unsafe { gelu_avx2(data) };
|
||||
}
|
||||
gelu_scalar(data);
|
||||
}
|
||||
ActivationType::Silu | ActivationType::Swish => {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if features.has_avx2 {
|
||||
return unsafe { silu_avx2(data) };
|
||||
}
|
||||
silu_scalar(data);
|
||||
}
|
||||
ActivationType::Identity => { /* no-op */ }
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&self, a: &mut [f32], b: &[f32]) {
|
||||
debug_assert_eq!(a.len(), b.len());
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if get_simd_features().has_avx2 {
|
||||
return unsafe { add_avx2(a, b) };
|
||||
}
|
||||
|
||||
for (x, y) in a.iter_mut().zip(b.iter()) {
|
||||
*x += y;
|
||||
}
|
||||
}
|
||||
|
||||
fn axpy(&self, a: &mut [f32], b: &[f32], scalar: f32) {
|
||||
debug_assert_eq!(a.len(), b.len());
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
if get_simd_features().has_avx2 {
|
||||
return unsafe { axpy_avx2(a, b, scalar) };
|
||||
}
|
||||
|
||||
for (x, y) in a.iter_mut().zip(b.iter()) {
|
||||
*x += y * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
let features = get_simd_features();
|
||||
if features.has_avx2 {
|
||||
return "CPU-AVX2";
|
||||
} else if features.has_sse41 {
|
||||
return "CPU-SSE4.1";
|
||||
}
|
||||
return "CPU-Scalar";
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
return "CPU-NEON";
|
||||
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
"CPU-Scalar"
|
||||
}
|
||||
|
||||
fn simd_width(&self) -> usize {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
let features = get_simd_features();
|
||||
if features.has_avx2 {
|
||||
return 8;
|
||||
}
|
||||
if features.has_sse41 {
|
||||
return 4;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
return 4;
|
||||
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// ============ AVX2 Implementations ============
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn dot_product_avx2(a: &[f32], b: &[f32]) -> f32 {
|
||||
let n = a.len();
|
||||
let chunks = n / 8;
|
||||
|
||||
let mut sum = _mm256_setzero_ps();
|
||||
|
||||
for i in 0..chunks {
|
||||
let va = _mm256_loadu_ps(a.as_ptr().add(i * 8));
|
||||
let vb = _mm256_loadu_ps(b.as_ptr().add(i * 8));
|
||||
sum = _mm256_fmadd_ps(va, vb, sum);
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
let sum128 = _mm_add_ps(_mm256_extractf128_ps(sum, 0), _mm256_extractf128_ps(sum, 1));
|
||||
let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128));
|
||||
let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1));
|
||||
let mut result = _mm_cvtss_f32(sum32);
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 8)..n {
|
||||
result += a[i] * b[i];
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn relu_avx2(data: &mut [f32]) {
|
||||
let zero = _mm256_setzero_ps();
|
||||
let chunks = data.len() / 8;
|
||||
|
||||
for i in 0..chunks {
|
||||
let ptr = data.as_mut_ptr().add(i * 8);
|
||||
let v = _mm256_loadu_ps(ptr);
|
||||
let result = _mm256_max_ps(v, zero);
|
||||
_mm256_storeu_ps(ptr, result);
|
||||
}
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 8)..data.len() {
|
||||
data[i] = data[i].max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD GELU using polynomial approximation
|
||||
/// GELU(x) ≈ 0.5 * x * (1 + tanh(sqrt(2/π) * (x + 0.044715 * x³)))
|
||||
/// Using fast tanh approximation for SIMD
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "avx2", enable = "fma")]
|
||||
unsafe fn gelu_avx2(data: &mut [f32]) {
|
||||
let chunks = data.len() / 8;
|
||||
|
||||
// Constants for GELU approximation
|
||||
let half = _mm256_set1_ps(0.5);
|
||||
let one = _mm256_set1_ps(1.0);
|
||||
let sqrt_2_over_pi = _mm256_set1_ps(0.7978845608); // sqrt(2/π)
|
||||
let coef = _mm256_set1_ps(0.044715);
|
||||
|
||||
// Constants for fast tanh approximation: tanh(x) ≈ x * (27 + x²) / (27 + 9x²)
|
||||
let c27 = _mm256_set1_ps(27.0);
|
||||
let c9 = _mm256_set1_ps(9.0);
|
||||
|
||||
for i in 0..chunks {
|
||||
let ptr = data.as_mut_ptr().add(i * 8);
|
||||
let x = _mm256_loadu_ps(ptr);
|
||||
|
||||
// x³
|
||||
let x2 = _mm256_mul_ps(x, x);
|
||||
let x3 = _mm256_mul_ps(x2, x);
|
||||
|
||||
// inner = sqrt(2/π) * (x + 0.044715 * x³)
|
||||
let inner = _mm256_mul_ps(sqrt_2_over_pi, _mm256_fmadd_ps(coef, x3, x));
|
||||
|
||||
// Fast tanh approximation
|
||||
let inner2 = _mm256_mul_ps(inner, inner);
|
||||
let num = _mm256_fmadd_ps(inner2, one, c27); // 27 + inner²
|
||||
let den = _mm256_fmadd_ps(inner2, c9, c27); // 27 + 9*inner²
|
||||
let tanh_approx = _mm256_mul_ps(inner, _mm256_div_ps(num, den));
|
||||
|
||||
// 0.5 * x * (1 + tanh)
|
||||
let result = _mm256_mul_ps(half, _mm256_mul_ps(x, _mm256_add_ps(one, tanh_approx)));
|
||||
_mm256_storeu_ps(ptr, result);
|
||||
}
|
||||
|
||||
// Handle remainder with scalar
|
||||
for i in (chunks * 8)..data.len() {
|
||||
let x = data[i];
|
||||
let x3 = x * x * x;
|
||||
let inner = 0.7978845608 * (x + 0.044715 * x3);
|
||||
data[i] = 0.5 * x * (1.0 + inner.tanh());
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD SiLU (Swish) using fast sigmoid approximation
|
||||
/// SiLU(x) = x * sigmoid(x) = x / (1 + exp(-x))
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "avx2", enable = "fma")]
|
||||
unsafe fn silu_avx2(data: &mut [f32]) {
|
||||
let chunks = data.len() / 8;
|
||||
|
||||
// For sigmoid, use: 1/(1+e^-x) ≈ 0.5 + 0.5*tanh(x/2)
|
||||
let half = _mm256_set1_ps(0.5);
|
||||
let c27 = _mm256_set1_ps(27.0);
|
||||
let c9 = _mm256_set1_ps(9.0);
|
||||
let one = _mm256_set1_ps(1.0);
|
||||
|
||||
for i in 0..chunks {
|
||||
let ptr = data.as_mut_ptr().add(i * 8);
|
||||
let x = _mm256_loadu_ps(ptr);
|
||||
|
||||
// Use sigmoid(x) = 0.5 + 0.5 * tanh(x/2)
|
||||
let x_half = _mm256_mul_ps(x, half);
|
||||
|
||||
// Fast tanh(x/2)
|
||||
let xh2 = _mm256_mul_ps(x_half, x_half);
|
||||
let num = _mm256_fmadd_ps(xh2, one, c27);
|
||||
let den = _mm256_fmadd_ps(xh2, c9, c27);
|
||||
let tanh_approx = _mm256_mul_ps(x_half, _mm256_div_ps(num, den));
|
||||
|
||||
// sigmoid = 0.5 + 0.5 * tanh
|
||||
let sigmoid = _mm256_fmadd_ps(half, tanh_approx, half);
|
||||
|
||||
// silu = x * sigmoid
|
||||
let result = _mm256_mul_ps(x, sigmoid);
|
||||
_mm256_storeu_ps(ptr, result);
|
||||
}
|
||||
|
||||
// Handle remainder with scalar
|
||||
for i in (chunks * 8)..data.len() {
|
||||
let x = data[i];
|
||||
data[i] = x / (1.0 + (-x).exp());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn add_avx2(a: &mut [f32], b: &[f32]) {
|
||||
let chunks = a.len() / 8;
|
||||
|
||||
for i in 0..chunks {
|
||||
let pa = a.as_mut_ptr().add(i * 8);
|
||||
let pb = b.as_ptr().add(i * 8);
|
||||
let va = _mm256_loadu_ps(pa);
|
||||
let vb = _mm256_loadu_ps(pb);
|
||||
_mm256_storeu_ps(pa, _mm256_add_ps(va, vb));
|
||||
}
|
||||
|
||||
for i in (chunks * 8)..a.len() {
|
||||
a[i] += b[i];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "avx2")]
|
||||
unsafe fn axpy_avx2(a: &mut [f32], b: &[f32], scalar: f32) {
|
||||
let vs = _mm256_set1_ps(scalar);
|
||||
let chunks = a.len() / 8;
|
||||
|
||||
for i in 0..chunks {
|
||||
let pa = a.as_mut_ptr().add(i * 8);
|
||||
let pb = b.as_ptr().add(i * 8);
|
||||
let va = _mm256_loadu_ps(pa);
|
||||
let vb = _mm256_loadu_ps(pb);
|
||||
let result = _mm256_fmadd_ps(vb, vs, va);
|
||||
_mm256_storeu_ps(pa, result);
|
||||
}
|
||||
|
||||
for i in (chunks * 8)..a.len() {
|
||||
a[i] += b[i] * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ SSE4.1 Implementations ============
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[target_feature(enable = "sse4.1")]
|
||||
unsafe fn dot_product_sse(a: &[f32], b: &[f32]) -> f32 {
|
||||
let n = a.len();
|
||||
let chunks = n / 4;
|
||||
|
||||
let mut sum = _mm_setzero_ps();
|
||||
|
||||
for i in 0..chunks {
|
||||
let va = _mm_loadu_ps(a.as_ptr().add(i * 4));
|
||||
let vb = _mm_loadu_ps(b.as_ptr().add(i * 4));
|
||||
sum = _mm_add_ps(sum, _mm_mul_ps(va, vb));
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
let sum2 = _mm_add_ps(sum, _mm_movehl_ps(sum, sum));
|
||||
let sum1 = _mm_add_ss(sum2, _mm_shuffle_ps(sum2, sum2, 1));
|
||||
let mut result = _mm_cvtss_f32(sum1);
|
||||
|
||||
for i in (chunks * 4)..n {
|
||||
result += a[i] * b[i];
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ============ NEON Implementations (ARM) ============
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
unsafe fn dot_product_neon(a: &[f32], b: &[f32]) -> f32 {
|
||||
let n = a.len();
|
||||
let chunks = n / 4;
|
||||
|
||||
let mut sum = vdupq_n_f32(0.0);
|
||||
|
||||
for i in 0..chunks {
|
||||
let va = vld1q_f32(a.as_ptr().add(i * 4));
|
||||
let vb = vld1q_f32(b.as_ptr().add(i * 4));
|
||||
sum = vfmaq_f32(sum, va, vb);
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
let mut result = vaddvq_f32(sum);
|
||||
|
||||
for i in (chunks * 4)..n {
|
||||
result += a[i] * b[i];
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ============ Scalar Fallbacks ============
|
||||
|
||||
fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
|
||||
}
|
||||
|
||||
fn relu_scalar(data: &mut [f32]) {
|
||||
for x in data.iter_mut() {
|
||||
*x = x.max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn gelu_scalar(data: &mut [f32]) {
|
||||
const SQRT_2_OVER_PI: f32 = 0.7978845608;
|
||||
const GELU_COEF: f32 = 0.044715;
|
||||
|
||||
for x in data.iter_mut() {
|
||||
let x3 = *x * *x * *x;
|
||||
let inner = SQRT_2_OVER_PI * (*x + GELU_COEF * x3);
|
||||
*x = 0.5 * *x * (1.0 + inner.tanh());
|
||||
}
|
||||
}
|
||||
|
||||
fn silu_scalar(data: &mut [f32]) {
|
||||
for x in data.iter_mut() {
|
||||
*x = *x / (1.0 + (-*x).exp());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dot_product() {
|
||||
let backend = CpuBackend;
|
||||
let a = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let b = vec![2.0, 3.0, 4.0, 5.0];
|
||||
let result = backend.dot_product(&a, &b);
|
||||
assert!((result - 40.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relu() {
|
||||
let backend = CpuBackend;
|
||||
let mut data = vec![-1.0, 0.0, 1.0, 2.0];
|
||||
backend.activation(&mut data, ActivationType::Relu);
|
||||
assert_eq!(data, vec![0.0, 0.0, 1.0, 2.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let backend = CpuBackend;
|
||||
let mut a = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let b = vec![5.0, 6.0, 7.0, 8.0];
|
||||
backend.add(&mut a, &b);
|
||||
assert_eq!(a, vec![6.0, 8.0, 10.0, 12.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_axpy() {
|
||||
let backend = CpuBackend;
|
||||
let mut a = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let b = vec![1.0, 1.0, 1.0, 1.0];
|
||||
backend.axpy(&mut a, &b, 2.0);
|
||||
assert_eq!(a, vec![3.0, 4.0, 5.0, 6.0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Backend abstraction for hardware-specific optimizations
|
||||
|
||||
use crate::config::ActivationType;
|
||||
use ndarray::Array2;
|
||||
|
||||
pub mod cpu;
|
||||
pub mod wasm;
|
||||
|
||||
#[cfg(feature = "npu")]
|
||||
pub mod npu;
|
||||
|
||||
/// Backend trait for SIMD/vectorized operations
|
||||
pub trait Backend: Send + Sync {
|
||||
/// Dot product of two vectors
|
||||
fn dot_product(&self, a: &[f32], b: &[f32]) -> f32;
|
||||
|
||||
/// Sparse matrix-vector multiplication
|
||||
/// Only computes rows specified in `rows`
|
||||
fn sparse_matmul(&self, matrix: &Array2<f32>, input: &[f32], rows: &[usize]) -> Vec<f32>;
|
||||
|
||||
/// Sparse matrix-vector multiplication with column-major accumulation
|
||||
fn sparse_matmul_accumulate(
|
||||
&self,
|
||||
matrix: &Array2<f32>,
|
||||
input: &[f32],
|
||||
cols: &[usize],
|
||||
output: &mut [f32],
|
||||
);
|
||||
|
||||
/// Apply activation function in-place
|
||||
fn activation(&self, data: &mut [f32], activation_type: ActivationType);
|
||||
|
||||
/// Vectorized addition
|
||||
fn add(&self, a: &mut [f32], b: &[f32]);
|
||||
|
||||
/// Vectorized multiply-add: a[i] += b[i] * scalar
|
||||
fn axpy(&self, a: &mut [f32], b: &[f32], scalar: f32);
|
||||
|
||||
/// Backend name for debugging
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// SIMD width (number of f32s per vector register)
|
||||
fn simd_width(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Get the best available backend for the current platform
|
||||
pub fn get_backend() -> Box<dyn Backend> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Box::new(wasm::WasmBackend);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
#[cfg(feature = "npu")]
|
||||
if npu::is_available() {
|
||||
return Box::new(npu::NpuBackend::new());
|
||||
}
|
||||
|
||||
Box::new(cpu::CpuBackend)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! NPU (Neural Processing Unit) backend - placeholder for future hardware acceleration
|
||||
|
||||
use crate::config::ActivationType;
|
||||
use ndarray::Array2;
|
||||
|
||||
use super::Backend;
|
||||
|
||||
/// Check if NPU hardware is available
|
||||
pub fn is_available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// NPU Backend for hardware-accelerated inference
|
||||
pub struct NpuBackend;
|
||||
|
||||
impl NpuBackend {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for NpuBackend {
|
||||
fn dot_product(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
|
||||
}
|
||||
|
||||
fn sparse_matmul(&self, matrix: &Array2<f32>, input: &[f32], rows: &[usize]) -> Vec<f32> {
|
||||
// Fallback to CPU implementation
|
||||
rows.iter()
|
||||
.map(|&r| {
|
||||
matrix
|
||||
.row(r)
|
||||
.iter()
|
||||
.zip(input.iter())
|
||||
.map(|(m, i)| m * i)
|
||||
.sum()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sparse_matmul_accumulate(
|
||||
&self,
|
||||
matrix: &Array2<f32>,
|
||||
input: &[f32],
|
||||
cols: &[usize],
|
||||
output: &mut [f32],
|
||||
) {
|
||||
for &c in cols {
|
||||
let val = input[c];
|
||||
for (i, o) in output.iter_mut().enumerate() {
|
||||
*o += matrix[[i, c]] * val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn activation(&self, data: &mut [f32], activation_type: ActivationType) {
|
||||
for x in data.iter_mut() {
|
||||
*x = match activation_type {
|
||||
ActivationType::ReLU => x.max(0.0),
|
||||
ActivationType::Sigmoid => 1.0 / (1.0 + (-*x).exp()),
|
||||
ActivationType::Tanh => x.tanh(),
|
||||
ActivationType::None => *x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&self, a: &mut [f32], b: &[f32]) {
|
||||
for (x, y) in a.iter_mut().zip(b.iter()) {
|
||||
*x += y;
|
||||
}
|
||||
}
|
||||
|
||||
fn axpy(&self, a: &mut [f32], b: &[f32], scalar: f32) {
|
||||
for (x, y) in a.iter_mut().zip(b.iter()) {
|
||||
*x += y * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"npu"
|
||||
}
|
||||
|
||||
fn simd_width(&self) -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! WebAssembly backend with portable SIMD
|
||||
|
||||
use super::Backend;
|
||||
use crate::config::ActivationType;
|
||||
use ndarray::Array2;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::arch::wasm32::*;
|
||||
|
||||
/// WASM backend using wasm32 SIMD instructions
|
||||
pub struct WasmBackend;
|
||||
|
||||
impl Backend for WasmBackend {
|
||||
fn dot_product(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
debug_assert_eq!(a.len(), b.len());
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return dot_product_wasm_simd(a, b);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
dot_product_scalar(a, b)
|
||||
}
|
||||
|
||||
fn sparse_matmul(&self, matrix: &Array2<f32>, input: &[f32], rows: &[usize]) -> Vec<f32> {
|
||||
rows.iter()
|
||||
.map(|&row_idx| {
|
||||
let row = matrix.row(row_idx);
|
||||
self.dot_product(row.as_slice().unwrap(), input)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sparse_matmul_accumulate(
|
||||
&self,
|
||||
matrix: &Array2<f32>,
|
||||
input: &[f32],
|
||||
cols: &[usize],
|
||||
output: &mut [f32],
|
||||
) {
|
||||
for (i, &col_idx) in cols.iter().enumerate() {
|
||||
let col = matrix.column(col_idx);
|
||||
self.axpy(output, col.as_slice().unwrap(), input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn activation(&self, data: &mut [f32], activation_type: ActivationType) {
|
||||
match activation_type {
|
||||
ActivationType::Relu => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
relu_wasm_simd(data);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
relu_scalar(data);
|
||||
}
|
||||
ActivationType::Gelu => gelu_scalar(data),
|
||||
ActivationType::Silu | ActivationType::Swish => silu_scalar(data),
|
||||
ActivationType::Identity => { /* no-op */ }
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&self, a: &mut [f32], b: &[f32]) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
add_wasm_simd(a, b);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
for (x, y) in a.iter_mut().zip(b.iter()) {
|
||||
*x += y;
|
||||
}
|
||||
}
|
||||
|
||||
fn axpy(&self, a: &mut [f32], b: &[f32], scalar: f32) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
axpy_wasm_simd(a, b, scalar);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
for (x, y) in a.iter_mut().zip(b.iter()) {
|
||||
*x += y * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"WASM-SIMD"
|
||||
}
|
||||
|
||||
fn simd_width(&self) -> usize {
|
||||
4 // 128-bit SIMD = 4 x f32
|
||||
}
|
||||
}
|
||||
|
||||
// ============ WASM SIMD Implementations ============
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn dot_product_wasm_simd(a: &[f32], b: &[f32]) -> f32 {
|
||||
let n = a.len();
|
||||
let chunks = n / 4;
|
||||
|
||||
let mut sum = f32x4_splat(0.0);
|
||||
|
||||
for i in 0..chunks {
|
||||
let va = v128_load(a[i * 4..].as_ptr() as *const v128);
|
||||
let vb = v128_load(b[i * 4..].as_ptr() as *const v128);
|
||||
sum = f32x4_add(sum, f32x4_mul(va, vb));
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
let sum_arr = [
|
||||
f32x4_extract_lane::<0>(sum),
|
||||
f32x4_extract_lane::<1>(sum),
|
||||
f32x4_extract_lane::<2>(sum),
|
||||
f32x4_extract_lane::<3>(sum),
|
||||
];
|
||||
let mut result: f32 = sum_arr.iter().sum();
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 4)..n {
|
||||
result += a[i] * b[i];
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn relu_wasm_simd(data: &mut [f32]) {
|
||||
let zero = f32x4_splat(0.0);
|
||||
let chunks = data.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let ptr = data[i * 4..].as_ptr() as *const v128;
|
||||
let v = v128_load(ptr);
|
||||
let result = f32x4_max(v, zero);
|
||||
v128_store(data[i * 4..].as_mut_ptr() as *mut v128, result);
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..data.len() {
|
||||
data[i] = data[i].max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn add_wasm_simd(a: &mut [f32], b: &[f32]) {
|
||||
let chunks = a.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let pa = a[i * 4..].as_ptr() as *const v128;
|
||||
let pb = b[i * 4..].as_ptr() as *const v128;
|
||||
let va = v128_load(pa);
|
||||
let vb = v128_load(pb);
|
||||
let result = f32x4_add(va, vb);
|
||||
v128_store(a[i * 4..].as_mut_ptr() as *mut v128, result);
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] += b[i];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn axpy_wasm_simd(a: &mut [f32], b: &[f32], scalar: f32) {
|
||||
let vs = f32x4_splat(scalar);
|
||||
let chunks = a.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let pa = a[i * 4..].as_ptr() as *const v128;
|
||||
let pb = b[i * 4..].as_ptr() as *const v128;
|
||||
let va = v128_load(pa);
|
||||
let vb = v128_load(pb);
|
||||
let result = f32x4_add(va, f32x4_mul(vb, vs));
|
||||
v128_store(a[i * 4..].as_mut_ptr() as *mut v128, result);
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] += b[i] * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Scalar Fallbacks ============
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn relu_scalar(data: &mut [f32]) {
|
||||
for x in data.iter_mut() {
|
||||
*x = x.max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn gelu_scalar(data: &mut [f32]) {
|
||||
const SQRT_2_OVER_PI: f32 = 0.7978845608;
|
||||
const GELU_COEF: f32 = 0.044715;
|
||||
for x in data.iter_mut() {
|
||||
let x3 = *x * *x * *x;
|
||||
let inner = SQRT_2_OVER_PI * (*x + GELU_COEF * x3);
|
||||
*x = 0.5 * *x * (1.0 + inner.tanh());
|
||||
}
|
||||
}
|
||||
|
||||
fn silu_scalar(data: &mut [f32]) {
|
||||
for x in data.iter_mut() {
|
||||
*x = *x / (1.0 + (-*x).exp());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dot_product() {
|
||||
let backend = WasmBackend;
|
||||
let a = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let b = vec![2.0, 3.0, 4.0, 5.0];
|
||||
let result = backend.dot_product(&a, &b);
|
||||
assert!((result - 40.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let backend = WasmBackend;
|
||||
let mut a = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let b = vec![5.0, 6.0, 7.0, 8.0];
|
||||
backend.add(&mut a, &b);
|
||||
assert_eq!(a, vec![6.0, 8.0, 10.0, 12.0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user