#[cfg(target_arch = "x86_64")]
mod avx2;
#[cfg(target_arch = "aarch64")]
mod neon;
mod scalar;
#[cfg(test)]
mod tests;
use std::sync::OnceLock;
use urna_format::Int8EmbeddingsView;
pub use scalar::{
dot_f32_f16_scalar, dot_f32_i4_blocked_scalar, dot_f32_i8_scalar, dot_f32_scalar,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SimdBackend {
Scalar,
Avx2,
Neon,
}
impl SimdBackend {
pub fn name(self) -> &'static str {
match self {
Self::Scalar => "scalar",
Self::Avx2 => "avx2",
Self::Neon => "neon",
}
}
}
static BACKEND: OnceLock<SimdBackend> = OnceLock::new();
pub fn detect_backend() -> SimdBackend {
*BACKEND.get_or_init(|| {
if std::env::var("URNA_FORCE_SCALAR")
.map(|v| v != "0")
.unwrap_or(false)
{
return SimdBackend::Scalar;
}
#[cfg(target_arch = "x86_64")]
{
if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("fma") {
return SimdBackend::Avx2;
}
}
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
return SimdBackend::Neon;
}
}
SimdBackend::Scalar
})
}
#[inline]
pub fn dot_f32_bytes(q: &[f32], row_bytes: &[u8]) -> f32 {
assert_eq!(
row_bytes.len(),
q.len() * 4,
"dot_f32_bytes: row byte length must be 4 * query dim"
);
match detect_backend() {
#[cfg(target_arch = "x86_64")]
SimdBackend::Avx2 => unsafe { avx2::dot_f32_avx2(q, row_bytes) },
#[cfg(target_arch = "aarch64")]
SimdBackend::Neon => unsafe { neon::dot_f32_neon(q, row_bytes) },
_ => scalar::dot_f32_scalar(q, row_bytes),
}
}
#[inline]
pub fn dot_f32_f16_bytes(q: &[f32], row_bytes: &[u8]) -> f32 {
assert_eq!(
row_bytes.len(),
q.len() * 2,
"dot_f32_f16_bytes: row byte length must be 2 * query dim"
);
match detect_backend() {
#[cfg(all(target_arch = "aarch64", neon_f16))]
SimdBackend::Neon => unsafe { neon::dot_f32_f16_neon(q, row_bytes) },
_ => scalar::dot_f32_f16_scalar(q, row_bytes),
}
}
#[inline]
pub fn dot_f32_i8(q: &[f32], row: &[i8], scale: f32) -> f32 {
assert_eq!(
row.len(),
q.len(),
"dot_f32_i8: int8 row length must equal query dim"
);
let acc = match detect_backend() {
#[cfg(target_arch = "x86_64")]
SimdBackend::Avx2 => unsafe { avx2::dot_f32_i8_avx2(q, row) },
#[cfg(target_arch = "aarch64")]
SimdBackend::Neon => unsafe { neon::dot_f32_i8_neon(q, row) },
_ => scalar::dot_f32_i8_scalar(q, row),
};
acc * scale
}
#[inline]
pub fn dot_f32_i4_blocked(
q: &[f32],
codes: &[u8],
group_scales: &[f32],
dim: usize,
block: usize,
scratch: &mut [f32],
) -> f32 {
assert!(
block > 0 && dim % block == 0,
"int4: dim must be a multiple of block"
);
assert_eq!(q.len(), dim, "int4: query length must equal dim");
assert_eq!(
codes.len(),
dim / 2,
"int4: packed codes must be dim/2 bytes"
);
assert_eq!(group_scales.len(), dim / block, "int4: one scale per block");
assert_eq!(scratch.len(), dim, "int4: scratch row must be dim long");
match detect_backend() {
#[cfg(target_arch = "x86_64")]
SimdBackend::Avx2 => unsafe {
avx2::dot_f32_i4_avx2(q, codes, group_scales, dim, block, scratch)
},
#[cfg(target_arch = "aarch64")]
SimdBackend::Neon => unsafe {
neon::dot_f32_i4_neon(q, codes, group_scales, dim, block, scratch)
},
_ => scalar::dot_f32_i4_blocked_scalar(q, codes, group_scales, dim, block),
}
}
pub fn score_int8_section(q: &[f32], view: &Int8EmbeddingsView<'_>, out: &mut [f32]) {
assert_eq!(
out.len(),
view.n,
"score_int8_section: one output slot per row"
);
for (i, slot) in out.iter_mut().enumerate().take(view.n) {
let scale = view.scale(i);
let row = view.row(i);
*slot = dot_f32_i8(q, row, scale);
}
}