use std::time::Instant;
use yo_vector::{Any, 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 <dataset directory> [queries] [highest probe] [spill] [slack] [posting]"
);
std::process::exit(2);
};
let queries: usize = args.next().map_or(1_000, |a| {
a.parse()
.expect("the second argument is a number of queries")
});
let top: usize = args.next().map_or(128, |a| {
a.parse()
.expect("the third argument is the highest probe count")
});
let spill: usize = args.next().map_or(Tuning::default().spill, |a| {
a.parse().expect("spill is a count")
});
let slack: f32 = args.next().map_or(Tuning::default().slack, |a| {
a.parse().expect("slack is a fraction")
});
let posting: usize = args.next().map_or(Tuning::default().posting, |a| {
a.parse().expect("posting is a size")
});
let set = prefix(&dir);
let t = Instant::now();
let (dim, base) = read_vecs::<f32>(&format!("{dir}/{set}_base.fvecs"));
let (qdim, query) = read_vecs::<f32>(&format!("{dir}/{set}_query.fvecs"));
let (gdim, truth) = read_vecs::<i32>(&format!("{dir}/{set}_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 tuning = Tuning {
spill,
slack,
posting,
..Tuning::default()
};
let mut ix = Partitions::new(dim, bits, 0x51f7, tuning);
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, spill {spill} slack {slack} posting {posting}, {} partitions, {:.3} copies a vector, built in {built:?}, {:.0} vectors a second on one core",
ix.partitions(),
ix.entries() as f64 / ix.len() as f64,
rate
);
println!(
"{:>6}{:>8}{:>10}{:>12}{:>11}{:>11}{:>9}",
"probe", "rerank", "patience", "recall@10", "p50", "p99", "read"
);
let mut probe = 8;
while probe <= top {
for rerank in [4usize, 8, 16, 32] {
let mut t = ix.tuning();
t.probe = probe;
t.rerank = rerank;
t.patience = 0;
ix.retune(t);
measure(&ix, &bench, probe, rerank, 0);
}
probe *= 2;
}
println!();
println!(
"{:>6}{:>8}{:>10}{:>12}{:>11}{:>11}{:>9}",
"probe", "rerank", "patience", "recall@10", "p50", "p99", "read"
);
for rerank in [4usize, 8, 16, 32] {
for patience in [0usize, 1, 2, 3, 4, 8, 16] {
let mut t = ix.tuning();
t.probe = top;
t.rerank = rerank;
t.patience = patience;
ix.retune(t);
measure(&ix, &bench, top, rerank, patience);
}
}
}
}
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, patience: usize) {
const K: usize = 10;
let mut hit = 0usize;
let mut read = 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, work) = ix.search_costed(v, K, &Any, &b.base);
took.push(t.elapsed().as_secs_f64() * 1e6);
read += work.probed;
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}{patience:>10}{:>12.4}{:>9.0} us{:>9.0} us{:>9.1}",
hit as f64 / (b.queries * K) as f64,
at(0.50),
at(0.99),
read as f64 / b.queries as f64
);
}
fn prefix(dir: &str) -> &str {
dir.trim_end_matches('/')
.rsplit(['/', '\\'])
.next()
.unwrap_or(dir)
}
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)
}
}