use std::time::Instant;
use yo_vector::{Bits, Partitions, Tuning, Vectors};
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 main() {
let mut args = std::env::args().skip(1);
let Some(dir) = args.next() else {
eprintln!("usage: recall <sift directory> [queries]");
std::process::exit(2);
};
let queries: usize = args.next().map_or(1_000, |a| a.parse().unwrap());
let t = Instant::now();
let (dim, base) = read_vecs::<f32>(&format!("{dir}/sift_base.fvecs"));
let (qdim, query) = read_vecs::<f32>(&format!("{dir}/sift_query.fvecs"));
let (gdim, truth) = read_vecs::<i32>(&format!("{dir}/sift_groundtruth.ivecs"));
assert_eq!(dim, qdim, "base and query dimensions differ");
let n = base.len() / dim;
let queries = queries.min(query.len() / dim);
println!(
"{dir}: {n} base, {} query, {gdim} true neighbours each, read in {:?}",
query.len() / dim,
t.elapsed()
);
let bench = Bench {
base: Base { dim, data: base },
query,
truth,
gdim,
dim,
queries,
};
let base = &bench.base;
for bits in [Bits::One, Bits::Four] {
let t = Instant::now();
let mut ix = Partitions::new(dim, bits, 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 built = t.elapsed();
let rate = n as f64 / built.as_secs_f64();
println!();
println!(
"{bits:?} bit, {} partitions, built in {built:?}, {:.0} vectors a second on one core",
ix.partitions(),
rate
);
println!(
"{:>6}{:>8}{:>12}{:>11}{:>11}",
"probe", "rerank", "recall@10", "p50", "p99"
);
for probe in [8usize, 16, 32, 64, 128] {
for rerank in [4usize, 8, 16, 32] {
let mut t = ix.tuning();
t.probe = probe;
t.rerank = rerank;
ix.retune(t);
measure(&ix, &bench, probe, rerank);
}
}
}
}
struct Bench {
base: Base,
query: Vec<f32>,
truth: Vec<i32>,
gdim: usize,
dim: usize,
queries: usize,
}
fn measure(ix: &Partitions, b: &Bench, probe: usize, rerank: usize) {
const K: usize = 10;
let mut hit = 0usize;
let mut took = Vec::with_capacity(b.queries);
for q in 0..b.queries {
let v = &b.query[q * b.dim..(q + 1) * b.dim];
let t = Instant::now();
let got = ix.search(v, K, &b.base);
took.push(t.elapsed().as_secs_f64() * 1e6);
let want = &b.truth[q * b.gdim..q * b.gdim + K];
hit += got.iter().filter(|h| want.contains(&(h.id as i32))).count();
}
took.sort_by(f64::total_cmp);
let at = |p: f64| took[((took.len() - 1) as f64 * p) as usize];
println!(
"{probe:>6}{rerank:>8}{:>12.4}{:>9.0} us{:>9.0} us",
hit as f64 / (b.queries * K) as f64,
at(0.50),
at(0.99)
);
}
fn read_vecs<T: Le>(path: &str) -> (usize, Vec<T>) {
let bytes = std::fs::read(path).unwrap_or_else(|e| {
eprintln!("{path}: {e}");
std::process::exit(1);
});
assert!(bytes.len() >= 4, "{path} is too short to hold a dimension");
let dim = i32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize;
let record = 4 + dim * 4;
assert!(
dim > 0 && bytes.len().is_multiple_of(record),
"{path} is not {dim} dimensional records of {record} bytes"
);
let mut out = Vec::with_capacity(bytes.len() / record * dim);
for (i, rec) in bytes.chunks_exact(record).enumerate() {
let d = i32::from_le_bytes(rec[..4].try_into().unwrap()) as usize;
assert_eq!(d, dim, "{path} record {i} has {d} dimensions, not {dim}");
out.extend(rec[4..].as_chunks::<4>().0.iter().copied().map(T::le));
}
(dim, out)
}
trait Le {
fn le(bytes: [u8; 4]) -> Self;
}
impl Le for f32 {
fn le(bytes: [u8; 4]) -> f32 {
f32::from_le_bytes(bytes)
}
}
impl Le for i32 {
fn le(bytes: [u8; 4]) -> i32 {
i32::from_le_bytes(bytes)
}
}