use rayon::prelude::*;
use crate::rank::{bucket_ranks, rank_transform, rankquant_norm};
use crate::util::{assert_all_finite, l2_normalise, result_buffer_len, TopK};
use crate::SearchResults;
pub(crate) fn pack_fastscan_b2(buckets: &[u8], n: usize, dim: usize) -> Vec<u8> {
assert_eq!(dim % 2, 0, "fastscan b=2 needs dim % 2 == 0");
assert_eq!(buckets.len(), n * dim, "buckets must be n*dim");
let pairs = dim / 2;
let n_blocks = n.div_ceil(32);
let bytes_per_block = pairs * 32;
let mut out = vec![0u8; n_blocks * bytes_per_block];
for b in 0..n_blocks {
let block_offset = b * bytes_per_block;
let doc_base = b * 32;
let docs_in_block = (n - doc_base).min(32);
for lane in 0..docs_in_block {
let row = (doc_base + lane) * dim;
for p in 0..pairs {
let a = buckets[row + 2 * p] & 0x3;
let c = buckets[row + 2 * p + 1] & 0x3;
let nibble = (a << 2) | c;
out[block_offset + p * 32 + lane] = nibble;
}
}
}
out
}
fn build_fastscan_b2_query(q: &[f32], dim: usize) -> (Vec<u8>, f32, f32) {
let pairs = dim / 2;
let centres = [-1.5_f32, -0.5, 0.5, 1.5];
let mut lut_f = vec![0f32; pairs * 16];
let mut g_min = f32::INFINITY;
let mut g_max = f32::NEG_INFINITY;
for p in 0..pairs {
let qa = q[2 * p];
let qc = q[2 * p + 1];
for a in 0..4 {
for c in 0..4 {
let nibble = (a << 2) | c;
let v = qa * centres[a] + qc * centres[c];
lut_f[p * 16 + nibble] = v;
if v < g_min {
g_min = v;
}
if v > g_max {
g_max = v;
}
}
}
}
let span = (g_max - g_min).max(1e-12);
let scale = 255.0_f32 / span;
let inv_q = 1.0_f32 / scale;
let bias_sum = pairs as f32 * g_min;
let mut lut_u8 = vec![0u8; pairs * 16];
for i in 0..pairs * 16 {
let v = ((lut_f[i] - g_min) * scale).round().clamp(0.0, 255.0) as u8;
lut_u8[i] = v;
}
(lut_u8, bias_sum, inv_q)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512bw,avx512dq")]
#[allow(clippy::too_many_arguments)] unsafe fn scan_b2_fastscan_avx512(
packed_fs: &[u8],
n: usize,
dim: usize,
lut_u8: &[u8],
bias_sum: f32,
inv_q: f32,
scale: f32,
top: &mut TopK,
) {
use std::arch::x86_64::*;
let pairs = dim / 2;
let bytes_per_block = pairs * 32;
let n_blocks = n.div_ceil(32);
const FLUSH: usize = 256;
unsafe {
for b in 0..n_blocks {
let block_ptr = packed_fs.as_ptr().add(b * bytes_per_block);
let mut acc32_lo = _mm512_setzero_si512();
let mut acc32_hi = _mm512_setzero_si512();
let mut p = 0usize;
while p < pairs {
let chunk = (pairs - p).min(FLUSH);
let mut acc16_lo = _mm512_setzero_si512(); let mut acc16_hi = _mm512_setzero_si512();
let inner_end = p + chunk;
let inner_chunks_4 = chunk / 4;
let mut pp = p;
macro_rules! step {
($off:expr) => {{
let codes256 =
_mm256_loadu_si256(block_ptr.add((pp + $off) * 32) as *const __m256i);
let lut128 = _mm_loadu_si128(
lut_u8.as_ptr().add((pp + $off) * 16) as *const __m128i
);
let lut256 = _mm256_broadcastsi128_si256(lut128);
let contrib = _mm256_shuffle_epi8(lut256, codes256);
let lo128 = _mm256_castsi256_si128(contrib);
let hi128 = _mm256_extracti128_si256(contrib, 1);
let lo256 = _mm256_cvtepu8_epi16(lo128);
let hi256 = _mm256_cvtepu8_epi16(hi128);
acc16_lo = _mm512_add_epi16(acc16_lo, _mm512_castsi256_si512(lo256));
acc16_hi = _mm512_add_epi16(acc16_hi, _mm512_castsi256_si512(hi256));
}};
}
for _ in 0..inner_chunks_4 {
step!(0);
step!(1);
step!(2);
step!(3);
pp += 4;
}
while pp < inner_end {
step!(0);
pp += 1;
}
let lo256_u16 = _mm512_castsi512_si256(acc16_lo);
let hi256_u16 = _mm512_castsi512_si256(acc16_hi);
let lo32 = _mm512_cvtepu16_epi32(lo256_u16);
let hi32 = _mm512_cvtepu16_epi32(hi256_u16);
acc32_lo = _mm512_add_epi32(acc32_lo, lo32);
acc32_hi = _mm512_add_epi32(acc32_hi, hi32);
p = inner_end;
}
let mut tmp_lo = [0u32; 16];
let mut tmp_hi = [0u32; 16];
_mm512_storeu_si512(tmp_lo.as_mut_ptr() as *mut _, acc32_lo);
_mm512_storeu_si512(tmp_hi.as_mut_ptr() as *mut _, acc32_hi);
let doc_base = b * 32;
let docs_in_block = (n - doc_base).min(32);
for lane in 0..docs_in_block {
let acc = if lane < 16 {
tmp_lo[lane]
} else {
tmp_hi[lane - 16]
};
let raw = bias_sum + (acc as f32) * inv_q;
top.maybe_insert(raw * scale, doc_base + lane);
}
}
}
}
#[allow(clippy::too_many_arguments)] fn scan_b2_fastscan_scalar(
packed_fs: &[u8],
n: usize,
dim: usize,
lut_u8: &[u8],
bias_sum: f32,
inv_q: f32,
scale: f32,
top: &mut TopK,
) {
let pairs = dim / 2;
let bytes_per_block = pairs * 32;
let n_blocks = n.div_ceil(32);
for b in 0..n_blocks {
let block_ptr = &packed_fs[b * bytes_per_block..(b + 1) * bytes_per_block];
let doc_base = b * 32;
let docs_in_block = (n - doc_base).min(32);
let mut accs = [0u32; 32];
for p in 0..pairs {
for lane in 0..docs_in_block {
let nibble = block_ptr[p * 32 + lane] as usize;
accs[lane] += lut_u8[p * 16 + nibble] as u32;
}
}
#[allow(clippy::needless_range_loop)]
for lane in 0..docs_in_block {
let raw = bias_sum + (accs[lane] as f32) * inv_q;
top.maybe_insert(raw * scale, doc_base + lane);
}
}
}
pub(crate) fn search_asymmetric_fastscan_b2(
packed_fs: &[u8],
n: usize,
dim: usize,
queries: &[f32],
k: usize,
) -> SearchResults {
assert!(dim >= 2, "FastScan b=2: dim must be >= 2");
assert_eq!(
dim % 2,
0,
"FastScan b=2: dim {dim} must be even (pair-encoding)"
);
let pairs = dim / 2;
let n_blocks = n.div_ceil(32);
let expected_packed = n_blocks
.checked_mul(pairs)
.and_then(|x| x.checked_mul(32))
.unwrap_or_else(|| {
panic!(
"FastScan b=2: n={n} dim={dim} packed-length \
computation overflows usize"
)
});
assert_eq!(
packed_fs.len(),
expected_packed,
"FastScan b=2: packed_fs.len()={} does not match n={n} dim={dim} (expected {expected_packed})",
packed_fs.len(),
);
assert_eq!(
queries.len() % dim,
0,
"FastScan b=2: queries.len()={} must be a multiple of dim={dim}",
queries.len(),
);
let nq = queries.len() / dim;
let k = k.min(n);
let buf_len = result_buffer_len(nq, k);
if k == 0 {
return SearchResults {
scores: vec![0.0; buf_len],
indices: vec![-1; buf_len],
nq,
k,
};
}
let mut scores = vec![f32::NEG_INFINITY; buf_len];
let mut indices = vec![-1i64; buf_len];
let centred_norm = rankquant_norm(dim, 2);
let inv_norm = 1.0_f32 / centred_norm;
queries
.par_chunks(dim)
.zip(scores.par_chunks_mut(k))
.zip(indices.par_chunks_mut(k))
.for_each(|((q, out_scores), out_indices)| {
let q_unit = l2_normalise(q);
let (lut_u8, bias_sum, inv_q) = build_fastscan_b2_query(&q_unit, dim);
let mut top = TopK::new(k);
#[cfg(target_arch = "x86_64")]
unsafe {
if is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512bw")
&& is_x86_feature_detected!("avx512dq")
{
scan_b2_fastscan_avx512(
packed_fs, n, dim, &lut_u8, bias_sum, inv_q, inv_norm, &mut top,
);
} else {
scan_b2_fastscan_scalar(
packed_fs, n, dim, &lut_u8, bias_sum, inv_q, inv_norm, &mut top,
);
}
}
#[cfg(not(target_arch = "x86_64"))]
scan_b2_fastscan_scalar(
packed_fs, n, dim, &lut_u8, bias_sum, inv_q, inv_norm, &mut top,
);
top.finalize_into(out_scores, out_indices);
});
SearchResults {
scores,
indices,
nq,
k,
}
}
#[doc(hidden)]
pub struct RankQuantFastscan {
dim: usize,
n_vectors: usize,
packed_fs: Vec<u8>,
}
impl RankQuantFastscan {
pub fn new(dim: usize) -> Self {
assert!(dim >= 2, "FastScan b=2: dim must be >= 2");
assert!(
dim <= u16::MAX as usize,
"FastScan b=2: dim must fit in u16"
);
assert_eq!(
dim % 4,
0,
"FastScan b=2: dim {dim} must be divisible by 4 \
(b=2 constant composition; matches RankQuant::new(dim, 2))"
);
Self {
dim,
n_vectors: 0,
packed_fs: Vec::new(),
}
}
pub fn add(&mut self, vectors: &[f32]) {
assert_all_finite(vectors);
let n = vectors.len() / self.dim;
assert_eq!(
vectors.len(),
n * self.dim,
"vectors length must be a multiple of dim"
);
if n == 0 {
return;
}
assert!(
self.n_vectors == 0,
"FastScan v1: incremental add() not supported (block-32 \
layout has tail-padding semantics that don't compose); \
construct a new RankQuantFastscan instead"
);
let mut buckets = Vec::with_capacity(n * self.dim);
for d in 0..n {
let r = rank_transform(&vectors[d * self.dim..(d + 1) * self.dim]);
let b = bucket_ranks(&r, 2);
buckets.extend_from_slice(&b);
}
self.packed_fs = pack_fastscan_b2(&buckets, n, self.dim);
self.n_vectors = n;
}
pub fn search(&self, queries: &[f32], k: usize) -> SearchResults {
assert_all_finite(queries);
search_asymmetric_fastscan_b2(&self.packed_fs, self.n_vectors, self.dim, queries, k)
}
pub fn len(&self) -> usize {
self.n_vectors
}
pub fn is_empty(&self) -> bool {
self.n_vectors == 0
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn bytes_per_vec(&self) -> usize {
self.dim / 2
}
pub fn byte_size(&self) -> usize {
self.packed_fs.len()
}
}