use std::{
collections::HashMap,
path::{Path, PathBuf},
time::Instant,
};
use anyhow::Context;
use clap::Parser;
use diskann::neighbor::{Neighbor, NeighborPriorityQueue};
use diskann::utils::{IntoUsize, VectorRepr};
use diskann_benchmark_core::streaming::executors::bigann::{
FindGroundtruth, RunBook, ScanDirectory,
};
use diskann_benchmark_core::streaming::{self, Executor};
use diskann_providers::storage::{FileStorageProvider, StorageReadProvider};
use diskann_tools::utils::{
init_subscriber, write_ground_truth, CMDResult, CMDToolError, DataType,
};
use diskann_utils::io::read_bin;
use diskann_utils::views::Matrix;
use diskann_vector::{distance::Metric, DistanceFunction};
use rayon::prelude::*;
trait GroundtruthDistance: Send + Sync {
fn n_base(&self) -> usize;
fn n_queries(&self) -> usize;
fn distance(&self, query: usize, internal_id: usize) -> anyhow::Result<f32>;
}
struct MatrixDistance<'a, V: VectorRepr + Send + Sync> {
dataset: &'a Matrix<V>,
queries: &'a Matrix<V>,
distance_fn: V::Distance,
}
impl<'a, V> GroundtruthDistance for MatrixDistance<'a, V>
where
V: VectorRepr + Send + Sync,
{
fn n_base(&self) -> usize {
self.dataset.nrows()
}
fn n_queries(&self) -> usize {
self.queries.nrows()
}
fn distance(&self, query: usize, internal_id: usize) -> anyhow::Result<f32> {
if query >= self.n_queries() {
return Err(anyhow::anyhow!("query index {} out of bounds", query));
}
if internal_id >= self.n_base() {
return Err(anyhow::anyhow!("internal id {} out of bounds", internal_id));
}
let query_row = self.queries.row(query);
let data_row = self.dataset.row(internal_id);
Ok(self.distance_fn.evaluate_similarity(data_row, query_row))
}
}
fn compute_groundtruth_results(
distance: &dyn GroundtruthDistance,
active_entries: &[(u32, usize)],
recall_at: usize,
) -> anyhow::Result<Vec<NeighborPriorityQueue<u32>>> {
#[allow(clippy::disallowed_methods)]
let results = (0..distance.n_queries())
.into_par_iter()
.map(|query_id| {
let mut pq = NeighborPriorityQueue::new(recall_at);
let query_result: anyhow::Result<()> =
active_entries
.iter()
.try_for_each(|&(external_id, internal_id)| {
let dist = distance.distance(query_id, internal_id)?;
pq.insert(Neighbor::new(external_id, dist));
Ok(())
});
query_result.map(|()| pq)
})
.collect::<Vec<anyhow::Result<NeighborPriorityQueue<u32>>>>();
results.into_iter().collect()
}
fn main() -> CMDResult<()> {
init_subscriber();
let args = Args::parse();
match args.data_type {
DataType::Float => run::<f32>(&args),
DataType::Fp16 => run::<diskann_vector::Half>(&args),
DataType::Uint8 => run::<u8>(&args),
DataType::Int8 => run::<i8>(&args),
}
}
fn run<V: VectorRepr + Send + Sync>(args: &Args) -> CMDResult<()> {
let storage = FileStorageProvider;
tracing::info!("Loading dataset from {}", args.base_file);
let dataset =
read_bin::<V>(&mut storage.open_reader(&args.base_file)?).map_err(|e| CMDToolError {
details: e.to_string(),
})?;
tracing::info!("Loading queries from {}", args.query_file);
let queries =
read_bin::<V>(&mut storage.open_reader(&args.query_file)?).map_err(|e| CMDToolError {
details: e.to_string(),
})?;
let recall_at = args.recall_at as usize;
if dataset.ncols() != queries.ncols() {
return Err(CMDToolError {
details: format!(
"Dataset dimension {} does not match query dimension {}",
dataset.ncols(),
queries.ncols()
),
});
}
tracing::info!(
"Dataset: {} vectors, Queries: {} vectors, dim: {}, recall@{}",
dataset.nrows(),
queries.nrows(),
dataset.ncols(),
recall_at,
);
run_inner(
&MatrixDistance {
dataset: &dataset,
queries: &queries,
distance_fn: V::distance(args.distance_function, Some(dataset.ncols())),
},
args,
)?;
Ok(())
}
fn run_inner(distance: &dyn GroundtruthDistance, args: &Args) -> CMDResult<()> {
let storage = FileStorageProvider;
let output_dir = Path::new(&args.output_dir);
std::fs::create_dir_all(output_dir)
.with_context(|| format!("creating output directory {}", output_dir.display()))
.map_err(|e| CMDToolError {
details: e.to_string(),
})?;
let gt_suffix = format!("gt{}", args.recall_at);
struct AllowMissing {
dir: PathBuf,
suffix: String,
}
impl FindGroundtruth for AllowMissing {
fn find_groundtruth(&mut self, stage: usize) -> anyhow::Result<PathBuf> {
Ok(self
.dir
.join(ScanDirectory::groundtruth_filename(stage, &self.suffix)))
}
}
let mut finder = AllowMissing {
dir: output_dir.to_path_buf(),
suffix: gt_suffix,
};
tracing::info!(
"Parsing runbook {} for dataset \"{}\"",
args.runbook_file,
args.dataset_name
);
let mut runbook = RunBook::load(
Path::new(&args.runbook_file),
&args.dataset_name,
&mut finder,
)
.map_err(|e| CMDToolError {
details: e.to_string(),
})?;
tracing::info!("Runbook has {} stages", runbook.len());
let mut stream = GroundtruthStream {
storage: &storage,
distance,
recall_at: args.recall_at.into_usize(),
external_to_internal: HashMap::new(),
};
runbook
.run_with(&mut stream, |_| Ok(()))
.map_err(|e| CMDToolError {
details: e.to_string(),
})?;
tracing::info!("Done.");
Ok(())
}
struct GroundtruthStream<'a> {
storage: &'a FileStorageProvider,
distance: &'a dyn GroundtruthDistance,
recall_at: usize,
external_to_internal: HashMap<u32, usize>,
}
impl<'a> GroundtruthStream<'a> {
fn remove_active_external_id(&mut self, external_id: u32) {
self.external_to_internal.remove(&external_id);
}
}
impl<'a> streaming::Stream<diskann_benchmark_core::streaming::executors::bigann::Args>
for GroundtruthStream<'a>
{
type Output = ();
fn search(
&mut self,
args: diskann_benchmark_core::streaming::executors::bigann::Search<'_>,
) -> anyhow::Result<Self::Output> {
let timer = Instant::now();
let n_active = self.external_to_internal.len();
if n_active < self.recall_at {
return Err(anyhow::anyhow!(
"{}: active set has {} vectors, which is fewer than recall_at={} required to compute groundtruth",
args.groundtruth.display(),
n_active,
self.recall_at,
));
}
tracing::info!(
"Computing top-{} groundtruth for {} active vectors against {} queries",
self.recall_at,
n_active,
self.distance.n_queries(),
);
let active_entries: Vec<(u32, usize)> = self
.external_to_internal
.iter()
.map(|(external_id, internal_id)| (*external_id, *internal_id))
.collect();
let results = compute_groundtruth_results(self.distance, &active_entries, self.recall_at)?;
write_ground_truth::<()>(
self.storage,
args.groundtruth.to_str().ok_or_else(|| {
anyhow::anyhow!("Non-UTF8 groundtruth path: {}", args.groundtruth.display())
})?,
self.distance.n_queries(),
self.recall_at,
results,
None,
)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
tracing::info!(
"Groundtruth written to {} in {:?}",
args.groundtruth.display(),
timer.elapsed(),
);
Ok(())
}
fn insert(
&mut self,
args: diskann_benchmark_core::streaming::executors::bigann::Insert,
) -> anyhow::Result<Self::Output> {
let n_base = self.distance.n_base();
for internal_id in args.offsets.clone() {
if internal_id < n_base {
self.external_to_internal
.insert(internal_id as u32, internal_id);
}
}
Ok(())
}
fn replace(
&mut self,
args: diskann_benchmark_core::streaming::executors::bigann::Replace,
) -> anyhow::Result<Self::Output> {
for external_id in args.ids.clone() {
self.remove_active_external_id(external_id as u32);
}
let n_base = self.distance.n_base();
for (internal_id, external_id) in args.offsets.clone().zip(args.ids.clone()) {
if internal_id < n_base {
self.external_to_internal
.insert(external_id as u32, internal_id);
}
}
Ok(())
}
fn delete(
&mut self,
args: diskann_benchmark_core::streaming::executors::bigann::Delete,
) -> anyhow::Result<Self::Output> {
for external_id in args.ids.clone() {
self.remove_active_external_id(external_id as u32);
}
Ok(())
}
fn maintain(&mut self, _args: ()) -> anyhow::Result<Self::Output> {
Ok(())
}
fn needs_maintenance(&mut self) -> bool {
false
}
}
#[derive(Debug, Parser)]
struct Args {
#[arg(long = "data-type", default_value = "float")]
pub data_type: DataType,
#[arg(long = "dist-fn", default_value = "l2")]
pub distance_function: Metric,
#[arg(long = "base-file", short, required = true)]
pub base_file: String,
#[arg(long = "query-file", short, required = true)]
pub query_file: String,
#[arg(long = "runbook-file", required = true)]
pub runbook_file: String,
#[arg(long = "dataset-name", required = true)]
pub dataset_name: String,
#[arg(long = "recall-at", short = 'K', required = true)]
pub recall_at: u32,
#[arg(long = "gt-dir", short, required = true)]
pub output_dir: String,
}