use rayon::prelude::*;
use crate::quant_kernels::{
scan_b1_to_topk, scan_b2_to_topk, scan_b4_to_topk, scan_via_lut_scalar,
};
#[cfg(target_arch = "x86_64")]
use crate::quant_kernels::{
scan_b2_asym_avx2, scan_b2_asym_avx512, scan_b4_asym_avx2, scan_b4_asym_avx512,
};
use crate::rank::{
bucket_centre, bucket_ranks, pack_buckets, rank_to_bucket, rank_transform,
rankquant_bytes_per_vec, rankquant_norm,
};
use crate::util::{assert_all_finite, l2_normalise, result_buffer_len, TopK};
use crate::SearchResults;
pub struct RankQuant {
pub(crate) dim: usize,
pub(crate) bits: u8,
pub(crate) n_vectors: usize,
pub(crate) packed: Vec<u8>,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
enum SimdTier {
None,
Avx2,
Avx512,
}
#[inline]
fn select_simd_tier(dim: usize, bits: u8) -> SimdTier {
if !matches!(bits, 2 | 4) {
return SimdTier::None;
}
#[cfg(target_arch = "x86_64")]
{
let avx512 = is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512dq");
let avx2 = is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma");
if avx512 && dim.is_multiple_of(64) {
return SimdTier::Avx512;
}
if avx2 && ((bits == 2 && dim.is_multiple_of(16)) || (bits == 4 && dim.is_multiple_of(8))) {
return SimdTier::Avx2;
}
SimdTier::None
}
#[cfg(not(target_arch = "x86_64"))]
{
let _ = (dim, bits);
SimdTier::None
}
}
impl RankQuant {
pub fn new(dim: usize, bits: u8) -> Self {
assert!(matches!(bits, 1 | 2 | 4), "bits must be 1, 2, or 4");
assert!(dim >= 2, "dim must be >= 2");
assert!(dim <= u16::MAX as usize, "dim must fit in u16");
let codes_per_byte = (8 / bits) as usize;
assert_eq!(
dim % codes_per_byte,
0,
"dim must be a multiple of {codes_per_byte} for bits = {bits}",
);
let n_buckets = 1usize << bits;
assert_eq!(
dim % n_buckets,
0,
"dim must be divisible by 2^bits = {n_buckets} so every \
bucket receives exactly dim / 2^bits rank entries; this \
keeps the analytical rankquant_norm exact per document",
);
Self {
dim,
bits,
n_vectors: 0,
packed: Vec::new(),
}
}
pub fn add(&mut self, vectors: &[f32]) {
let n = vectors.len() / self.dim;
assert_eq!(
vectors.len(),
n * self.dim,
"vectors length must be a multiple of dim",
);
assert_all_finite(vectors);
let bytes_per_vec = rankquant_bytes_per_vec(self.dim, self.bits);
let new_n = crate::util::checked_new_len(self.n_vectors, n, bytes_per_vec);
let start = self.packed.len();
self.packed.resize(start + n * bytes_per_vec, 0);
let dim = self.dim;
let bits = self.bits;
self.packed[start..]
.par_chunks_mut(bytes_per_vec)
.zip(vectors.par_chunks(dim))
.for_each(|(out, v)| {
let ranks = rank_transform(v);
let buckets = bucket_ranks(&ranks, bits);
let packed = pack_buckets(&buckets, bits);
out.copy_from_slice(&packed);
});
self.n_vectors = new_n;
}
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);
if k_eff == 0 {
return SearchResults {
scores: vec![0.0; buf_len],
indices: vec![-1; buf_len],
nq,
k,
};
}
let dim = self.dim;
let bits = self.bits;
let n = self.n_vectors;
let norm = rankquant_norm(dim, bits);
let inv_norm_sq = 1.0_f32 / (norm * norm);
let bytes_per_vec = rankquant_bytes_per_vec(dim, bits);
let mut scores_flat = vec![0.0f32; buf_len];
let mut indices_flat = vec![-1i64; buf_len];
let n_buckets = 1usize << bits;
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 q_ranks = rank_transform(q);
let mut lut = vec![0.0f32; dim * n_buckets];
for d in 0..dim {
let qb = rank_to_bucket(q_ranks[d], dim, bits);
let qc = bucket_centre(qb, bits);
for b in 0..n_buckets {
lut[d * n_buckets + b] = qc * bucket_centre(b as u8, bits);
}
}
let mut top = TopK::new(k_eff);
match bits {
1 => scan_b1_to_topk(&self.packed, n, dim, &lut, inv_norm_sq, &mut top),
2 => scan_b2_to_topk(&self.packed, n, dim, &lut, inv_norm_sq, &mut top),
4 => scan_b4_to_topk(&self.packed, n, dim, &lut, inv_norm_sq, &mut top),
_ => unreachable!(),
}
top.finalize_into(out_scores, out_indices);
let _ = bytes_per_vec; });
SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
k,
}
}
pub fn search_asymmetric(&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);
if k_eff == 0 {
return SearchResults {
scores: vec![0.0; buf_len],
indices: vec![-1; buf_len],
nq,
k,
};
}
let dim = self.dim;
let bits = self.bits;
let n = self.n_vectors;
let norm = rankquant_norm(dim, bits);
let inv_norm = 1.0_f32 / norm;
let n_buckets = 1usize << bits;
let bytes_per_vec = rankquant_bytes_per_vec(dim, bits);
let mut scores_flat = vec![0.0f32; buf_len];
let mut indices_flat = vec![-1i64; buf_len];
#[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
let simd_tier = select_simd_tier(dim, bits);
let centre = ((1u32 << bits) as f32 - 1.0) / 2.0;
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 q_unit = l2_normalise(q);
let mut top = TopK::new(k_eff);
#[cfg_attr(not(target_arch = "x86_64"), allow(unused_mut))]
let mut centre_drop_used = false;
#[cfg(target_arch = "x86_64")]
unsafe {
match (simd_tier, bits) {
(SimdTier::Avx512, 2) => {
scan_b2_asym_avx512(&self.packed, n, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
(SimdTier::Avx512, 4) => {
scan_b4_asym_avx512(&self.packed, n, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
(SimdTier::Avx2, 2) => {
scan_b2_asym_avx2(&self.packed, n, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
(SimdTier::Avx2, 4) => {
scan_b4_asym_avx2(&self.packed, n, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
_ => scan_via_lut_scalar(
&self.packed,
n,
dim,
bits,
n_buckets,
&q_unit,
inv_norm,
&mut top,
),
}
}
#[cfg(not(target_arch = "x86_64"))]
scan_via_lut_scalar(
&self.packed,
n,
dim,
bits,
n_buckets,
&q_unit,
inv_norm,
&mut top,
);
top.finalize_into(out_scores, out_indices);
if centre_drop_used {
let q_sum: f32 = q_unit.iter().sum();
let offset = -centre * q_sum * inv_norm;
for s in out_scores.iter_mut() {
if s.is_finite() {
*s += offset;
}
}
}
let _ = bytes_per_vec; });
SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
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 bits(&self) -> u8 {
self.bits
}
pub fn bytes_per_vec(&self) -> usize {
rankquant_bytes_per_vec(self.dim, self.bits)
}
pub fn byte_size(&self) -> usize {
self.packed.len()
}
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 bpv = self.bytes_per_vec();
if idx != last {
let src = last * bpv;
let dst = idx * bpv;
self.packed.copy_within(src..src + bpv, dst);
}
self.packed.truncate(last * bpv);
self.n_vectors -= 1;
last
}
pub fn write(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
crate::rank_io::write_rankquant(path, self.bits, self.dim, self.n_vectors, &self.packed)
}
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (bits, dim, n_vectors, packed) = crate::rank_io::load_rankquant(path)?;
let n_buckets = 1usize << bits;
if dim % n_buckets != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"TVRQ dim {dim} is not a multiple of 2^bits = {n_buckets}; \
constant-composition invariant violated"
),
));
}
let codes_per_byte = (8 / bits) as usize;
if dim % codes_per_byte != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("TVRQ dim {dim} is not a multiple of codes_per_byte = {codes_per_byte}",),
));
}
let nv_dim = n_vectors.checked_mul(dim).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TVRQ n_vectors * dim overflows usize",
)
})?;
let expected_bytes = nv_dim
.checked_mul(bits as usize)
.map(|x| x / 8)
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TVRQ (n_vectors * dim) * bits overflows usize",
)
})?;
if packed.len() != expected_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"TVRQ payload length {} does not match expected {expected_bytes}",
packed.len(),
),
));
}
Ok(Self {
dim,
bits,
n_vectors,
packed,
})
}
pub fn search_asymmetric_subset(
&self,
query: &[f32],
candidates: &[u32],
k: usize,
) -> (Vec<f32>, Vec<i64>) {
assert_eq!(query.len(), self.dim);
assert_all_finite(query);
assert!(
candidates.iter().all(|&di| (di as usize) < self.n_vectors),
"search_asymmetric_subset: candidate id out of range (n_vectors {})",
self.n_vectors,
);
let dim = self.dim;
let bits = self.bits;
let bpv = self.bytes_per_vec();
let n_buckets = 1usize << bits;
let m = candidates.len();
let k_eff = k.min(m);
if k_eff == 0 {
return (Vec::new(), Vec::new());
}
let norm = rankquant_norm(dim, bits);
let inv_norm = 1.0_f32 / norm;
let centre = ((1u32 << bits) as f32 - 1.0) / 2.0;
let q_unit = l2_normalise(query);
let q_sum: f32 = q_unit.iter().sum();
let centre_offset = -centre * q_sum * inv_norm;
let mut sub_packed = vec![0u8; m * bpv];
for (i, &di) in candidates.iter().enumerate() {
let src = (di as usize) * bpv;
sub_packed[i * bpv..(i + 1) * bpv].copy_from_slice(&self.packed[src..src + bpv]);
}
#[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
let simd_tier = select_simd_tier(dim, bits);
let mut top = TopK::new(k_eff);
#[cfg_attr(not(target_arch = "x86_64"), allow(unused_mut))]
let mut centre_drop_used = false;
#[cfg(target_arch = "x86_64")]
unsafe {
match (simd_tier, bits) {
(SimdTier::Avx512, 2) => {
scan_b2_asym_avx512(&sub_packed, m, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
(SimdTier::Avx512, 4) => {
scan_b4_asym_avx512(&sub_packed, m, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
(SimdTier::Avx2, 2) => {
scan_b2_asym_avx2(&sub_packed, m, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
(SimdTier::Avx2, 4) => {
scan_b4_asym_avx2(&sub_packed, m, dim, &q_unit, inv_norm, &mut top);
centre_drop_used = true;
}
_ => scan_via_lut_scalar(
&sub_packed,
m,
dim,
bits,
n_buckets,
&q_unit,
inv_norm,
&mut top,
),
}
}
#[cfg(not(target_arch = "x86_64"))]
scan_via_lut_scalar(
&sub_packed,
m,
dim,
bits,
n_buckets,
&q_unit,
inv_norm,
&mut top,
);
let mut scores = vec![f32::NEG_INFINITY; k_eff];
let mut local_indices = vec![-1i64; k_eff];
top.finalize_into(&mut scores, &mut local_indices);
if centre_drop_used {
for s in scores.iter_mut() {
if s.is_finite() {
*s += centre_offset;
}
}
}
let global_indices: Vec<i64> = local_indices
.iter()
.map(|&loc| {
if loc < 0 {
-1
} else {
candidates[loc as usize] as i64
}
})
.collect();
(scores, global_indices)
}
}
fn build_byte_lut_b2(q_unit: &[f32]) -> Vec<f32> {
let dim = q_unit.len();
debug_assert_eq!(dim % 4, 0);
let n_groups = dim / 4;
let mut lut = vec![0.0f32; n_groups * 256];
for g in 0..n_groups {
let q0 = q_unit[g * 4];
let q1 = q_unit[g * 4 + 1];
let q2 = q_unit[g * 4 + 2];
let q3 = q_unit[g * 4 + 3];
for byte in 0u32..256 {
let c0 = ((byte >> 6) & 3) as f32 - 1.5;
let c1 = ((byte >> 4) & 3) as f32 - 1.5;
let c2 = ((byte >> 2) & 3) as f32 - 1.5;
let c3 = (byte & 3) as f32 - 1.5;
lut[g * 256 + byte as usize] = q0 * c0 + q1 * c1 + q2 * c2 + q3 * c3;
}
}
lut
}
fn build_byte_lut_b4(q_unit: &[f32]) -> Vec<f32> {
let dim = q_unit.len();
debug_assert_eq!(dim % 2, 0);
let n_groups = dim / 2;
let mut lut = vec![0.0f32; n_groups * 256];
for g in 0..n_groups {
let q0 = q_unit[g * 2];
let q1 = q_unit[g * 2 + 1];
for byte in 0u32..256 {
let hi = ((byte >> 4) & 0xF) as f32 - 7.5;
let lo = (byte & 0xF) as f32 - 7.5;
lut[g * 256 + byte as usize] = q0 * hi + q1 * lo;
}
}
lut
}
fn scan_b2_asym_byte_lut(
packed: &[u8],
n: usize,
dim: usize,
q_unit: &[f32],
scale: f32,
top: &mut TopK,
) {
let bytes_per_vec = dim / 4;
let lut = build_byte_lut_b2(q_unit);
for di in 0..n {
let doc = &packed[di * bytes_per_vec..(di + 1) * bytes_per_vec];
let mut acc = 0.0f32;
for (g, &byte) in doc.iter().enumerate() {
acc += lut[g * 256 + byte as usize];
}
top.maybe_insert(acc * scale, di);
}
}
fn scan_b4_asym_byte_lut(
packed: &[u8],
n: usize,
dim: usize,
q_unit: &[f32],
scale: f32,
top: &mut TopK,
) {
let bytes_per_vec = dim / 2;
let lut = build_byte_lut_b4(q_unit);
for di in 0..n {
let doc = &packed[di * bytes_per_vec..(di + 1) * bytes_per_vec];
let mut acc = 0.0f32;
for (g, &byte) in doc.iter().enumerate() {
acc += lut[g * 256 + byte as usize];
}
top.maybe_insert(acc * scale, di);
}
}
pub fn search_asymmetric_byte_lut(index: &RankQuant, queries: &[f32], k: usize) -> SearchResults {
let dim = index.dim;
let bits = index.bits;
let n = index.n_vectors;
let nq = queries.len() / dim;
assert_eq!(queries.len(), nq * dim);
assert_all_finite(queries);
let k = k.min(n);
let k_eff = k;
let buf_len = result_buffer_len(nq, k);
if k_eff == 0 {
return SearchResults {
scores: vec![0.0; buf_len],
indices: vec![-1; buf_len],
nq,
k,
};
}
let norm = rankquant_norm(dim, bits);
let inv_norm = 1.0_f32 / norm;
let mut scores_flat = vec![0.0f32; buf_len];
let mut indices_flat = vec![-1i64; buf_len];
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 q_unit = l2_normalise(q);
let mut top = TopK::new(k_eff);
match bits {
2 => scan_b2_asym_byte_lut(&index.packed, n, dim, &q_unit, inv_norm, &mut top),
4 => scan_b4_asym_byte_lut(&index.packed, n, dim, &q_unit, inv_norm, &mut top),
_ => panic!("byte-LUT path only supports bits in {{2, 4}}"),
}
top.finalize_into(out_scores, out_indices);
});
SearchResults {
scores: scores_flat,
indices: indices_flat,
nq,
k,
}
}