use std::time::{Duration, Instant};
use yo_common::Rng;
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
}
}
const EVERY: Duration = Duration::from_secs(600);
const PROBE: usize = 64;
const RERANK: usize = 16;
const K: usize = 10;
fn main() {
let mut args = std::env::args().skip(1);
let Some(dir) = args.next() else {
eprintln!("usage: drift <dataset directory> [hours] [queries]");
std::process::exit(2);
};
let hours: f64 = args.next().map_or(24.0, |a| {
a.parse().expect("the second argument is a number of hours")
});
let queries: usize = args.next().map_or(1_000, |a| {
a.parse()
.expect("the third argument is a number of queries")
});
let set = prefix(&dir);
let t = Instant::now();
let (dim, data) = 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 = data.len() / dim;
let queries = queries.min(query.len() / dim);
let base = Base { dim, data };
println!(
"{dir}: {n} base, {queries} of the queries, read in {:?}",
t.elapsed()
);
println!("holding recall at {K} with probe {PROBE} rerank {RERANK} for {hours} hours");
let tuning = Tuning {
probe: PROBE,
rerank: RERANK,
..Tuning::default()
};
let mut ix = Partitions::new(dim, Bits::One, 0x51f7, tuning);
let mut buf = vec![0f32; dim];
let t = Instant::now();
for id in 0..n as u64 {
base.get(id, &mut buf);
ix.insert(id, &buf);
if ix.needs_maintenance() {
ix.maintain(&base, 4);
}
}
println!("built in {:?}, {} partitions", t.elapsed(), ix.partitions());
println!(
"{:>8}{:>14}{:>12}{:>12}{:>11}{:>11}",
"hours", "writes", "partitions", "recall@10", "p50", "p99"
);
let start = Instant::now();
let stop = Duration::from_secs_f64(hours * 3600.0);
let mut rng = Rng::new(0xD81F7);
let mut writes = 0u64;
let mut next = Duration::ZERO;
loop {
let now = start.elapsed();
let last = now >= stop;
if now >= next || last {
row(&ix, &base, &query, &truth, gdim, dim, queries, now, writes);
next = now + EVERY;
}
if last {
break;
}
for _ in 0..10_000 {
let id = rng.below(n) as u64;
base.get(id, &mut buf);
ix.remove(id);
ix.insert(id, &buf);
if ix.needs_maintenance() {
ix.maintain(&base, 4);
}
}
writes += 10_000;
}
}
#[allow(clippy::too_many_arguments)]
fn row(
ix: &Partitions,
base: &Base,
query: &[f32],
truth: &[i32],
gdim: usize,
dim: usize,
queries: usize,
at: Duration,
writes: u64,
) {
let mut hit = 0usize;
let mut took = Vec::with_capacity(queries);
for q in 0..queries {
let v = &query[q * dim..(q + 1) * dim];
let t = Instant::now();
let got = ix.search(v, K, base);
took.push(t.elapsed().as_secs_f64() * 1e6);
let want = &truth[q * gdim..q * gdim + K];
hit += got.iter().filter(|h| want.contains(&(h.id as i32))).count();
}
took.sort_by(f64::total_cmp);
let quantile = |p: f64| took[((took.len() - 1) as f64 * p) as usize];
println!(
"{:>8.2}{writes:>14}{:>12}{:>12.4}{:>9.0} us{:>9.0} us",
at.as_secs_f64() / 3600.0,
ix.partitions(),
hit as f64 / (queries * K) as f64,
quantile(0.50),
quantile(0.99)
);
}
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)
}
}