use std::sync::Arc;
use arrow_array::{
Array, FixedSizeListArray, Float32Array,
cast::AsArray,
types::{Float16Type, Float32Type, Float64Type, Int8Type},
};
use arrow_schema::DataType;
use half::{bf16, f16};
use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray};
#[allow(unused_imports)]
use lance_core::utils::cpu::{SIMD_SUPPORT, SimdSupport};
use super::{Dot, norm_l2::norm_l2};
use super::{Normalize, dot::dot};
#[allow(unused_imports)]
use crate::simd::{
FloatSimd, SIMD,
f32::{f32x8, f32x16},
};
use crate::{Error, Result};
pub trait Cosine: Dot + Normalize {
#[inline]
fn cosine(x: &[Self], other: &[Self]) -> f32 {
let x_norm = norm_l2(x);
Self::cosine_fast(x, x_norm, other)
}
#[inline]
fn cosine_fast(x: &[Self], x_norm: f32, y: &[Self]) -> f32 {
cosine_scalar(x, x_norm, y)
}
#[inline]
fn cosine_with_norms(x: &[Self], x_norm: f32, y_norm: f32, y: &[Self]) -> f32 {
cosine_scalar_fast(x, x_norm, y, y_norm)
}
fn cosine_batch<'a>(
x: &'a [Self],
batch: &'a [Self],
dimension: usize,
) -> Box<dyn Iterator<Item = f32> + 'a> {
let x_norm = norm_l2(x);
Box::new(
batch
.chunks_exact(dimension)
.map(move |y| Self::cosine_fast(x, x_norm, y)),
)
}
}
impl Cosine for u8 {
#[inline]
fn cosine(x: &[Self], other: &[Self]) -> f32 {
super::cosine_u8::cosine_u8(x, other)
}
}
#[cfg(feature = "fp16kernels")]
mod bf16_kernel {
use half::bf16;
unsafe extern "C" {
#[cfg(target_arch = "aarch64")]
pub fn cosine_bf16_neon(x: *const bf16, x_norm: f32, y: *const bf16, dimension: u32)
-> f32;
#[cfg(all(kernel_support = "avx512_bf16", target_arch = "x86_64"))]
pub fn cosine_bf16_avx512(
x: *const bf16,
x_norm: f32,
y: *const bf16,
dimension: u32,
) -> f32;
#[cfg(target_arch = "x86_64")]
pub fn cosine_bf16_avx2(x: *const bf16, x_norm: f32, y: *const bf16, dimension: u32)
-> f32;
#[cfg(target_arch = "loongarch64")]
pub fn cosine_bf16_lsx(x: *const bf16, x_norm: f32, y: *const bf16, dimension: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn cosine_bf16_lasx(x: *const bf16, x_norm: f32, y: *const bf16, dimension: u32)
-> f32;
}
}
impl Cosine for bf16 {
fn cosine_fast(x: &[Self], x_norm: f32, y: &[Self]) -> f32 {
match *SIMD_SUPPORT {
#[cfg(all(feature = "fp16kernels", target_arch = "aarch64"))]
SimdSupport::Neon => unsafe {
bf16_kernel::cosine_bf16_neon(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(
feature = "fp16kernels",
kernel_support = "avx512_bf16",
target_arch = "x86_64"
))]
SimdSupport::Avx512FP16 => unsafe {
bf16_kernel::cosine_bf16_avx512(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "x86_64"))]
SimdSupport::Avx2 | SimdSupport::Avx512 => unsafe {
bf16_kernel::cosine_bf16_avx2(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lasx => unsafe {
bf16_kernel::cosine_bf16_lasx(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lsx => unsafe {
bf16_kernel::cosine_bf16_lsx(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
_ => cosine_scalar(x, x_norm, y),
}
}
}
#[cfg(feature = "fp16kernels")]
mod kernel {
use super::*;
unsafe extern "C" {
#[cfg(target_arch = "aarch64")]
pub fn cosine_f16_neon(x: *const f16, x_norm: f32, y: *const f16, dimension: u32) -> f32;
#[cfg(all(kernel_support = "avx512_f16", target_arch = "x86_64"))]
pub fn cosine_f16_avx512(x: *const f16, x_norm: f32, y: *const f16, dimension: u32) -> f32;
#[cfg(target_arch = "x86_64")]
pub fn cosine_f16_avx2(x: *const f16, x_norm: f32, y: *const f16, dimension: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn cosine_f16_lsx(x: *const f16, x_norm: f32, y: *const f16, dimension: u32) -> f32;
#[cfg(target_arch = "loongarch64")]
pub fn cosine_f16_lasx(x: *const f16, x_norm: f32, y: *const f16, dimension: u32) -> f32;
}
}
impl Cosine for f16 {
fn cosine_fast(x: &[Self], x_norm: f32, y: &[Self]) -> f32 {
match *SIMD_SUPPORT {
#[cfg(all(feature = "fp16kernels", target_arch = "aarch64"))]
SimdSupport::Neon => unsafe {
kernel::cosine_f16_neon(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(
feature = "fp16kernels",
kernel_support = "avx512_f16",
target_arch = "x86_64"
))]
SimdSupport::Avx512FP16 => unsafe {
kernel::cosine_f16_avx512(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "x86_64"))]
SimdSupport::Avx2 => unsafe {
kernel::cosine_f16_avx2(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lasx => unsafe {
kernel::cosine_f16_lasx(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
#[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
SimdSupport::Lsx => unsafe {
kernel::cosine_f16_lsx(x.as_ptr(), x_norm, y.as_ptr(), y.len() as u32)
},
_ => cosine_scalar(x, x_norm, y),
}
}
}
mod f32 {
use super::*;
#[inline]
pub(super) fn cosine_once_8(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe {
cosine_once_x86::cosine_once_8_avx512(x, x_norm, y)
},
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe {
cosine_once_x86::cosine_once_8_avx_fma(x, x_norm, y)
},
SimdSupport::Avx => unsafe { cosine_once_x86::cosine_once_8_avx(x, x_norm, y) },
_ => cosine_once_8_scalar(x, x_norm, y),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
cosine_once_8_other(x, x_norm, y)
}
}
#[inline]
pub(super) fn cosine_once_16(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe {
cosine_once_x86::cosine_once_16_avx512(x, x_norm, y)
},
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe {
cosine_once_x86::cosine_once_16_avx_fma(x, x_norm, y)
},
SimdSupport::Avx => unsafe { cosine_once_x86::cosine_once_16_avx(x, x_norm, y) },
_ => cosine_once_16_scalar(x, x_norm, y),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
cosine_once_16_other(x, x_norm, y)
}
}
#[cfg(target_arch = "x86_64")]
#[inline]
pub(super) fn cosine_once_8_scalar(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let mut xy = 0.0f32;
let mut y2 = 0.0f32;
for i in 0..8 {
xy += x[i] * y[i];
y2 += y[i] * y[i];
}
1.0 - xy / x_norm / y2.sqrt()
}
#[cfg(target_arch = "x86_64")]
#[inline]
pub(super) fn cosine_once_16_scalar(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let mut xy = 0.0f32;
let mut y2 = 0.0f32;
for i in 0..16 {
xy += x[i] * y[i];
y2 += y[i] * y[i];
}
1.0 - xy / x_norm / y2.sqrt()
}
#[cfg(target_arch = "x86_64")]
pub(super) mod cosine_once_x86 {
use std::arch::x86_64::*;
use super::{f32x8, f32x16};
use crate::simd::SIMD;
#[target_feature(enable = "avx,fma")]
pub unsafe fn cosine_once_8_avx_fma(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = f32x8::load_unaligned(x.as_ptr());
let yv = f32x8::load_unaligned(y.as_ptr());
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
#[target_feature(enable = "avx,fma")]
pub unsafe fn cosine_once_16_avx_fma(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = f32x16::load_unaligned(x.as_ptr());
let yv = f32x16::load_unaligned(y.as_ptr());
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
#[target_feature(enable = "avx")]
pub unsafe fn cosine_once_8_avx(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = f32x8::load_unaligned(x.as_ptr());
let yv = f32x8::load_unaligned(y.as_ptr());
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
#[target_feature(enable = "avx")]
pub unsafe fn cosine_once_16_avx(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = f32x16::load_unaligned(x.as_ptr());
let yv = f32x16::load_unaligned(y.as_ptr());
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
#[target_feature(enable = "avx512f")]
pub unsafe fn cosine_once_8_avx512(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let mask: __mmask16 = 0x00FF;
let xv = _mm512_maskz_loadu_ps(mask, x.as_ptr());
let yv = _mm512_maskz_loadu_ps(mask, y.as_ptr());
let xy = _mm512_mul_ps(xv, yv);
let y2 = _mm512_mul_ps(yv, yv);
let xy_sum = _mm512_reduce_add_ps(xy);
let y2_sum = _mm512_reduce_add_ps(y2);
1.0 - xy_sum / x_norm / y2_sum.sqrt()
}
#[target_feature(enable = "avx512f")]
pub unsafe fn cosine_once_16_avx512(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = _mm512_loadu_ps(x.as_ptr());
let yv = _mm512_loadu_ps(y.as_ptr());
let xy = _mm512_mul_ps(xv, yv);
let y2 = _mm512_mul_ps(yv, yv);
let xy_sum = _mm512_reduce_add_ps(xy);
let y2_sum = _mm512_reduce_add_ps(y2);
1.0 - xy_sum / x_norm / y2_sum.sqrt()
}
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn cosine_once_8_other(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = unsafe { f32x8::load_unaligned(x.as_ptr()) };
let yv = unsafe { f32x8::load_unaligned(y.as_ptr()) };
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn cosine_once_16_other(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
let xv = unsafe { f32x16::load_unaligned(x.as_ptr()) };
let yv = unsafe { f32x16::load_unaligned(y.as_ptr()) };
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx,fma")]
pub(super) unsafe fn cosine_batch_avx_fma(
x: &[f32],
x_norm: f32,
batch: &[f32],
dimension: usize,
) -> Vec<f32> {
match dimension {
8 => batch
.chunks_exact(8)
.map(|y| unsafe { cosine_once_x86::cosine_once_8_avx_fma(x, x_norm, y) })
.collect(),
16 => batch
.chunks_exact(16)
.map(|y| unsafe { cosine_once_x86::cosine_once_16_avx_fma(x, x_norm, y) })
.collect(),
_ => batch
.chunks_exact(dimension)
.map(|y| unsafe { super::f32_x86::cosine_fast_avx_fma(x, x_norm, y) })
.collect(),
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f")]
pub(super) unsafe fn cosine_batch_avx512(
x: &[f32],
x_norm: f32,
batch: &[f32],
dimension: usize,
) -> Vec<f32> {
match dimension {
8 => batch
.chunks_exact(8)
.map(|y| unsafe { cosine_once_x86::cosine_once_8_avx512(x, x_norm, y) })
.collect(),
16 => batch
.chunks_exact(16)
.map(|y| unsafe { cosine_once_x86::cosine_once_16_avx512(x, x_norm, y) })
.collect(),
_ => batch
.chunks_exact(dimension)
.map(|y| unsafe { super::f32_x86::cosine_fast_avx512(x, x_norm, y) })
.collect(),
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx")]
pub(super) unsafe fn cosine_batch_avx(
x: &[f32],
x_norm: f32,
batch: &[f32],
dimension: usize,
) -> Vec<f32> {
match dimension {
8 => batch
.chunks_exact(8)
.map(|y| unsafe { cosine_once_x86::cosine_once_8_avx(x, x_norm, y) })
.collect(),
16 => batch
.chunks_exact(16)
.map(|y| unsafe { cosine_once_x86::cosine_once_16_avx(x, x_norm, y) })
.collect(),
_ => batch
.chunks_exact(dimension)
.map(|y| unsafe { super::f32_x86::cosine_fast_avx(x, x_norm, y) })
.collect(),
}
}
}
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
mod f32_baseline {
use super::{dot, f32x8, f32x16, norm_l2};
use crate::simd::{FloatSimd, SIMD};
#[inline]
pub fn cosine_once_8(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
unsafe {
let xv = f32x8::load_unaligned(x.as_ptr());
let yv = f32x8::load_unaligned(y.as_ptr());
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
}
#[inline]
pub fn cosine_once_16(x: &[f32], x_norm: f32, y: &[f32]) -> f32 {
unsafe {
let xv = f32x16::load_unaligned(x.as_ptr());
let yv = f32x16::load_unaligned(y.as_ptr());
let y2 = yv * yv;
let xy = xv * yv;
1.0 - xy.reduce_sum() / x_norm / y2.reduce_sum().sqrt()
}
}
#[inline]
pub fn cosine_fast(x: &[f32], x_norm: f32, other: &[f32]) -> f32 {
unsafe {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut y_norm16 = f32x16::zeros();
let mut xy16 = f32x16::zeros();
for i in (0..unrolled_len).step_by(16) {
let xv = f32x16::load_unaligned(x.as_ptr().add(i));
let yv = f32x16::load_unaligned(other.as_ptr().add(i));
xy16.multiply_add(xv, yv);
y_norm16.multiply_add(yv, yv);
}
let aligned_len = dim / 8 * 8;
let mut y_norm8 = f32x8::zeros();
let mut xy8 = f32x8::zeros();
for i in (unrolled_len..aligned_len).step_by(8) {
let xv = f32x8::load_unaligned(x.as_ptr().add(i));
let yv = f32x8::load_unaligned(other.as_ptr().add(i));
xy8.multiply_add(xv, yv);
y_norm8.multiply_add(yv, yv);
}
let y_norm = y_norm16.reduce_sum()
+ y_norm8.reduce_sum()
+ norm_l2(&other[aligned_len..]).powi(2);
let xy = xy16.reduce_sum()
+ xy8.reduce_sum()
+ dot(&x[aligned_len..], &other[aligned_len..]);
1.0 - xy / x_norm / y_norm.sqrt()
}
}
}
impl Cosine for f32 {
#[inline]
fn cosine_fast(x: &[Self], x_norm: Self, other: &[Self]) -> f32 {
cosine_fast_f32_dispatched(x, x_norm, other)
}
#[inline]
fn cosine_with_norms(x: &[Self], x_norm: Self, y_norm: Self, y: &[Self]) -> Self {
cosine_with_norms_f32_dispatched(x, x_norm, y_norm, y)
}
#[allow(unreachable_code)]
fn cosine_batch<'a>(
x: &'a [Self],
batch: &'a [Self],
dimension: usize,
) -> Box<dyn Iterator<Item = f32> + 'a> {
let x_norm = norm_l2(x);
#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
{
return match dimension {
8 => Box::new(
batch
.chunks_exact(8)
.map(move |y| f32_baseline::cosine_once_8(x, x_norm, y)),
),
16 => Box::new(
batch
.chunks_exact(16)
.map(move |y| f32_baseline::cosine_once_16(x, x_norm, y)),
),
_ => {
if matches!(*SIMD_SUPPORT, SimdSupport::Avx512 | SimdSupport::Avx512FP16) {
Box::new(
unsafe { f32::cosine_batch_avx512(x, x_norm, batch, dimension) }
.into_iter(),
)
} else {
Box::new(
batch
.chunks_exact(dimension)
.map(move |y| f32_baseline::cosine_fast(x, x_norm, y)),
)
}
}
};
}
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => {
return Box::new(
unsafe { f32::cosine_batch_avx512(x, x_norm, batch, dimension) }
.into_iter(),
);
}
SimdSupport::Avx2 | SimdSupport::AvxFma => {
return Box::new(
unsafe { f32::cosine_batch_avx_fma(x, x_norm, batch, dimension) }
.into_iter(),
);
}
SimdSupport::Avx => {
return Box::new(
unsafe { f32::cosine_batch_avx(x, x_norm, batch, dimension) }.into_iter(),
);
}
_ => {}
}
}
match dimension {
8 => Box::new(
batch
.chunks_exact(dimension)
.map(move |y| f32::cosine_once_8(x, x_norm, y)),
),
16 => Box::new(
batch
.chunks_exact(dimension)
.map(move |y| f32::cosine_once_16(x, x_norm, y)),
),
_ => Box::new(
batch
.chunks_exact(dimension)
.map(move |y| Self::cosine_fast(x, x_norm, y)),
),
}
}
}
impl Cosine for f64 {
#[inline]
fn cosine_fast(x: &[Self], x_norm: f32, y: &[Self]) -> f32 {
cosine_fast_f64_dispatched(x, x_norm, y)
}
}
#[inline]
fn cosine_fast_f64_dispatched(x: &[f64], x_norm: f32, y: &[f64]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe {
f64_x86::cosine_fast_avx512(x, x_norm, y)
},
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe {
f64_x86::cosine_fast_avx_fma(x, x_norm, y)
},
SimdSupport::Avx => unsafe { f64_x86::cosine_fast_avx(x, x_norm, y) },
_ => cosine_scalar(x, x_norm, y),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
cosine_fast_f64_simd_other(x, x_norm, y)
}
}
#[cfg(target_arch = "x86_64")]
mod f64_x86 {
use std::arch::x86_64::*;
use crate::simd::f64::{f64x4, f64x8};
use crate::simd::x86::hsum256_pd;
use crate::simd::{FloatSimd, SIMD};
#[target_feature(enable = "avx512f")]
pub unsafe fn cosine_fast_avx512(x: &[f64], x_norm: f32, y: &[f64]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut acc_xy = _mm512_setzero_pd();
let mut acc_yy = _mm512_setzero_pd();
for i in (0..unrolled_len).step_by(8) {
let xv = _mm512_loadu_pd(x.as_ptr().add(i));
let yv = _mm512_loadu_pd(y.as_ptr().add(i));
acc_xy = _mm512_fmadd_pd(xv, yv, acc_xy);
acc_yy = _mm512_fmadd_pd(yv, yv, acc_yy);
}
let mut xy = _mm512_reduce_add_pd(acc_xy);
let mut yy = _mm512_reduce_add_pd(acc_yy);
for i in unrolled_len..dim {
xy += x[i] * y[i];
yy += y[i] * y[i];
}
let y_norm_sq = yy as f32;
let xy_f32 = xy as f32;
1.0 - xy_f32 / x_norm / y_norm_sq.sqrt()
}
#[target_feature(enable = "avx,fma")]
pub unsafe fn cosine_fast_avx_fma(x: &[f64], x_norm: f32, y: &[f64]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 8 * 8;
let mut y_norm8 = f64x8::zeros();
let mut xy8 = f64x8::zeros();
for i in (0..unrolled_len).step_by(8) {
let xv = f64x8::load_unaligned(x.as_ptr().add(i));
let yv = f64x8::load_unaligned(y.as_ptr().add(i));
xy8.multiply_add(xv, yv);
y_norm8.multiply_add(yv, yv);
}
let aligned_len = dim / 4 * 4;
let mut y_norm4 = f64x4::zeros();
let mut xy4 = f64x4::zeros();
for i in (unrolled_len..aligned_len).step_by(4) {
let xv = f64x4::load_unaligned(x.as_ptr().add(i));
let yv = f64x4::load_unaligned(y.as_ptr().add(i));
xy4.multiply_add(xv, yv);
y_norm4.multiply_add(yv, yv);
}
let tail_y_norm: f64 = y[aligned_len..].iter().map(|&v| v * v).sum();
let tail_xy: f64 = x[aligned_len..]
.iter()
.zip(y[aligned_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
let y_norm_sq = (y_norm8.reduce_sum() + y_norm4.reduce_sum() + tail_y_norm) as f32;
let xy = (xy8.reduce_sum() + xy4.reduce_sum() + tail_xy) as f32;
1.0 - xy / x_norm / y_norm_sq.sqrt()
}
#[target_feature(enable = "avx")]
pub unsafe fn cosine_fast_avx(x: &[f64], x_norm: f32, y: &[f64]) -> f32 {
let dim = x.len();
let aligned_len = dim / 4 * 4;
let mut acc_xy = _mm256_setzero_pd();
let mut acc_yy = _mm256_setzero_pd();
for i in (0..aligned_len).step_by(4) {
let xv = _mm256_loadu_pd(x.as_ptr().add(i));
let yv = _mm256_loadu_pd(y.as_ptr().add(i));
acc_xy = _mm256_add_pd(acc_xy, _mm256_mul_pd(xv, yv));
acc_yy = _mm256_add_pd(acc_yy, _mm256_mul_pd(yv, yv));
}
let xy_main = hsum256_pd(acc_xy);
let yy_main = hsum256_pd(acc_yy);
let tail_y_norm: f64 = y[aligned_len..].iter().map(|&v| v * v).sum();
let tail_xy: f64 = x[aligned_len..]
.iter()
.zip(y[aligned_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
let y_norm_sq = (yy_main + tail_y_norm) as f32;
let xy = (xy_main + tail_xy) as f32;
1.0 - xy / x_norm / y_norm_sq.sqrt()
}
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn cosine_fast_f64_simd_other(x: &[f64], x_norm: f32, 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 y_norm8 = f64x8::zeros();
let mut xy8 = f64x8::zeros();
for i in (0..unrolled_len).step_by(8) {
unsafe {
let xv = f64x8::load_unaligned(x.as_ptr().add(i));
let yv = f64x8::load_unaligned(y.as_ptr().add(i));
xy8.multiply_add(xv, yv);
y_norm8.multiply_add(yv, yv);
}
}
let aligned_len = dim / 4 * 4;
let mut y_norm4 = f64x4::zeros();
let mut xy4 = f64x4::zeros();
for i in (unrolled_len..aligned_len).step_by(4) {
unsafe {
let xv = f64x4::load_unaligned(x.as_ptr().add(i));
let yv = f64x4::load_unaligned(y.as_ptr().add(i));
xy4.multiply_add(xv, yv);
y_norm4.multiply_add(yv, yv);
}
}
let tail_y_norm: f64 = y[aligned_len..].iter().map(|&v| v * v).sum();
let tail_xy: f64 = x[aligned_len..]
.iter()
.zip(y[aligned_len..].iter())
.map(|(&a, &b)| a * b)
.sum();
let y_norm_sq = (y_norm8.reduce_sum() + y_norm4.reduce_sum() + tail_y_norm) as f32;
let xy = (xy8.reduce_sum() + xy4.reduce_sum() + tail_xy) as f32;
1.0 - xy / x_norm / y_norm_sq.sqrt()
}
#[inline]
fn cosine_with_norms_f32_dispatched(x: &[f32], x_norm: f32, y_norm: f32, y: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe {
f32_x86::cosine_with_norms_avx512(x, x_norm, y_norm, y)
},
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe {
f32_x86::cosine_with_norms_avx_fma(x, x_norm, y_norm, y)
},
SimdSupport::Avx => unsafe { f32_x86::cosine_with_norms_avx(x, x_norm, y_norm, y) },
_ => cosine_scalar_fast(x, x_norm, y, y_norm),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
cosine_with_norms_f32_simd_other(x, x_norm, y_norm, y)
}
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn cosine_with_norms_f32_simd_other(x: &[f32], x_norm: f32, y_norm: f32, y: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut xy16 = f32x16::zeros();
for i in (0..unrolled_len).step_by(16) {
unsafe {
let xv = f32x16::load_unaligned(x.as_ptr().add(i));
let yv = f32x16::load_unaligned(y.as_ptr().add(i));
xy16.multiply_add(xv, yv);
}
}
let aligned_len = dim / 8 * 8;
let mut xy8 = f32x8::zeros();
for i in (unrolled_len..aligned_len).step_by(8) {
unsafe {
let xv = f32x8::load_unaligned(x.as_ptr().add(i));
let yv = f32x8::load_unaligned(y.as_ptr().add(i));
xy8.multiply_add(xv, yv);
}
}
let xy = xy16.reduce_sum() + xy8.reduce_sum() + dot(&x[aligned_len..], &y[aligned_len..]);
1.0 - xy / x_norm / y_norm
}
#[inline]
fn cosine_fast_f32_dispatched(x: &[f32], x_norm: f32, other: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")]
{
match *SIMD_SUPPORT {
SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe {
f32_x86::cosine_fast_avx512(x, x_norm, other)
},
SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe {
f32_x86::cosine_fast_avx_fma(x, x_norm, other)
},
SimdSupport::Avx => unsafe { f32_x86::cosine_fast_avx(x, x_norm, other) },
_ => cosine_scalar(x, x_norm, other),
}
}
#[cfg(not(target_arch = "x86_64"))]
{
cosine_fast_f32_simd_other(x, x_norm, other)
}
}
#[cfg(target_arch = "x86_64")]
mod f32_x86 {
use std::arch::x86_64::*;
use super::{dot, f32x8, f32x16, norm_l2};
use crate::simd::x86::hsum256_ps;
use crate::simd::{FloatSimd, SIMD};
#[target_feature(enable = "avx,fma")]
pub unsafe fn cosine_fast_avx_fma(x: &[f32], x_norm: f32, other: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut y_norm16 = f32x16::zeros();
let mut xy16 = f32x16::zeros();
for i in (0..unrolled_len).step_by(16) {
let xv = f32x16::load_unaligned(x.as_ptr().add(i));
let yv = f32x16::load_unaligned(other.as_ptr().add(i));
xy16.multiply_add(xv, yv);
y_norm16.multiply_add(yv, yv);
}
let aligned_len = dim / 8 * 8;
let mut y_norm8 = f32x8::zeros();
let mut xy8 = f32x8::zeros();
for i in (unrolled_len..aligned_len).step_by(8) {
let xv = f32x8::load_unaligned(x.as_ptr().add(i));
let yv = f32x8::load_unaligned(other.as_ptr().add(i));
xy8.multiply_add(xv, yv);
y_norm8.multiply_add(yv, yv);
}
let y_norm =
y_norm16.reduce_sum() + y_norm8.reduce_sum() + norm_l2(&other[aligned_len..]).powi(2);
let xy =
xy16.reduce_sum() + xy8.reduce_sum() + dot(&x[aligned_len..], &other[aligned_len..]);
1.0 - xy / x_norm / y_norm.sqrt()
}
#[target_feature(enable = "avx")]
pub unsafe fn cosine_fast_avx(x: &[f32], x_norm: f32, other: &[f32]) -> f32 {
let dim = x.len();
let aligned_len = dim / 8 * 8;
let mut acc_xy = _mm256_setzero_ps();
let mut acc_yy = _mm256_setzero_ps();
for i in (0..aligned_len).step_by(8) {
let xv = _mm256_loadu_ps(x.as_ptr().add(i));
let yv = _mm256_loadu_ps(other.as_ptr().add(i));
acc_xy = _mm256_add_ps(acc_xy, _mm256_mul_ps(xv, yv));
acc_yy = _mm256_add_ps(acc_yy, _mm256_mul_ps(yv, yv));
}
let xy_main = hsum256_ps(acc_xy);
let yy_main = hsum256_ps(acc_yy);
let y_norm = yy_main + norm_l2(&other[aligned_len..]).powi(2);
let xy = xy_main + dot(&x[aligned_len..], &other[aligned_len..]);
1.0 - xy / x_norm / y_norm.sqrt()
}
#[target_feature(enable = "avx512f")]
pub unsafe fn cosine_fast_avx512(x: &[f32], x_norm: f32, other: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut acc_xy = _mm512_setzero_ps();
let mut acc_yy = _mm512_setzero_ps();
for i in (0..unrolled_len).step_by(16) {
let xv = _mm512_loadu_ps(x.as_ptr().add(i));
let yv = _mm512_loadu_ps(other.as_ptr().add(i));
acc_xy = _mm512_fmadd_ps(xv, yv, acc_xy);
acc_yy = _mm512_fmadd_ps(yv, yv, acc_yy);
}
let mut xy = _mm512_reduce_add_ps(acc_xy);
let mut yy = _mm512_reduce_add_ps(acc_yy);
for i in unrolled_len..dim {
xy += x[i] * other[i];
yy += other[i] * other[i];
}
1.0 - xy / x_norm / yy.sqrt()
}
#[target_feature(enable = "avx512f")]
pub unsafe fn cosine_with_norms_avx512(x: &[f32], x_norm: f32, y_norm: 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 xv = _mm512_loadu_ps(x.as_ptr().add(i));
let yv = _mm512_loadu_ps(y.as_ptr().add(i));
acc = _mm512_fmadd_ps(xv, yv, acc);
}
let mut xy = _mm512_reduce_add_ps(acc);
for i in unrolled_len..dim {
xy += x[i] * y[i];
}
1.0 - xy / x_norm / y_norm
}
#[target_feature(enable = "avx,fma")]
pub unsafe fn cosine_with_norms_avx_fma(x: &[f32], x_norm: f32, y_norm: f32, y: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut xy16 = f32x16::zeros();
for i in (0..unrolled_len).step_by(16) {
let xv = f32x16::load_unaligned(x.as_ptr().add(i));
let yv = f32x16::load_unaligned(y.as_ptr().add(i));
xy16.multiply_add(xv, yv);
}
let aligned_len = dim / 8 * 8;
let mut xy8 = f32x8::zeros();
for i in (unrolled_len..aligned_len).step_by(8) {
let xv = f32x8::load_unaligned(x.as_ptr().add(i));
let yv = f32x8::load_unaligned(y.as_ptr().add(i));
xy8.multiply_add(xv, yv);
}
let xy = xy16.reduce_sum() + xy8.reduce_sum() + dot(&x[aligned_len..], &y[aligned_len..]);
1.0 - xy / x_norm / y_norm
}
#[target_feature(enable = "avx")]
pub unsafe fn cosine_with_norms_avx(x: &[f32], x_norm: f32, y_norm: f32, y: &[f32]) -> f32 {
let dim = x.len();
let aligned_len = dim / 8 * 8;
let mut acc = _mm256_setzero_ps();
for i in (0..aligned_len).step_by(8) {
let xv = _mm256_loadu_ps(x.as_ptr().add(i));
let yv = _mm256_loadu_ps(y.as_ptr().add(i));
acc = _mm256_add_ps(acc, _mm256_mul_ps(xv, yv));
}
let xy_main = hsum256_ps(acc);
let xy = xy_main + dot(&x[aligned_len..], &y[aligned_len..]);
1.0 - xy / x_norm / y_norm
}
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn cosine_fast_f32_simd_other(x: &[f32], x_norm: f32, other: &[f32]) -> f32 {
let dim = x.len();
let unrolled_len = dim / 16 * 16;
let mut y_norm16 = f32x16::zeros();
let mut xy16 = f32x16::zeros();
for i in (0..unrolled_len).step_by(16) {
unsafe {
let xv = f32x16::load_unaligned(x.as_ptr().add(i));
let yv = f32x16::load_unaligned(other.as_ptr().add(i));
xy16.multiply_add(xv, yv);
y_norm16.multiply_add(yv, yv);
}
}
let aligned_len = dim / 8 * 8;
let mut y_norm8 = f32x8::zeros();
let mut xy8 = f32x8::zeros();
for i in (unrolled_len..aligned_len).step_by(8) {
unsafe {
let xv = f32x8::load_unaligned(x.as_ptr().add(i));
let yv = f32x8::load_unaligned(other.as_ptr().add(i));
xy8.multiply_add(xv, yv);
y_norm8.multiply_add(yv, yv);
}
}
let y_norm =
y_norm16.reduce_sum() + y_norm8.reduce_sum() + norm_l2(&other[aligned_len..]).powi(2);
let xy = xy16.reduce_sum() + xy8.reduce_sum() + dot(&x[aligned_len..], &other[aligned_len..]);
1.0 - xy / x_norm / y_norm.sqrt()
}
#[inline]
fn cosine_scalar<T: Dot>(x: &[T], x_norm: f32, y: &[T]) -> f32 {
let y_sq = dot(y, y);
let xy = dot(x, y);
1.0 - xy / (x_norm * y_sq.sqrt())
}
#[inline]
pub(crate) fn cosine_scalar_fast<T: Dot>(x: &[T], x_norm: f32, y: &[T], y_norm: f32) -> f32 {
let xy = dot(x, y);
1.0 - (xy / (x_norm * y_norm))
}
pub fn cosine_distance<T: Cosine>(from: &[T], to: &[T]) -> f32 {
T::cosine(from, to)
}
pub fn cosine_distance_batch<'a, T: Cosine>(
from: &'a [T],
batch: &'a [T],
dimension: usize,
) -> Box<dyn Iterator<Item = f32> + 'a> {
T::cosine_batch(from, batch, dimension)
}
fn do_cosine_distance_arrow_batch<T: ArrowFloatType>(
from: &T::ArrayType,
to: &FixedSizeListArray,
) -> Result<Arc<Float32Array>>
where
T::Native: Cosine,
{
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!(
"Unsupported data type {:?}",
to.values().data_type()
)))?;
let dists = cosine_distance_batch(from.as_slice(), to_values.as_slice(), dimension);
Ok(Arc::new(Float32Array::new(
dists.collect(),
to.nulls().cloned(),
)))
}
pub fn cosine_distance_arrow_batch(
from: &dyn Array,
to: &FixedSizeListArray,
) -> Result<Arc<Float32Array>> {
match *from.data_type() {
DataType::Float16 => do_cosine_distance_arrow_batch::<Float16Type>(from.as_primitive(), to),
DataType::Float32 => do_cosine_distance_arrow_batch::<Float32Type>(from.as_primitive(), to),
DataType::Float64 => do_cosine_distance_arrow_batch::<Float64Type>(from.as_primitive(), to),
DataType::Int8 => do_cosine_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)]
fn cosine_fast_scalar(x: &[f64], x_norm: f32, y: &[f64]) -> f32 {
let xy: f64 = x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum();
let y_norm_sq: f64 = y.iter().map(|&v| v * v).sum();
1.0 - (xy as f32) / x_norm / (y_norm_sq as f32).sqrt()
}
#[cfg(test)]
fn cosine_with_norms_scalar(x: &[f64], x_norm: f32, y_norm: f32, y: &[f64]) -> f32 {
let xy: f64 = x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum();
1.0 - (xy as f32) / x_norm / y_norm
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{
arbitrary_bf16, arbitrary_f16, arbitrary_f32, arbitrary_f64, arbitrary_vector_pair,
};
use approx::assert_relative_eq;
use num_traits::AsPrimitive;
use proptest::prelude::*;
fn cosine_dist_brute_force(x: &[f32], y: &[f32]) -> f32 {
let xy = x
.iter()
.zip(y.iter())
.map(|(&xi, &yi)| xi * yi)
.sum::<f32>();
let x_sq = x.iter().map(|&xi| xi * xi).sum::<f32>().sqrt();
let y_sq = y.iter().map(|&yi| yi * yi).sum::<f32>().sqrt();
1.0 - xy / x_sq / y_sq
}
#[test]
fn test_cosine() {
let x: Float32Array = (1..9).map(|v| v as f32).collect();
let y: Float32Array = (100..108).map(|v| v as f32).collect();
let d = cosine_distance_batch(x.values(), y.values(), 8).collect::<Vec<_>>();
assert_relative_eq!(d[0], 1.0 - 0.900_957);
let x = Float32Array::from_iter_values([3.0, 45.0, 7.0, 2.0, 5.0, 20.0, 13.0, 12.0]);
let y = Float32Array::from_iter_values([2.0, 54.0, 13.0, 15.0, 22.0, 34.0, 50.0, 1.0]);
let d = cosine_distance_batch(x.values(), y.values(), 8).collect::<Vec<_>>();
assert_relative_eq!(d[0], 1.0 - 0.873_580_63);
}
#[test]
fn test_cosine_large() {
let total = 1024;
let x = (0..total).map(|v| v as f32).collect::<Vec<_>>();
let y = (1024..1024 + total).map(|v| v as f32).collect::<Vec<_>>();
let d = cosine_distance_batch(&x, &y, total).collect::<Vec<_>>();
assert_relative_eq!(d[0], cosine_dist_brute_force(&x, &y));
}
#[test]
fn test_cosine_not_aligned() {
let x: Float32Array = vec![16_f32, 32_f32].into();
let y: Float32Array = vec![1_f32, 2_f32, 4_f32, 8_f32].into();
let d = cosine_distance_batch(x.values(), y.values(), 2).collect::<Vec<_>>();
assert_relative_eq!(d[0], 0.0);
assert_relative_eq!(d[0], 0.0);
}
fn cosine_ref(x: &[f64], y: &[f64], rel_err: f64) -> (f32, f32) {
let xy = x
.iter()
.zip(y.iter())
.map(|(&xi, &yi)| xi * yi)
.sum::<f64>();
let x_sq = x.iter().map(|&xi| xi * xi).sum::<f64>().sqrt();
let y_sq = y.iter().map(|&yi| yi * yi).sum::<f64>().sqrt();
let expected = (1.0 - xy / x_sq / y_sq) as f32;
let factor = 1.0 + rel_err;
let low = (1.0 - (xy * factor) / (x_sq / factor) / (y_sq / factor)) as f32;
let high = (1.0 - (xy / factor) / (x_sq * factor) / (y_sq * factor)) as f32;
let low = (expected - low).abs();
let high = (expected - high).abs();
let error = low.max(high);
(expected, error)
}
fn do_cosine_test<T: Cosine + AsPrimitive<f64>>(
x: &[T],
y: &[T],
) -> std::result::Result<(), TestCaseError> {
let x_f64 = x.iter().map(|&v| v.as_()).collect::<Vec<_>>();
let y_f64 = y.iter().map(|&v| v.as_()).collect::<Vec<_>>();
let (expected, max_error) = cosine_ref(&x_f64, &y_f64, 1e-6);
let result = T::cosine(x, y);
prop_assert!(approx::relative_eq!(result, expected, epsilon = max_error));
Ok(())
}
proptest::proptest! {
#[test]
fn test_cosine_f16((x, y) in arbitrary_vector_pair(arbitrary_f16, 4..4048)) {
prop_assume!(norm_l2(&x) > 1e-6);
prop_assume!(norm_l2(&y) > 1e-6);
do_cosine_test(&x, &y)?;
}
#[test]
fn test_cosine_bf16((x, y) in arbitrary_vector_pair(arbitrary_bf16, 4..4048)){
prop_assume!(norm_l2(&x) > 1e-6);
prop_assume!(norm_l2(&y) > 1e-6);
do_cosine_test(&x, &y)?;
}
#[test]
fn test_cosine_f32((x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)){
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
do_cosine_test(&x, &y)?;
}
#[test]
fn test_cosine_f64((x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)){
prop_assume!(norm_l2(&x) > 1e-20);
prop_assume!(norm_l2(&y) > 1e-20);
do_cosine_test(&x, &y)?;
}
#[test]
fn test_cosine_fast_f32_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
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 = cosine_fast_scalar(&x_f64, x_norm, &y_f64);
let simd = <f32 as Cosine>::cosine_fast(&x, x_norm, &y);
prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-3));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_fast_f32_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = cosine_scalar(&x, x_norm, &y);
let avx512 = unsafe { f32_x86::cosine_fast_avx512(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_fast_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(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = cosine_scalar(&x, x_norm, &y);
let avx_fma = unsafe { f32_x86::cosine_fast_avx_fma(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_fast_f32_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = cosine_scalar(&x, x_norm, &y);
let avx = unsafe { f32_x86::cosine_fast_avx(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-5));
}
#[test]
fn test_cosine_with_norms_f32_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let y_norm = norm_l2(&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 scalar = cosine_with_norms_scalar(&x_f64, x_norm, y_norm, &y_f64);
let simd = <f32 as Cosine>::cosine_with_norms(&x, x_norm, y_norm, &y);
prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-3));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_with_norms_f32_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let y_norm = norm_l2(&y);
let scalar = cosine_scalar_fast(&x, x_norm, &y, y_norm);
let avx512 = unsafe { f32_x86::cosine_with_norms_avx512(&x, x_norm, y_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_with_norms_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(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let y_norm = norm_l2(&y);
let scalar = cosine_scalar_fast(&x, x_norm, &y, y_norm);
let avx_fma = unsafe { f32_x86::cosine_with_norms_avx_fma(&x, x_norm, y_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_with_norms_f32_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let y_norm = norm_l2(&y);
let scalar = cosine_scalar_fast(&x, x_norm, &y, y_norm);
let avx = unsafe { f32_x86::cosine_with_norms_avx(&x, x_norm, y_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-5));
}
#[test]
fn test_cosine_fast_f64_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
prop_assume!(norm_l2(&x) > 1e-20);
prop_assume!(norm_l2(&y) > 1e-20);
let x_norm = norm_l2(&x);
let scalar = cosine_fast_scalar(&x, x_norm, &y);
let simd = <f64 as Cosine>::cosine_fast(&x, x_norm, &y);
prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-3));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_fast_f64_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-20);
prop_assume!(norm_l2(&y) > 1e-20);
let x_norm = norm_l2(&x);
let scalar = cosine_fast_scalar(&x, x_norm, &y);
let avx512 = unsafe { f64_x86::cosine_fast_avx512(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_fast_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(());
}
prop_assume!(norm_l2(&x) > 1e-20);
prop_assume!(norm_l2(&y) > 1e-20);
let x_norm = norm_l2(&x);
let scalar = cosine_fast_scalar(&x, x_norm, &y);
let avx_fma = unsafe { f64_x86::cosine_fast_avx_fma(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_fast_f64_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-20);
prop_assume!(norm_l2(&y) > 1e-20);
let x_norm = norm_l2(&x);
let scalar = cosine_fast_scalar(&x, x_norm, &y);
let avx = unsafe { f64_x86::cosine_fast_avx(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-5));
}
#[test]
fn test_cosine_once_8_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 8..9)
) {
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
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 = cosine_fast_scalar(&x_f64, x_norm, &y_f64);
let simd = f32::cosine_once_8(&x, x_norm, &y);
prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-3, epsilon = 1e-6));
}
#[test]
fn test_cosine_once_16_scalar_simd_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 16..17)
) {
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
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 = cosine_fast_scalar(&x_f64, x_norm, &y_f64);
let simd = f32::cosine_once_16(&x, x_norm, &y);
prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-3, epsilon = 1e-6));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_once_8_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 8..9)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = super::f32::cosine_once_8_scalar(&x, x_norm, &y);
let avx512 =
unsafe { super::f32::cosine_once_x86::cosine_once_8_avx512(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_once_16_scalar_vs_avx512_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 16..17)
) {
if !std::is_x86_feature_detected!("avx512f") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = super::f32::cosine_once_16_scalar(&x, x_norm, &y);
let avx512 =
unsafe { super::f32::cosine_once_x86::cosine_once_16_avx512(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_once_8_scalar_vs_avx_fma_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 8..9)
) {
if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = super::f32::cosine_once_8_scalar(&x, x_norm, &y);
let avx_fma =
unsafe { super::f32::cosine_once_x86::cosine_once_8_avx_fma(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_once_16_scalar_vs_avx_fma_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 16..17)
) {
if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = super::f32::cosine_once_16_scalar(&x, x_norm, &y);
let avx_fma =
unsafe { super::f32::cosine_once_x86::cosine_once_16_avx_fma(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_once_8_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 8..9)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = super::f32::cosine_once_8_scalar(&x, x_norm, &y);
let avx = unsafe { super::f32::cosine_once_x86::cosine_once_8_avx(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-5));
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_once_16_scalar_vs_avx_parity(
(x, y) in arbitrary_vector_pair(arbitrary_f32, 16..17)
) {
if !std::is_x86_feature_detected!("avx") {
return Ok(());
}
prop_assume!(norm_l2(&x) > 1e-10);
prop_assume!(norm_l2(&y) > 1e-10);
let x_norm = norm_l2(&x);
let scalar = super::f32::cosine_once_16_scalar(&x, x_norm, &y);
let avx = unsafe { super::f32::cosine_once_x86::cosine_once_16_avx(&x, x_norm, &y) };
prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-5));
}
}
#[cfg(target_arch = "x86_64")]
fn check_cosine_batch_kernel(kernel: unsafe fn(&[f32], f32, &[f32], usize) -> Vec<f32>) {
for dimension in [8_usize, 16, 40] {
let x: Vec<f32> = (0..dimension).map(|i| (i as f32) * 0.5 + 1.0).collect();
let x_norm = norm_l2(&x);
let num_vectors = 3;
let batch: Vec<f32> = (0..dimension * num_vectors)
.map(|i| ((i % 7) as f32) + 1.0)
.collect();
let got = unsafe { kernel(&x, x_norm, &batch, dimension) };
assert_eq!(got.len(), num_vectors);
let x_f64: Vec<f64> = x.iter().map(|&v| v as f64).collect();
for (chunk, &g) in batch.chunks_exact(dimension).zip(got.iter()) {
let y_f64: Vec<f64> = chunk.iter().map(|&v| v as f64).collect();
let expected = cosine_fast_scalar(&x_f64, x_norm, &y_f64);
assert_relative_eq!(g, expected, max_relative = 1e-3, epsilon = 1e-6);
}
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_batch_avx_fma_matches_scalar() {
if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
return;
}
check_cosine_batch_kernel(super::f32::cosine_batch_avx_fma);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_batch_avx_matches_scalar() {
if !std::is_x86_feature_detected!("avx") {
return;
}
check_cosine_batch_kernel(super::f32::cosine_batch_avx);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn test_cosine_batch_avx512_matches_scalar() {
if !std::is_x86_feature_detected!("avx512f") {
return;
}
check_cosine_batch_kernel(super::f32::cosine_batch_avx512);
}
}