Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6ed3673
pipnn: add HashPrune candidate merging
SeliMeli Jul 29, 2026
de9c287
build: configure HashPrune integration
SeliMeli Jul 29, 2026
bbd1ef9
refactor(pipnn): dispatch HashPrune through diskann-wide
SeliMeli Jul 29, 2026
4c7ea05
perf(pipnn): move unique HashPrune rows into adjacency
SeliMeli Jul 29, 2026
b1c4475
fix(pipnn): validate HashPrune capacity against graph degree
SeliMeli Jul 29, 2026
2f07e14
ci: run Miri on HashPrune pointer kernels
SeliMeli Jul 29, 2026
482af91
refactor(pipnn): scope extraction scratch to Rayon jobs
SeliMeli Jul 29, 2026
11174da
fix(pipnn): validate HashPrune hash-space capacity
SeliMeli Jul 29, 2026
3659621
perf(pipnn): retain HashPrune sketch scratch capacity
SeliMeli Jul 30, 2026
8546cc2
docs(pipnn): map HashPrune reservoir flow
SeliMeli Jul 31, 2026
062279a
test(pipnn): exercise sketch scratch reuse
SeliMeli Jul 31, 2026
f4ad3eb
test(disk): adapt PiPNN adapter mismatch case
SeliMeli Jul 31, 2026
269cb11
fix(pipnn): make HashPrune history independent
SeliMeli Aug 3, 2026
0545bc0
docs(pipnn): name point reservoirs
SeliMeli Aug 3, 2026
4db9898
refactor(pipnn): finish in-crate HashPrune move
SeliMeli Aug 5, 2026
c5fff9d
bench(pipnn): add IAI candidate-merge scenarios
SeliMeli Aug 5, 2026
9e9e2b5
fix(pipnn): keep HashPrune explicitly opt-in
SeliMeli Aug 6, 2026
1116c2e
ci(pipnn): gate unsafe pointer boundaries
SeliMeli Aug 6, 2026
d9169a9
test(pipnn): colocate HashPrune coverage
SeliMeli Aug 6, 2026
d9f28da
fix(pipnn): satisfy Windows FFI lint
SeliMeli Aug 6, 2026
9946a3f
ci(pipnn): keep one Miri gate
SeliMeli Aug 6, 2026
ca50bba
docs(pipnn): make unsafe proofs local
SeliMeli Aug 7, 2026
15df4ea
refactor(pipnn): remove unused wrapper layers
SeliMeli Aug 7, 2026
21a9d79
docs(pipnn): simplify HashPrune comments
SeliMeli Aug 7, 2026
7bb780c
fix(pipnn): reject invalid HashPrune leaf input
SeliMeli Aug 7, 2026
099aa45
refactor(pipnn): return HashPrune ingestion errors
SeliMeli Aug 7, 2026
6e4617b
docs(pipnn): state core HashPrune contracts
SeliMeli Aug 7, 2026
e1d6d52
refactor(pipnn): use domain names in HashPrune flow
SeliMeli Aug 7, 2026
debcbee
docs(pipnn): describe HashPrune domain actions
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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
- test-workspace
- test-workspace-features
- coverage
- miri-pipnn
- vectorset-clippy
- vectorset-fmt
- vectorset-build
Expand All @@ -95,6 +96,35 @@ jobs:
steps:
- run: exit 0

miri-pipnn:
needs: basics
name: strict-provenance PiPNN kernels
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust nightly with Miri
run: rustup toolchain install nightly --component miri

- uses: Swatinem/rust-cache@v2

- name: Run HashPrune pointer boundaries
env:
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance
run: |
cargo +nightly miri test --locked -p diskann --features pipnn --lib \
graph::pipnn::hash_prune::tests::find_hash_handles_padded_boundaries_and_all_bit_patterns \
-- --exact
cargo +nightly miri test --locked -p diskann --features pipnn --lib \
graph::pipnn::hash_prune::tests::relative_hash_matches_numeric_reference \
-- --exact
cargo +nightly miri test --locked -p diskann --features pipnn --lib \
graph::pipnn::hash_prune::tests::full_reservoir_evicts_the_farthest_candidate \
-- --exact
cargo +nightly miri test --locked -p diskann --features pipnn --lib \
graph::pipnn::hash_prune::tests::hot_slot_contention_serializes_state_mutation \
-- --exact

fmt:
name: format check
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion diskann-benchmark/src/index/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,15 @@ where

let started = std::time::Instant::now();
let adjacency = {
let context = diskann::graph::pipnn::PiPNNBuildContext::new(
let mut context = diskann::graph::pipnn::PiPNNBuildContext::new(
parameters.into(),
&graph,
metric,
&pool,
)?;
if let Some(hash_prune) = &parameters.hash_prune {
context = context.with_hash_prune(hash_prune.into())?;
}
diskann::graph::pipnn::build_graph(data.as_view(), &context)?
};
let start_points = input
Expand Down
12 changes: 8 additions & 4 deletions diskann-disk/src/build/builder/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ where
index_writer: DiskIndexWriter,
) -> ANNResult<Self> {
#[cfg(feature = "pipnn")]
if let Some(config) = disk_build_param.pipnn_config() {
config.validate()?;
if let Some(parameters) = disk_build_param.pipnn_parameters() {
diskann::graph::pipnn::PiPNNConfig::from(parameters).validate()?;
if let Some(hash_prune) = &parameters.hash_prune {
diskann::graph::pipnn::HashPruneConfig::from(hash_prune)
.validate_for_degree(index_configuration.config.pruned_degree().get())?;
}
}

let pq_storage = PQStorage::new(
Expand Down Expand Up @@ -182,8 +186,8 @@ where

async fn build_graph(&mut self, pool: RayonThreadPoolRef<'_>) -> ANNResult<()> {
#[cfg(feature = "pipnn")]
if let Some(config) = self.disk_build_param.pipnn_config() {
return pipnn::build_graph(self, pool, config);
if let Some(parameters) = self.disk_build_param.pipnn_parameters().cloned() {
return pipnn::build_graph(self, pool, &parameters);
}

match determine_build_strategy::<Data>(
Expand Down
48 changes: 41 additions & 7 deletions diskann-disk/src/build/builder/build/pipnn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//!
//! PiPNN and Vamana use the same disk graph format.

use diskann::graph::pipnn::{PiPNNBuildContext, PiPNNConfig};
use diskann::graph::pipnn::PiPNNBuildContext;
use diskann::{utils::VectorRepr, ANNError, ANNResult};
use diskann_providers::{
storage::{save_adjacency_graph, StorageReadProvider, StorageWriteProvider},
Expand All @@ -20,13 +20,13 @@ use diskann_providers::{
use diskann_utils::io::{read_bin, Metadata};

use super::{u32_try_from, DiskIndexBuilder};
use crate::data_model::GraphDataType;
use crate::{data_model::GraphDataType, PiPNNParameters};

/// Build PiPNN adjacency and persist it through the canonical disk graph writer.
pub(super) fn build_graph<Data, StorageProvider>(
builder: &DiskIndexBuilder<'_, Data, StorageProvider>,
pool: RayonThreadPoolRef<'_>,
config: PiPNNConfig,
parameters: &PiPNNParameters,
) -> ANNResult<()>
where
Data: GraphDataType<VectorIdType = u32>,
Expand Down Expand Up @@ -55,12 +55,15 @@ where
// supplied Rayon pool.
let data =
read_bin::<Data::VectorDataType>(&mut builder.storage_provider.open_reader(&data_path)?)?;
let context = PiPNNBuildContext::new(
config,
let mut context = PiPNNBuildContext::new(
parameters.into(),
&builder.index_configuration.config,
builder.index_configuration.dist_metric,
pool.as_rayon(),
)?;
if let Some(hash_prune) = &parameters.hash_prune {
context = context.with_hash_prune(hash_prune.into())?;
}
let adjacency = diskann::graph::pipnn::build_graph(data.as_view(), &context)?;

// The disk header requires a start point. Use the same sampled medoid policy
Expand Down Expand Up @@ -116,6 +119,7 @@ mod tests {
fanout: vec![10, 3],
k: 2,
replicas: 1,
hash_prune: Some(crate::HashPruneParameters::default()),
}
}

Expand Down Expand Up @@ -199,7 +203,7 @@ mod tests {
let builder = builder(&storage, 3, 8, 1.0, 1.2, parameters.clone());
let pool = create_thread_pool(1).unwrap();

let error = super::build_graph(&builder, pool.as_ref(), (&parameters).into()).unwrap_err();
let error = super::build_graph(&builder, pool.as_ref(), &parameters).unwrap_err();
assert!(format!("{error:?}").contains("configured point count 3"));
assert!(!storage.exists(&builder.index_writer.get_mem_index_file()));
}
Expand All @@ -213,7 +217,7 @@ mod tests {
let builder = builder(&storage, points, dimensions, 1.0, 1.2, parameters.clone());
let pool = create_thread_pool(1).unwrap();

super::build_graph(&builder, pool.as_ref(), (&parameters).into()).unwrap();
super::build_graph(&builder, pool.as_ref(), &parameters).unwrap();

let mut header = [0_u8; 24];
std::io::Read::read_exact(
Expand Down Expand Up @@ -274,4 +278,34 @@ mod tests {

assert!(format!("{error:?}").contains("c_max must be greater than zero"));
}

#[test]
fn builder_rejects_hash_prune_capacity_before_quantizer_artifacts() {
let storage = VirtualStorageProvider::new_memory();
let parameters = PiPNNParameters {
hash_prune: Some(crate::HashPruneParameters {
num_hash_planes: 12,
l_max: 16,
final_prune: true,
}),
..PiPNNParameters::default()
};
let params = DiskIndexBuildParameters::new_pipnn(
MemoryBudget::try_from_gb(1.0).unwrap(),
NumPQChunks::new_with(1, 1).unwrap(),
parameters,
);
let config = IndexConfiguration::new(Metric::L2, 1, 1, ONE, 1, graph_config(32, 1.2));
let writer =
DiskIndexWriter::new("/data.fbin".into(), "/index".into(), None, 4096).unwrap();

let error = match DiskIndexBuilder::<AdHoc<f32>, _>::new(&storage, params, config, writer) {
Ok(_) => panic!("HashPrune capacity below graph degree must be rejected"),
Err(error) => error,
};

assert!(format!("{error:?}").contains("must be at least the graph degree (32)"));
assert!(!storage.exists("/index_pq_pivots.bin"));
assert!(!storage.exists("/index_pq_compressed.bin"));
}
}
47 changes: 47 additions & 0 deletions diskann-disk/src/build/configuration/build_algorithm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,43 @@ pub struct PiPNNParameters {
pub k: usize,
/// Number of independent partition passes.
pub replicas: usize,
/// HashPrune policy. `None` keeps all unique direct candidates.
pub hash_prune: Option<HashPruneParameters>,
}

/// HashPrune parameters in the JSON build configuration.
#[cfg(feature = "pipnn")]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct HashPruneParameters {
/// Number of random-hyperplane sketch dimensions.
pub num_hash_planes: usize,
/// Maximum number of candidates retained per point.
pub l_max: usize,
/// Apply Vamana RobustPrune after reservoir extraction.
pub final_prune: bool,
}

#[cfg(feature = "pipnn")]
impl Default for HashPruneParameters {
fn default() -> Self {
Self {
num_hash_planes: 12,
l_max: 64,
final_prune: true,
}
}
}

#[cfg(feature = "pipnn")]
impl From<&HashPruneParameters> for diskann::graph::pipnn::HashPruneConfig {
fn from(config: &HashPruneParameters) -> Self {
Self {
num_hash_planes: config.num_hash_planes,
l_max: config.l_max,
final_prune: config.final_prune,
}
}
}

#[cfg(feature = "pipnn")]
Expand All @@ -41,6 +78,7 @@ impl Default for PiPNNParameters {
fanout: vec![8, 3],
k: 2,
replicas: 1,
hash_prune: None,
}
}
}
Expand Down Expand Up @@ -116,6 +154,15 @@ mod tests {
assert_eq!(config.fanout, [10, 3]);
assert_eq!(config.k, 3);
assert_eq!(config.replicas, 1);
assert_eq!(config.hash_prune, None);

let explicit: BuildAlgorithm =
serde_json::from_str(r#"{"algorithm":"PiPNN","hash_prune":{}}"#).unwrap();
let BuildAlgorithm::PiPNN(explicit) = explicit else {
panic!("expected PiPNN");
};
assert_eq!(explicit.hash_prune, Some(HashPruneParameters::default()));

assert!(
serde_json::from_str::<BuildAlgorithm>(r#"{"algorithm":"PiPNN","l_max":72}"#).is_err()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,9 @@ impl DiskIndexBuildParameters {
}

#[cfg(feature = "pipnn")]
pub(crate) fn pipnn_config(&self) -> Option<diskann::graph::pipnn::PiPNNConfig> {
pub(crate) fn pipnn_parameters(&self) -> Option<&PiPNNParameters> {
match &self.build_algorithm {
BuildAlgorithm::PiPNN(config) => Some(config.into()),
BuildAlgorithm::PiPNN(config) => Some(config),
BuildAlgorithm::Vamana => None,
}
}
Expand Down
2 changes: 1 addition & 1 deletion diskann-disk/src/build/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
pub mod build_algorithm;
pub use build_algorithm::BuildAlgorithm;
#[cfg(feature = "pipnn")]
pub use build_algorithm::PiPNNParameters;
pub use build_algorithm::{HashPruneParameters, PiPNNParameters};

pub mod disk_index_build_parameter;
pub use disk_index_build_parameter::{DiskIndexBuildParameters, MemoryBudget, NumPQChunks};
Expand Down
4 changes: 2 additions & 2 deletions diskann-disk/src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ pub mod builder;
pub mod configuration;

// Re-export key types for convenience
#[cfg(feature = "pipnn")]
pub use configuration::PiPNNParameters;
pub use configuration::{
disk_index_build_parameter, filter_parameter, BuildAlgorithm, DiskIndexBuildParameters,
QuantizationType,
};
#[cfg(feature = "pipnn")]
pub use configuration::{HashPruneParameters, PiPNNParameters};
4 changes: 2 additions & 2 deletions diskann-disk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ pub(crate) mod test_utils;
pub mod error;

pub mod build;
#[cfg(feature = "pipnn")]
pub use build::PiPNNParameters;
pub use build::{
disk_index_build_parameter, filter_parameter, BuildAlgorithm, DiskIndexBuildParameters,
QuantizationType,
};
#[cfg(feature = "pipnn")]
pub use build::{HashPruneParameters, PiPNNParameters};

pub mod data_model;
pub mod search;
Expand Down
Loading
Loading