use rayon::prelude::*;
use crate::rank::rank_transform;
use crate::util::{and_popcount, assert_all_finite, result_buffer_len, TopK};
use crate::{OrdvecError, SearchResults};
pub struct Bitmap {
dim: usize,
n_top: usize,
qwords_per_vec: usize,
n_vectors: usize,
bitmaps: Vec<u64>,
}
impl std::fmt::Debug for Bitmap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bitmap")
.field("dim", &self.dim)
.field("n_top", &self.n_top)
.field("n_vectors", &self.n_vectors)
.field("bytes_per_vector", &self.bytes_per_vec())
.finish()
}
}
impl Bitmap {
pub fn validate_params(dim: usize, n_top: usize) -> Result<(), OrdvecError> {
if dim == 0 {
return Err(OrdvecError::InvalidParameter {
name: "dim",
message: "must be > 0".to_string(),
});
}
if !dim.is_multiple_of(64) {
return Err(OrdvecError::InvalidParameter {
name: "dim",
message: "must be a multiple of 64".to_string(),
});
}
if dim > crate::rank_io::MAX_DIM {
return Err(OrdvecError::InvalidParameter {
name: "dim",
message: format!(
"must be <= {} (u16 rank invariant)",
crate::rank_io::MAX_DIM
),
});
}
if !(n_top > 0 && n_top < dim) {
return Err(OrdvecError::InvalidParameter {
name: "n_top",
message: "must satisfy 0 < n_top < dim".to_string(),
});
}
Ok(())
}
pub fn new(dim: usize, n_top: usize) -> Self {
assert_eq!(dim % 64, 0, "dim must be a multiple of 64");
assert!(
dim <= crate::rank_io::MAX_DIM,
"dim must be <= {} (u16 rank invariant)",
crate::rank_io::MAX_DIM,
);
assert!(n_top > 0 && n_top < dim, "0 < n_top < dim");
Self {
dim,
n_top,
qwords_per_vec: dim / 64,
n_vectors: 0,
bitmaps: Vec::new(),
}
}
pub fn add(&mut self, vectors: &[f32]) {
let n = vectors.len() / self.dim;
assert_eq!(vectors.len(), n * self.dim);
assert_all_finite(vectors);
let new_n = crate::util::checked_new_count(self.n_vectors, n, self.qwords_per_vec);
let qpv = self.qwords_per_vec;
let cutoff = (self.dim - self.n_top) as u16;
let start = self.bitmaps.len();
self.bitmaps.resize(start + n * qpv, 0u64);
let dim = self.dim;
self.bitmaps[start..]
.par_chunks_mut(qpv)
.zip(vectors.par_chunks(dim))
.for_each(|(out, v)| {
let ranks = rank_transform(v);
for j in 0..dim {
if ranks[j] >= cutoff {
out[j / 64] |= 1u64 << (j % 64);
}
}
});
self.n_vectors = new_n;
}
pub fn build_query_bitmap_fp32(&self, q: &[f32]) -> Vec<u64> {
assert_eq!(q.len(), self.dim);
assert_all_finite(q);
let mut idx: Vec<u16> = (0..self.dim as u16).collect();
idx.sort_by(|&a, &b| {
q[b as usize]
.partial_cmp(&q[a as usize])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut bm = vec![0u64; self.qwords_per_vec];
for &j in &idx[..self.n_top] {
bm[j as usize / 64] |= 1u64 << (j as usize % 64);
}
bm
}
pub fn search(&self, queries: &[f32], k: usize) -> SearchResults {
let nq = queries.len() / self.dim;
assert_eq!(queries.len(), nq * self.dim);
assert_all_finite(queries);
let k = k.min(self.n_vectors);
let k_eff = k;
let buf_len = result_buffer_len(nq, k);
let mut scores_flat = vec![f32::NEG_INFINITY; buf_len];
let mut indices_flat = vec![-1i64; buf_len];
if k_eff == 0 {
return SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
k,
};
}
let dim = self.dim;
let qpv = self.qwords_per_vec;
let n = self.n_vectors;
let bitmaps = &self.bitmaps;
queries
.par_chunks(dim)
.zip(scores_flat.par_chunks_mut(k))
.zip(indices_flat.par_chunks_mut(k))
.for_each(|((q, out_scores), out_indices)| {
let qb = self.build_query_bitmap_fp32(q);
let mut top = TopK::new(k_eff);
bitmap_scan(bitmaps, n, qpv, &qb, &mut top);
top.finalize_into(out_scores, out_indices);
});
SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
k,
}
}
#[must_use = "this scans the corpus to generate candidates; dropping the result discards that work"]
pub fn top_m_candidates(&self, q: &[f32], m: usize) -> Vec<u32> {
assert_eq!(q.len(), self.dim);
assert_all_finite(q);
let m_eff = m.min(self.n_vectors);
if m_eff == 0 {
return Vec::new();
}
let qb = self.build_query_bitmap_fp32(q);
let mut scores = vec![0u32; self.n_vectors];
bitmap_scan_collect(
&self.bitmaps,
self.n_vectors,
self.qwords_per_vec,
&qb,
&mut scores,
);
let mut idx: Vec<u32> = (0..self.n_vectors as u32).collect();
let cmp = |a: &u32, b: &u32| {
scores[*b as usize]
.cmp(&scores[*a as usize])
.then_with(|| a.cmp(b))
};
idx.select_nth_unstable_by(m_eff - 1, cmp);
let mut head = idx[..m_eff].to_vec();
head.sort_unstable_by(cmp);
head
}
#[must_use = "this scans the corpus per query to generate candidates; dropping the result discards that work"]
pub fn top_m_candidates_batched(&self, queries: &[f32], m: usize) -> Vec<Vec<u32>> {
let dim = self.dim;
let batch = queries.len() / dim;
assert_eq!(queries.len(), batch * dim);
assert_all_finite(queries);
let m_eff = m.min(self.n_vectors);
if batch == 0 || m_eff == 0 {
return vec![Vec::new(); batch];
}
let n = self.n_vectors;
let qpv = self.qwords_per_vec;
let q_batch_len = batch
.checked_mul(qpv)
.expect("batched query-bitmap buffer length (batch * qpv) overflows usize");
let mut q_batch = vec![0u64; q_batch_len];
for bi in 0..batch {
let qb = self.build_query_bitmap_fp32(&queries[bi * dim..(bi + 1) * dim]);
q_batch[bi * qpv..(bi + 1) * qpv].copy_from_slice(&qb);
}
let scores_len = batch
.checked_mul(n)
.expect("batched candidate score buffer length (batch * n) overflows usize");
let mut scores = vec![0u32; scores_len];
bitmap_scan_collect_batched(&self.bitmaps, n, qpv, &q_batch, batch, &mut scores);
let n_eff = n;
scores
.par_chunks(n_eff)
.map(|q_scores| {
let mut idx: Vec<u32> = (0..n_eff as u32).collect();
let cmp = |a: &u32, b: &u32| {
q_scores[*b as usize]
.cmp(&q_scores[*a as usize])
.then_with(|| a.cmp(b))
};
idx.select_nth_unstable_by(m_eff - 1, cmp);
let mut head = idx[..m_eff].to_vec();
head.sort_unstable_by(cmp);
head
})
.collect()
}
#[must_use = "this scans the corpus per query to generate candidates; dropping the result discards that work"]
pub fn top_m_candidates_batched_chunked(
&self,
queries: &[f32],
m: usize,
batch_size: usize,
) -> Vec<Vec<u32>> {
assert!(batch_size > 0, "batch_size must be > 0");
let dim = self.dim;
let n_queries = queries.len() / dim;
assert_eq!(queries.len(), n_queries * dim);
if n_queries == 0 {
return Vec::new();
}
let chunk_floats = batch_size
.checked_mul(dim)
.expect("batch_size * dim overflows usize");
queries
.par_chunks(chunk_floats)
.flat_map_iter(|chunk| self.top_m_candidates_batched(chunk, m).into_iter())
.collect()
}
pub fn body_overlap_scores_subset(&self, q_bitmap: &[u64], doc_ids: &[u32], out: &mut [u32]) {
let qpv = self.qwords_per_vec;
assert_eq!(q_bitmap.len(), qpv);
assert_eq!(out.len(), doc_ids.len());
assert!(
doc_ids.iter().all(|&di| (di as usize) < self.n_vectors),
"body_overlap_scores_subset: doc_id out of range [0, {})",
self.n_vectors,
);
let use_avx512vpop = crate::avx512vpop_supported();
if use_avx512vpop {
#[cfg(target_arch = "x86_64")]
unsafe {
body_overlap_scores_subset_avx512vpop(&self.bitmaps, qpv, q_bitmap, doc_ids, out);
return;
}
}
for (i, &di) in doc_ids.iter().enumerate() {
let off = (di as usize) * qpv;
let doc = &self.bitmaps[off..off + qpv];
out[i] = and_popcount(doc, q_bitmap);
}
}
pub fn search_subset(&self, query: &[f32], doc_ids: &[u32], k: usize) -> (Vec<f32>, Vec<i64>) {
assert_eq!(query.len(), self.dim);
assert_all_finite(query);
let k_eff = k.min(doc_ids.len());
if k_eff == 0 {
return (Vec::new(), Vec::new());
}
let q_bitmap = self.build_query_bitmap_fp32(query);
let mut scores = vec![0u32; doc_ids.len()];
self.body_overlap_scores_subset(&q_bitmap, doc_ids, &mut scores);
let mut entries: Vec<(u32, u32)> = doc_ids
.iter()
.copied()
.zip(scores)
.map(|(row, score)| (score, row))
.collect();
let cmp = |a: &(u32, u32), b: &(u32, u32)| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1));
entries.select_nth_unstable_by(k_eff - 1, cmp);
let head = &mut entries[..k_eff];
head.sort_unstable_by(cmp);
let out_scores = head.iter().map(|(score, _)| *score as f32).collect();
let out_indices = head.iter().map(|(_, row)| i64::from(*row)).collect();
(out_scores, out_indices)
}
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 n_top(&self) -> usize {
self.n_top
}
pub fn bytes_per_vec(&self) -> usize {
self.qwords_per_vec * 8
}
pub fn byte_size(&self) -> usize {
self.bitmaps.len() * std::mem::size_of::<u64>()
}
pub fn swap_remove(&mut self, idx: usize) -> usize {
assert!(idx < self.n_vectors, "index out of bounds");
let last = self.n_vectors - 1;
let qpv = self.qwords_per_vec;
if idx != last {
let src = last * qpv;
let dst = idx * qpv;
self.bitmaps.copy_within(src..src + qpv, dst);
}
self.bitmaps.truncate(last * qpv);
self.n_vectors -= 1;
last
}
pub fn write(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
crate::rank_io::write_bitmap(path, self.dim, self.n_top, self.n_vectors, &self.bitmaps)
}
pub fn write_to<W: std::io::Write>(&self, writer: W) -> std::io::Result<()> {
crate::rank_io::write_bitmap_to(writer, self.dim, self.n_top, self.n_vectors, &self.bitmaps)
}
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (dim, n_top, n_vectors, bitmaps) = crate::rank_io::load_bitmap(path)?;
Self::from_persisted_parts(dim, n_top, n_vectors, bitmaps)
}
pub fn read_from<R: std::io::Read + std::io::Seek>(reader: R) -> std::io::Result<Self> {
let (dim, n_top, n_vectors, bitmaps) = crate::rank_io::load_bitmap_from(reader)?;
Self::from_persisted_parts(dim, n_top, n_vectors, bitmaps)
}
pub fn load_from_bytes(bytes: &[u8]) -> std::io::Result<Self> {
Self::read_from(std::io::Cursor::new(bytes))
}
fn from_persisted_parts(
dim: usize,
n_top: usize,
n_vectors: usize,
bitmaps: Vec<u64>,
) -> std::io::Result<Self> {
let qpv = dim / 64;
let expected = n_vectors.checked_mul(qpv).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"OVBM n_vectors * dim/64 overflows usize",
)
})?;
if bitmaps.len() != expected {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"OVBM payload length {} does not match expected {expected} u64 lanes",
bitmaps.len(),
),
));
}
Ok(Self {
dim,
n_top,
qwords_per_vec: qpv,
n_vectors,
bitmaps,
})
}
}
fn bitmap_scan(bitmaps: &[u64], n: usize, qpv: usize, q: &[u64], top: &mut TopK) {
debug_assert_eq!(q.len(), qpv);
let use_avx512vpop = crate::avx512vpop_supported();
if use_avx512vpop {
#[cfg(target_arch = "x86_64")]
unsafe {
bitmap_scan_avx512vpop(bitmaps, n, qpv, q, top);
return;
}
}
bitmap_scan_scalar(bitmaps, n, qpv, q, top);
}
fn bitmap_scan_scalar(bitmaps: &[u64], n: usize, qpv: usize, q: &[u64], top: &mut TopK) {
for di in 0..n {
let doc = &bitmaps[di * qpv..(di + 1) * qpv];
top.maybe_insert(and_popcount(doc, q) as f32, di);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512vpopcntdq")]
unsafe fn bitmap_scan_avx512vpop(bitmaps: &[u64], n: usize, qpv: usize, q: &[u64], top: &mut TopK) {
use std::arch::x86_64::*;
unsafe {
debug_assert!(qpv > 0);
let lanes = qpv / 8;
let rem = qpv % 8;
let tail_mask: __mmask8 = if rem != 0 { (1u8 << rem) - 1 } else { 0 };
let mut q_zmms: Vec<__m512i> = Vec::with_capacity(lanes);
#[allow(clippy::needless_range_loop)]
for l in 0..lanes {
q_zmms.push(_mm512_loadu_si512(q.as_ptr().add(l * 8) as *const __m512i));
}
let q_tail = if rem != 0 {
_mm512_maskz_loadu_epi64(tail_mask, q.as_ptr().add(lanes * 8) as *const i64)
} else {
_mm512_setzero_si512()
};
for di in 0..n {
let doc_base = bitmaps.as_ptr().add(di * qpv);
let doc_ptr = doc_base as *const __m512i;
let mut acc_zmm = _mm512_setzero_si512();
#[allow(clippy::needless_range_loop)]
for l in 0..lanes {
let d_zmm = _mm512_loadu_si512(doc_ptr.add(l));
let and_zmm = _mm512_and_si512(d_zmm, q_zmms[l]);
let pop_zmm = _mm512_popcnt_epi64(and_zmm);
acc_zmm = _mm512_add_epi64(acc_zmm, pop_zmm);
}
if rem != 0 {
let d_tail =
_mm512_maskz_loadu_epi64(tail_mask, doc_base.add(lanes * 8) as *const i64);
let and_zmm = _mm512_and_si512(d_tail, q_tail);
acc_zmm = _mm512_add_epi64(acc_zmm, _mm512_popcnt_epi64(and_zmm));
}
let acc_sum: i64 = _mm512_reduce_add_epi64(acc_zmm);
top.maybe_insert(acc_sum as f32, di);
}
}
}
fn bitmap_scan_collect(bitmaps: &[u64], n: usize, qpv: usize, q: &[u64], scores: &mut [u32]) {
debug_assert_eq!(scores.len(), n);
debug_assert_eq!(q.len(), qpv);
let use_avx512vpop = crate::avx512vpop_supported();
if use_avx512vpop {
#[cfg(target_arch = "x86_64")]
unsafe {
bitmap_scan_collect_avx512vpop(bitmaps, n, qpv, q, scores);
return;
}
}
#[allow(clippy::needless_range_loop)] for di in 0..n {
let doc = &bitmaps[di * qpv..(di + 1) * qpv];
scores[di] = and_popcount(doc, q);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512vpopcntdq")]
unsafe fn bitmap_scan_collect_avx512vpop(
bitmaps: &[u64],
n: usize,
qpv: usize,
q: &[u64],
scores: &mut [u32],
) {
use std::arch::x86_64::*;
unsafe {
debug_assert!(qpv > 0);
let lanes = qpv / 8;
let rem = qpv % 8;
let tail_mask: __mmask8 = if rem != 0 { (1u8 << rem) - 1 } else { 0 };
let mut q_zmms: Vec<__m512i> = Vec::with_capacity(lanes);
#[allow(clippy::needless_range_loop)]
for l in 0..lanes {
q_zmms.push(_mm512_loadu_si512(q.as_ptr().add(l * 8) as *const __m512i));
}
let q_tail = if rem != 0 {
_mm512_maskz_loadu_epi64(tail_mask, q.as_ptr().add(lanes * 8) as *const i64)
} else {
_mm512_setzero_si512()
};
#[allow(clippy::needless_range_loop)]
for di in 0..n {
let doc_base = bitmaps.as_ptr().add(di * qpv);
let doc_ptr = doc_base as *const __m512i;
let mut acc_zmm = _mm512_setzero_si512();
for l in 0..lanes {
let d_zmm = _mm512_loadu_si512(doc_ptr.add(l));
let and_zmm = _mm512_and_si512(d_zmm, q_zmms[l]);
let pop_zmm = _mm512_popcnt_epi64(and_zmm);
acc_zmm = _mm512_add_epi64(acc_zmm, pop_zmm);
}
if rem != 0 {
let d_tail =
_mm512_maskz_loadu_epi64(tail_mask, doc_base.add(lanes * 8) as *const i64);
let and_zmm = _mm512_and_si512(d_tail, q_tail);
acc_zmm = _mm512_add_epi64(acc_zmm, _mm512_popcnt_epi64(and_zmm));
}
let acc_sum: i64 = _mm512_reduce_add_epi64(acc_zmm);
scores[di] = acc_sum as u32;
}
}
}
fn bitmap_scan_collect_batched_scalar(
bitmaps: &[u64],
n: usize,
qpv: usize,
q_batch: &[u64],
batch: usize,
scores: &mut [u32],
) {
debug_assert_eq!(q_batch.len(), batch * qpv);
debug_assert_eq!(scores.len(), batch * n);
for di in 0..n {
let doc = &bitmaps[di * qpv..(di + 1) * qpv];
for bi in 0..batch {
let q = &q_batch[bi * qpv..(bi + 1) * qpv];
scores[bi * n + di] = and_popcount(doc, q);
}
}
}
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
const BATCHED_AVX512_CHUNK: usize = 8;
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512vpopcntdq")]
unsafe fn bitmap_scan_collect_batched_avx512vpop(
bitmaps: &[u64],
n: usize,
qpv: usize,
q_batch: &[u64],
batch: usize,
scores: &mut [u32],
) {
use std::arch::x86_64::*;
unsafe {
debug_assert!(qpv > 0);
debug_assert_eq!(q_batch.len(), batch * qpv);
debug_assert_eq!(scores.len(), batch * n);
let lanes = qpv / 8;
let rem = qpv % 8;
let tail_mask: __mmask8 = if rem != 0 { (1u8 << rem) - 1 } else { 0 };
const CHUNK: usize = BATCHED_AVX512_CHUNK;
let mut q_zmms: Vec<__m512i> = Vec::with_capacity(batch * lanes);
for bi in 0..batch {
for l in 0..lanes {
q_zmms.push(_mm512_loadu_si512(
q_batch.as_ptr().add(bi * qpv + l * 8) as *const __m512i
));
}
}
let mut q_tails: Vec<__m512i> = Vec::with_capacity(if rem != 0 { batch } else { 0 });
if rem != 0 {
for bi in 0..batch {
q_tails.push(_mm512_maskz_loadu_epi64(
tail_mask,
q_batch.as_ptr().add(bi * qpv + lanes * 8) as *const i64,
));
}
}
let mut chunk_start = 0usize;
while chunk_start + CHUNK <= batch {
for di in 0..n {
let mut accs: [__m512i; CHUNK] = [_mm512_setzero_si512(); CHUNK];
let doc_base = bitmaps.as_ptr().add(di * qpv);
let doc_ptr = doc_base as *const __m512i;
for l in 0..lanes {
let d_zmm = _mm512_loadu_si512(doc_ptr.add(l));
for bi in 0..CHUNK {
let q_zmm = q_zmms[(chunk_start + bi) * lanes + l];
let and_zmm = _mm512_and_si512(d_zmm, q_zmm);
let pop_zmm = _mm512_popcnt_epi64(and_zmm);
accs[bi] = _mm512_add_epi64(accs[bi], pop_zmm);
}
}
if rem != 0 {
let d_tail =
_mm512_maskz_loadu_epi64(tail_mask, doc_base.add(lanes * 8) as *const i64);
for bi in 0..CHUNK {
let and_zmm = _mm512_and_si512(d_tail, q_tails[chunk_start + bi]);
accs[bi] = _mm512_add_epi64(accs[bi], _mm512_popcnt_epi64(and_zmm));
}
}
for bi in 0..CHUNK {
let acc_sum: i64 = _mm512_reduce_add_epi64(accs[bi]);
scores[(chunk_start + bi) * n + di] = acc_sum as u32;
}
}
chunk_start += CHUNK;
}
let tail = batch - chunk_start;
if tail > 0 {
for di in 0..n {
let mut accs: [__m512i; CHUNK] = [_mm512_setzero_si512(); CHUNK];
let doc_base = bitmaps.as_ptr().add(di * qpv);
let doc_ptr = doc_base as *const __m512i;
for l in 0..lanes {
let d_zmm = _mm512_loadu_si512(doc_ptr.add(l));
for bi in 0..tail {
let q_zmm = q_zmms[(chunk_start + bi) * lanes + l];
let and_zmm = _mm512_and_si512(d_zmm, q_zmm);
let pop_zmm = _mm512_popcnt_epi64(and_zmm);
accs[bi] = _mm512_add_epi64(accs[bi], pop_zmm);
}
}
if rem != 0 {
let d_tail =
_mm512_maskz_loadu_epi64(tail_mask, doc_base.add(lanes * 8) as *const i64);
for bi in 0..tail {
let and_zmm = _mm512_and_si512(d_tail, q_tails[chunk_start + bi]);
accs[bi] = _mm512_add_epi64(accs[bi], _mm512_popcnt_epi64(and_zmm));
}
}
for bi in 0..tail {
let acc_sum: i64 = _mm512_reduce_add_epi64(accs[bi]);
scores[(chunk_start + bi) * n + di] = acc_sum as u32;
}
}
}
}
}
fn bitmap_scan_collect_batched(
bitmaps: &[u64],
n: usize,
qpv: usize,
q_batch: &[u64],
batch: usize,
scores: &mut [u32],
) {
let use_avx512vpop = crate::avx512vpop_supported();
if use_avx512vpop {
#[cfg(target_arch = "x86_64")]
unsafe {
bitmap_scan_collect_batched_avx512vpop(bitmaps, n, qpv, q_batch, batch, scores);
return;
}
}
bitmap_scan_collect_batched_scalar(bitmaps, n, qpv, q_batch, batch, scores);
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512vpopcntdq")]
unsafe fn body_overlap_scores_subset_avx512vpop(
bitmaps: &[u64],
qpv: usize,
q_bitmap: &[u64],
doc_ids: &[u32],
out: &mut [u32],
) {
use std::arch::x86_64::*;
unsafe {
debug_assert!(qpv > 0);
let lanes = qpv / 8;
let rem = qpv % 8;
let tail_mask: __mmask8 = if rem != 0 { (1u8 << rem) - 1 } else { 0 };
let mut q_zmms: Vec<__m512i> = Vec::with_capacity(lanes);
#[allow(clippy::needless_range_loop)]
for l in 0..lanes {
q_zmms.push(_mm512_loadu_si512(
q_bitmap.as_ptr().add(l * 8) as *const __m512i
));
}
let q_tail = if rem != 0 {
_mm512_maskz_loadu_epi64(tail_mask, q_bitmap.as_ptr().add(lanes * 8) as *const i64)
} else {
_mm512_setzero_si512()
};
for (i, &di) in doc_ids.iter().enumerate() {
let doc_base = bitmaps.as_ptr().add((di as usize) * qpv);
let doc_ptr = doc_base as *const __m512i;
let mut acc_zmm = _mm512_setzero_si512();
#[allow(clippy::needless_range_loop)]
for l in 0..lanes {
let d_zmm = _mm512_loadu_si512(doc_ptr.add(l));
let and_zmm = _mm512_and_si512(d_zmm, q_zmms[l]);
let pop_zmm = _mm512_popcnt_epi64(and_zmm);
acc_zmm = _mm512_add_epi64(acc_zmm, pop_zmm);
}
if rem != 0 {
let d_tail =
_mm512_maskz_loadu_epi64(tail_mask, doc_base.add(lanes * 8) as *const i64);
let and_zmm = _mm512_and_si512(d_tail, q_tail);
acc_zmm = _mm512_add_epi64(acc_zmm, _mm512_popcnt_epi64(and_zmm));
}
let acc_sum: i64 = _mm512_reduce_add_epi64(acc_zmm);
out[i] = acc_sum as u32;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn scalar_overlap(doc: &[u64], q: &[u64]) -> u32 {
doc.iter().zip(q).map(|(d, qq)| (d & qq).count_ones()).sum()
}
fn require_avx512_or_skip(test_name: &str) -> bool {
if crate::avx512vpop_supported() {
return true;
}
let required = std::env::var("ORDVEC_REQUIRE_AVX512")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);
if required {
panic!(
"SKIP {test_name}: host lacks AVX-512 VPOPCNTDQ but \
ORDVEC_REQUIRE_AVX512 is set — AVX-512 kernels are not enforced"
);
}
eprintln!(
"SKIP {test_name}: host lacks AVX-512 VPOPCNTDQ; \
set ORDVEC_REQUIRE_AVX512=1 to enforce"
);
false
}
const PARITY_DIMS: [usize; 13] = [
64, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1536,
];
#[test]
fn avx512_path_matches_scalar_across_residues_and_common_dims() {
if !require_avx512_or_skip("avx512_path_matches_scalar_across_residues_and_common_dims") {
return;
}
for &dim in &PARITY_DIMS {
let n = 300usize;
let n_top = (dim / 4).max(1);
let m = 32usize;
let nq = 4usize;
let mut rng = ChaCha8Rng::seed_from_u64(9000 + dim as u64);
let corpus: Vec<f32> = (0..n * dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let mut idx = Bitmap::new(dim, n_top);
idx.add(&corpus);
let qpv = idx.qwords_per_vec;
let queries: Vec<f32> = (0..nq * dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let batched = idx.top_m_candidates_batched(&queries, m);
for qi in 0..nq {
let q = &queries[qi * dim..(qi + 1) * dim];
let qbm = idx.build_query_bitmap_fp32(q);
let all_ids: Vec<u32> = (0..n as u32).collect();
let mut out = vec![0u32; n];
idx.body_overlap_scores_subset(&qbm, &all_ids, &mut out);
let mut ref_pairs: Vec<(u32, u32)> = Vec::with_capacity(n);
#[allow(clippy::needless_range_loop)]
for di in 0..n {
let off = di * qpv;
let ov = scalar_overlap(&idx.bitmaps[off..off + qpv], &qbm);
assert_eq!(out[di], ov, "body_overlap dim={dim} qi={qi} di={di}");
ref_pairs.push((ov, di as u32));
}
ref_pairs.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
let reference: Vec<u32> = ref_pairs.iter().take(m).map(|&(_, d)| d).collect();
assert_eq!(
idx.top_m_candidates(q, m),
reference,
"top_m dim={dim} qi={qi}"
);
assert_eq!(batched[qi], reference, "batched dim={dim} qi={qi}");
let res = idx.search(q, m);
let got = res.indices_for_query(0);
assert_eq!(got.len(), m, "search len dim={dim} qi={qi}");
let got_set: std::collections::HashSet<i64> = got.iter().copied().collect();
let ov_of =
|di: usize| scalar_overlap(&idx.bitmaps[di * qpv..(di + 1) * qpv], &qbm);
let min_in = got.iter().map(|&id| ov_of(id as usize)).min().unwrap();
let max_out = (0..n)
.filter(|di| !got_set.contains(&(*di as i64)))
.map(ov_of)
.max()
.unwrap_or(0);
assert!(
min_in >= max_out,
"search not a valid top-m: dim={dim} qi={qi} min_in={min_in} max_out={max_out}"
);
}
}
}
#[test]
fn unchanged_at_512bit_multiple_dims() {
for &dim in &[1024usize, 1536] {
let n = 200usize;
let n_top = dim / 4;
let mut rng = ChaCha8Rng::seed_from_u64(123 + dim as u64);
let corpus: Vec<f32> = (0..n * dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let mut idx = Bitmap::new(dim, n_top);
idx.add(&corpus);
let qpv = idx.qwords_per_vec;
let qbm = idx.build_query_bitmap_fp32(&corpus[..dim]);
let all_ids: Vec<u32> = (0..n as u32).collect();
let mut out = vec![0u32; n];
idx.body_overlap_scores_subset(&qbm, &all_ids, &mut out);
#[allow(clippy::needless_range_loop)]
for di in 0..n {
let off = di * qpv;
let ov = scalar_overlap(&idx.bitmaps[off..off + qpv], &qbm);
assert_eq!(
out[di], ov,
"512-bit-multiple dim={dim} regressed at di={di}"
);
}
}
}
#[test]
fn masked_tail_kernel_matches_scalar_when_avx512_present() {
if !require_avx512_or_skip("masked_tail_kernel_matches_scalar_when_avx512_present") {
return;
}
for &dim in &[384usize, 768, 832] {
let n = 200usize;
let n_top = (dim / 4).max(1);
let mut rng = ChaCha8Rng::seed_from_u64(424_242 + dim as u64);
let corpus: Vec<f32> = (0..n * dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let mut idx = Bitmap::new(dim, n_top);
idx.add(&corpus);
let qpv = idx.qwords_per_vec;
assert_ne!(qpv % 8, 0, "dim={dim} must force the masked tail");
let qbm = idx.build_query_bitmap_fp32(&corpus[..dim]);
let all_ids: Vec<u32> = (0..n as u32).collect();
let mut out = vec![0u32; n];
idx.body_overlap_scores_subset(&qbm, &all_ids, &mut out);
#[allow(clippy::needless_range_loop)]
for di in 0..n {
let off = di * qpv;
let ov = scalar_overlap(&idx.bitmaps[off..off + qpv], &qbm);
assert_eq!(out[di], ov, "masked-tail AVX-512 dim={dim} di={di}");
}
}
}
}