Refactor disk index build structure - #1271
Conversation
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
- Moved disk index builder tests to a new module structure for better organization. - Updated import paths in `disk_provider.rs` to reflect the new location of `IndexBuildFixture` and `TestParams`. - Enhanced the test suite for disk index building, including additional test cases and improved parameter handling.
There was a problem hiding this comment.
Pull request overview
This PR refactors the diskann-disk disk index build path to clarify module ownership: merged-index building logic is separated from top-level orchestration, and the in-memory single-graph build pipeline is consolidated behind an internal entry point, while keeping behavior unchanged.
Changes:
- Extract merged-index build implementation (partition/shard build/merge/cleanup) into a dedicated
merged_indexmodule. - Move the in-memory build pipeline into
inmem_builderand call it from the top-level disk builder. - Centralize builder test fixtures under
build::builder::testsand update test imports accordingly.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| diskann-disk/src/search/provider/disk_provider.rs | Updates test fixture import path to the new builder test module location. |
| diskann-disk/src/build/builder/tests.rs | Introduces centralized builder test fixtures and helpers under build::builder::tests. |
| diskann-disk/src/build/builder/quantizer.rs | Updates documentation wording for build quantizer types. |
| diskann-disk/src/build/builder/mod.rs | Adjusts module layout (adds merged_index, exposes tests as pub(crate) under cfg(test)). |
| diskann-disk/src/build/builder/merged_index.rs | New module containing merged-index build implementation and RAM estimation tests. |
| diskann-disk/src/build/builder/inmem_builder.rs | Consolidates the one-shot in-memory build pipeline behind an internal entry point. |
| diskann-disk/src/build/builder/build.rs | Keeps top-level orchestration and policy; wires in merged_index + inmem_builder entry points. |
| diskann-disk/src/build/builder/core.rs | Deletes the previous combined “core” module after refactor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
Harsha Simhadri (harsha-simhadri)
left a comment
There was a problem hiding this comment.
could you please briefly list the tests in this PR and their intent in the PR description? thanks
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1271 +/- ##
==========================================
- Coverage 90.68% 90.66% -0.03%
==========================================
Files 515 516 +1
Lines 99230 98856 -374
==========================================
- Hits 89987 89625 -362
+ Misses 9243 9231 -12
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
I’ve updated the PR description with a brief list of the covered build scenarios and the intent of each test. |
|
Before this change,
Making both modules private, together with reducing those items to If these APIs are intentionally being removed, could the PR description call out the breaking API change? Otherwise, should the old public paths be preserved or deprecated for a compatibility window? |
Thanks for calling this out. The visibility reduction is intentional. |
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
diskann-disk/src/build/builder/tests.rs:18
VectorRepris imported here but not used anywhere in this test module, which can triggerunused_importswarnings under-D warnings/ clippy-all-targets. Drop it from the import list.
use diskann::{
graph::config,
utils::{IntoUsize, VectorRepr, ONE},
ANNResult,
};
diskann-disk/src/build/builder/tests.rs:491
- The return value from
search_internalis anANNResult<...>, but it’s currently ignored. If the search errors, the test can keep going with default-filled output buffers and produce misleading passes/failures. Propagate the error (or unwrap) so failures are visible.
_ = search_engine.search_internal(
query_data,
top_k,
search_l,
None, // beam_width
Co-authored-by: Copilot <[email protected]> Copilot-Session: ecf6179f-7467-44be-8a77-bf7a96e319af
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
diskann-disk/src/build/builder/merged_index.rs:412
- Same issue as above: the final node neighbor list is converted into a new Vec before writing. Writing the u32 slice directly avoids an allocation on the hot path.
let bytes = final_nbrs
.iter()
.take(nnbrs as usize)
.flat_map(|x| x.to_le_bytes())
.collect::<Vec<u8>>();
merged_vamana_cached_writer.write(&bytes)?;
diskann-disk/src/build/builder/merged_index.rs:367
- This allocates a fresh Vec for every node when serializing neighbors (via collect::<Vec>()), which can add significant CPU/memory overhead in large merged builds. You can write the existing u32 neighbor slice directly as bytes (bytemuck) to avoid the per-node allocation.
let bytes = final_nbrs
.iter()
.take(nnbrs as usize)
.flat_map(|x| x.to_le_bytes())
.collect::<Vec<u8>>();
merged_vamana_cached_writer.write(&bytes)?;
juchen-ms (partychen)
left a comment
There was a problem hiding this comment.
I will close #1306 to avoid maintaining two competing refactors. My feedback here is not a request to adopt its exact layout. My main concern is that a structural refactor should make the domain model visible from the module boundaries: the disk build orchestrates PQ construction, Vamana construction, and disk layout, while Vamana construction has one-shot and merged strategies.
I would like the naming and ownership in this PR to reflect that model more directly. The enum-dispatch suggestion can be handled separately; the module boundaries, strategy ownership, and overloaded build_inmem_index terminology are the main changes I am requesting.
| build::builder::{ | ||
| core::{determine_build_strategy, IndexBuildStrategy, MergedVamanaIndexBuilder}, | ||
| inmem_builder::{new_inmem_index_builder, InmemIndexBuilder}, | ||
| inmem_builder::build_inmem_index, |
There was a problem hiding this comment.
The name build_inmem_index is now used at two different abstraction levels. DiskIndexBuilder::build_inmem_index selects either the one-shot or merged strategy, while this imported helper builds and persists one complete Vamana graph (for either the full dataset or one shard). The outer operation is not necessarily in-memory, and the inner helper is specifically the one-shot leaf operation.
Could we distinguish these responsibilities explicitly - for example, build_vamana_index for the outer phase and build_one_shot_vamana or build_vamana_graph for the leaf helper? That would make the call graph understandable without relying on the historical _mem.index terminology.
| mod inmem_builder; | ||
| mod merged_index; |
There was a problem hiding this comment.
With core removed, I expected the new module boundaries to expose the build model more directly, but these sibling names still use different concepts: inmem_builder describes memory residency/an implementation detail, while merged_index describes a build strategy or output. Their contents are not symmetric either: the former owns quantizer dispatch plus the complete graph-construction pipeline, while the latter owns the merged-build workflow.
Could we align the ownership and naming around the actual domain - Vamana construction with one-shot and merged strategies - for example through private vamana::{one_shot, merged} modules or an equivalent layout? The exact number of files is not important; the goal is for the module names to state which algorithm and responsibility they own.
| pub(crate) enum IndexBuildStrategy { | ||
| OneShot, | ||
| Merged, | ||
| } | ||
|
|
||
| #[cfg(debug_assertions)] | ||
| /// Log statistics about the build process | ||
| async fn log_build_stats<T: VectorRepr>(index: &Arc<dyn InmemIndexBuilder<T>>) -> ANNResult<()> { | ||
| debug!( | ||
| "Number of points reachable in the graph: {}", | ||
| index.count_reachable_nodes().await? | ||
| pub(crate) fn determine_build_strategy<Data: GraphDataType>( |
There was a problem hiding this comment.
IndexBuildStrategy sounds like it controls the complete disk-index build, but it only selects how the Vamana graph is constructed. Its current placement also leaves the decision split across modules: build.rs owns the strategy, while the RAM estimate it requires remains in merged_index.rs, even though that estimate is not specific to the merged path.
Could the strategy and its estimate be named and owned together with the Vamana builders (for example, VamanaBuildStrategy)? That would leave build.rs focused on the top-level PQ, Vamana, and disk-layout orchestration.
| /// Thread safety: | ||
| /// Implementors must be `Send` and `Sync`. Methods can be called from many tasks. | ||
| pub(super) trait InmemIndexBuilder<T: Sized>: Send + Sync { | ||
| trait InmemIndexBuilder<T: Sized>: Send + Sync { |
There was a problem hiding this comment.
Now that this facade is private, it only erases the closed set of implementations selected by BuildQuantizer (FP, SQ, and PQ). That requires boxed futures and dynamic dispatch for operations such as every vector insertion, while also hiding the supported variants from the type structure.
Would an explicit enum over those variants make this pipeline easier to follow and avoid the per-operation boxing? I see this as a useful simplification, but it need not block the module and naming cleanup if it is better handled as a follow-up.
Why
This is a follow-up to #1254. Removing checkpoint and continuation support eliminated the workflow state that previously justified much of the disk-build indirection, allowing the remaining build path to be simplified and its module ownership clarified.
Module changes
core.rstomerged_index.rs; the renamed module retains the merged-build implementation and RAM estimation.tests.rs.IndexBuildStrategyanddetermine_build_strategyfrom the renamed module tobuild.rs, where the one-shot versus merged decision is made.build.rstoinmem_builder.rs.Compatibility
This change makes
builder::quantizerandbuilder::tokioprivate, including the previously publicBuildQuantizer,BuildQuantizer::train, andcreate_runtimeAPIs. These are implementation details ofdiskann-diskrather than supported downstream extension points. Downstream crates should use the public disk-index build APIs and manage their own Tokio runtime when needed.Tests
test_build_from_iter_one_shot_with_metricverifies one-shot builds for L2, inner-product, and cosine distance.test_build_multi_sector_per_nodeexercises the disk layout and search path when one node spans multiple 512-byte sectors.test_build_from_iter_one_shot_with_associated_dataverifies that vectors, neighbors, and associated data are persisted correctly without intermediate in-memory index files.test_build_from_iter_merged_indexforces the low-memory merged-build path and verifies search results and the configured maximum degree.test_build_quantization_type_failure_casesverifies that unsupported build quantization settings are rejected with the expected error.test_disk_index_buildercovers one-shot and merged builds with full precision, 1-bit scalar quantization, and product quantization, then validates persisted PQ data and search results.test_disk_minmax_index_buildercovers one-shot and merged builds for MinMax vector data and validates the resulting search behavior.