#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum QuantizedDotKernel {
#[cfg(target_arch = "aarch64")]
Neon,
#[cfg(target_arch = "x86_64")]
Avx2Fma,
#[cfg_attr(target_arch = "aarch64", allow(dead_code))]
Scalar,
}
impl QuantizedDotKernel {
pub(crate) fn resolve() -> Self {
#[cfg(target_arch = "aarch64")]
{
Self::Neon
}
#[cfg(target_arch = "x86_64")]
{
if std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("fma")
{
return Self::Avx2Fma;
}
Self::Scalar
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
Self::Scalar
}
}
#[inline]
pub(crate) fn dot(self, weights: &[f32], codes: &[u8]) -> f32 {
debug_assert_eq!(weights.len(), codes.len());
let len = weights.len().min(codes.len());
match self {
#[cfg(target_arch = "aarch64")]
Self::Neon => unsafe { dot_neon(weights, codes, len) },
#[cfg(target_arch = "x86_64")]
Self::Avx2Fma => unsafe { dot_avx2_fma(weights, codes, len) },
Self::Scalar => dot_scalar(&weights[..len], &codes[..len]),
}
}
}
#[inline]
pub(crate) fn dot_scalar(weights: &[f32], codes: &[u8]) -> f32 {
weights
.iter()
.zip(codes)
.fold(0.0f32, |acc, (&weight, &code)| {
acc.algebraic_add(weight.algebraic_mul(f32::from(code)))
})
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn dot_neon(weights: &[f32], codes: &[u8], len: usize) -> f32 {
use std::arch::aarch64::*;
let chunks = len / 16;
let mut acc0 = vdupq_n_f32(0.0);
let mut acc1 = vdupq_n_f32(0.0);
let mut acc2 = vdupq_n_f32(0.0);
let mut acc3 = vdupq_n_f32(0.0);
unsafe {
for chunk in 0..chunks {
let base = chunk * 16;
let bytes = vld1q_u8(codes.as_ptr().add(base));
let low = vmovl_u8(vget_low_u8(bytes));
let high = vmovl_high_u8(bytes);
let f0 = vcvtq_f32_u32(vmovl_u16(vget_low_u16(low)));
let f1 = vcvtq_f32_u32(vmovl_high_u16(low));
let f2 = vcvtq_f32_u32(vmovl_u16(vget_low_u16(high)));
let f3 = vcvtq_f32_u32(vmovl_high_u16(high));
acc0 = vfmaq_f32(acc0, vld1q_f32(weights.as_ptr().add(base)), f0);
acc1 = vfmaq_f32(acc1, vld1q_f32(weights.as_ptr().add(base + 4)), f1);
acc2 = vfmaq_f32(acc2, vld1q_f32(weights.as_ptr().add(base + 8)), f2);
acc3 = vfmaq_f32(acc3, vld1q_f32(weights.as_ptr().add(base + 12)), f3);
}
}
let total = vaddvq_f32(vaddq_f32(vaddq_f32(acc0, acc1), vaddq_f32(acc2, acc3)));
let base = chunks * 16;
total.algebraic_add(dot_scalar(&weights[base..len], &codes[base..len]))
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn dot_avx2_fma(weights: &[f32], codes: &[u8], len: usize) -> f32 {
use std::arch::x86_64::*;
let chunks = len / 32;
let mut acc0 = _mm256_setzero_ps();
let mut acc1 = _mm256_setzero_ps();
let mut acc2 = _mm256_setzero_ps();
let mut acc3 = _mm256_setzero_ps();
unsafe {
for chunk in 0..chunks {
let base = chunk * 32;
let c0 = _mm256_cvtepi32_ps(_mm256_cvtepu8_epi32(_mm_loadl_epi64(
codes.as_ptr().add(base).cast(),
)));
let c1 = _mm256_cvtepi32_ps(_mm256_cvtepu8_epi32(_mm_loadl_epi64(
codes.as_ptr().add(base + 8).cast(),
)));
let c2 = _mm256_cvtepi32_ps(_mm256_cvtepu8_epi32(_mm_loadl_epi64(
codes.as_ptr().add(base + 16).cast(),
)));
let c3 = _mm256_cvtepi32_ps(_mm256_cvtepu8_epi32(_mm_loadl_epi64(
codes.as_ptr().add(base + 24).cast(),
)));
acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(weights.as_ptr().add(base)), c0, acc0);
acc1 = _mm256_fmadd_ps(_mm256_loadu_ps(weights.as_ptr().add(base + 8)), c1, acc1);
acc2 = _mm256_fmadd_ps(_mm256_loadu_ps(weights.as_ptr().add(base + 16)), c2, acc2);
acc3 = _mm256_fmadd_ps(_mm256_loadu_ps(weights.as_ptr().add(base + 24)), c3, acc3);
}
}
let sum = _mm256_add_ps(_mm256_add_ps(acc0, acc1), _mm256_add_ps(acc2, acc3));
let low = _mm256_castps256_ps128(sum);
let high = _mm256_extractf128_ps::<1>(sum);
let quad = _mm_add_ps(low, high);
let pair = _mm_add_ps(quad, _mm_movehl_ps(quad, quad));
let total = _mm_cvtss_f32(_mm_add_ss(pair, _mm_shuffle_ps::<0b01>(pair, pair)));
let base = chunks * 32;
total.algebraic_add(dot_scalar(&weights[base..len], &codes[base..len]))
}
#[cfg(test)]
mod tests {
use super::*;
fn reference(weights: &[f32], codes: &[u8]) -> f64 {
weights
.iter()
.zip(codes)
.map(|(&weight, &code)| f64::from(weight) * f64::from(code))
.sum()
}
#[test]
fn resolved_quantized_dot_matches_f64_reference_for_every_tail() {
let kernel = QuantizedDotKernel::resolve();
for len in [
0usize, 1, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 100, 768, 1024, 1027,
] {
let weights: Vec<f32> = (0..len)
.map(|index| ((index * 37 % 101) as f32 / 50.0 - 1.0) * 0.01)
.collect();
let codes: Vec<u8> = (0..len).map(|index| (index * 73 % 256) as u8).collect();
let expected = reference(&weights, &codes);
let got = kernel.dot(&weights, &codes);
let scalar = dot_scalar(&weights, &codes);
let tolerance = 1e-5 * (1.0 + expected.abs());
assert!(
(f64::from(got) - expected).abs() <= tolerance,
"len {len}: kernel {got} vs reference {expected}"
);
assert!(
(f64::from(scalar) - expected).abs() <= tolerance,
"len {len}: scalar {scalar} vs reference {expected}"
);
}
}
#[test]
fn quantized_dot_propagates_non_finite_weights() {
let kernel = QuantizedDotKernel::resolve();
let weights = vec![f32::NAN; 40];
let codes = vec![1u8; 40];
assert!(kernel.dot(&weights, &codes).is_nan());
}
}