const CENTROID_ABS: f32 = 0.797_884_6;
const CENTROID_SQ: f32 = CENTROID_ABS * CENTROID_ABS;
const BLOCK_BYTES: usize = 16;
pub struct Query1bitSimd<const BITS: usize = 8> {
planes: Vec<u8>,
num_full_blocks: usize,
tail_bytes: u8,
postprocess_scale: f32,
sum_q_signed: i64,
}
impl<const BITS: usize> Query1bitSimd<BITS> {
pub fn new(data: &[f32]) -> Self {
assert!(
(2..=16).contains(&BITS),
"Query1bitSimd: BITS must be in [2, 16], got {BITS}",
);
assert!(
data.len().is_multiple_of(8),
"Query1bitSimd: dim must be a multiple of 8 (got {})",
data.len(),
);
let q_abs_max_int = (1i64 << (BITS - 1)) - 1;
let q_abs_max = data
.iter()
.copied()
.map(f32::abs)
.fold(0.0_f32, f32::max)
.max(f32::EPSILON);
let q_scale = q_abs_max_int as f32 / q_abs_max;
let clamp_hi = q_abs_max_int as f32;
let clamp_lo = -clamp_hi;
let encode =
|value: f32| -> i64 { (value * q_scale).round().clamp(clamp_lo, clamp_hi) as i64 };
let num_full_blocks = data.len() / (8 * BLOCK_BYTES);
let full_dims = num_full_blocks * 8 * BLOCK_BYTES;
let tail_dims = data.len() - full_dims;
debug_assert!(tail_dims < 8 * BLOCK_BYTES && tail_dims.is_multiple_of(8));
let tail_bytes = tail_dims / 8;
let has_tail = tail_bytes > 0;
let total_blocks = num_full_blocks + usize::from(has_tail);
let mut planes = vec![0u8; total_blocks * BITS * BLOCK_BYTES];
let mut sum_q_signed: i64 = 0;
let bits_mask = (1u64 << BITS) - 1;
let mut deposit = |q: i64, block_idx: usize, byte_in_block: usize, bit_in_byte: usize| {
let q_bits = (q as u64) & bits_mask;
let block_base = block_idx * BITS * BLOCK_BYTES;
for b in 0..BITS {
let bit = ((q_bits >> b) & 1) as u8;
planes[block_base + b * BLOCK_BYTES + byte_in_block] |= bit << bit_in_byte;
}
};
for block_idx in 0..num_full_blocks {
for byte_in_block in 0..BLOCK_BYTES {
for bit_in_byte in 0..8 {
let dim = block_idx * 8 * BLOCK_BYTES + byte_in_block * 8 + bit_in_byte;
let q = encode(data[dim]);
sum_q_signed += q;
deposit(q, block_idx, byte_in_block, bit_in_byte);
}
}
}
if has_tail {
for i in 0..tail_dims {
let q = encode(data[full_dims + i]);
sum_q_signed += q;
deposit(q, num_full_blocks, i / 8, i % 8);
}
}
Self {
planes,
num_full_blocks,
tail_bytes: tail_bytes as u8,
postprocess_scale: CENTROID_ABS / q_scale,
sum_q_signed,
}
}
pub fn dotprod(&self, vector: &[u8]) -> f32 {
let v_dot_q = self.dotprod_raw_best(vector);
let signed_dot = 2 * v_dot_q - self.sum_q_signed;
self.postprocess_scale * signed_dot as f32
}
#[inline]
fn dotprod_raw_best(&self, vector: &[u8]) -> i64 {
#[cfg(target_arch = "x86_64")]
{
if std::is_x86_feature_detected!("avx512vl")
&& std::is_x86_feature_detected!("avx512vpopcntdq")
{
return unsafe { self.dotprod_raw_avx512_vpopcntdq(vector) };
}
if std::is_x86_feature_detected!("sse4.1") && std::is_x86_feature_detected!("ssse3") {
return unsafe { self.dotprod_raw_sse(vector) };
}
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
{
return unsafe { self.dotprod_raw_neon(vector) };
}
#[allow(unreachable_code)]
self.dotprod_raw(vector)
}
pub fn dotprod_raw(&self, vector: &[u8]) -> i64 {
let mut v_dot_q: i64 = 0;
for block_idx in 0..self.num_full_blocks {
let data_block = &vector[block_idx * BLOCK_BYTES..(block_idx + 1) * BLOCK_BYTES];
v_dot_q += self.score_block_scalar(data_block, block_idx);
}
if let Some((buf, block_idx)) = self.tail_block_scratch(vector) {
v_dot_q += self.score_block_scalar(&buf, block_idx);
}
v_dot_q
}
#[inline]
fn score_block_scalar(&self, data_block: &[u8], block_idx: usize) -> i64 {
let plane_base = block_idx * BITS * BLOCK_BYTES;
let mut v_dot_q: i64 = 0;
for b in 0..BITS {
let plane = &self.planes[plane_base + b * BLOCK_BYTES..][..BLOCK_BYTES];
let mut c: u32 = 0;
for i in 0..BLOCK_BYTES {
c += (data_block[i] & plane[i]).count_ones();
}
let w_b: i64 = if b == BITS - 1 {
-(1i64 << (BITS - 1))
} else {
1i64 << b
};
v_dot_q += w_b * i64::from(c);
}
v_dot_q
}
#[inline]
pub(super) fn tail_block_scratch(&self, vector: &[u8]) -> Option<([u8; BLOCK_BYTES], usize)> {
if self.tail_bytes == 0 {
return None;
}
let mut buf = [0u8; BLOCK_BYTES];
let tail_start = self.num_full_blocks * BLOCK_BYTES;
let tail_len = self.tail_bytes as usize;
buf[..tail_len].copy_from_slice(&vector[tail_start..tail_start + tail_len]);
Some((buf, self.num_full_blocks))
}
#[inline]
pub(super) fn num_full_blocks(&self) -> usize {
self.num_full_blocks
}
}
pub fn score_1bit_internal(a: &[u8], b: &[u8]) -> f32 {
assert_eq!(
a.len(),
b.len(),
"score_1bit_internal: vector length mismatch ({} vs {})",
a.len(),
b.len(),
);
#[cfg(target_arch = "x86_64")]
{
if std::is_x86_feature_detected!("avx512f")
&& std::is_x86_feature_detected!("avx512vpopcntdq")
{
return unsafe { x64::score_1bit_internal_avx512_vpopcntdq(a, b) };
}
if std::is_x86_feature_detected!("avx2") {
return unsafe { x64::score_1bit_internal_avx2(a, b) };
}
if std::is_x86_feature_detected!("sse4.1") && std::is_x86_feature_detected!("ssse3") {
return unsafe { x64::score_1bit_internal_sse(a, b) };
}
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
{
return unsafe { arm::score_1bit_internal_neon(a, b) };
}
#[allow(unreachable_code)]
score_1bit_internal_scalar(a, b)
}
pub fn score_1bit_internal_scalar(a: &[u8], b: &[u8]) -> f32 {
let mut popcnt: u64 = 0;
for (&ba, &bb) in a.iter().zip(b.iter()) {
popcnt += u64::from((ba ^ bb).count_ones());
}
popcount_to_score(a.len(), popcnt)
}
#[inline]
pub(super) fn popcount_to_score(byte_len: usize, popcnt: u64) -> f32 {
let total_bits = (byte_len as i64) * 8;
let sign_sum = total_bits - 2 * (popcnt as i64);
CENTROID_SQ * sign_sum as f32
}
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
mod arm;
#[cfg(target_arch = "x86_64")]
mod x64;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
pub use arm::score_1bit_internal_neon;
#[cfg(target_arch = "x86_64")]
pub use x64::{
score_1bit_internal_avx2, score_1bit_internal_avx512_vpopcntdq, score_1bit_internal_sse,
};
#[cfg(test)]
pub(super) mod shared {
pub const PARITY_BYTE_LENS: &[usize] = &[1, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 257, 513];
}
#[cfg(test)]
mod tests {
use rand::SeedableRng as _;
use rand::prelude::StdRng;
use super::super::shared::random_bytes;
use super::*;
use crate::quantization::turboquant::TQBits;
#[test]
fn test_codebook_matches_lloyd_max() {
let centroids = TQBits::Bits1.get_centroids();
assert_eq!(centroids.len(), 2);
assert!((centroids[0] + CENTROID_ABS).abs() < 1e-6);
assert!((centroids[1] - CENTROID_ABS).abs() < 1e-6);
}
#[test]
fn test_scalar_matches_centroid_product() {
let mut rng = StdRng::seed_from_u64(0xC0FFEE);
for &byte_len in &[1usize, 4, 8, 9, 16, 32, 128, 257] {
let a = random_bytes(&mut rng, byte_len);
let b = random_bytes(&mut rng, byte_len);
let mut expected = 0.0_f32;
for byte_idx in 0..byte_len {
for bit in 0..8 {
let sa = if (a[byte_idx] >> bit) & 1 == 1 {
CENTROID_ABS
} else {
-CENTROID_ABS
};
let sb = if (b[byte_idx] >> bit) & 1 == 1 {
CENTROID_ABS
} else {
-CENTROID_ABS
};
expected += sa * sb;
}
}
let got = score_1bit_internal_scalar(&a, &b);
assert!(
(expected - got).abs() < 1e-3,
"byte_len={byte_len} expected {expected} got {got}",
);
}
}
#[test]
fn test_dispatch_matches_scalar() {
let mut rng = StdRng::seed_from_u64(42);
for &byte_len in super::shared::PARITY_BYTE_LENS {
let a = random_bytes(&mut rng, byte_len);
let b = random_bytes(&mut rng, byte_len);
let s = score_1bit_internal_scalar(&a, &b);
let d = score_1bit_internal(&a, &b);
assert_eq!(s.to_bits(), d.to_bits(), "byte_len={byte_len}");
}
}
#[test]
fn test_score_extremes() {
let mut rng = StdRng::seed_from_u64(1);
let byte_len = 64;
let a = random_bytes(&mut rng, byte_len);
let not_a: Vec<u8> = a.iter().map(|&x| !x).collect();
let n_bits = (byte_len * 8) as f32;
let max = CENTROID_SQ * n_bits;
assert!((score_1bit_internal(&a, &a) - max).abs() < 1e-3);
assert!((score_1bit_internal(&a, ¬_a) + max).abs() < 1e-3);
}
#[rstest::rstest]
#[case::full_blocks(1024)]
#[case::tail_only(120)]
#[case::block_plus_small_tail(136)]
#[case::block_plus_max_tail(1144)] #[case::matryoshka_640(640)]
#[case::matryoshka_768(768)]
#[case::matryoshka_896(896)]
fn test_query_dotprod_matches_reference(#[case] dim: usize) {
use rand_distr::{Distribution, StandardNormal};
let byte_len = dim / 8;
let mut rng = StdRng::seed_from_u64(1234);
let query: Vec<f32> = (0..dim).map(|_| StandardNormal.sample(&mut rng)).collect();
let data: Vec<u8> = random_bytes(&mut rng, byte_len);
let mut expected = 0.0_f32;
for (i, &q_i) in query.iter().enumerate() {
let bit = (data[i / 8] >> (i % 8)) & 1;
let sign = if bit == 1 {
CENTROID_ABS
} else {
-CENTROID_ABS
};
expected += q_i * sign;
}
let q8 = Query1bitSimd::<8>::new(&query);
let got8 = q8.dotprod(&data);
let q12 = Query1bitSimd::<12>::new(&query);
let got12 = q12.dotprod(&data);
let scale = expected.abs().max((dim as f32).sqrt());
let rel_err_8 = (got8 - expected).abs() / scale;
let rel_err_12 = (got12 - expected).abs() / scale;
assert!(
rel_err_8 < 2e-2,
"dim={dim} BITS=8 rel_err={rel_err_8} (got {got8} vs {expected})"
);
assert!(
rel_err_12 < 2e-3,
"dim={dim} BITS=12 rel_err={rel_err_12} (got {got12} vs {expected})"
);
assert!(
rel_err_12 <= rel_err_8,
"dim={dim} BITS=12 err {rel_err_12} should be ≤ BITS=8 err {rel_err_8}",
);
}
}