add bf_tree benchmark infrastructure#1106
Conversation
4201a33 to
1661985
Compare
Codecov Report❌ Patch coverage is ❌ Your patch status has failed because the patch coverage (61.43%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #1106 +/- ##
==========================================
- Coverage 89.46% 89.41% -0.06%
==========================================
Files 482 482
Lines 91092 91192 +100
==========================================
+ Hits 81497 81540 +43
- Misses 9595 9652 +57
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
2981ffc to
c36e2a5
Compare
There was a problem hiding this comment.
Pull request overview
Adds bf_tree-backed benchmark support to diskann-benchmark (full-precision + spherical quantization; static + streaming variants) and updates diskann-bftree to address inplace-delete behavior and reduce per-call allocations in neighbor serialization.
Changes:
- Add a new
bftreeinput schema + backend benchmarks (static build/search and streaming runbook modes, including spherical quantization bit-width dispatch). - Add delete-time vector caching in
diskann-bftreeto support inplace-delete pruning after the underlying storage entries are removed. - Rework neighbor list serialization to avoid per-call heap allocations (stack buffer fast path).
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| diskann-bftree/src/quant.rs | Makes get_vector_sync available outside tests for delete-time caching. |
| diskann-bftree/src/provider.rs | Adds delete caches + accessor fallbacks for inplace-delete pruning; exposes clear_delete_caches(). |
| diskann-bftree/src/neighbors.rs | Switches neighbor serialization to a stack buffer fast path. |
| diskann-benchmark/src/inputs/mod.rs | Centralizes PRINT_WIDTH + write_field! for consistent summaries. |
| diskann-benchmark/src/inputs/graph_index.rs | Exposes IndexBuild helpers for reuse by bf_tree inputs. |
| diskann-benchmark/src/inputs/bftree.rs | New bf_tree input schemas (FP + spherical; static + streaming) and display/validation. |
| diskann-benchmark/src/backend/index/streaming/stats.rs | Adds a bf_tree-only GenericStats::noop() helper for skipped maintenance steps. |
| diskann-benchmark/src/backend/index/mod.rs | Registers bf_tree benchmarks behind the bftree feature. |
| diskann-benchmark/src/backend/index/bftree/mod.rs | New bf_tree benchmark module + shared streaming runner. |
| diskann-benchmark/src/backend/index/bftree/full_precision.rs | Implements bf_tree full-precision static + streaming benchmarks. |
| diskann-benchmark/src/backend/index/bftree/spherical.rs | Implements bf_tree spherical quantization static + streaming benchmarks. |
| diskann-benchmark/example/graph-index-bftree.json | Example config for bf_tree full-precision static build/search. |
| diskann-benchmark/example/graph-index-bftree-stream.json | Example config for bf_tree full-precision streaming runbook. |
| diskann-benchmark/example/graph-index-bftree-spherical.json | Example config for bf_tree spherical quantization static build/search. |
| diskann-benchmark/example/graph-index-bftree-spherical-stream.json | Example config for bf_tree spherical quantization streaming runbook. |
| diskann-benchmark/Cargo.toml | Adds bf_tree dependency + feature flag. |
| Cargo.toml | Adds diskann-bftree to workspace dependencies. |
| Cargo.lock | Locks in the new dependency edge. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b8cdc3a to
8b0469c
Compare
|
@copilot re-evaluate please |
Re-evaluated: you’re right, those |
Add benchmark support for the BfTreeProvider backend, including: - Full-precision static and streaming benchmarks - Spherical quantization (1/2/4-bit) static and streaming benchmarks - Delete cache in BfTreeProvider for inplace_delete graph repair - Stack buffer optimization for neighbor serialization - Early validation of max_degree vs buffer capacity - Runtime num_bits validation (1/2/4) for spherical inputs - ADDING_PROVIDERS.md guide for wiring new backends Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
96b5580 to
dc64d96
Compare
0461814 to
cfddda1
Compare
hildebrandmw
left a comment
There was a problem hiding this comment.
Thanks Jordan, this is looking good!
| } | ||
|
|
||
| /// Construct a type-erased `Poly<dyn Quantizer>` for the given NBITS. | ||
| macro_rules! make_quantizer_poly { |
There was a problem hiding this comment.
This need not be a macro:
fn new_quantizer<const NBITS: usize>(
quantizer: SphericalQuantizer,
) -> Result<Poly<dyn Quantizer>, AllocatorError>
where
spherical_iface::Impl<NBITS>: spherical_iface::Constructible + Quantizer,
{
let imp = spherical_iface::Impl::<NBITS>::new(quantizer)?;
diskann_quantization::poly!(Quantizer, imp, GlobalAllocator)
}| 1 => make_quantizer_poly!(1, quantizer), | ||
| 2 => make_quantizer_poly!(2, quantizer), | ||
| 4 => make_quantizer_poly!(4, quantizer), | ||
| n => anyhow::bail!("Unsupported num_bits: {n}"), |
There was a problem hiding this comment.
We can add this check to try_match? The goal of try_match is to catch input issues like this as early as possible, so a user doesn't queue up a list of benchmarks just to have them fail mid-run.
I guess this is added as part of input validation, so maybe not entirely needed, but is still a nice safety mechanism?
| // During `inplace_delete`, the algorithm first removes vectors from storage via | ||
| // `Delete::delete`, then prunes the neighbors of the deleted node which requires | ||
| // reading the deleted vector back through the accessor. We cache vectors here | ||
| // before deletion so the accessor can fall back to the cache on a `Transient` error. |
There was a problem hiding this comment.
I don't think is the right way to handle this. The issue is multi-insert wanting to access the adjacency list, right? I think there are several better options:
- In
bf-tree, we can delete the data, but not the adjacency lists. That way inplace delete will continue to work. This is a local fix. - Adjust the semantics of inplace delete or how it's called in benchmarks to actually delete the vector only after inplace delete runs so the algorithm still has adjacency list information?
In general, if we see a mismatch like this, we should focus on not working around the problem.
| /// Stack buffer size for neighbor serialization. Supports up to 512 neighbors | ||
| /// with u32 IDs (512 * 4 = 2048 bytes). Any practical ANN workload uses | ||
| /// max_degree well below this limit. | ||
| const MAX_NEIGHBOR_BUF_BYTES: usize = 2048; |
There was a problem hiding this comment.
This is a huge stack allocation. Is this showing up in profiles?
We already create auxiliary accessors for neighbor accesses - can't we use a Vec inside those as the scratch space? The accessors are already reused across calls and so we can amortize the Vec allocation.
| | `src/inputs/<provider>.rs` | Input structs (JSON schema), `Display`, `Checker`, `Example` impls | | ||
| | `src/backend/index/<provider>/mod.rs` | Module root, `register_benchmarks()`, shared helpers | | ||
| | `src/backend/index/<provider>/full_precision.rs` | Static + streaming FP benchmarks | | ||
| | `src/backend/index/<provider>/spherical.rs` | Quantized benchmarks (if applicable) | |
There was a problem hiding this comment.
Some of these seem pretty prescriptive for BF-tree in particular? Can this be trimmed down to more general instructions? Or better yet, folded into the README?
| write_field!(f, "cb_size_byte", self.cb_size_byte)?; | ||
| write_field!(f, "leaf_page_size", self.leaf_page_size)?; | ||
| if let Some(v) = self.cb_max_record_size { | ||
| write_field!(f, "cb_max_record_size", v)?; |
There was a problem hiding this comment.
Along with my other comment, we should at least record what the default values are, even if they aren't explicitly set by the user (though they probably should be).
| build: IndexBuild, | ||
| search_phase: SearchPhase, | ||
| #[serde(default)] | ||
| vector_store_config: Option<BfTreeStoreConfig>, |
There was a problem hiding this comment.
I really think it's a good idea to not use defaults here. It's more verbose, but also means we have more information.
|
|
||
| /// Construct an empty stats entry (e.g., when maintenance is skipped). | ||
| #[cfg(feature = "bftree")] | ||
| pub(crate) fn empty(kind: Cow<'static, str>) -> Self { |
There was a problem hiding this comment.
Is there a way to avoid this kind of empty representation? The generic stats was written in a way that was intentionally pretty hard to construct with out data, and Rust already has a way of representing optional data with Option. I get that this is to reuse StreamStats, but maybe there's a better way?
| ))?); | ||
|
|
||
| let max_points = | ||
| ((max_points_arg as f32) * (1.0 + 2.0 * consolidate_threshold)).ceil() as usize; |
There was a problem hiding this comment.
I believe this exists to work around limitations in delete tracking for the inmem index. Since bf-tree is using "hard deletes" instead, I think we can potentially avoid needing this extra overhead. Happy to talk about it offline.
| use diskann::graph::InplaceDeleteMethod; | ||
| use diskann::utils::ONE; | ||
| use diskann_benchmark_core::{recall::Rows, streaming::executors::bigann}; | ||
| use diskann_utils::views::MatrixView; |
There was a problem hiding this comment.
Does it make sense to either group all imports together at the top or split this into two files?
Adds benchmark support for the bf_tree storage backend — both full-precision and spherical-quantized, in static (build-once) and streaming (insert/delete/search) modes.
What's included
Benchmark variants (4 tags):
Bug fixes in diskann-bftree:
Benchmark infrastructure:
Example configs
See diskann-benchmark/example/graph-index-bftree*.json for ready-to-run configurations targeting MSTuring-1M.
tests on CosmosDBDevBox VM (16 vCPU 64GB RAM 2,048GB SSD)
Results from MSTuring-1M
Full Precision streaming:
Spherical 2-bit streaming
Spherical 4-bit streaming
Comparison across bit widths at L=200:
The configuration used in the above tests was enough to keep all of the data in memory and didn't overflow to disk. I also tested full-precision using 32mb of cb size and experienced a 2x performance degradation due to the disk lookups.
CI reports an increase in IR size of 11%, which is over the 5% allowed in the CI file. This is largely unavoidable given the amount of code added here, so that CI gate should be ignored for this PR.