use std::sync::OnceLock;
use crate::scalar;
#[cfg(target_arch = "x86_64")]
use crate::{avx2, avx512};
#[cfg(target_arch = "aarch64")]
use crate::neon;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
Scalar,
Avx2,
Avx512,
Neon,
}
impl Tier {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Tier::Scalar => "scalar",
Tier::Avx2 => "avx2",
Tier::Avx512 => "avx512",
Tier::Neon => "neon",
}
}
}
#[must_use]
pub fn detect_tier() -> Tier {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx512f") {
return Tier::Avx512;
}
if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
return Tier::Avx2;
}
}
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
return Tier::Neon;
}
}
Tier::Scalar
}
#[must_use]
pub fn active_tier() -> Tier {
static TIER: OnceLock<Tier> = OnceLock::new();
*TIER.get_or_init(detect_tier)
}
pub type BinaryKernel = fn(&[f32], &[f32]) -> f32;
#[cfg(target_arch = "x86_64")]
fn l2_sq_avx2(a: &[f32], b: &[f32]) -> f32 {
unsafe { avx2::l2_sq(a, b) }
}
#[cfg(target_arch = "x86_64")]
fn l2_sq_avx512(a: &[f32], b: &[f32]) -> f32 {
unsafe { avx512::l2_sq(a, b) }
}
#[cfg(target_arch = "x86_64")]
fn dot_avx2(a: &[f32], b: &[f32]) -> f32 {
unsafe { avx2::dot(a, b) }
}
#[cfg(target_arch = "x86_64")]
fn dot_avx512(a: &[f32], b: &[f32]) -> f32 {
unsafe { avx512::dot(a, b) }
}
#[cfg(target_arch = "aarch64")]
fn l2_sq_neon(a: &[f32], b: &[f32]) -> f32 {
unsafe { neon::l2_sq(a, b) }
}
#[cfg(target_arch = "aarch64")]
fn dot_neon(a: &[f32], b: &[f32]) -> f32 {
unsafe { neon::dot(a, b) }
}
fn resolve_l2_sq() -> BinaryKernel {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => l2_sq_avx512,
#[cfg(target_arch = "x86_64")]
Tier::Avx2 => l2_sq_avx2,
#[cfg(target_arch = "aarch64")]
Tier::Neon => l2_sq_neon,
_ => scalar::l2_sq,
}
}
fn resolve_dot() -> BinaryKernel {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => dot_avx512,
#[cfg(target_arch = "x86_64")]
Tier::Avx2 => dot_avx2,
#[cfg(target_arch = "aarch64")]
Tier::Neon => dot_neon,
_ => scalar::dot,
}
}
#[must_use]
pub fn l2_sq_kernel() -> BinaryKernel {
static K: OnceLock<BinaryKernel> = OnceLock::new();
*K.get_or_init(resolve_l2_sq)
}
#[must_use]
pub fn dot_kernel() -> BinaryKernel {
static K: OnceLock<BinaryKernel> = OnceLock::new();
*K.get_or_init(resolve_dot)
}
#[must_use]
pub fn cosine_parts(a: &[f32], b: &[f32]) -> (f32, f32, f32) {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => unsafe { avx512::cosine_parts(a, b) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => unsafe { avx2::cosine_parts(a, b) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => unsafe { neon::cosine_parts(a, b) }, _ => {
let mut d = 0.0f32;
let mut na = 0.0f32;
let mut nb = 0.0f32;
for i in 0..a.len() {
d += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
(d, na, nb)
}
}
}
#[must_use]
fn f16_simd_ok() -> bool {
static OK: OnceLock<bool> = OnceLock::new();
*OK.get_or_init(|| match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => true,
#[cfg(target_arch = "x86_64")]
Tier::Avx2 => is_x86_feature_detected!("f16c"),
#[cfg(target_arch = "aarch64")]
Tier::Neon => std::arch::is_aarch64_feature_detected!("fp16"),
_ => false,
})
}
#[must_use]
pub fn l2_sq_f16(query: &[f32], stored: &[u8]) -> f32 {
if f16_simd_ok() {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => return unsafe { avx512::l2_sq_f16(query, stored) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => return unsafe { avx2::l2_sq_f16(query, stored) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => return unsafe { neon::l2_sq_f16(query, stored) }, _ => {}
}
}
scalar::l2_sq_f16(query, stored)
}
#[must_use]
pub fn dot_f16(query: &[f32], stored: &[u8]) -> f32 {
if f16_simd_ok() {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => return unsafe { avx512::dot_f16(query, stored) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => return unsafe { avx2::dot_f16(query, stored) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => return unsafe { neon::dot_f16(query, stored) }, _ => {}
}
}
scalar::dot_f16(query, stored)
}
#[must_use]
pub fn cosine_parts_f16(query: &[f32], stored: &[u8]) -> (f32, f32, f32) {
if f16_simd_ok() {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => return unsafe { avx512::cosine_parts_f16(query, stored) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => return unsafe { avx2::cosine_parts_f16(query, stored) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => return unsafe { neon::cosine_parts_f16(query, stored) }, _ => {}
}
}
scalar::cosine_parts_f16(query, stored)
}
#[must_use]
pub fn l2_sq_i8(query: &[f32], scale: f32, codes: &[i8]) -> f32 {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => unsafe { avx512::l2_sq_i8(query, scale, codes) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => unsafe { avx2::l2_sq_i8(query, scale, codes) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => unsafe { neon::l2_sq_i8(query, scale, codes) }, _ => scalar::l2_sq_i8(query, scale, codes),
}
}
#[must_use]
pub fn dot_i8(query: &[f32], scale: f32, codes: &[i8]) -> f32 {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => unsafe { avx512::dot_i8(query, scale, codes) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => unsafe { avx2::dot_i8(query, scale, codes) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => unsafe { neon::dot_i8(query, scale, codes) }, _ => scalar::dot_i8(query, scale, codes),
}
}
#[must_use]
pub fn cosine_parts_i8(query: &[f32], scale: f32, codes: &[i8]) -> (f32, f32, f32) {
match active_tier() {
#[cfg(target_arch = "x86_64")]
Tier::Avx512 => unsafe { avx512::cosine_parts_i8(query, scale, codes) }, #[cfg(target_arch = "x86_64")]
Tier::Avx2 => unsafe { avx2::cosine_parts_i8(query, scale, codes) }, #[cfg(target_arch = "aarch64")]
Tier::Neon => unsafe { neon::cosine_parts_i8(query, scale, codes) }, _ => scalar::cosine_parts_i8(query, scale, codes),
}
}