Skip to content

Commit ad17a52

Browse files
author
Alex Razumov (from Dev Box)
committed
Merge branch 'main' into u/arrayka/diskann-disk-tests3
2 parents 86e0fc8 + 999fa5d commit ad17a52

22 files changed

Lines changed: 1836 additions & 98 deletions

File tree

diskann-benchmark-core/src/search/graph/knn.rs

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::{
2323
};
2424

2525
/// A built-in helper for benchmarking the K-nearest neighbors method
26-
/// [`graph::DiskANNIndex::search`].
26+
/// [`graph::DiskANNIndex::search`] with optional post-processing support.
2727
///
2828
/// This is intended to be used in conjunction with [`search::search`] or
2929
/// [`search::search_all`] and provides some basic additional metrics for
@@ -32,21 +32,31 @@ use crate::{
3232
///
3333
/// The provided implementation of [`Search`] accepts [`graph::search::Knn`]
3434
/// and returns [`Metrics`] as additional output.
35+
///
36+
/// # Type Parameters
37+
///
38+
/// - `DP`: The data provider type
39+
/// - `T`: The query element type
40+
/// - `S`: The search strategy type
41+
/// - `PP`: Post-processor selector. Defaults to [`Defaulted`], which uses the
42+
/// strategy's default post-processor. Use [`KNN::with_postprocessor`] to
43+
/// supply an explicit post-processor.
3544
#[derive(Debug)]
36-
pub struct KNN<DP, T, S>
45+
pub struct KNN<DP, T, S, PP = Defaulted>
3746
where
3847
DP: provider::DataProvider,
3948
{
4049
index: Arc<graph::DiskANNIndex<DP>>,
4150
queries: Arc<Matrix<T>>,
4251
strategy: Strategy<S>,
52+
post_processor: PP,
4353
}
4454

45-
impl<DP, T, S> KNN<DP, T, S>
55+
impl<DP, T, S> KNN<DP, T, S, Defaulted>
4656
where
4757
DP: provider::DataProvider,
4858
{
49-
/// Construct a new [`KNN`] searcher.
59+
/// Construct a new [`KNN`] searcher using the strategy's default post-processor.
5060
///
5161
/// If `strategy` is one of the container variants of [`Strategy`], its length
5262
/// must match the number of rows in `queries`. If this is the case, then the
@@ -68,10 +78,98 @@ where
6878
index,
6979
queries,
7080
strategy,
81+
post_processor: Defaulted,
82+
}))
83+
}
84+
}
85+
86+
impl<DP, T, S, PP> KNN<DP, T, S, Forwarded<PP>>
87+
where
88+
DP: provider::DataProvider,
89+
{
90+
/// Construct a new [`KNN`] searcher with an explicit post-processor.
91+
///
92+
/// # Errors
93+
///
94+
/// Returns an error if the number of elements in `strategy` is not compatible with
95+
/// the number of rows in `queries`.
96+
pub fn with_postprocessor(
97+
index: Arc<graph::DiskANNIndex<DP>>,
98+
queries: Arc<Matrix<T>>,
99+
strategy: Strategy<S>,
100+
post_processor: PP,
101+
) -> anyhow::Result<Arc<Self>> {
102+
strategy.length_compatible(queries.nrows())?;
103+
104+
Ok(Arc::new(Self {
105+
index,
106+
queries,
107+
strategy,
108+
post_processor: Forwarded(post_processor),
71109
}))
72110
}
73111
}
74112

113+
impl<DP, T, S, PP> KNN<DP, T, S, PP>
114+
where
115+
DP: provider::DataProvider,
116+
{
117+
/// Access the index.
118+
pub fn index(&self) -> &Arc<graph::DiskANNIndex<DP>> {
119+
&self.index
120+
}
121+
}
122+
123+
/// Resolves a post-processor for [`KNN`] given a search strategy.
124+
///
125+
/// This trait lets [`KNN`] support both "use the strategy's default post-processor"
126+
/// ([`Defaulted`]) and "use this explicit post-processor" ([`Forwarded`]) without
127+
/// duplicating the search loop.
128+
pub trait AsPostProcessor<'a, S, DP, T>
129+
where
130+
DP: provider::DataProvider,
131+
S: glue::SearchStrategy<'a, DP, T>,
132+
{
133+
/// The concrete post-processor used for a single search.
134+
type Processor: glue::SearchPostProcess<S::SearchAccessor, T, DP::ExternalId> + Send + Sync;
135+
136+
/// Construct the post-processor to use for a single search.
137+
fn as_post_processor(&'a self, strategy: &'a S) -> Self::Processor;
138+
}
139+
140+
/// Marker indicating that [`KNN`] should use the strategy's default post-processor.
141+
#[derive(Debug, Clone, Copy)]
142+
pub struct Defaulted;
143+
144+
impl<'a, S, DP, T> AsPostProcessor<'a, S, DP, T> for Defaulted
145+
where
146+
DP: provider::DataProvider,
147+
S: glue::DefaultPostProcessor<'a, DP, T, DP::ExternalId>,
148+
{
149+
type Processor = S::Processor;
150+
151+
fn as_post_processor(&'a self, strategy: &'a S) -> Self::Processor {
152+
strategy.default_post_processor()
153+
}
154+
}
155+
156+
/// Wraps an explicit post-processor for use with [`KNN::with_postprocessor`].
157+
#[derive(Debug, Clone, Copy)]
158+
pub struct Forwarded<PP>(PP);
159+
160+
impl<'a, S, DP, T, PP> AsPostProcessor<'a, S, DP, T> for Forwarded<PP>
161+
where
162+
DP: provider::DataProvider,
163+
S: glue::SearchStrategy<'a, DP, T>,
164+
PP: glue::SearchPostProcess<S::SearchAccessor, T, DP::ExternalId> + Clone + AsyncFriendly,
165+
{
166+
type Processor = PP;
167+
168+
fn as_post_processor(&'a self, _strategy: &'a S) -> Self::Processor {
169+
self.0.clone()
170+
}
171+
}
172+
75173
/// Additional metrics collected during [`KNN`] search.
76174
///
77175
/// # Note
@@ -86,10 +184,11 @@ pub struct Metrics {
86184
pub hops: u32,
87185
}
88186

89-
impl<DP, T, S> Search for KNN<DP, T, S>
187+
impl<DP, T, S, PP> Search for KNN<DP, T, S, PP>
90188
where
91189
DP: provider::DataProvider<Context: Default, ExternalId: search::Id>,
92-
S: for<'a> glue::DefaultSearchStrategy<'a, DP, &'a [T], DP::ExternalId> + Clone + AsyncFriendly,
190+
S: for<'a> glue::SearchStrategy<'a, DP, &'a [T]> + Clone + AsyncFriendly,
191+
PP: for<'a> AsPostProcessor<'a, S, DP, &'a [T]> + AsyncFriendly,
93192
graph::search::Knn:
94193
for<'a> graph::Search<'a, DP, S, &'a [T], Output = graph::index::SearchStats>,
95194
T: AsyncFriendly + Clone,
@@ -117,11 +216,15 @@ where
117216
{
118217
let context = DP::Context::default();
119218
let knn_search = *parameters;
219+
let strategy = self.strategy.get(index)?;
220+
let processor = self.post_processor.as_post_processor(strategy);
221+
120222
let stats = self
121223
.index
122-
.search(
224+
.search_with(
123225
knn_search,
124-
self.strategy.get(index)?,
226+
strategy,
227+
processor,
125228
&context,
126229
self.queries.row(index),
127230
buffer,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"search_directories": [
3+
"test_data/disk_index_search"
4+
],
5+
"jobs": [
6+
{
7+
"type": "graph-index-build",
8+
"content": {
9+
"source": {
10+
"index-source": "Build",
11+
"data_type": "float32",
12+
"data": "disk_index_siftsmall_learn_256pts_data.fbin",
13+
"distance": "squared_l2",
14+
"max_degree": 32,
15+
"l_build": 50,
16+
"alpha": 1.2,
17+
"backedge_ratio": 1.0,
18+
"num_threads": 1,
19+
"start_point_strategy": "medoid",
20+
"num_insert_attempts": 1,
21+
"saturate_inserts": false
22+
},
23+
"search_phase": {
24+
"search-type": "topk-determinant-diversity",
25+
"queries": "disk_index_sample_query_10pts.fbin",
26+
"groundtruth": "disk_index_10pts_idx_uint32_truth_search_res.bin",
27+
"reps": 5,
28+
"num_threads": [
29+
1
30+
],
31+
"power": 2.0,
32+
"eta": 0.01,
33+
"runs": [
34+
{
35+
"search_n": 20,
36+
"search_l": [
37+
20,
38+
30,
39+
40
40+
],
41+
"recall_k": 10
42+
}
43+
]
44+
}
45+
}
46+
}
47+
]
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"search_directories": [
3+
"test_data/disk_index_search"
4+
],
5+
"jobs": [
6+
{
7+
"type": "disk-index",
8+
"content": {
9+
"source": {
10+
"disk-index-source": "Build",
11+
"data_type": "float32",
12+
"data": "disk_index_siftsmall_learn_256pts_data.fbin",
13+
"distance": "squared_l2",
14+
"dim": 128,
15+
"max_degree": 32,
16+
"l_build": 50,
17+
"num_threads": 1,
18+
"build_ram_limit_gb": 2.0,
19+
"num_pq_chunks": 128,
20+
"quantization_type": "FP",
21+
"save_path": "siftsmall_index_full_det_div"
22+
},
23+
"search_phase": {
24+
"queries": "disk_index_sample_query_10pts.fbin",
25+
"groundtruth": "disk_index_10pts_idx_uint32_truth_search_res.bin",
26+
"search_list": [10, 20, 40],
27+
"beam_width": 4,
28+
"recall_at": 10,
29+
"num_threads": 1,
30+
"is_flat_search": false,
31+
"distance": "squared_l2",
32+
"vector_filters_file": null,
33+
"post_processor": {
34+
"type": "determinant-diversity",
35+
"power": 2.0,
36+
"eta": 1.0
37+
}
38+
}
39+
}
40+
}
41+
]
42+
}

diskann-benchmark/src/disk_index/search.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use diskann_benchmark_runner::{files::InputFile, utils::MicroSeconds};
1414
use diskann_disk::{
1515
data_model::{AdHoc, CachingStrategy},
1616
search::provider::{
17-
disk_provider::DiskIndexSearcher, disk_vertex_provider_factory::DiskVertexProviderFactory,
17+
disk_provider::{DiskIndexSearcher, SearchPostProcessorKind},
18+
disk_vertex_provider_factory::DiskVertexProviderFactory,
1819
},
1920
storage::disk_index_reader::DiskIndexReader,
2021
utils::{instrumentation::PerfLogger, statistics, AlignedFileReaderFactory, QueryStatistics},
@@ -32,7 +33,10 @@ use serde::{Deserialize, Serialize};
3233

3334
use crate::{
3435
disk_index::json_spancollector::JsonSpanCollector,
35-
inputs::disk::{DiskIndexLoad, DiskSearchPhase},
36+
inputs::{
37+
disk::{DiskIndexLoad, DiskSearchPhase},
38+
post_processor::TopkPostProcessor,
39+
},
3640
utils::{datafiles, SimilarityMeasure},
3741
};
3842

@@ -264,6 +268,12 @@ where
264268
zipped.for_each_in_pool(
265269
pool.as_ref(),
266270
|(((((q, vf), id_chunk), dist_chunk), stats), rc)| {
271+
let post_processor = search_params.post_processor.as_ref().map_or(
272+
SearchPostProcessorKind::None,
273+
|TopkPostProcessor::DeterminantDiversity(params)| {
274+
SearchPostProcessorKind::DeterminantDiversity(*params)
275+
},
276+
);
267277
let vector_filter = if search_params.vector_filters_file.is_none() {
268278
None
269279
} else {
@@ -277,20 +287,21 @@ where
277287
l,
278288
Some(search_params.beam_width),
279289
vector_filter,
290+
post_processor,
280291
search_params.is_flat_search,
281292
) {
282293
Ok(search_result) => {
283294
*stats = search_result.stats.query_statistics;
284-
*rc = search_result.results.len() as u32;
285-
let actual_results = search_result
286-
.results
287-
.len()
288-
.min(search_params.recall_at as usize);
289-
for (i, result_item) in search_result
290-
.results
291-
.iter()
292-
.take(actual_results)
293-
.enumerate()
295+
let base_count = (search_result.stats.result_count as usize)
296+
.min(search_params.recall_at as usize)
297+
.min(search_result.results.len());
298+
299+
*rc = base_count as u32;
300+
id_chunk.fill(0);
301+
dist_chunk.fill(0.0);
302+
303+
for (i, result_item) in
304+
search_result.results.iter().take(base_count).enumerate()
294305
{
295306
id_chunk[i] = result_item.vertex_id;
296307
dist_chunk[i] = result_item.distance;

0 commit comments

Comments
 (0)