use rayon::prelude::*;
use crate::rank::rank_transform;
use crate::util::{and_popcount, assert_all_finite, result_buffer_len, TopK};
use crate::SearchResults;
pub struct Bitmap {
dim: usize,
n_top: usize,
qwords_per_vec: usize,
n_vectors: usize,
bitmaps: Vec<u64>,
}
impl Bitmap {
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_len(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,
}
}
pub fn top_m_candidates(&self, q: &[f32], m: usize) -> Vec<u32> {
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
}
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()
}
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,
);
#[cfg(target_arch = "x86_64")]
let use_avx512vpop = is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512vpopcntdq")
&& qpv.is_multiple_of(8);
#[cfg(not(target_arch = "x86_64"))]
let use_avx512vpop = false;
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 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 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 load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (dim, n_top, n_vectors, bitmaps) = crate::rank_io::load_bitmap(path)?;
let qpv = dim / 64;
let expected = n_vectors.checked_mul(qpv).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TVBM n_vectors * dim/64 overflows usize",
)
})?;
if bitmaps.len() != expected {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"TVBM 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);
#[cfg(target_arch = "x86_64")]
let use_avx512vpop = is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512vpopcntdq")
&& qpv.is_multiple_of(8);
#[cfg(not(target_arch = "x86_64"))]
let use_avx512vpop = false;
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_eq!(qpv % 8, 0, "AVX-512 bitmap scan needs qpv % 8 == 0");
let lanes = qpv / 8;
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));
}
for di in 0..n {
let doc_ptr = bitmaps.as_ptr().add(di * qpv) 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);
}
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);
#[cfg(target_arch = "x86_64")]
let use_avx512vpop = is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512vpopcntdq")
&& qpv.is_multiple_of(8);
#[cfg(not(target_arch = "x86_64"))]
let use_avx512vpop = false;
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_eq!(qpv % 8, 0);
let lanes = qpv / 8;
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));
}
#[allow(clippy::needless_range_loop)]
for di in 0..n {
let doc_ptr = bitmaps.as_ptr().add(di * qpv) 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);
}
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_eq!(qpv % 8, 0);
debug_assert_eq!(q_batch.len(), batch * qpv);
debug_assert_eq!(scores.len(), batch * n);
let lanes = qpv / 8;
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 chunk_start = 0usize;
while chunk_start + CHUNK <= batch {
for di in 0..n {
let mut accs: [__m512i; CHUNK] = [_mm512_setzero_si512(); CHUNK];
let doc_ptr = bitmaps.as_ptr().add(di * qpv) 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);
}
}
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_ptr = bitmaps.as_ptr().add(di * qpv) 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);
}
}
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],
) {
#[cfg(target_arch = "x86_64")]
let use_avx512vpop = is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512vpopcntdq")
&& qpv.is_multiple_of(8);
#[cfg(not(target_arch = "x86_64"))]
let use_avx512vpop = false;
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_eq!(qpv % 8, 0);
let lanes = qpv / 8;
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
));
}
for (i, &di) in doc_ids.iter().enumerate() {
let doc_ptr = bitmaps.as_ptr().add((di as usize) * qpv) 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);
}
let acc_sum: i64 = _mm512_reduce_add_epi64(acc_zmm);
out[i] = acc_sum as u32;
}
}
}