Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
bb3739c
pipnn: assemble direct-candidate graph builder
SeliMeli Jul 28, 2026
915ca62
pipnn: cover partition validation boundaries
SeliMeli Jul 28, 2026
e1cc09d
pipnn: harden core graph construction
SeliMeli Jul 28, 2026
accd667
docs(pipnn): describe integer cosine policy
SeliMeli Jul 29, 2026
f3c6345
pipnn: expose core config validation
SeliMeli Jul 29, 2026
bd0889f
refactor(pipnn): consume leaves during leaf build
SeliMeli Jul 29, 2026
c414f92
fix(pipnn): preserve established replica seeds
SeliMeli Jul 29, 2026
a1f0a94
docs(pipnn): define graph-construction boundary
SeliMeli Jul 29, 2026
286c27a
refactor(pipnn): own partition-stage configuration
SeliMeli Jul 29, 2026
7913f77
perf(pipnn): release owned leaves after leaf stage
SeliMeli Jul 29, 2026
e581fe8
docs(pipnn): explain partition stage contract
SeliMeli Jul 29, 2026
965c99b
perf(pipnn): reduce partition and leaf overhead
SeliMeli Jul 30, 2026
51ccbfe
perf(pipnn): reuse partition scratch across work items
SeliMeli Jul 30, 2026
ec12ab7
docs(pipnn): document core stage invariants
SeliMeli Jul 31, 2026
1ed7fd1
docs(pipnn): diagram core stage ownership
SeliMeli Jul 31, 2026
2faeb46
fix(pipnn): preserve partition quality and scratch reuse
SeliMeli Aug 3, 2026
daf5d6a
refactor(pipnn): reuse prepared kernels
SeliMeli Aug 3, 2026
d65db18
refactor(pipnn): name candidate lists
SeliMeli Aug 3, 2026
83746da
refactor(pipnn): complete graph module migration
SeliMeli Aug 5, 2026
c6f6373
refactor(pipnn): prepare RobustPrune inputs locally
SeliMeli Aug 5, 2026
be32980
refactor(pipnn): use shared robust prune core
SeliMeli Aug 5, 2026
eff6736
test(pipnn): adapt and colocate core tests
SeliMeli Aug 6, 2026
edb6130
test(pipnn): adapt assertions to main errors
SeliMeli Aug 6, 2026
52aa238
refactor(pipnn): consume positional robust prune
SeliMeli Aug 6, 2026
dc89b1e
refactor(pipnn): use sorted prune input
SeliMeli Aug 7, 2026
97e3cdd
refactor(pipnn): use direct leaf matrix input
SeliMeli Aug 7, 2026
8597d75
fix(pipnn): validate leaf k capacity
SeliMeli Aug 7, 2026
1ba163a
refactor(pipnn): require sorted leaf IDs
SeliMeli Aug 7, 2026
b7b5ea3
refactor(pipnn): borrow partition configuration
SeliMeli Aug 7, 2026
8ec67b7
refactor(pipnn): dispatch partition stages once
SeliMeli Aug 7, 2026
4d8807f
refactor(pipnn): dispatch leaf stages once
SeliMeli Aug 7, 2026
f010e2a
refactor(pipnn): dispatch once per graph build
SeliMeli Aug 7, 2026
578944e
docs(pipnn): describe the active core flow
SeliMeli Aug 7, 2026
7528e76
docs(pipnn): remove core diagrams and tuning notes
SeliMeli Aug 7, 2026
15287f8
refactor(pipnn): remove duplicate partition checks
SeliMeli Aug 7, 2026
dba32c7
refactor(pipnn): keep leaf shape validation local
SeliMeli Aug 7, 2026
fcea445
refactor(pipnn): propagate partition worker errors
SeliMeli Aug 7, 2026
0881120
refactor(pipnn): remove partition assertions
SeliMeli Aug 7, 2026
0a80878
docs(pipnn): state core function contracts
SeliMeli Aug 7, 2026
84d34af
refactor(pipnn): use domain names in core flow
SeliMeli Aug 7, 2026
e529f31
docs(pipnn): remove layout-restatement comments
SeliMeli Aug 7, 2026
2edf470
docs(pipnn): define leaf domain term
SeliMeli Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion diskann/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ targets = ["x86_64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "x86_64-pc-win
[dependencies]
anyhow.workspace = true
bytemuck = { workspace = true, features = ["must_cast"]}
diskann-linalg = { workspace = true, optional = true }
diskann-utils = { workspace = true, default-features = false }
futures-util = { workspace = true, default-features = false }
half = { workspace = true, features = ["bytemuck", "num-traits"] }
Expand All @@ -22,6 +23,7 @@ half = { workspace = true, features = ["bytemuck", "num-traits"] }
hashbrown = { version = "0.16.0", default-features = false, features = ["default-hasher"] }
num-traits.workspace = true
rand.workspace = true
rayon = { workspace = true, optional = true }
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
tracing = { workspace = true, optional = true }
Expand Down Expand Up @@ -57,7 +59,7 @@ panic = "warn"
default = ["tracing"]

# Enable PiPNN batch graph construction.
pipnn = []
pipnn = ["dep:diskann-linalg", "dep:rayon", "tracing"]

# Enable "tracing" diagnostics.
tracing = ["dep:tracing"]
Expand Down
293 changes: 293 additions & 0 deletions diskann/src/graph/pipnn/finalization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
/*
* 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());

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 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
})
));
}
}
Loading
Loading