PiPNN 1/6: extract shared RobustPrune - #1315
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extracts Vamana’s reusable RobustPrune alpha-round occlusion state machine into a new allocation-free kernel (graph/internal/robust_prune.rs) while keeping Vamana-specific scratch space, provider error handling, and adapter logic in graph/internal/vamana_prune.rs. The DiskANNIndex::occlude_list path is updated to prepare “available-only” candidates for the shared kernel, then translate selected positions back to IDs and apply optional saturation.
Changes:
- Introduces
internal::robust_pruneas a provider-independent RobustPrune kernel over prepared candidates + reusable per-candidate state. - Moves Vamana-owned scratch/context and ranked provider error types into
internal::vamana_prunewith co-located tests. - Refactors
graph/index.rs::occlude_listto: validate bounds → prepare available candidates → call shared kernel → write adjacency → saturate from available-only candidates.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| diskann/src/graph/internal/vamana_prune.rs | Adds Vamana-owned scratch/context + provider error ranking and Vamana integration tests. |
| diskann/src/graph/internal/robust_prune.rs | Adds allocation-free RobustPrune kernel + pure state-machine tests. |
| diskann/src/graph/internal/prune.rs | Removes the previous combined prune implementation/state. |
| diskann/src/graph/internal/mod.rs | Rewires internal modules to expose robust_prune and vamana_prune. |
| diskann/src/graph/index.rs | Switches pruning implementation to prepare candidates and call the shared robust_prune kernel; updates saturation behavior to be available-only. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mut current_alpha = 1.0f32; | ||
| let increment_factor = alpha.min(1.2); | ||
| let mut selected = 0; | ||
|
|
||
| while selected < degree { | ||
| for (index, candidate) in candidates.iter().enumerate() { | ||
| if selected >= degree { | ||
| break; | ||
| } |
There was a problem hiding this comment.
Intentionally unchanged in this refactor. Every states[...] access remains bounds-checked, so malformed/non-finite alpha can panic but cannot write out of bounds. Capping the selection count or changing alpha handling would alter pre-existing Vamana behavior and is outside this behavior-preserving extraction.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1315 +/- ##
=======================================
Coverage 91.26% 91.27%
=======================================
Files 517 517
Lines 98511 98541 +30
=======================================
+ Hits 89910 89945 +35
+ Misses 8601 8596 -5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
25066a6 to
748d38f
Compare
There was a problem hiding this comment.
This PR would have been easy to review, but at nontrivially regresses internal documentation and comments. For such important internal algorithms, please avoid changing comments unnecessarily, particularly when unsafe is involved.
Please restore the deleted or altered comments, making only the necessary updates for the new location. Also, please avoid renaming variables when just moving code around. A simpler diff stands on its own.
| "index {index} is out of bounds" | ||
| ); | ||
| // SAFETY: `index` comes from iterating `candidates`, which has | ||
| // the same length as `states` by the caller contract. |
There was a problem hiding this comment.
This comment is not a valid safety argument as is. While the contract on the function states that candidates and states must have the same length, the function itself is not marked as unsafe and therefore unsafe code cannot rely on that contract being upheld. Nor is this invariant checked in the function preamble (and it need not be with the original justification).
The original comment gave a correct (albeit terse) reason why the indexing was safe: states was already accessed at the given index earlier and would have panicked if index was out of bounds.
Same for the other occurances.
If someone later turns the first destructure into get_unchecked, the cited justification still reads true while soundness silently evaporates.
There was a problem hiding this comment.
Fixed in c2e6be6. robust_prune checks pool/cache/state length equality before the loop, and each unsafe comment again relies on the preceding checked states[i] access exactly as the original code did.
| /// | ||
| /// The caller retains ownership of candidate preparation, allocation, ID translation, | ||
| /// and saturation. `candidates` and `states` must have equal lengths, and the caller | ||
| /// must enforce the existing `u16` candidate-position bound before entering. |
There was a problem hiding this comment.
Write docs as if they are the current contract ... because they are. If this merged all references to "existing" and "retains" are immediately stale and lose all relevance. This is not the only occurrence of this pattern.
There was a problem hiding this comment.
Fixed. Source documentation now states the current contract in present tense. Existing prune types and their documentation remain in graph/internal/prune.rs.
| // `states[i]` tracks candidate `i`, while `states[..selected]` stores the | ||
| // positions of candidates already promoted to neighbors. `last_checked` | ||
| // indexes that selected prefix, allowing later alpha rounds to resume rather | ||
| // than repeat distance comparisons. |
There was a problem hiding this comment.
Please restore the deleted comments, relocating the loop-level ones alongside the code they describe. The State doc link needs repointing at the extracted function rather than restoring verbatim.
There was a problem hiding this comment.
Fixed. Loop-level algorithm/state comments, inline comments, and variable terminology are restored beside the extracted loop. State now links to robust_prune.
| /// and saturation. `candidates` and `states` must have equal lengths, and the caller | ||
| /// must enforce the existing `u16` candidate-position bound before entering. | ||
| pub(in crate::graph) fn robust_prune<V, D>( | ||
| candidates: &[(f32, Option<V>)], |
There was a problem hiding this comment.
This adds the real requirement that candidates must be sorted by increasing distance, but that is not documented. This is a new function where-as before the invariant was satisfied by construction. Please document the additional requirements at the very least.
There was a problem hiding this comment.
Fixed by making order part of the accepted type: robust_prune takes &SortedNeighbors, whose constructor sorts and caps by source distance. A raw unsorted candidate slice cannot be passed through this interface; the function docs state the ordering contract.
| pub use search::{KnnSearchError, RangeSearchError, Search}; | ||
|
|
||
| mod internal; | ||
| mod robust_prune; |
There was a problem hiding this comment.
Call this module prune. It's one user already does that via aliasing. Also please keep it in the internal module.
There was a problem hiding this comment.
Fixed. The helper now remains graph::internal::prune::robust_prune; no new module or public API is introduced.
Keep the prune module, comments, names, preparation, output, and saturation in place; expose only the selection loop and require the existing SortedNeighbors witness.
748d38f to
c2e6be6
Compare
|
Addressed review in
Validation: 311 |
DiskANN keeps Vamana prune scratch, provider-error state, and candidate ordering in
graph/internal/prune.rs, while the alpha-round selection loop was embedded inDiskANNIndex::occlude_list. PiPNN later needs that same selection loop with its own candidate values. This PR extracts only that loop and leaves the module name, file location, caller flow, comments, and Vamana behavior in place.No public API or semver surface is introduced.
Code map
diskann/src/graph/internal/prune.rsOptions,Scratch,Context,State, and ranked provider-error types remain unchanged.robust_prunecontains the selection loop moved fromindex.rs.SortedNeighbors, the existing type whose constructor sorts and caps by source distance. A raw unsorted candidate slice cannot be passed.diskann/src/graph/index.rsSortedNeighborspool.End-to-end flow
Vamana fills provider state →
occlude_listconstructs the positional cache fromSortedNeighbors→internal::prune::robust_pruneselects positions →occlude_listmaps positions through the original pool → the unchanged saturation loop appends candidates → the existing caller writes provider adjacency.Invariants and preserved behavior
SortedNeighborsenforces nondecreasing source-distance order at the function boundary.Noneentries and are skipped exactly as before.u16::MAXassertion, state initialization, alpha progression, prune-kind inputs, and selected-position representation are unchanged.index.rs.graph::internal::prune; only graph descendants can call it.Review path
internal/prune.rs: the existing types remain in place and the function body is the loop removed fromindex.rs.main:index.rs::occlude_list.index.rsremains the original preparation, mapping, and saturation flow.SortedNeighbors, not a raw candidate slice, and checks cache/state alignment locally.Validation
diskannlibrary tests pass.Stack relation
Stack 1/6. #1287 adds numerical kernels next. #1290 later uses the same internal selection function with PiPNN-owned preparation and ID translation.
Stack 1/6 → #1287