use std::iter::Sum;
use std::ops::AddAssign;
use std::sync::Arc;
use crate::Error;
use arrow_array::types::{Float16Type, Float64Type, Int8Type};
use arrow_array::{Array, FixedSizeListArray, Float32Array, cast::AsArray, types::Float32Type};
use arrow_schema::DataType;
use half::{bf16, f16};
use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray};
use lance_core::assume_eq;
#[allow(unused_imports)]
use lance_core::utils::cpu::{SIMD_SUPPORT, SimdSupport};
use num_traits::{AsPrimitive, Num, real::Real};
use crate::Result;
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
use crate::distance::BatchIter;
#[inline]
fn dot_scalar<
T: AsPrimitive<Output>,
Output: Real + Sum + AddAssign + 'static,
const LANES: usize,
>(
from: &[T],
to: &[T],
) -> Output {
let x_chunks = to.chunks_exact(LANES);
let y_chunks = from.chunks_exact(LANES);
let sum = if x_chunks.remainder().is_empty() {
Output::zero()
} else {
x_chunks
.remainder()
.iter()
.zip(y_chunks.remainder().iter())
.map(|(&x, &y)| x.as_() * y.as_())
.sum::<Output>()
};
let mut sums = [Output::zero(); LANES];
for (x, y) in x_chunks.zip(y_chunks) {
for i in 0..LANES {
sums[i] += x[i].as_() * y[i].as_();
}
}
sum + sums.iter().copied().sum::<Output>()
}
#[inline]
pub fn dot<T: Dot>(from: &[T], to: &[T]) -> f32 {
T::dot(from, to)
}
#[inline]
pub fn dot_f32(x: &[f32], y: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
use lance_core::utils::cpu::SimdSupport;
if matches!(*SIMD_SUPPORT, SimdSupport::Avx512 | SimdSupport::Avx512FP16) {
return unsafe { dot_f32_avx512(x, y) };
}
}
dot(x, y)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f")]
unsafe fn dot_f32_avx512(x: &[f32], y: &[f32]) -> f32 {
use std::arch::x86_64::*;
debug_assert_eq!(x.len(), y.len());
let n = x.len();
let mut acc = _mm512_setzero_ps();
let mut i = 0usize;
while i + 16 <= n {
let a = _mm512_loadu_ps(x.as_ptr().add(i));
let b = _mm512_loadu_ps(y.as_ptr().add(i));
acc = _mm512_fmadd_ps(a, b, acc);
i += 16;
}
let mut sum = _mm512_reduce_add_ps(acc);
while i < n {
sum += x[i] * y[i];
i += 1;
}
sum
}
#[inline]
pub fn dot_distance<T: Dot>(from: &[T], to: &[T]) -> f32 {
1.0 - T::dot(from, to)
}
pub trait Dot: Num {
fn dot(x: &[Self], y: &[Self]) -> f32;
fn dot_batch<'a>(
x: &'a [Self],
batch: &'a [Self],
dimension: usize,
) -> impl Iterator<Item = f32> + 'a {
batch.chunks_exact(dimension).map(move |y| Self::dot(x, y))
}
}
#[cfg(feature = "fp16kernels")]
mod bf16_kernel {
use half::bf16;
unsafe extern "C" {
#[cfg(target_arch = "aarch64")]
pub fn dot_bf16_neon(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
#[cfg(all(kernel_support = "avx512_bf16", target_arch = "x86_64"))]
pub fn dot_bf16_avx512(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
#[cfg(target_arch = "x86_64")]
pub fn dot_bf16_avx2(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn dot_bf16_lsx(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn dot_bf16_lasx(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
}
}
impl Dot for bf16 {
#[inline]
fn dot(x: &[Self], y: &[Self]) -> f32 {
match *SIMD_SUPPORT {
#[cfg(all(feature = "fp16kernels", target_arch = "aarch64"))]
SimdSupport::Neon => unsafe {
bf16_kernel::dot_bf16_neon(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(
feature = "fp16kernels",
kernel_support = "avx512_bf16",
target_arch = "x86_64"
))]
SimdSupport::Avx512FP16 => unsafe {
bf16_kernel::dot_bf16_avx512(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "x86_64"))]
SimdSupport::Avx2 | SimdSupport::Avx512 => unsafe {
bf16_kernel::dot_bf16_avx2(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lasx => unsafe {
bf16_kernel::dot_bf16_lasx(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lsx => unsafe {
bf16_kernel::dot_bf16_lsx(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
_ => dot_scalar::<Self, f32, 32>(x, y),
}
}
}
#[cfg(feature = "fp16kernels")]
mod kernel {
use super::*;
unsafe extern "C" {
#[cfg(target_arch = "aarch64")]
pub fn dot_f16_neon(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
#[cfg(all(kernel_support = "avx512_f16", target_arch = "x86_64"))]
pub fn dot_f16_avx512(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
#[cfg(target_arch = "x86_64")]
pub fn dot_f16_avx2(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn dot_f16_lsx(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn dot_f16_lasx(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
}
}
impl Dot for f16 {
#[inline]
fn dot(x: &[Self], y: &[Self]) -> f32 {
match *SIMD_SUPPORT {
#[cfg(all(feature = "fp16kernels", target_arch = "aarch64"))]
SimdSupport::Neon => unsafe {
kernel::dot_f16_neon(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(
feature = "fp16kernels",
kernel_support = "avx512_f16",
target_arch = "x86_64"
))]
SimdSupport::Avx512FP16 => unsafe {
kernel::dot_f16_avx512(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "x86_64"))]
SimdSupport::Avx2 => unsafe {
kernel::dot_f16_avx2(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lasx => unsafe {
kernel::dot_f16_lasx(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lsx => unsafe {
kernel::dot_f16_lsx(x.as_ptr(), y.as_ptr(), x.len() as u32)
},
_ => dot_scalar::<Self, f32, 32>(x, y),
}
}
}
impl Dot for f32 {
#[inline]
fn dot(x: &[Self], y: &[Self]) -> f32 {
dot_f32_dispatched(x, y)
}
fn dot_batch<'a>(
x: &'a [Self],
batch: &'a [Self],
dimension: usize,
) -> impl Iterator<Item = Self> + 'a {
#[cfg(all(
target_arch = "x86_64",
target_feature = "avx2",
target_feature = "fma"
))]
{
let narrow = dimension <= 16;
batch.chunks_exact(dimension).map(move |y| {
if narrow {
unsafe { x86::dot_f32_avx_fma(x, y) }
} else {
dot_f32_scalar(x, y)
}
})
}
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
{
dot_batch_f32_runtime_dispatch(x, batch, dimension)
}
#[cfg(not(target_arch = "x86_64"))]
{
batch.chunks_exact(dimension).map(move |y| Self::dot(x, y))
}
}
}
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
#[inline]
fn dot_batch_f32_runtime_dispatch<'a>(
x: &'a [f32],
batch: &'a [f32],
dimension: usize,
) -> impl Iterator<Item = f32> + 'a {
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => {
BatchIter::Eager(unsafe { x86::dot_batch_f32_avx512(x, batch, dimension) }.into_iter())
}
SimdSupport::Avx2 | SimdSupport::AvxFma => {
BatchIter::Eager(unsafe { x86::dot_batch_f32_avx_fma(x, batch, dimension) }.into_iter())
}
SimdSupport::Avx => {
BatchIter::Eager(unsafe { x86::dot_batch_f32_avx(x, batch, dimension) }.into_iter())
}
_ => BatchIter::Lazy(
batch
.chunks_exact(dimension)
.map(move |y| dot_f32_scalar(x, y)),
),
}
}
#[inline]
fn dot_f32_dispatched(x: &[f32], y: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe { x86::dot_f32_avx512(x, y) },
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe { x86::dot_f32_avx_fma(x, y) },
SimdSupport::Avx => unsafe { x86::dot_f32_avx(x, y) },
_ => dot_f32_scalar(x, y),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
dot_f32_scalar(x, y)
}
}
#[inline]
fn dot_f32_scalar(x: &[f32], y: &[f32]) -> f32 {
dot_scalar::<f32, f32, 16>(x, y)
}
impl Dot for f64 {
#[inline]
fn dot(x: &[Self], y: &[Self]) -> f32 {
dot_f64_simd(x, y)
}
}
#[inline]
fn dot_f64_simd(x: &[f64], y: &[f64]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe { x86::dot_f64_avx512(x, y) },
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe { x86::dot_f64_avx_fma(x, y) },
SimdSupport::Avx => unsafe { x86::dot_f64_avx(x, y) },
_ => dot_f64_scalar(x, y),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
dot_f64_simd_other(x, y)
}
}
#[cfg(target_arch = "x86_64")]
#[inline]
fn dot_f64_scalar(x: &[f64], y: &[f64]) -> f32 {
x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum::<f64>() as f32
}
#[cfg(target_arch = "x86_64")]
mod x86 {
use std::arch::x86_64::*;
use crate::simd::f64::{f64x4, f64x8};
use crate::simd::x86::hsum256_ps;
use crate::simd::{FloatSimd, SIMD};
#[cfg(not(all(target_feature = "avx2", target_feature = "fma")))]
#[target_feature(enable = "avx512f")]
pub(super) unsafe fn dot_batch_f32_avx512(
x: &[f32],
batch: &[f32],
dimension: usize,
) -> Vec<f32> {
batch
.chunks_exact(dimension)
.map(|y| unsafe { dot_f32_avx512(x, y) })
.collect()
}
#[cfg(not(all(target_feature = "avx2", target_feature = "fma")))]
#[target_feature(enable = "avx,fma")]
pub(super) unsafe fn dot_batch_f32_avx_fma(
x: &[f32],
batch: &[f32],
dimension: usize,
) -> Vec<f32> {
batch
.chunks_exact(dimension)
.map(|y| unsafe { dot_f32_avx_fma(x, y) })
.collect()
}
#[cfg(not(all(target_feature = "avx2", target_feature = "fma")))]
#[target_feature(enable = "avx")]
pub(super) unsafe fn dot_batch_f32_avx(x: &[f32], batch: &[f32], dimension: usize) -> Vec<f32> {
batch
.chunks_exact(dimension)
.map(|y| unsafe { dot_f32_avx(x, y) })
.collect()
}
#[target_feature(enable = "avx512f")]
pub unsafe fn dot_f64_avx512(x: &[f64], y: &[f64]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut acc = _mm512_setzero_pd();
for i in (0..unrolled_len).step_by(8) {
let a = _mm512_loadu_pd(x.as_ptr().add(i));
let b = _mm512_loadu_pd(y.as_ptr().add(i));
acc = _mm512_fmadd_pd(a, b, acc);
}
let tail: f64 = x[unrolled_len..]
.iter()
.zip(y[unrolled_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
(_mm512_reduce_add_pd(acc) + tail) as f32
}
#[target_feature(enable = "avx,fma")]
pub unsafe fn dot_f64_avx_fma(x: &[f64], y: &[f64]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut acc8 = f64x8::zeros();
for i in (0..unrolled_len).step_by(8) {
let a = f64x8::load_unaligned(x.as_ptr().add(i));
let b = f64x8::load_unaligned(y.as_ptr().add(i));
acc8.multiply_add(a, b);
}
let aligned_len = dim / 4 * 4;
let mut acc4 = f64x4::zeros();
for i in (unrolled_len..aligned_len).step_by(4) {
let a = f64x4::load_unaligned(x.as_ptr().add(i));
let b = f64x4::load_unaligned(y.as_ptr().add(i));
acc4.multiply_add(a, b);
}
let tail: f64 = x[aligned_len..]
.iter()
.zip(y[aligned_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
(acc8.reduce_sum() + acc4.reduce_sum() + tail) as f32
}
#[target_feature(enable = "avx")]
pub unsafe fn dot_f64_avx(x: &[f64], y: &[f64]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 4 * 4;
let mut acc = _mm256_setzero_pd();
for i in (0..unrolled_len).step_by(4) {
let a = _mm256_loadu_pd(x.as_ptr().add(i));
let b = _mm256_loadu_pd(y.as_ptr().add(i));
acc = _mm256_add_pd(acc, _mm256_mul_pd(a, b));
}
let lo = _mm256_castpd256_pd128(acc);
let hi = _mm256_extractf128_pd(acc, 1);
let sum128 = _mm_add_pd(lo, hi);
let sum64 = _mm_add_pd(sum128, _mm_unpackhi_pd(sum128, sum128));
let acc_sum = _mm_cvtsd_f64(sum64);
let tail: f64 = x[unrolled_len..]
.iter()
.zip(y[unrolled_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
(acc_sum + tail) as f32
}
#[target_feature(enable = "avx512f")]
pub unsafe fn dot_f32_avx512(x: &[f32], y: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut acc = _mm512_setzero_ps();
for i in (0..unrolled_len).step_by(16) {
let a = _mm512_loadu_ps(x.as_ptr().add(i));
let b = _mm512_loadu_ps(y.as_ptr().add(i));
acc = _mm512_fmadd_ps(a, b, acc);
}
let tail: f32 = x[unrolled_len..]
.iter()
.zip(y[unrolled_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
_mm512_reduce_add_ps(acc) + tail
}
#[target_feature(enable = "avx,fma")]
pub unsafe fn dot_f32_avx_fma(x: &[f32], y: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut acc = _mm256_setzero_ps();
for i in (0..unrolled_len).step_by(8) {
let a = _mm256_loadu_ps(x.as_ptr().add(i));
let b = _mm256_loadu_ps(y.as_ptr().add(i));
acc = _mm256_fmadd_ps(a, b, acc);
}
let tail: f32 = x[unrolled_len..]
.iter()
.zip(y[unrolled_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
hsum256_ps(acc) + tail
}
#[target_feature(enable = "avx")]
pub unsafe fn dot_f32_avx(x: &[f32], y: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut acc = _mm256_setzero_ps();
for i in (0..unrolled_len).step_by(8) {
let a = _mm256_loadu_ps(x.as_ptr().add(i));
let b = _mm256_loadu_ps(y.as_ptr().add(i));
acc = _mm256_add_ps(acc, _mm256_mul_ps(a, b));
}
let tail: f32 = x[unrolled_len..]
.iter()
.zip(y[unrolled_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
hsum256_ps(acc) + tail
}
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn dot_f64_simd_other(x: &[f64], y: &[f64]) -> f32 {
use crate::simd::f64::{f64x4, f64x8};
use crate::simd::{FloatSimd, SIMD};
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut acc8 = f64x8::zeros();
for i in (0..unrolled_len).step_by(8) {
unsafe {
let a = f64x8::load_unaligned(x.as_ptr().add(i));
let b = f64x8::load_unaligned(y.as_ptr().add(i));
acc8.multiply_add(a, b);
}
}
let aligned_len = dim / 4 * 4;
let mut acc4 = f64x4::zeros();
for i in (unrolled_len..aligned_len).step_by(4) {
unsafe {
let a = f64x4::load_unaligned(x.as_ptr().add(i));
let b = f64x4::load_unaligned(y.as_ptr().add(i));
acc4.multiply_add(a, b);
}
}
let tail: f64 = x[aligned_len..]
.iter()
.zip(y[aligned_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
(acc8.reduce_sum() + acc4.reduce_sum() + tail) as f32
}
impl Dot for u8 {
#[inline]
fn dot(x: &[Self], y: &[Self]) -> f32 {
super::dot_u8::dot_u8(x, y) as f32
}
}
pub fn dot_distance_batch<'a, T: Dot>(
from: &'a [T],
to: &'a [T],
dimension: usize,
) -> Box<dyn Iterator<Item = f32> + 'a> {
assume_eq!(from.len(), dimension);
assume_eq!(to.len() % dimension, 0);
Box::new(T::dot_batch(from, to, dimension).map(|d| 1.0 - d))
}
fn do_dot_distance_arrow_batch<T: ArrowFloatType>(
from: &T::ArrayType,
to: &FixedSizeListArray,
) -> Result<Arc<Float32Array>>
where
T::Native: Dot,
{
let dimension = to.value_length() as usize;
debug_assert_eq!(from.len(), dimension);
let to_values =
to.values()
.as_any()
.downcast_ref::<T::ArrayType>()
.ok_or(Error::InvalidArgumentError(format!(
"Invalid type: expect {:?} got {:?}",
from.data_type(),
to.value_type()
)))?;
let dists = dot_distance_batch(from.as_slice(), to_values.as_slice(), dimension);
Ok(Arc::new(Float32Array::new(
dists.collect(),
to.nulls().cloned(),
)))
}
pub fn dot_distance_arrow_batch(
from: &dyn Array,
to: &FixedSizeListArray,
) -> Result<Arc<Float32Array>> {
let dimension = to.value_length() as usize;
debug_assert_eq!(from.len(), dimension);
match *from.data_type() {
DataType::Float16 => do_dot_distance_arrow_batch::<Float16Type>(from.as_primitive(), to),
DataType::Float32 => do_dot_distance_arrow_batch::<Float32Type>(from.as_primitive(), to),
DataType::Float64 => do_dot_distance_arrow_batch::<Float64Type>(from.as_primitive(), to),
DataType::Int8 => do_dot_distance_arrow_batch::<Float32Type>(
&from
.as_primitive::<Int8Type>()
.into_iter()
.map(|x| x.unwrap() as f32)
.collect(),
&to.convert_to_floating_point()?,
),
_ => Err(Error::InvalidArgumentError(format!(
"Unsupported data type: {:?}",
from.data_type()
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{
arbitrary_bf16, arbitrary_f16, arbitrary_f32, arbitrary_f64, arbitrary_vector_pair,
};
use num_traits::{Float, FromPrimitive};
use proptest::prelude::*;
#[test]
fn test_dot_f32_dispatch_matches_scalar() {
use approx::assert_relative_eq;
for dim in [1usize, 7, 15, 16, 17, 31, 33, 64, 100, 1024] {
let x: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.5 - 3.0).collect();
let y: Vec<f32> = (0..dim).map(|i| (i as f32) * -0.25 + 1.5).collect();
assert_relative_eq!(dot_f32(&x, &y), dot(&x, &y), max_relative = 1e-5);
}
}
#[test]
fn test_dot() {
let x: Vec<f32> = (0..20).map(|v| v as f32).collect();
let y: Vec<f32> = (100..120).map(|v| v as f32).collect();
assert_eq!(f32::dot(&x, &y), dot(&x, &y));
let x: Vec<f32> = (0..512).map(|v| v as f32).collect();
let y: Vec<f32> = (100..612).map(|v| v as f32).collect();
assert_eq!(f32::dot(&x, &y), dot(&x, &y));
let x: Vec<f16> = (0..20).map(|v| f16::from_i32(v).unwrap()).collect();
let y: Vec<f16> = (100..120).map(|v| f16::from_i32(v).unwrap()).collect();
assert_eq!(f16::dot(&x, &y), dot(&x, &y));
let x: Vec<f64> = (20..40).map(|v| f64::from_i32(v).unwrap()).collect();
let y: Vec<f64> = (120..140).map(|v| f64::from_i32(v).unwrap()).collect();
assert_eq!(f64::dot(&x, &y), dot(&x, &y));
}
fn dot_scalar_ref(x: &[f64], y: &[f64]) -> f32 {
x.iter().zip(y.iter()).map(|(&x, &y)| x * y).sum::<f64>() as f32
}
fn max_error<T: Float + AsPrimitive<f64>>(x: &[f64], y: &[f64]) -> f32 {
let dot = x
.iter()
.cloned()
.zip(y.iter().cloned())
.map(|(x, y)| x.abs() * y.abs())
.sum::<f64>();
let k = ((2 * x.len()) - 1) as f64;
let k_epsilon = k * T::epsilon().as_();
let error = if k_epsilon < 1.0 {
k_epsilon * dot
} else {
2.0 * T::epsilon().as_() * dot
};
let subnormal_rounding_floor = x.len() as f64 * f64::from(f32::from_bits(1));
error.max(subnormal_rounding_floor) as f32
}
fn do_dot_test<T: Dot + AsPrimitive<f64> + Float>(
x: &[T],
y: &[T],
) -> std::result::Result<(), TestCaseError> {
let f64_x = x.iter().map(|&v| v.as_()).collect::<Vec<f64>>();
let f64_y = y.iter().map(|&v| v.as_()).collect::<Vec<f64>>();
let expected = dot_scalar_ref(&f64_x, &f64_y);
let result = dot(x, y);
let max_error = max_error::<T>(&f64_x, &f64_y);
prop_assert!(approx::relative_eq!(expected, result, epsilon = max_error));
Ok(())
}
proptest::proptest! {
#[test]
fn test_dot_f16((x, y) in arbitrary_vector_pair(arbitrary_f16, 4..4048)) {
do_dot_test(&x, &y)?;
}
#[test]
fn test_dot_bf16((x, y) in arbitrary_vector_pair(arbitrary_bf16, 4..4048)){
do_dot_test(&x, &y)?;
}
#[test]
fn test_dot_f32((x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)){
do_dot_test(&x, &y)?;
}
#[test]
fn test_dot_f64((x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)){
do_dot_test(&x, &y)?;
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f64_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
let scalar = dot_f64_scalar(&x, &y);
let simd = dot_f64_simd(&x, &y);
let max_error = max_error::<f64>(&x, &y);
prop_assert!(approx::relative_eq!(scalar, simd, epsilon = max_error));
}
#[test]
fn test_dot_f32_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
let x_f64: Vec<f64> = x.iter().map(|&v| v as f64).collect();
let y_f64: Vec<f64> = y.iter().map(|&v| v as f64).collect();
let scalar = x_f64
.iter()
.zip(y_f64.iter())
.map(|(&a, &b)| a * b)
.sum::<f64>() as f32;
let simd = <f32 as Dot>::dot(&x, &y);
let max_error = max_error::<f32>(&x_f64, &y_f64);
prop_assert!(approx::relative_eq!(scalar, simd, epsilon = max_error));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f32_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
let scalar = dot_f32_scalar(&x, &y);
let avx512 = unsafe { x86::dot_f32_avx512(&x, &y) };
let x_f64: Vec<f64> = x.iter().map(|&v| v as f64).collect();
let y_f64: Vec<f64> = y.iter().map(|&v| v as f64).collect();
let max_error = max_error::<f32>(&x_f64, &y_f64);
prop_assert!(approx::relative_eq!(scalar, avx512, epsilon = max_error));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f32_scalar_vs_avx_fma_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
return Ok(());
}
let scalar = dot_f32_scalar(&x, &y);
let avx_fma = unsafe { x86::dot_f32_avx_fma(&x, &y) };
let x_f64: Vec<f64> = x.iter().map(|&v| v as f64).collect();
let y_f64: Vec<f64> = y.iter().map(|&v| v as f64).collect();
let max_error = max_error::<f32>(&x_f64, &y_f64);
prop_assert!(approx::relative_eq!(scalar, avx_fma, epsilon = max_error));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f32_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
let scalar = dot_f32_scalar(&x, &y);
let avx = unsafe { x86::dot_f32_avx(&x, &y) };
let x_f64: Vec<f64> = x.iter().map(|&v| v as f64).collect();
let y_f64: Vec<f64> = y.iter().map(|&v| v as f64).collect();
let max_error = max_error::<f32>(&x_f64, &y_f64);
prop_assert!(approx::relative_eq!(scalar, avx, epsilon = max_error));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f64_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
let scalar = dot_f64_scalar(&x, &y);
let avx512 = unsafe { x86::dot_f64_avx512(&x, &y) };
let max_error = max_error::<f64>(&x, &y);
prop_assert!(approx::relative_eq!(scalar, avx512, epsilon = max_error));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f64_scalar_vs_avx_fma_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
return Ok(());
}
let scalar = dot_f64_scalar(&x, &y);
let avx_fma = unsafe { x86::dot_f64_avx_fma(&x, &y) };
let max_error = max_error::<f64>(&x, &y);
prop_assert!(approx::relative_eq!(scalar, avx_fma, epsilon = max_error));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_dot_f64_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
let scalar = dot_f64_scalar(&x, &y);
let avx = unsafe { x86::dot_f64_avx(&x, &y) };
let max_error = max_error::<f64>(&x, &y);
prop_assert!(approx::relative_eq!(scalar, avx, epsilon = max_error));
}
}
#[rstest::rstest]
#[case::dim_8(8)]
#[case::dim_16(16)]
#[case::dim_32(32)]
#[case::dim_1024(1024)]
fn test_dot_batch_f32_matches_per_vector_dot(#[case] dimension: usize) {
let num_vectors = 5;
let x: Vec<f32> = (0..dimension)
.map(|i| ((i % 13) as f32) * 0.25 + 1.0)
.collect();
let batch: Vec<f32> = (0..dimension * num_vectors)
.map(|i| ((i % 11) as f32) * 0.5 - 2.0)
.collect();
let got: Vec<f32> = f32::dot_batch(&x, &batch, dimension).collect();
let want: Vec<f32> = batch
.chunks_exact(dimension)
.map(|y| f32::dot(&x, y))
.collect();
assert_eq!(got.len(), num_vectors);
for (g, w) in got.iter().zip(want.iter()) {
assert!(
approx::relative_eq!(g, w, epsilon = 1e-4),
"dim {dimension}: batch {g} != per-vector {w}"
);
}
}
#[test]
fn test_dot_distance_batch_preserves_distance_semantics() {
let dimension = 32;
let x: Vec<f32> = (0..dimension).map(|i| (i as f32) * 0.1).collect();
let batch: Vec<f32> = (0..dimension * 3).map(|i| (i as f32) * 0.05).collect();
let got: Vec<f32> = dot_distance_batch(&x, &batch, dimension).collect();
for (chunk, &g) in batch.chunks_exact(dimension).zip(got.iter()) {
assert!(approx::relative_eq!(
g,
1.0 - f32::dot(&x, chunk),
epsilon = 1e-5
));
}
}
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
fn check_dot_batch_kernel(kernel: unsafe fn(&[f32], &[f32], usize) -> Vec<f32>) {
for dimension in [8_usize, 16, 40] {
let num_vectors = 3;
let x: Vec<f32> = (0..dimension).map(|i| (i as f32) * 0.5 + 1.0).collect();
let batch: Vec<f32> = (0..dimension * num_vectors)
.map(|i| ((i % 7) as f32) + 1.0)
.collect();
let got = unsafe { kernel(&x, &batch, dimension) };
assert_eq!(got.len(), num_vectors);
for (chunk, &g) in batch.chunks_exact(dimension).zip(got.iter()) {
let want = dot_scalar::<f32, f32, 16>(&x, chunk);
assert!(
approx::relative_eq!(g, want, epsilon = 1e-4),
"dim {dimension}: kernel {g} != scalar {want}"
);
}
}
}
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
#[test]
fn test_dot_batch_avx_fma_matches_scalar() {
if !std::is_x86_feature_detected!("avx") || !std::is_x86_feature_detected!("fma") {
return;
}
check_dot_batch_kernel(x86::dot_batch_f32_avx_fma);
}
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
#[test]
fn test_dot_batch_avx_matches_scalar() {
if !std::is_x86_feature_detected!("avx") {
return;
}
check_dot_batch_kernel(x86::dot_batch_f32_avx);
}
#[cfg(all(
target_arch = "x86_64",
not(all(target_feature = "avx2", target_feature = "fma"))
))]
#[test]
fn test_dot_batch_avx512_matches_scalar() {
if !std::is_x86_feature_detected!("avx512f") {
return;
}
check_dot_batch_kernel(x86::dot_batch_f32_avx512);
}
}