-
Notifications
You must be signed in to change notification settings - Fork 444
PiPNN 3/6: add core graph construction #1290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
39a4ef0
pipnn: assemble direct-candidate graph builder
SeliMeli f290b1a
pipnn: cover partition validation boundaries
SeliMeli 0b30b79
pipnn: harden core graph construction
SeliMeli 15937a6
docs(pipnn): describe integer cosine policy
SeliMeli b78e659
pipnn: expose core config validation
SeliMeli b874f3b
refactor(pipnn): consume leaves during leaf build
SeliMeli f0f5f1c
fix(pipnn): preserve established replica seeds
SeliMeli 1010282
docs(pipnn): define graph-construction boundary
SeliMeli 8203e60
refactor(pipnn): own partition-stage configuration
SeliMeli 3c2d3bd
perf(pipnn): release owned leaves after leaf stage
SeliMeli 95d7b91
docs(pipnn): explain partition stage contract
SeliMeli 45fe745
perf(pipnn): reduce partition and leaf overhead
SeliMeli 7d1d092
perf(pipnn): reuse partition scratch across work items
SeliMeli 744aa8c
docs(pipnn): document core stage invariants
SeliMeli 9d05327
docs(pipnn): diagram core stage ownership
SeliMeli 279e17c
fix(pipnn): preserve partition quality and scratch reuse
SeliMeli 045ce2a
refactor(pipnn): reuse prepared kernels
SeliMeli c80957a
refactor(pipnn): name candidate lists
SeliMeli e592b0c
refactor(pipnn): complete graph module migration
SeliMeli efb5b7b
refactor(pipnn): prepare RobustPrune inputs locally
SeliMeli 5ce996c
refactor(pipnn): use shared robust prune core
SeliMeli d6b7c41
test(pipnn): adapt and colocate core tests
SeliMeli bcdf199
test(pipnn): adapt assertions to main errors
SeliMeli 929b06e
refactor(pipnn): consume positional robust prune
SeliMeli 04065c1
refactor(pipnn): use sorted prune input
SeliMeli c5fb416
refactor(pipnn): use direct leaf matrix input
SeliMeli 127c747
fix(pipnn): validate leaf k capacity
SeliMeli 9683331
refactor(pipnn): require sorted leaf IDs
SeliMeli 55d8d62
refactor(pipnn): borrow partition configuration
SeliMeli 22bc014
refactor(pipnn): dispatch partition stages once
SeliMeli 47a5310
refactor(pipnn): dispatch leaf stages once
SeliMeli 17c3b06
refactor(pipnn): dispatch once per graph build
SeliMeli 6fbc523
docs(pipnn): describe the active core flow
SeliMeli 9dfd8d4
docs(pipnn): remove core diagrams and tuning notes
SeliMeli 56a75ec
refactor(pipnn): remove duplicate partition checks
SeliMeli abe48e4
refactor(pipnn): keep leaf shape validation local
SeliMeli ea63e25
refactor(pipnn): propagate partition worker errors
SeliMeli 6d3f0ce
refactor(pipnn): remove partition assertions
SeliMeli db66844
docs(pipnn): state core function contracts
SeliMeli 6af4db5
refactor(pipnn): use domain names in core flow
SeliMeli fa7012f
docs(pipnn): remove layout-restatement comments
SeliMeli 6b34cc9
docs(pipnn): define leaf domain term
SeliMeli 284c0b4
refactor(pipnn): inline poisoned-list errors
SeliMeli 353d45c
refactor(pipnn): clarify leaf policy names
SeliMeli 538bb2a
fix(pipnn): reject malformed assignments
SeliMeli 63d98a5
refactor(pipnn): use stage metric contracts
SeliMeli 62fee71
refactor(pipnn): use partition metric identity
SeliMeli 5b74dd8
refactor(pipnn): prepare norms before kernel calls
SeliMeli 9fb95c4
refactor(pipnn): dispatch norm preparation through metric types
SeliMeli eb88bad
refactor(pipnn): pass metric-owned ranking context
SeliMeli 83ba25c
fix(pipnn): reset reused prune states
SeliMeli c22cddc
refactor(pipnn): prepare leaf norms in metric policy
SeliMeli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,331 @@ | ||
| /* | ||
| * Copyright (c) Microsoft Corporation. | ||
| * Licensed under the MIT license. | ||
| */ | ||
|
|
||
| //! Graph-degree enforcement with the Vamana RobustPrune kernel. | ||
| //! | ||
| //! Candidate merging can produce more than `R` IDs for one point. This module | ||
| //! checks every global ID before parallel work starts. A list at or below `R` | ||
| //! returns without distance calculations. | ||
| //! | ||
| //! For a longer list, the module computes each source distance. It sorts the | ||
| //! candidates and calls RobustPrune. The module then writes the selected IDs into | ||
| //! the original list allocation. | ||
| //! | ||
| //! RobustPrune defines occlusion and alpha-round behavior. This module supplies | ||
| //! source vectors and metric distances. | ||
|
|
||
| use crate::{ | ||
| ANNError, ANNResult, | ||
| graph::{ | ||
| AdjacencyList, Config, | ||
| internal::{SortedNeighbors, prune}, | ||
| }, | ||
| neighbor::Neighbor, | ||
| utils::VectorRepr, | ||
| }; | ||
| use diskann_utils::views::MatrixView; | ||
| use diskann_vector::{DistanceFunction, distance::Metric}; | ||
| use rayon::prelude::*; | ||
|
|
||
| #[derive(Debug, thiserror::Error)] | ||
| pub(crate) enum FinalizationError { | ||
| #[error("candidate list count {lists} does not match the dataset point count {points}")] | ||
| CandidateListCountMismatch { lists: usize, points: usize }, | ||
| #[error( | ||
| "candidate ID {candidate} for source {source_index} is outside a {points}-point dataset" | ||
| )] | ||
| InvalidCandidateId { | ||
| source_index: usize, | ||
| candidate: u32, | ||
| points: usize, | ||
| }, | ||
| #[error("candidate count {actual} exceeds the u16 position limit {max}")] | ||
| TooManyCandidates { actual: usize, max: usize }, | ||
| } | ||
|
|
||
| /// RobustPrune state for one Rayon job. | ||
| /// | ||
| /// `candidate_slots` and `prune_states` stay positionally aligned with | ||
| /// `sorted_candidates`. | ||
| #[derive(Default)] | ||
| struct PruneWorkspace { | ||
| sorted_candidates: Vec<Neighbor<u32>>, | ||
| candidate_slots: Vec<(f32, Option<u32>)>, | ||
| prune_states: Vec<prune::State>, | ||
| } | ||
|
|
||
| /// Check candidate IDs and prune each list that exceeds the graph degree. | ||
| pub(crate) fn prune_overfull<T>( | ||
| data: MatrixView<'_, T>, | ||
| candidates: Vec<AdjacencyList<u32>>, | ||
| graph: &Config, | ||
| metric: Metric, | ||
| ) -> ANNResult<Vec<AdjacencyList<u32>>> | ||
| where | ||
| T: VectorRepr + Send + Sync, | ||
| { | ||
| validate_candidate_lists(&candidates, data.nrows()).map_err(ANNError::new)?; | ||
|
|
||
| let degree = graph.pruned_degree().get(); | ||
| let distance = T::distance(metric, Some(data.ncols())); | ||
|
|
||
| // `build_graph` runs this Rayon operation in the pool from the build context. | ||
| #[allow(clippy::disallowed_methods)] | ||
| candidates | ||
| .into_par_iter() | ||
| .enumerate() | ||
| .map_init( | ||
| PruneWorkspace::default, | ||
| |workspace, (source, mut source_candidates)| { | ||
| // Candidate merging already removes duplicate IDs. A list within | ||
| // the degree limit needs no distance calculation. | ||
| if source_candidates.len() <= degree { | ||
| return Ok(source_candidates); | ||
| } | ||
|
|
||
| let source_id = u32::try_from(source).map_err(ANNError::new)?; | ||
| let source_vector = data.row(source); | ||
| workspace.sorted_candidates.clear(); | ||
| workspace | ||
| .sorted_candidates | ||
| .try_reserve(source_candidates.len()) | ||
| .map_err(ANNError::new)?; | ||
| workspace | ||
| .sorted_candidates | ||
| .extend(source_candidates.iter().copied().map(|candidate| { | ||
| Neighbor::new( | ||
| candidate, | ||
| distance | ||
| .evaluate_similarity(source_vector, data.row(candidate as usize)), | ||
| ) | ||
| })); | ||
|
|
||
| let candidate_count = workspace.sorted_candidates.len(); | ||
| if candidate_count > u16::MAX as usize { | ||
| return Err(ANNError::new(FinalizationError::TooManyCandidates { | ||
| actual: candidate_count, | ||
| max: u16::MAX as usize, | ||
| })); | ||
| } | ||
| workspace.candidate_slots.clear(); | ||
| workspace | ||
| .candidate_slots | ||
| .try_reserve(candidate_count) | ||
| .map_err(ANNError::new)?; | ||
|
|
||
| // Sort all candidates before the code marks a self-edge as absent. | ||
| // Thus, self-edge removal cannot add a farther candidate. The | ||
| // `SortedNeighbors` value carries this order into RobustPrune. | ||
| let sorted = | ||
| SortedNeighbors::new(&mut workspace.sorted_candidates, candidate_count); | ||
| workspace | ||
| .candidate_slots | ||
| .extend(sorted.iter().map(|neighbor| { | ||
| let id = *neighbor.id(); | ||
| (*neighbor.distance(), (id != source_id).then_some(id)) | ||
| })); | ||
| workspace | ||
| .prune_states | ||
| .try_reserve( | ||
| workspace | ||
| .candidate_slots | ||
| .len() | ||
| .saturating_sub(workspace.prune_states.len()), | ||
| ) | ||
| .map_err(ANNError::new)?; | ||
| workspace | ||
| .prune_states | ||
| .resize(workspace.candidate_slots.len(), prune::State::default()); | ||
| // Each candidate list starts a separate RobustPrune state machine. | ||
| // Reset retained entries because resize initializes only new entries. | ||
| workspace.prune_states.fill(prune::State::default()); | ||
|
|
||
| let selected = prune::robust_prune( | ||
| &sorted, | ||
| &workspace.candidate_slots, | ||
| workspace.prune_states.as_mut_slice(), | ||
| degree, | ||
| graph.alpha(), | ||
| graph.prune_kind(), | ||
| |left, right| { | ||
| distance.evaluate_similarity( | ||
| data.row(*left as usize), | ||
| data.row(*right as usize), | ||
| ) | ||
| }, | ||
| ); | ||
|
|
||
| let mut guard = source_candidates.resize(selected); | ||
| for (destination, state) in guard.iter_mut().zip(workspace.prune_states.iter()) { | ||
| *destination = *sorted[state.neighbor as usize].id(); | ||
| } | ||
| guard.finish(selected); | ||
| Ok(source_candidates) | ||
| }, | ||
| ) | ||
| .collect() | ||
| } | ||
|
|
||
| fn validate_candidate_lists( | ||
| candidates: &[AdjacencyList<u32>], | ||
| points: usize, | ||
| ) -> Result<(), FinalizationError> { | ||
| if candidates.len() != points { | ||
| return Err(FinalizationError::CandidateListCountMismatch { | ||
| lists: candidates.len(), | ||
| points, | ||
| }); | ||
| } | ||
| for (source, source_candidates) in candidates.iter().enumerate() { | ||
| if let Some(&candidate) = source_candidates.iter().find(|&&id| id as usize >= points) { | ||
| return Err(FinalizationError::InvalidCandidateId { | ||
| source_index: source, | ||
| candidate, | ||
| points, | ||
| }); | ||
| } | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::graph::{ | ||
| AdjacencyList, | ||
| config::{self, MaxDegree}, | ||
| }; | ||
| use diskann_utils::views::MatrixView; | ||
|
|
||
| use super::*; | ||
|
|
||
| fn graph_config(degree: usize) -> Config { | ||
| config::Builder::new_with( | ||
| degree, | ||
| MaxDegree::same(), | ||
| degree, | ||
| Metric::L2.into(), | ||
| |builder| { | ||
| builder.alpha(1.2); | ||
| }, | ||
| ) | ||
| .build() | ||
| .unwrap() | ||
| } | ||
|
|
||
| fn candidate_list(ids: impl IntoIterator<Item = u32>) -> AdjacencyList<u32> { | ||
| AdjacencyList::from_iter_untrusted(ids) | ||
| } | ||
|
|
||
| #[test] | ||
| fn preserves_lists_within_the_degree_bound() { | ||
| let data = [0.0_f32, 1.0, 2.0, 3.0]; | ||
| let data = MatrixView::try_from(&data[..], 4, 1).unwrap(); | ||
| let candidates = vec![ | ||
| candidate_list([3, 1]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| ]; | ||
|
|
||
| let actual = prune_overfull(data, candidates, &graph_config(2), Metric::L2).unwrap(); | ||
|
|
||
| assert_eq!(&*actual[0], &[1, 3]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn prunes_an_overfull_list_with_the_vamana_kernel() { | ||
| let data = [0.0_f32, 1.0, 2.0, -3.0]; | ||
| let data = MatrixView::try_from(&data[..], 4, 1).unwrap(); | ||
| let candidates = vec![ | ||
| candidate_list([3, 2, 1]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| ]; | ||
|
|
||
| let actual = prune_overfull(data, candidates, &graph_config(2), Metric::L2).unwrap(); | ||
|
|
||
| assert_eq!(&*actual[0], &[1, 3]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn reused_workspace_matches_fresh_pruning() { | ||
| let data = [0.0_f32, 1.0, 2.0, -3.0, 4.0]; | ||
| let data = MatrixView::try_from(&data[..], 5, 1).unwrap(); | ||
| let first = [3, 2, 1]; | ||
| let second = [4, 3, 2]; | ||
| let candidates = |first: &[u32], second: &[u32]| { | ||
| vec![ | ||
| candidate_list(first.iter().copied()), | ||
| candidate_list(second.iter().copied()), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| ] | ||
| }; | ||
| let graph = graph_config(2); | ||
| let pool = rayon::ThreadPoolBuilder::new() | ||
| .num_threads(1) | ||
| .build() | ||
| .unwrap(); | ||
| let fresh_first = pool | ||
| .install(|| prune_overfull(data, candidates(&first, &[]), &graph, Metric::L2)) | ||
| .unwrap(); | ||
| let fresh_second = pool | ||
| .install(|| prune_overfull(data, candidates(&[], &second), &graph, Metric::L2)) | ||
| .unwrap(); | ||
|
|
||
| let reused = pool | ||
| .install(|| prune_overfull(data, candidates(&first, &second), &graph, Metric::L2)) | ||
| .unwrap(); | ||
|
|
||
| assert_eq!(&*reused[0], &*fresh_first[0]); | ||
| assert_eq!(&*reused[1], &*fresh_second[1]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn rejects_invalid_candidate_ids_without_panicking() { | ||
| let data = [0.0_f32, 1.0, 2.0]; | ||
| let data = MatrixView::try_from(&data[..], 3, 1).unwrap(); | ||
| let candidates = vec![ | ||
| candidate_list([1, 3]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| ]; | ||
|
|
||
| let error = prune_overfull(data, candidates, &graph_config(1), Metric::L2).unwrap_err(); | ||
|
|
||
| assert!(matches!( | ||
| error.downcast_ref::<FinalizationError>(), | ||
| Some(FinalizationError::InvalidCandidateId { | ||
| source_index: 0, | ||
| candidate: 3, | ||
| points: 3, | ||
| }) | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn rejects_candidate_list_count_mismatch_without_panicking() { | ||
| let data = [0.0_f32, 1.0, 2.0]; | ||
| let data = MatrixView::try_from(&data[..], 3, 1).unwrap(); | ||
| let candidates = vec![ | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| candidate_list([]), | ||
| ]; | ||
|
|
||
| let error = prune_overfull(data, candidates, &graph_config(1), Metric::L2).unwrap_err(); | ||
|
|
||
| assert!(matches!( | ||
| error.downcast_ref::<FinalizationError>(), | ||
| Some(FinalizationError::CandidateListCountMismatch { | ||
| lists: 4, | ||
| points: 3 | ||
| }) | ||
| )); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.