Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
448fd68
pipnn: assemble direct-candidate graph builder
SeliMeli Jul 28, 2026
b6de20a
pipnn: cover partition validation boundaries
SeliMeli Jul 28, 2026
9f831ef
pipnn: harden core graph construction
SeliMeli Jul 28, 2026
8ff316d
docs(pipnn): describe integer cosine policy
SeliMeli Jul 29, 2026
978c338
pipnn: expose core config validation
SeliMeli Jul 29, 2026
f2db8c7
refactor(pipnn): consume leaves during leaf build
SeliMeli Jul 29, 2026
24dc3f5
fix(pipnn): preserve established replica seeds
SeliMeli Jul 29, 2026
dc99ee8
docs(pipnn): define graph-construction boundary
SeliMeli Jul 29, 2026
7246e30
refactor(pipnn): own partition-stage configuration
SeliMeli Jul 29, 2026
89865dc
perf(pipnn): release owned leaves after leaf stage
SeliMeli Jul 29, 2026
6b188bf
docs(pipnn): explain partition stage contract
SeliMeli Jul 29, 2026
b071054
perf(pipnn): reduce partition and leaf overhead
SeliMeli Jul 30, 2026
9c33e29
perf(pipnn): reuse partition scratch across work items
SeliMeli Jul 30, 2026
98aff90
docs(pipnn): document core stage invariants
SeliMeli Jul 31, 2026
c82e40e
docs(pipnn): diagram core stage ownership
SeliMeli Jul 31, 2026
d459416
fix(pipnn): preserve partition quality and scratch reuse
SeliMeli Aug 3, 2026
d35252b
refactor(pipnn): reuse prepared kernels
SeliMeli Aug 3, 2026
64598ff
refactor(pipnn): name candidate lists
SeliMeli Aug 3, 2026
00a768c
refactor(pipnn): complete graph module migration
SeliMeli Aug 5, 2026
2344e20
refactor(pipnn): prepare RobustPrune inputs locally
SeliMeli Aug 5, 2026
64047a3
refactor(pipnn): use shared robust prune core
SeliMeli Aug 5, 2026
53aa7ce
test(pipnn): adapt and colocate core tests
SeliMeli Aug 6, 2026
297ce3c
test(pipnn): adapt assertions to main errors
SeliMeli Aug 6, 2026
6ad9f9f
refactor(pipnn): consume positional robust prune
SeliMeli Aug 6, 2026
ac4f42a
refactor(pipnn): use sorted prune input
SeliMeli Aug 7, 2026
b0104a8
refactor(pipnn): use direct leaf matrix input
SeliMeli Aug 7, 2026
4715868
fix(pipnn): validate leaf k capacity
SeliMeli Aug 7, 2026
a37263a
refactor(pipnn): require sorted leaf IDs
SeliMeli Aug 7, 2026
91943b1
refactor(pipnn): borrow partition configuration
SeliMeli Aug 7, 2026
0e3e40d
refactor(pipnn): dispatch partition stages once
SeliMeli Aug 7, 2026
083b8e6
refactor(pipnn): dispatch leaf stages once
SeliMeli Aug 7, 2026
c9aa2f0
refactor(pipnn): dispatch once per graph build
SeliMeli Aug 7, 2026
84c1c03
docs(pipnn): describe the active core flow
SeliMeli Aug 7, 2026
36fe65f
docs(pipnn): remove core diagrams and tuning notes
SeliMeli Aug 7, 2026
a3308e8
refactor(pipnn): remove duplicate partition checks
SeliMeli Aug 7, 2026
177516e
refactor(pipnn): keep leaf shape validation local
SeliMeli Aug 7, 2026
d33a3af
refactor(pipnn): propagate partition worker errors
SeliMeli Aug 7, 2026
961ba1f
refactor(pipnn): remove partition assertions
SeliMeli Aug 7, 2026
f887823
docs(pipnn): state core function contracts
SeliMeli Aug 7, 2026
4f96bfb
refactor(pipnn): use domain names in core flow
SeliMeli Aug 7, 2026
d809396
docs(pipnn): remove layout-restatement comments
SeliMeli Aug 7, 2026
a275fd7
docs(pipnn): define leaf domain term
SeliMeli Aug 7, 2026
d88ee74
refactor(pipnn): inline poisoned-list errors
SeliMeli Aug 10, 2026
1e35d4a
refactor(pipnn): clarify leaf policy names
SeliMeli Aug 10, 2026
41e1961
fix(pipnn): reject malformed assignments
SeliMeli Aug 10, 2026
1f2c746
refactor(pipnn): use stage metric contracts
SeliMeli Aug 10, 2026
2d8c503
refactor(pipnn): use partition metric identity
SeliMeli Aug 10, 2026
c40258c
refactor(pipnn): prepare norms before kernel calls
SeliMeli Aug 11, 2026
0c9361f
refactor(pipnn): dispatch norm preparation through metric types
SeliMeli Aug 11, 2026
469078a
refactor(pipnn): pass metric-owned ranking context
SeliMeli Aug 11, 2026
2fe7438
fix(pipnn): reset reused prune states
SeliMeli Aug 11, 2026
ec9044c
refactor(pipnn): prepare leaf norms in metric policy
SeliMeli Aug 11, 2026
eb5ed4a
refactor(pipnn): simplify graph build stages
SeliMeli Aug 12, 2026
38ca417
fix(pipnn): omit unrankable graph candidates
SeliMeli Aug 18, 2026
76e18d4
test(pipnn): cover non-rankable build candidates
SeliMeli Aug 18, 2026
5dc3b7f
perf(pipnn): deduplicate direct leaf edges once
SeliMeli Aug 19, 2026
5246d79
refactor(pipnn): use the SIMD schema in stages
SeliMeli Aug 20, 2026
3e27ca8
test(pipnn): clarify core graph contracts
SeliMeli Aug 21, 2026
0a0b126
test(pipnn): use semantic graph fixtures
SeliMeli Aug 24, 2026
d611b28
test(pipnn): name zero-k fixture inputs
SeliMeli Aug 24, 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.

8 changes: 5 additions & 3 deletions diskann/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ 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"] }
# Note: hashbrown not using workspace because diskann needs default-features = false
# while other crates use default-features = true. Keeping version 0.16.0 consistent.
hashbrown = { version = "0.16.0", default-features = false, features = ["default-hasher"] }
num-traits.workspace = true
parking_lot = { version = "0.12.5", optional = 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 All @@ -30,7 +33,6 @@ diskann-wide = { workspace = true }

# Optional Dependencies
dashmap = { workspace = true, optional = true }
diskann-linalg = { workspace = true, optional = true }

[dev-dependencies]
futures-util = { workspace = true, default-features = false }
Expand Down Expand Up @@ -58,8 +60,8 @@ panic = "warn"
[features]
default = ["tracing"]

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

# Enable "tracing" diagnostics.
tracing = ["dep:tracing"]
Expand Down
256 changes: 256 additions & 0 deletions diskann/src/graph/pipnn/finalization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/*
* 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 count {actual} exceeds the u16 position limit {max}")]
TooManyCandidates { actual: usize, max: usize },
}

/// RobustPrune state for one Rayon job.
///
/// `sorted_cache` and `prune_states` stay positionally aligned with
/// `sorted_candidates`.
#[derive(Default)]
struct PruneWorkspace {
sorted_candidates: Vec<Neighbor<u32>>,
sorted_cache: Vec<(f32, Option<u32>)>,
prune_states: Vec<prune::State>,
}

/// Prune each candidate list that exceeds the graph degree.
///
/// Candidate builders supply one list per data row and valid dataset IDs.
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,
{
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
.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.sorted_cache.clear();
// Sort all candidates before the code marks a self-edge as absent.
// Thus, self-edge removal cannot add a farther candidate. Cache
// construction preserves this order for RobustPrune.
let sorted =
SortedNeighbors::new(&mut workspace.sorted_candidates, candidate_count);
workspace.sorted_cache.extend(sorted.iter().map(|neighbor| {
let id = *neighbor.id();
(*neighbor.distance(), (id != source_id).then_some(id))
}));
workspace
.prune_states
.resize(workspace.sorted_cache.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(
&workspace.sorted_cache,
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()
}

#[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 row_within_the_degree_bound_is_only_canonicalized() {
// Given
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 expected_canonical_row = [1, 3];

// When
let actual_rows = prune_overfull(data, candidates, &graph_config(2), Metric::L2).unwrap();

// Then
assert_eq!(&*actual_rows[0], &expected_canonical_row);
}

#[test]
fn overfull_row_keeps_the_nearest_unoccluded_neighbors() {
// Given
let point_values = [
0.0_f32, // source
1.0, // nearest point on the positive ray
2.0, // farther point occluded by point 1
-3.0, // point on the opposite ray
];
let data = MatrixView::try_from(&point_values[..], 4, 1).unwrap();
let candidates = vec![
candidate_list([3, 2, 1]),
candidate_list([]),
candidate_list([]),
candidate_list([]),
];
let expected_pruned_row = [1, 3];

// When
let actual_rows = prune_overfull(data, candidates, &graph_config(2), Metric::L2).unwrap();

// Then
assert_eq!(&*actual_rows[0], &expected_pruned_row);
}

#[test]
fn reused_workspace_matches_fresh_pruning() {
// Given
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();
// When
let expected_first_row_from_fresh_workspace = pool
.install(|| prune_overfull(data, candidates(&first, &[]), &graph, Metric::L2))
.unwrap();
let expected_second_row_from_fresh_workspace = pool
.install(|| prune_overfull(data, candidates(&[], &second), &graph, Metric::L2))
.unwrap();

let actual_rows_from_reused_workspace = pool
.install(|| prune_overfull(data, candidates(&first, &second), &graph, Metric::L2))
.unwrap();

// Then
assert_eq!(
&*actual_rows_from_reused_workspace[0],
&*expected_first_row_from_fresh_workspace[0]
);
assert_eq!(
&*actual_rows_from_reused_workspace[1],
&*expected_second_row_from_fresh_workspace[1]
);
}
}
Loading
Loading