Skip to content

add bf_tree benchmark infrastructure#1106

Open
JordanMaples wants to merge 6 commits into
mainfrom
jordanmaples/bftree-benchmark
Open

add bf_tree benchmark infrastructure#1106
JordanMaples wants to merge 6 commits into
mainfrom
jordanmaples/bftree-benchmark

Conversation

@JordanMaples
Copy link
Copy Markdown
Contributor

@JordanMaples JordanMaples commented May 26, 2026

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):

  • graph-index-bftree-full-precision-f32 — static build + search
  • graph-index-bftree-stream-full-precision-f32 — streaming with runbook
  • graph-index-build-bftree-spherical-quantization — static spherical (1/2/4-bit)
  • graph-index-stream-bftree-spherical-quantization — streaming spherical (1/2/4-bit)

Bug fixes in diskann-bftree:

  1. Deferred deletion for pruning correctness: inplace_delete removes a vector via Delete::delete, then prunes the deleted node's neighbors — which requires reading the deleted vector back for distance computations. Previously the vector was immediately removed from bf-tree storage, causing Transient errors during pruning. Fix: delete() now defers actual vector removal by recording the ID in pending_deletes. Vectors remain readable throughout pruning. flush_deletes() performs the actual bf-tree removal and is called from maintain() after each inplace_delete batch completes.
  2. Max-degree mismatch in append_vector: The old append_vector would resize the neighbor list to self.dim and write the count at dim - 1, but set_neighbors wrote a variable-length buffer. This inconsistency caused bf-tree page fragmentation and potential read errors when the two code paths produced different value sizes for the same key. Fix: both paths now write a fixed-size dim-element buffer using the same format (|neighbors|padding|count|).
  3. Reusable buffer in hot path: set_neighbors and append_vector used Vec::concat() / Vec::resize() on every call. Replaced with a single Mutex<Vec<u8>> buffer allocated once in NeighborProvider::new(), eliminating per-call heap allocations.

Benchmark infrastructure:

  • Expose bf_tree internal config (buffer sizes, fanout, promotion rates) as JSON input fields with default backfill in validate
  • Shared helpers to reduce duplication: bftree_parameters_from(), run_streaming()
  • flush_deletes() to perform deferred vector removal after maintenance passes
  • Removed QuantDelete trait — no longer needed with deferred deletion approach
  • GenericStats::empty replaced with Option<GenericStats> for maintain stats (bf_tree has no separate drop_deleted/release phases)

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:

 max_degree=64, L_build=128, alpha=1.2, 4GB bf_tree buffers, 4 build threads, 2 search threads

┌───────┬────────┬─────────┬───────────────────┬──────────────────────────┬────────────────────┐
│ Stage │ Op     │ Vectors │ Avg / p99 Latency │ Recall@10 (L=32 / L=200) │ QPS (L=32 / L=200) │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 1     │ Insert │ 1M      │ 11.9ms / 17.9ms   │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 2     │ Search │ —       │ —                 │ 82.8% / 97.0%            │ 460 / 99           │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 3     │ Delete │ 500K    │ 16.3ms / 20.9ms   │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 4     │ Search │ —       │ —                 │ 84.2% / 97.9%            │ 474 / 97           │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 5     │ Insert │ 500K    │ 15.2ms / 19.6ms   │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 6     │ Search │ —       │ —                 │ 83.9% / 97.4%            │ 430 / 92           │
└───────┴────────┴─────────┴───────────────────┴──────────────────────────┴────────────────────┘

Spherical 2-bit streaming:

 max_degree=64, L_build=128, alpha=1.2, 4GB bf_tree buffers, 4 build threads, 2 search threads

┌───────┬────────┬─────────┬───────────────────┬──────────────────────────┬────────────────────┐
│ Stage │ Op     │ Vectors │ Avg / p99 Latency │ Recall@10 (L=32 / L=200) │ QPS (L=32 / L=200) │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 1     │ Insert │ 1M      │ 4.8ms / 7.6ms     │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 2     │ Search │ —       │ —                 │ 74.7% / 94.0%            │ 1167 / 255         │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 3     │ Delete │ 500K    │ 6.9ms / 9.3ms     │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 4     │ Search │ —       │ —                 │ 73.4% / 94.1%            │ 1103 / 233         │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 5     │ Insert │ 500K    │ 6.0ms / 8.6ms     │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 6     │ Search │ —       │ —                 │ 75.1% / 94.3%            │ 1053 / 229         │
└───────┴────────┴─────────┴───────────────────┴──────────────────────────┴────────────────────┘

Spherical 4-bit streaming:

 max_degree=64, L_build=128, alpha=1.2, 4GB bf_tree buffers, 4 build threads, 2 search threads

┌───────┬────────┬─────────┬───────────────────┬──────────────────────────┬────────────────────┐
│ Stage │ Op     │ Vectors │ Avg / p99 Latency │ Recall@10 (L=32 / L=200) │ QPS (L=32 / L=200) │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 1     │ Insert │ 1M      │ 4.8ms / 7.7ms     │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 2     │ Search │ —       │ —                 │ 82.3% / 96.7%            │ 1127 / 253         │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 3     │ Delete │ 500K    │ 6.9ms / 9.5ms     │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 4     │ Search │ —       │ —                 │ 82.4% / 97.3%            │ 1095 / 231         │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 5     │ Insert │ 500K    │ 6.3ms / 9.1ms     │ —                        │ —                  │
├───────┼────────┼─────────┼───────────────────┼──────────────────────────┼────────────────────┤
│ 6     │ Search │ —       │ —                 │ 83.1% / 97.1%            │ 1019 / 217         │
└───────┴────────┴─────────┴───────────────────┴──────────────────────────┴────────────────────┘

Comparison across bit widths at L=200:

┌─────────────────┬────────┬─────┬────────────────┐
│ Variant         │ Recall │ QPS │ Insert Latency │
├─────────────────┼────────┼─────┼────────────────┤
│ Full Precision  │ 97.0%  │ 99  │ 11.9ms         │
├─────────────────┼────────┼─────┼────────────────┤
│ Spherical 4-bit │ 96.7%  │ 253 │ 4.8ms          │
├─────────────────┼────────┼─────┼────────────────┤
│ Spherical 2-bit │ 94.0%  │ 255 │ 4.8ms          │
└─────────────────┴────────┴─────┴────────────────┘

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.

@JordanMaples JordanMaples force-pushed the jordanmaples/bftree-benchmark branch from 4201a33 to 1661985 Compare May 26, 2026 21:21
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 26, 2026

Codecov Report

❌ Patch coverage is 75.57252% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.34%. Comparing base (edf1899) to head (3bc7e49).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...ann-benchmark/src/backend/index/streaming/stats.rs 0.00% 11 Missing ⚠️
diskann-benchmark/src/backend/index/benchmarks.rs 33.33% 6 Missing ⚠️
diskann-benchmark/src/inputs/graph_index.rs 73.91% 6 Missing ⚠️
...mark/src/backend/index/streaming/full_precision.rs 0.00% 5 Missing ⚠️
diskann-bftree/src/provider.rs 89.47% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1106      +/-   ##
==========================================
- Coverage   89.37%   89.34%   -0.04%     
==========================================
  Files         484      484              
  Lines       91331    91452     +121     
==========================================
+ Hits        81630    81704      +74     
- Misses       9701     9748      +47     
Flag Coverage Δ
miri 89.34% <75.57%> (-0.04%) ⬇️
unittests 88.99% <75.57%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
diskann-benchmark/src/backend/index/build.rs 85.92% <100.00%> (ø)
diskann-benchmark/src/backend/index/mod.rs 100.00% <100.00%> (ø)
diskann-benchmark/src/backend/index/product.rs 100.00% <ø> (ø)
diskann-benchmark/src/backend/index/scalar.rs 100.00% <ø> (ø)
diskann-benchmark/src/backend/index/spherical.rs 100.00% <ø> (ø)
diskann-benchmark/src/inputs/mod.rs 81.25% <ø> (ø)
diskann-bftree/src/neighbors.rs 92.80% <100.00%> (+0.58%) ⬆️
diskann-bftree/src/quant.rs 89.83% <ø> (-1.70%) ⬇️
diskann-bftree/src/provider.rs 91.55% <89.47%> (-0.39%) ⬇️
...mark/src/backend/index/streaming/full_precision.rs 0.00% <0.00%> (ø)
... and 3 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JordanMaples JordanMaples force-pushed the jordanmaples/bftree-benchmark branch 2 times, most recently from 2981ffc to c36e2a5 Compare May 26, 2026 22:44
@JordanMaples JordanMaples marked this pull request as ready for review May 27, 2026 15:15
@JordanMaples JordanMaples requested review from a team and Copilot May 27, 2026 15:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bftree input schema + backend benchmarks (static build/search and streaming runbook modes, including spherical quantization bit-width dispatch).
  • Add delete-time vector caching in diskann-bftree to 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.

Comment thread diskann-bftree/src/provider.rs Outdated
Comment thread diskann-bftree/src/provider.rs Outdated
Comment thread diskann-bftree/src/neighbors.rs Outdated
Comment thread diskann-bftree/src/neighbors.rs Outdated
Comment thread diskann-benchmark/Cargo.toml
Comment thread diskann-benchmark/example/graph-index-bftree-stream.json
Comment thread diskann-benchmark/example/graph-index-bftree-spherical-stream.json
Comment thread diskann-benchmark/src/inputs/bftree.rs
Comment thread diskann-benchmark/src/inputs/bftree.rs
Comment thread diskann-benchmark/src/inputs/bftree.rs
@JordanMaples JordanMaples force-pushed the jordanmaples/bftree-benchmark branch from b8cdc3a to 8b0469c Compare May 27, 2026 16:39
@JordanMaples
Copy link
Copy Markdown
Contributor Author

@copilot re-evaluate please

Copy link
Copy Markdown
Contributor

Copilot AI commented May 27, 2026

@copilot re-evaluate please

Re-evaluated: you’re right, those match result arms compile as written. I verified locally with cargo check -p diskann-bftree and did not find a moved-value compile error there, so no code change is needed for that feedback.

@JordanMaples JordanMaples linked an issue May 27, 2026 that may be closed by this pull request
@JordanMaples JordanMaples force-pushed the jordanmaples/bftree-benchmark branch 2 times, most recently from 0461814 to cfddda1 Compare May 29, 2026 16:53
Copy link
Copy Markdown
Contributor

@hildebrandmw hildebrandmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Jordan, this is looking good!

Comment thread diskann-benchmark/src/backend/index/bftree/spherical.rs Outdated
Comment thread diskann-benchmark/src/backend/index/bftree/spherical.rs Outdated
Comment thread diskann-bftree/src/provider.rs Outdated
// 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.
  2. 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.

Comment thread diskann-bftree/src/neighbors.rs Outdated
/// 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was something that I was experimenting with and forgot to revert. I'll use the reusable vec buffer though.

Comment thread diskann-benchmark/ADDING_PROVIDERS.md Outdated
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)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think it's a good idea to not use defaults here. It's more verbose, but also means we have more information.

Comment thread diskann-benchmark/src/backend/index/streaming/stats.rs Outdated
))?);

let max_points =
((max_points_arg as f32) * (1.0 + 2.0 * consolidate_threshold)).ceil() as usize;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread diskann-benchmark/src/backend/index/bftree/spherical.rs Outdated
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>
@JordanMaples JordanMaples force-pushed the jordanmaples/bftree-benchmark branch from 1170fe8 to 3bf48a1 Compare June 1, 2026 16:49
@magdalendobson
Copy link
Copy Markdown
Contributor

I'm not sure I fully understand the bug in deletion. Why do we need to keep the deleted vector around for pruning its neighbors? If it's in someone's neighbor list after deletion, it should be ignored during pruning. Is there some difference in how bf-tree handles this versus how in-mem providers handle this that meant deletes needed to be handled differently?

"search_phase": {
"search-type": "topk",
"queries": "query100K.fbin",
"groundtruth": "msturing-gt-1M",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of keeping the examples directory a bit tighter, maybe we don't need an example for streaming + spherical + bf-tree, since we already have separate examples of streaming and spherical.

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BfTreeStoreConfig {
/// Size of the circular buffer (in-memory write cache) in bytes.
pub(crate) cb_size_byte: usize,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc bf-tree has a minimum circular buffer size of 8192 bytes. Should we enforce that here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can add that check.


registry.register(SQ_NAME, spherical::BfTreeSpherical::new().search(Topk))?;

const SQ_STREAM: &str = "graph-index-stream-bftree-spherical-quantization";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to create separate name variables to pass clippy or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, I'll remove these and just put them inline.

mod spherical;
mod spherical_streaming;

/// Run a streaming benchmark using the given runbook parameters.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this function belongs in mod.rs, can we move it to its own file with a more descriptive name, like streaming_utils or similar?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a streaming_utils would work. I'll shuffle it over.

@magdalendobson
Copy link
Copy Markdown
Contributor

I think posting some experiments with a more complicated runbook would be a good idea, it looks like all the experimental results here are from simple_runbook.yaml which has just a few steps and might not be sufficient to catch bugs or perf issues. If you like I am happy to provide some suggestions for runbooks.

Comment on lines +46 to +56
/// Dispatches `num_bits` at runtime rather than using const generics to reduce
/// monomorphization.
pub(super) struct BfTreeSpherical {
search: Plugins<BfTreeSQProvider, SearchPhase, Strategy<Quantized>>,
}

impl BfTreeSpherical {
pub(super) fn new() -> Self {
Self {
search: Plugins::new(),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can some of these general helpers and structs be moved to their own file? Then we could have one file for spherical utils etc., one for generic search, and one for streaming. Would make it easier to understand the structure and navigate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already largely refactored all of this out in a follow-up pr. I just didn't want to overwhelm this pr with changes.

Copy link
Copy Markdown
Contributor

@magdalendobson magdalendobson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some cosmetic comments but overall the structure and code look good to me. For the streaming I have a couple of concerns:

  1. I would like to see experiments with a more complicated runbook to make sure we have caught any perf or correctness issues. I would especially want to make sure we've tested replace adequately.
  2. I am not sure I understand why encountering deleted nodes during pruning and throwing a Transient error is a bug. This seems like intended behavior and the transient error can be appropriately ignored. If we instead leave the deleted vectors in the graph until the end of deletion and take them into account during pruning of other nodes, we're just wasting work by computing distances to them anyway. Can you explain the reasoning here if I am wrong?

Thanks!

Ok(results)
}

pub(crate) fn register_benchmarks(registry: &mut Registry) -> anyhow::Result<()> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we model the tag names based off

// Dynamic Full Precision
registry.register(
"graph-index-dynamic-full-precision-f32",
DynamicFullPrecision::<f32>::new(),
)?;
registry.register(
"graph-index-dynamic-full-precision-f16",
DynamicFullPrecision::<f16>::new(),
)?;
registry.register(
"graph-index-dynamic-full-precision-u8",
DynamicFullPrecision::<u8>::new(),
)?;
registry.register(
"graph-index-dynamic-full-precision-i8",
DynamicFullPrecision::<i8>::new(),
)?;
? It's a bit confusing that one is "dynamic" and the other is "stream"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename these all in a follow-up pr to align with each other

@JordanMaples
Copy link
Copy Markdown
Contributor Author

I'm not sure I fully understand the bug in deletion. Why do we need to keep the deleted vector around for pruning its neighbors? If it's in someone's neighbor list after deletion, it should be ignored during pruning. Is there some difference in how bf-tree handles this versus how in-mem providers handle this that meant deletes needed to be handled differently?

In-mem marks the vectors as deleted but keeps them around to be used during the inplace delete as the query point for finding replacement neighbors. bf_tree's design decision to hard delete would break this flow, which is why storing them temporarily is necessary. The bug I'm addressing was introduced during the bftree migration out of providers and into its own crate and we opted to go for the hard deletes.

Copy link
Copy Markdown
Contributor

@hildebrandmw hildebrandmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - I think this is revealing issues with our inplace_delete API that need to get resolved. I'd say stick with hard deletes and avoid the inplace delete method that requires soft-deletes for now.

@@ -0,0 +1,143 @@
use std::{io::Write, sync::Arc};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget your header. We should add a CI lint for this.

pub(crate) graph_params: Option<GraphParams>,

// Nodes marked for deletion. Vector data is removed in `flush_deletes()`.
pending_deletes: Mutex<Vec<u32>>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still trying to understand this. I think we need to fix inplace_delete instead of hack around. I'm not sure how inplace_delete is even supposed to work at all since it calls delete and then immediately accesses the deleted element. For now, using the onehop or onehop_and_twohop should avoid fetching the element we just deleted.

TLDR: I propose we don't bend over to make soft-deletes work and instead use the inplace-delete methods that don't require soft deletes. Then we can work on getting rid of the whole concept of a soft delete.

let id_size = std::mem::size_of::<I>();
let total_len = self.dim * id_size;

let mut buf = self.buf.lock().unwrap();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was referring to scratch allocation, I meant passing in this scratch buffer as an argument. The various accessors can then allocate the buffer and pass it into these calls.

Having a central buffer behind a lock introduces a serialization bottleneck that isn't needed.


pub(crate) fn data_type(&self) -> DataType {
self.data_type
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in principle with moving these behind accessors. However, the larger goal I think should be towards not interacting with inputs piecemeal and instead having inputs do coarser grained things, like return a whole diskann::graph::Config.

The reasoning is that these piecemeal type operations can very easily lead to benchmarks trying to reuse inputs from other benchmarks and just "forgetting" about arguments, leading them to accept inputs they don't honor.

I'm not really sure what the right solution is outside of engineering discipline.

#[serde(skip_serializing_if = "Option::is_none")]
drop_deleted: Option<GenericStats>,
#[serde(skip_serializing_if = "Option::is_none")]
release: Option<GenericStats>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making these optional is not the move. It forces us into an awkward situation of "knowing" that "None" means bf-tree, but even in that case, the time spent in bf-tree's flushing isn't accounted for. Either make this an enum that is more explicit about the backend, or duplicate the struct to match what's more precisely needed.

None => diskann_quantization::spherical::PreScale::None,
};

let quantizer = diskann_quantization::spherical::SphericalQuantizer::train(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not necessary for this PR, but sequences like this are what I imagine we can use the inputs more directly for: Instead of pulling out all the configuration from the inputs, we can instead provide the input with raw data and let it construct the quantizer. This makes it much more difficult to forget about fields. Anyways, food for thought.

Doubly so that this is basically reimplemented in the static benchmarks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Diskann-benchmark should support Bf-tree and other providers

6 participants