use std::sync::OnceLock;
use wide::{f32x4, f32x8, f64x2, f64x4};
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
use wide::{f32x16, f64x8};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SimdLevel {
Scalar,
Sse,
Avx2,
Avx512,
}
static SIMD_LEVEL: OnceLock<SimdLevel> = OnceLock::new();
pub fn detect_simd_level() -> SimdLevel {
*SIMD_LEVEL.get_or_init(|| {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx512f") {
return SimdLevel::Avx512;
}
if is_x86_feature_detected!("avx2") {
return SimdLevel::Avx2;
}
if is_x86_feature_detected!("sse4.1") {
return SimdLevel::Sse;
}
SimdLevel::Scalar
}
#[cfg(target_arch = "aarch64")]
{
SimdLevel::Sse
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{
SimdLevel::Scalar
}
})
}
pub trait EdgeSimd: Sized + Copy {
fn dot_simd(a: &[Self], b: &[Self]) -> Self;
fn exp_in_place_simd(x: &mut [Self]);
}
#[inline]
fn dot_f64_scalar(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[inline]
fn dot_f64_sse(a: &[f64], b: &[f64]) -> f64 {
const LANES: usize = 2;
let n = a.len().min(b.len());
let mut total = f64x2::ZERO;
let mut i = 0;
while i + LANES <= n {
total += f64x2::from(&a[i..i + LANES]) * f64x2::from(&b[i..i + LANES]);
i += LANES;
}
total.reduce_add() + dot_f64_scalar(&a[i..n], &b[i..n])
}
#[inline]
fn dot_f64_avx2(a: &[f64], b: &[f64]) -> f64 {
const LANES: usize = 4;
let n = a.len().min(b.len());
let mut total = f64x4::ZERO;
let mut i = 0;
while i + LANES <= n {
total += f64x4::from(&a[i..i + LANES]) * f64x4::from(&b[i..i + LANES]);
i += LANES;
}
total.reduce_add() + dot_f64_sse(&a[i..n], &b[i..n])
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
#[inline]
fn dot_f64_avx512(a: &[f64], b: &[f64]) -> f64 {
const LANES: usize = 8;
let n = a.len().min(b.len());
let mut total = f64x8::ZERO;
let mut i = 0;
while i + LANES <= n {
total += f64x8::from(&a[i..i + LANES]) * f64x8::from(&b[i..i + LANES]);
i += LANES;
}
total.reduce_add() + dot_f64_avx2(&a[i..n], &b[i..n])
}
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
#[inline]
fn dot_f64_avx512(a: &[f64], b: &[f64]) -> f64 {
dot_f64_avx2(a, b)
}
#[inline]
fn dot_f32_scalar(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[inline]
fn dot_f32_sse(a: &[f32], b: &[f32]) -> f32 {
const LANES: usize = 4;
let n = a.len().min(b.len());
let mut total = f32x4::ZERO;
let mut i = 0;
while i + LANES <= n {
total += f32x4::from(&a[i..i + LANES]) * f32x4::from(&b[i..i + LANES]);
i += LANES;
}
total.reduce_add() + dot_f32_scalar(&a[i..n], &b[i..n])
}
#[inline]
fn dot_f32_avx2(a: &[f32], b: &[f32]) -> f32 {
const LANES: usize = 8;
let n = a.len().min(b.len());
let mut total = f32x8::ZERO;
let mut i = 0;
while i + LANES <= n {
total += f32x8::from(&a[i..i + LANES]) * f32x8::from(&b[i..i + LANES]);
i += LANES;
}
total.reduce_add() + dot_f32_sse(&a[i..n], &b[i..n])
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
#[inline]
fn dot_f32_avx512(a: &[f32], b: &[f32]) -> f32 {
const LANES: usize = 16;
let n = a.len().min(b.len());
let mut total = f32x16::ZERO;
let mut i = 0;
while i + LANES <= n {
total += f32x16::from(&a[i..i + LANES]) * f32x16::from(&b[i..i + LANES]);
i += LANES;
}
total.reduce_add() + dot_f32_avx2(&a[i..n], &b[i..n])
}
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
#[inline]
fn dot_f32_avx512(a: &[f32], b: &[f32]) -> f32 {
dot_f32_avx2(a, b)
}
#[inline]
fn exp_f64_scalar(x: &mut [f64]) {
for v in x.iter_mut() {
*v = v.exp();
}
}
#[inline]
fn exp_f64_sse(x: &mut [f64]) {
const LANES: usize = 2;
let n = x.len();
let mut i = 0;
while i + LANES <= n {
let v = f64x2::from(&x[i..i + LANES]).exp();
x[i..i + LANES].copy_from_slice(v.as_array());
i += LANES;
}
exp_f64_scalar(&mut x[i..]);
}
#[inline]
fn exp_f64_avx2(x: &mut [f64]) {
const LANES: usize = 4;
let n = x.len();
let mut i = 0;
while i + LANES <= n {
let v = f64x4::from(&x[i..i + LANES]).exp();
x[i..i + LANES].copy_from_slice(v.as_array());
i += LANES;
}
exp_f64_sse(&mut x[i..]);
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
#[inline]
fn exp_f64_avx512(x: &mut [f64]) {
const LANES: usize = 8;
let n = x.len();
let mut i = 0;
while i + LANES <= n {
let v = f64x8::from(&x[i..i + LANES]).exp();
x[i..i + LANES].copy_from_slice(v.as_array());
i += LANES;
}
exp_f64_avx2(&mut x[i..]);
}
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
#[inline]
fn exp_f64_avx512(x: &mut [f64]) {
exp_f64_avx2(x)
}
#[inline]
fn exp_f32_scalar(x: &mut [f32]) {
for v in x.iter_mut() {
*v = v.exp();
}
}
#[inline]
fn exp_f32_sse(x: &mut [f32]) {
const LANES: usize = 4;
let n = x.len();
let mut i = 0;
while i + LANES <= n {
let v = f32x4::from(&x[i..i + LANES]).exp();
x[i..i + LANES].copy_from_slice(v.as_array());
i += LANES;
}
exp_f32_scalar(&mut x[i..]);
}
#[inline]
fn exp_f32_avx2(x: &mut [f32]) {
const LANES: usize = 8;
let n = x.len();
let mut i = 0;
while i + LANES <= n {
let v = f32x8::from(&x[i..i + LANES]).exp();
x[i..i + LANES].copy_from_slice(v.as_array());
i += LANES;
}
exp_f32_sse(&mut x[i..]);
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
#[inline]
fn exp_f32_avx512(x: &mut [f32]) {
const LANES: usize = 16;
let n = x.len();
let mut i = 0;
while i + LANES <= n {
let v = f32x16::from(&x[i..i + LANES]).exp();
x[i..i + LANES].copy_from_slice(v.as_array());
i += LANES;
}
exp_f32_avx2(&mut x[i..]);
}
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
#[inline]
fn exp_f32_avx512(x: &mut [f32]) {
exp_f32_avx2(x)
}
impl EdgeSimd for f64 {
#[inline]
fn dot_simd(a: &[f64], b: &[f64]) -> f64 {
match detect_simd_level() {
SimdLevel::Avx512 => dot_f64_avx512(a, b),
SimdLevel::Avx2 => dot_f64_avx2(a, b),
SimdLevel::Sse => dot_f64_sse(a, b),
SimdLevel::Scalar => dot_f64_scalar(a, b),
}
}
#[inline]
fn exp_in_place_simd(x: &mut [f64]) {
match detect_simd_level() {
SimdLevel::Avx512 => exp_f64_avx512(x),
SimdLevel::Avx2 => exp_f64_avx2(x),
SimdLevel::Sse => exp_f64_sse(x),
SimdLevel::Scalar => exp_f64_scalar(x),
}
}
}
impl EdgeSimd for f32 {
#[inline]
fn dot_simd(a: &[f32], b: &[f32]) -> f32 {
match detect_simd_level() {
SimdLevel::Avx512 => dot_f32_avx512(a, b),
SimdLevel::Avx2 => dot_f32_avx2(a, b),
SimdLevel::Sse => dot_f32_sse(a, b),
SimdLevel::Scalar => dot_f32_scalar(a, b),
}
}
#[inline]
fn exp_in_place_simd(x: &mut [f32]) {
match detect_simd_level() {
SimdLevel::Avx512 => exp_f32_avx512(x),
SimdLevel::Avx2 => exp_f32_avx2(x),
SimdLevel::Sse => exp_f32_sse(x),
SimdLevel::Scalar => exp_f32_scalar(x),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const SIZES: [usize; 16] = [0, 1, 2, 3, 5, 6, 7, 8, 9, 15, 16, 17, 31, 32, 33, 45];
fn ramp_f64(n: usize) -> Vec<f64> {
(0..n).map(|i| (i as f64) * 0.5 - 3.0).collect()
}
fn ramp_f32(n: usize) -> Vec<f32> {
(0..n).map(|i| (i as f32) * 0.5 - 3.0).collect()
}
#[test]
fn test_dot_tiers_match_scalar_f64() {
for n in SIZES {
let a = ramp_f64(n);
let b: Vec<f64> = ramp_f64(n).iter().map(|v| v * 1.7 + 0.3).collect();
let expected = dot_f64_scalar(&a, &b);
assert_relative_eq!(dot_f64_sse(&a, &b), expected, epsilon = 1e-12);
assert_relative_eq!(dot_f64_avx2(&a, &b), expected, epsilon = 1e-12);
assert_relative_eq!(dot_f64_avx512(&a, &b), expected, epsilon = 1e-12);
assert_relative_eq!(f64::dot_simd(&a, &b), expected, epsilon = 1e-12);
}
}
#[test]
fn test_dot_tiers_match_scalar_f32() {
for n in SIZES {
let a = ramp_f32(n);
let b: Vec<f32> = ramp_f32(n).iter().map(|v| v * 1.7 + 0.3).collect();
let expected = dot_f32_scalar(&a, &b);
assert_relative_eq!(dot_f32_sse(&a, &b), expected, epsilon = 1e-4);
assert_relative_eq!(dot_f32_avx2(&a, &b), expected, epsilon = 1e-4);
assert_relative_eq!(dot_f32_avx512(&a, &b), expected, epsilon = 1e-4);
assert_relative_eq!(f32::dot_simd(&a, &b), expected, epsilon = 1e-4);
}
}
#[test]
fn test_exp_tiers_match_scalar() {
for n in SIZES {
let mut expected = ramp_f64(n);
exp_f64_scalar(&mut expected);
for kernel in [
exp_f64_sse as fn(&mut [f64]),
exp_f64_avx2,
exp_f64_avx512,
f64::exp_in_place_simd,
] {
let mut x = ramp_f64(n);
kernel(&mut x);
for (got, want) in x.iter().zip(expected.iter()) {
assert_relative_eq!(got, want, max_relative = 1e-12);
}
}
let mut expected = ramp_f32(n);
exp_f32_scalar(&mut expected);
for kernel in [
exp_f32_sse as fn(&mut [f32]),
exp_f32_avx2,
exp_f32_avx512,
f32::exp_in_place_simd,
] {
let mut x = ramp_f32(n);
kernel(&mut x);
for (got, want) in x.iter().zip(expected.iter()) {
assert_relative_eq!(got, want, max_relative = 1e-5);
}
}
}
}
#[test]
fn test_dot_truncates_on_length_mismatch() {
let a = vec![1.0_f64; 20];
let b = vec![2.0_f64; 5];
assert_relative_eq!(dot_f64_scalar(&a, &b), 10.0, epsilon = 1e-12);
assert_relative_eq!(dot_f64_sse(&a, &b), 10.0, epsilon = 1e-12);
assert_relative_eq!(dot_f64_avx2(&a, &b), 10.0, epsilon = 1e-12);
assert_relative_eq!(dot_f64_avx512(&a, &b), 10.0, epsilon = 1e-12);
assert_relative_eq!(f64::dot_simd(&a, &b), 10.0, epsilon = 1e-12);
}
#[test]
fn test_detect_simd_level_is_stable() {
assert_eq!(detect_simd_level(), detect_simd_level());
}
}