#![cfg(test)]
use crate::dist::sqdist;
use std::time::Instant;
use yo_common::Rng;
const LANES: usize = 8;
fn to_bf16(centroids: &[f32]) -> Vec<u16> {
centroids
.iter()
.map(|&x| {
let bits = x.to_bits();
let up = bits.wrapping_add(0x7fff).wrapping_add((bits >> 16) & 1);
(up >> 16) as u16
})
.collect()
}
fn bf16_sqdist(q: &[f32], c: &[u16]) -> f32 {
let n = q.len().min(c.len());
let (xs, x_tail) = q[..n].as_chunks::<LANES>();
let (ys, _) = c[..n].as_chunks::<LANES>();
let mut totals = [0.0f32; LANES];
for (x, y) in xs.iter().zip(ys) {
for k in 0..LANES {
let d = x[k] - f32::from_bits(u32::from(y[k]) << 16);
totals[k] += d * d;
}
}
let mut sum = 0.0f32;
for total in totals {
sum += total;
}
let from = n - x_tail.len();
for (k, x) in x_tail.iter().enumerate() {
let d = x - f32::from_bits(u32::from(c[from + k]) << 16);
sum += d * d;
}
sum
}
struct Bytes {
dim: usize,
mid: Vec<f32>,
step: Vec<f32>,
codes: Vec<i8>,
}
impl Bytes {
fn build(centroids: &[f32], dim: usize) -> Bytes {
let n = centroids.len() / dim;
let mut lo = vec![f32::INFINITY; dim];
let mut hi = vec![f32::NEG_INFINITY; dim];
for p in 0..n {
for (d, &x) in centroids[p * dim..(p + 1) * dim].iter().enumerate() {
lo[d] = lo[d].min(x);
hi[d] = hi[d].max(x);
}
}
let mid: Vec<f32> = lo.iter().zip(&hi).map(|(a, b)| (a + b) / 2.0).collect();
let step: Vec<f32> = lo
.iter()
.zip(&hi)
.map(|(a, b)| ((b - a) / 254.0).max(f32::MIN_POSITIVE))
.collect();
let mut codes = Vec::with_capacity(n * dim);
for p in 0..n {
for (d, &x) in centroids[p * dim..(p + 1) * dim].iter().enumerate() {
let at = ((x - mid[d]) / step[d]).round().clamp(-127.0, 127.0);
#[expect(clippy::cast_possible_truncation, reason = "clamped on the line above")]
codes.push(at as i8);
}
}
Bytes {
dim,
mid,
step,
codes,
}
}
fn prepare(&self, q: &[f32], at: &mut Vec<f32>, weight: &mut Vec<f32>) {
at.clear();
weight.clear();
for ((x, mid), step) in q.iter().zip(&self.mid).zip(&self.step) {
at.push((x - mid) / step);
weight.push(step * step);
}
}
fn sqdist(&self, at: &[f32], weight: &[f32], p: usize) -> f32 {
let c = &self.codes[p * self.dim..(p + 1) * self.dim];
let (xs, x_tail) = at.as_chunks::<LANES>();
let (ws, _) = weight.as_chunks::<LANES>();
let (ys, _) = c.as_chunks::<LANES>();
let mut totals = [0.0f32; LANES];
for ((x, w), y) in xs.iter().zip(ws).zip(ys) {
for k in 0..LANES {
let d = x[k] - f32::from(y[k]);
totals[k] += d * d * w[k];
}
}
let mut sum = 0.0f32;
for total in totals {
sum += total;
}
let from = self.dim - x_tail.len();
for (k, x) in x_tail.iter().enumerate() {
let d = x - f32::from(c[from + k]);
sum += d * d * weight[from + k];
}
sum
}
}
struct Prefix {
keep: usize,
rows: Vec<f32>,
}
impl Prefix {
fn build(centroids: &[f32], dim: usize, keep: usize) -> Prefix {
let keep = keep.min(dim);
let n = centroids.len() / dim;
let mut rows = Vec::with_capacity(n * keep);
for p in 0..n {
rows.extend_from_slice(¢roids[p * dim..p * dim + keep]);
}
Prefix { keep, rows }
}
fn sqdist(&self, q: &[f32], p: usize) -> f32 {
sqdist(
&q[..self.keep],
&self.rows[p * self.keep..(p + 1) * self.keep],
)
}
fn count(&self) -> usize {
self.rows.len() / self.keep
}
fn head(&self, q: &[f32], want: usize) -> Vec<usize> {
head(self.count(), want, |p| self.sqdist(q, p))
}
fn head_reranked(
&self,
q: &[f32],
centroids: &[f32],
dim: usize,
want: usize,
widen: usize,
) -> Vec<usize> {
let short = self.head(q, want * widen);
let mut by: Vec<(usize, f32)> = short
.into_iter()
.map(|p| (p, sqdist(q, ¢roids[p * dim..(p + 1) * dim])))
.collect();
by.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
by.truncate(want);
by.into_iter().map(|(p, _)| p).collect()
}
}
fn head(n: usize, want: usize, score: impl Fn(usize) -> f32) -> Vec<usize> {
let order = |a: &(usize, f32), b: &(usize, f32)| a.1.total_cmp(&b.1);
let mut by: Vec<(usize, f32)> = (0..n).map(|p| (p, score(p))).collect();
let want = want.min(n);
by.select_nth_unstable_by(want.saturating_sub(1), order);
by.truncate(want);
by.sort_unstable_by(order);
by.into_iter().map(|(p, _)| p).collect()
}
fn clumped(n: usize, dim: usize, seed: u64) -> Vec<f32> {
let mut rng = Rng::new(seed);
let mut unit = || (rng.next_u64() >> 40) as f32 / (1u32 << 24) as f32;
let groups = 24;
let hubs: Vec<f32> = (0..groups * dim).map(|_| unit()).collect();
let mut out = Vec::with_capacity(n * dim);
for i in 0..n {
let h = (i % groups) * dim;
for d in 0..dim {
out.push(hubs[h + d] + (unit() - 0.5) * 0.2);
}
}
out
}
fn timed(queries: usize, run: impl FnOnce()) -> f64 {
let t = Instant::now();
run();
t.elapsed().as_nanos() as f64 / queries as f64 / 1000.0
}
#[test]
#[cfg_attr(
miri,
ignore = "the count is the claim: a distance ratio over sixty queries against three thousand centroids at two widths, which is the table in the module doc"
)]
fn a_rougher_centroid_pass_picks_a_head_that_is_just_as_near() {
for dim in [128usize, 768] {
let n = 3000;
let centroids = clumped(n, dim, 2);
let queries = clumped(60, dim, 3);
let halves = to_bf16(¢roids);
let bytes = Bytes::build(¢roids, dim);
let prefix = Prefix::build(¢roids, dim, dim / 6);
let (mut at, mut weight) = (Vec::new(), Vec::new());
for keep in [dim / 6, dim / 3, dim / 2] {
let rough = Prefix::build(¢roids, dim, keep);
for widen in [1usize, 4, 16] {
let (mut far, mut base) = (0.0f64, 0.0f64);
for i in 0..60 {
let q = &queries[i * dim..(i + 1) * dim];
let truly =
|p: &usize| f64::from(sqdist(q, ¢roids[p * dim..(p + 1) * dim]));
let full = head(n, 128, |p| sqdist(q, ¢roids[p * dim..(p + 1) * dim]));
let got = if widen == 1 {
rough.head(q, 128)
} else {
rough.head_reranked(q, ¢roids, dim, 128, widen)
};
far += got.iter().map(truly).sum::<f64>();
base += full.iter().map(truly).sum::<f64>();
}
println!(
" {dim:4} dimensions, keep {keep:4}, widen {widen:2}: {:.4} as far",
far / base
);
}
}
for want in [32usize, 128] {
let mut far = [0.0f64; 5];
let mut kept = [0usize; 5];
let mut total = 0usize;
for i in 0..60 {
let q = &queries[i * dim..(i + 1) * dim];
let truly = |p: &usize| f64::from(sqdist(q, ¢roids[p * dim..(p + 1) * dim]));
let full = head(n, want, |p| sqdist(q, ¢roids[p * dim..(p + 1) * dim]));
let half = head(n, want, |p| bf16_sqdist(q, &halves[p * dim..(p + 1) * dim]));
bytes.prepare(q, &mut at, &mut weight);
let byte = head(n, want, |p| bytes.sqdist(&at, &weight, p));
let short = prefix.head(q, want);
let two = prefix.head_reranked(q, ¢roids, dim, want, 4);
for (k, picked) in [&full, &half, &byte, &short, &two].into_iter().enumerate() {
far[k] += picked.iter().map(truly).sum::<f64>();
kept[k] += picked.iter().filter(|p| full.contains(p)).count();
}
total += want;
}
let cost = |k: usize| far[k] / far[0];
let common = |k: usize| kept[k] as f64 / total as f64;
println!(
"{dim:4} dimensions, head of {want:3}: \
bf16 {:.4}/{:.3} i8 {:.4}/{:.3} \
prefix {:.4}/{:.3} prefix and rerank {:.4}/{:.3} (as far / in common)",
cost(1),
common(1),
cost(2),
common(2),
cost(3),
common(3),
cost(4),
common(4)
);
assert!(cost(1) < 1.001, "bf16 head is {:.4} as far", cost(1));
assert!(cost(2) < 1.01, "i8 head is {:.4} as far", cost(2));
assert!(
cost(3) > 1.02,
"a sixth of the coordinates was expected to be rough"
);
assert!(cost(3) < 1.30, "prefix head is {:.4} as far", cost(3));
assert!(
cost(4) < cost(3),
"reranking a prefix head did not make it nearer"
);
}
}
}
#[test]
#[ignore = "prints a table rather than asserting anything"]
fn what_a_rougher_centroid_pass_costs() {
let runs = 200;
let want = 128;
let mut sink = 0.0f32;
let mut kept = 0usize;
println!(" warm scan cold scan prefix");
println!(
"centroids dims MB full bf16 i8 full bf16 i8 1/6 +rerank head"
);
for &(n, dim) in &[
(1930usize, 768usize),
(2963, 1024),
(7719, 768),
(30000, 768),
] {
let mb = (n * dim * 4) as f64 / (1 << 20) as f64;
let centroids = clumped(n, dim, 2);
let queries = clumped(runs, dim, 3);
let halves = to_bf16(¢roids);
let bytes = Bytes::build(¢roids, dim);
let prefix = Prefix::build(¢roids, dim, dim / 6);
let (mut at, mut weight) = (Vec::new(), Vec::new());
let copies = (256.0 / mb).ceil() as usize + 1;
let cold: Vec<Vec<f32>> = (0..copies)
.map(|s| clumped(n, dim, 20 + s as u64))
.collect();
let cold_half: Vec<Vec<u16>> = cold.iter().map(|c| to_bf16(c)).collect();
let cold_byte: Vec<Bytes> = cold.iter().map(|c| Bytes::build(c, dim)).collect();
let each = |i: usize| &queries[i * dim..(i + 1) * dim];
for i in 0..runs {
let q = each(i);
for p in 0..n {
sink += sqdist(q, ¢roids[p * dim..(p + 1) * dim]);
sink += bf16_sqdist(q, &halves[p * dim..(p + 1) * dim]);
sink += prefix.sqdist(q, p);
}
bytes.prepare(q, &mut at, &mut weight);
sink += bytes.sqdist(&at, &weight, i % n);
}
let warm_full = timed(runs, || {
for i in 0..runs {
let q = each(i);
for p in 0..n {
sink += sqdist(q, ¢roids[p * dim..(p + 1) * dim]);
}
}
});
let warm_half = timed(runs, || {
for i in 0..runs {
let q = each(i);
for p in 0..n {
sink += bf16_sqdist(q, &halves[p * dim..(p + 1) * dim]);
}
}
});
let warm_byte = timed(runs, || {
for i in 0..runs {
let q = each(i);
bytes.prepare(q, &mut at, &mut weight);
for p in 0..n {
sink += bytes.sqdist(&at, &weight, p);
}
}
});
let cold_f = timed(runs, || {
for i in 0..runs {
let (q, c) = (each(i), &cold[i % copies]);
for p in 0..n {
sink += sqdist(q, &c[p * dim..(p + 1) * dim]);
}
}
});
let cold_h = timed(runs, || {
for i in 0..runs {
let (q, c) = (each(i), &cold_half[i % copies]);
for p in 0..n {
sink += bf16_sqdist(q, &c[p * dim..(p + 1) * dim]);
}
}
});
let cold_b = timed(runs, || {
for i in 0..runs {
let (q, c) = (each(i), &cold_byte[i % copies]);
c.prepare(q, &mut at, &mut weight);
for p in 0..n {
sink += c.sqdist(&at, &weight, p);
}
}
});
let short = timed(runs, || {
for i in 0..runs {
kept += prefix.head(each(i), want).len();
}
});
let two = timed(runs, || {
for i in 0..runs {
kept += prefix
.head_reranked(each(i), ¢roids, dim, want, 4)
.len();
}
});
let mut scores = vec![0f32; n];
for (p, s) in scores.iter_mut().enumerate() {
*s = sqdist(each(0), ¢roids[p * dim..(p + 1) * dim]);
}
let pick = timed(runs, || {
for _ in 0..runs {
kept += head(n, want, |p| scores[p]).len();
}
});
println!(
"{n:9} {dim:6} {mb:8.1} {warm_full:7.0} {warm_half:5.0} {warm_byte:5.0} \
{cold_f:7.0} {cold_h:5.0} {cold_b:5.0} {short:5.0} {two:8.0} {pick:7.0}"
);
}
println!("microseconds a query, {runs} queries, head of {want} ({sink} {kept})");
}
#[test]
#[ignore = "needs a dataset in YO_DATASET and prints a table rather than asserting"]
fn what_a_rougher_centroid_pass_gives_away() {
use crate::partition::{Partitions, Tuning, Vectors};
use crate::rabitq::Bits;
struct Base {
dim: usize,
data: Vec<f32>,
}
impl Vectors for Base {
fn get(&self, id: u64, into: &mut [f32]) -> bool {
let at = id as usize * self.dim;
let Some(v) = self.data.get(at..at + self.dim) else {
return false;
};
into.copy_from_slice(v);
true
}
}
fn read(path: &str, ints: bool) -> (usize, Vec<f32>) {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("reading {path}: {e}"));
let dim = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let stride = 4 + dim * 4;
assert_eq!(bytes.len() % stride, 0, "{path} is not whole vectors");
let mut out = Vec::with_capacity(bytes.len() / stride * dim);
for v in 0..bytes.len() / stride {
for d in 0..dim {
let at = v * stride + 4 + d * 4;
let word = [bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]];
out.push(if ints {
i32::from_le_bytes(word) as f32
} else {
f32::from_le_bytes(word)
});
}
}
(dim, out)
}
let dir = std::env::var("YO_DATASET")
.expect("set YO_DATASET to a directory holding <name>_base.fvecs and the two beside it");
let dir = dir.trim().to_string();
let set = dir
.trim_end_matches(['/', '\\'])
.rsplit(['/', '\\'])
.next()
.unwrap_or(&dir)
.to_string();
let queries: usize = std::env::var("YO_QUERIES")
.ok()
.and_then(|q| q.parse().ok())
.unwrap_or(500);
let (dim, data) = read(&format!("{dir}/{set}_base.fvecs"), false);
let (_, query) = read(&format!("{dir}/{set}_query.fvecs"), false);
let (gdim, truth) = read(&format!("{dir}/{set}_groundtruth.ivecs"), true);
let n = data.len() / dim;
let queries = queries.min(query.len() / dim);
println!("{dir}: {n} base at {dim} dimensions, {queries} queries");
let base = Base { dim, data };
let t = Instant::now();
let mut ix = Partitions::new(dim, Bits::One, 0x51f7, Tuning::default());
let mut buf = vec![0f32; dim];
for id in 0..n as u64 {
base.get(id, &mut buf);
ix.insert(id, &buf);
if ix.needs_maintenance() {
ix.maintain(&base, 4);
}
}
let parts = ix.partitions();
println!("{parts} partitions, built in {:?}", t.elapsed());
let centroids = ix.all_centroids().to_vec();
let halves = to_bf16(¢roids);
let bytes = Bytes::build(¢roids, dim);
let keeps = [dim / 12, dim / 6, dim / 3];
let prefixes: Vec<Prefix> = keeps
.iter()
.map(|&k| Prefix::build(¢roids, dim, k))
.collect();
let (mut at, mut weight) = (Vec::new(), Vec::new());
const K: usize = 10;
let probes = [8usize, 32, 128, 512];
let ways = 3 + keeps.len() * 2;
let mut found = vec![vec![0usize; ways]; probes.len()];
let mut total = 0usize;
for q in 0..queries {
let v = &query[q * dim..(q + 1) * dim];
let u = ix.quantizer().rotate(v);
bytes.prepare(&u, &mut at, &mut weight);
let holders: Vec<usize> = truth[q * gdim..q * gdim + K]
.iter()
.filter_map(|&id| ix.holder(id as u64))
.collect();
total += holders.len();
for (i, &probe) in probes.iter().enumerate() {
let mut picked = vec![
head(parts, probe, |p| {
sqdist(&u, ¢roids[p * dim..(p + 1) * dim])
}),
head(parts, probe, |p| {
bf16_sqdist(&u, &halves[p * dim..(p + 1) * dim])
}),
head(parts, probe, |p| bytes.sqdist(&at, &weight, p)),
];
for p in &prefixes {
picked.push(p.head(&u, probe));
}
for p in &prefixes {
picked.push(p.head_reranked(&u, ¢roids, dim, probe, 4));
}
for (k, way) in picked.iter().enumerate() {
for h in &holders {
found[i][k] += usize::from(way.contains(h));
}
}
}
}
let mut names = vec!["full".to_string(), "bf16".to_string(), "i8".to_string()];
names.extend(keeps.iter().map(|k| format!("{k}d")));
names.extend(keeps.iter().map(|k| format!("{k}d+rr")));
println!();
print!("{:>12}", "ceiling at");
for name in &names {
print!("{name:>9}");
}
println!();
for (row, &probe) in found.iter().zip(&probes) {
print!("{:>12}", format!("probe {probe}"));
for reached in row.iter().take(ways) {
print!("{:>9.4}", *reached as f64 / total as f64);
}
println!();
}
}