Skip to content

PiPNN 6/6: add HashPrune candidate merging - #1295

Merged
weiyaoluo (SeliMeli) merged 0 commit into
pipnn-stack/05-benchmarkfrom
pipnn-stack/06-hash-prune
Nov 11, 2026
Merged

weiyaoluo (SeliMeli) merged 0 commit into
pipnn-stack/05-benchmarkfrom
pipnn-stack/06-hash-prune

Conversation

@SeliMeli

@SeliMeli weiyaoluo (SeliMeli) commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

This PR adds optional HashPrune candidate merging to PiPNN.

Direct candidate merging remains the default.

Main changes

  • lsh.rs creates deterministic random-hyperplane sketches.
  • Each directed edge receives a relative-direction hash.
  • Leaf CSR conversion omits unassigned numerical-kernel slots.
  • Each source owns one bounded reservoir and one lock.
  • A hash bucket keeps its nearest candidate.
  • Full reservoirs reject or evict candidates by total order.
  • final_prune=true sends reservoir candidates to shared RobustPrune.
  • final_prune=false returns the nearest graph-degree candidates.
  • Disk and benchmark configuration accept the optional HashPrune policy.

Required invariants

  • Effective hash capacity covers graph degree.
  • l_max does not exceed the structural reservoir limit.
  • Padded cells never become candidates.
  • One source lock protects each reservoir mutation.
  • Leaf-local IDs become global IDs before insertion.
  • Tie order uses distance, relative hash, and neighbor ID.

Review order

  1. Review HashPruneConfig and with_hash_prune in mod.rs.
  2. Review sketch construction in lsh.rs.
  3. Review slab ownership and locking in hash_prune.rs.
  4. Review insertion and extraction in hash_prune.rs.
  5. Review leaf CSR conversion in leaf_build.rs.
  6. Review disk and benchmark configuration.

Validation

  • All 474 diskann library tests pass.
  • Seven disk-adapter tests pass.
  • The benchmark lifecycle test passes.
  • Targeted strict-provenance Miri tests pass.
  • Linux, Windows GNU, and AArch64 Clippy pass with -Dwarnings.

Stack

Stack 6/6. Depends on #1294 and completes the PiPNN stack.

@SeliMeli

Copy link
Copy Markdown
Contributor Author

Azure BigANN10M validation (Standard_D16as_v5, AVX2, 16 threads, 4× interleaved per binary) used R=64, c512/c64, fanout=[10,3], k=2, l_max=72, hp=12, final_prune=true.

  • Build: 76.777s clean → 79.601s stack (+3.68%)
  • Peak RSS: 12.224 → 11.709 GiB (-4.22%)
  • Recall@10 L=50: 0.957825 → 0.957790 (-0.0035pp)
  • Mean comparisons/hops L=50: +0.023% / +0.006%

Raw runs: ~/pipnn-pr5-ab/results/final-head-k2-20260729T171245Z on the benchmark VM. The residual build gap is being investigated only on local, unpushed pipnn-stack/07-perf-investigation; current attribution points to repeated per-row FP16 conversion dispatch and CSR bookkeeping, not graph quality or HashPrune semantics.

@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch 2 times, most recently from d4c2ba6 to 93cf436 Compare July 30, 2026 08:26
@SeliMeli

Copy link
Copy Markdown
Contributor Author

Direct final-stack QC

Reviewed directly against current origin/main; no subagents were used.

Fixed during QC

  • Removed the proposed PiPNN-owned unsafe FP16 gather because its measured ~0.48% gain did not meet the unsafe RFC's safety/CI-evidence bar.
  • Added PR6's missing direct bytemuck dependency (caught by a clean coverage build).
  • Updated shared RobustPrune for the latest Neighbor accessor API.
  • Rebuilt the stack from exact per-layer commit boundaries and rebased it onto current main.
  • Preserved current-main nightly feature coverage while retaining strict HashPrune Miri jobs.

Remaining findings

  1. Evidence gap: performance-motivated unsafe kernels have Criterion/Azure/Miri evidence, but no CI-integrated performance regression threshold as required by rfcs/00109-unsafe-rust.md.
  2. Performance objective not met: allocator-matched BigANN10M 4× interleaved A/B reports build median 75.899s clean vs 77.473s final (+2.07%); wall +1.62%; RSS -3.60%; recall effectively unchanged (0.957805 vs 0.957795). No parity/win claim is made.

Validation

  • Formatting and CI-style workspace Clippy: pass.
  • PiPNN all targets: pass (81 unit tests plus integration/config/kernel suites and benchmark smoke executables).
  • RobustPrune/Vamana tests: 22 pass.
  • Focused disk PiPNN tests: 5 pass.
  • pipnn and disk-index,pipnn feature compilation/Clippy: pass.
  • x86-64 baseline and AArch64 all-target checks: pass.
  • Strict-provenance Miri HashPrune kernel checks: pass.
  • PiPNN ISA/TLS/RefCell/broadcast-cleanup scans: zero hits.
  • Stable production-source coverage: 94.19% lines / 91.88% regions; experimental branch coverage 91.32%.
  • Complexity/duplication audit: no maintainability blocker.
  • Final Azure BigANN10M smoke: Recall@10 L=50 95.774%, build 77.918s, peak RSS 11.68 GiB.

Local runtime benchmark integration remains blocked by unavailable Git LFS (the fixture is a 123-byte pointer); CI's LFS checkout is the remaining runtime oracle.

Full report: /tmp/pipnn-qc-direct.md in the authoring environment.

@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from 93cf436 to efedce4 Compare July 30, 2026 08:55
@SeliMeli

Copy link
Copy Markdown
Contributor Author

QC follow-up

The first post-rebase CI run exposed a real AArch64-only failure in cosine_zero_norm_masks_nan_norm_at_simd_boundaries: NEON and x86 have different max(NaN) behavior, so a NaN pair could become distance zero on ARM. PR1 now preserves NaNs explicitly with a validity mask before selecting the non-negative clamp.

The replacement run is green on:

  • Ubuntu AArch64 runtime tests
  • x86-64 Nehalem/SDE baseline
  • AVX-512/SDE
  • Ubuntu and Windows default/all-feature tests
  • coverage, CodeQL, formatting, and all Clippy matrices

Remote stack metadata was also rebuilt as one stack, #1301:
#1287 → #1288 → #1290 → #1291 → #1294 → #1295.
The obsolete closed HashPrune PR is no longer a stack member.

Copilot AI lite review requested due to automatic review settings July 30, 2026 11:26
@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from efedce4 to f8f27bf Compare July 30, 2026 11:26
@SeliMeli
weiyaoluo (SeliMeli) requested a review from a team July 30, 2026 11:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional HashPrune/LSH-based candidate-merging path to PiPNN builds, wiring it through disk build configuration and benchmarks, and extending SIMD/mask utilities needed by the new kernels.

Changes:

  • Introduces HashPrune reservoirs plus random-hyperplane LSH sketch computation, and integrates them into PiPNN leaf building / extraction (optionally followed by RobustPrune).
  • Extends disk-build and benchmark pipelines to accept and validate HashPrune parameters for PiPNN.
  • Adds supporting utilities (trusted adjacency-list constructor, mask helpers, SIMD eq optimization) and CI Miri coverage for the raw-pointer kernels.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
diskann/src/graph/adjacencylist.rs Adds from_vec_trusted for zero-copy construction when uniqueness is guaranteed.
diskann-wide/src/doubled.rs Adds first() support for doubled masks.
diskann-wide/src/arch/x86_64/v3/i16x16_.rs Optimizes SIMD equality mask generation using movemask+pext.
diskann-vector/src/lib.rs Makes x86_64 prefetch helpers available without requiring AVX2.
diskann-pipnn/tests/config.rs Adds validation tests for HashPruneConfig parameters.
diskann-pipnn/tests/build_graph.rs Adds parallel HashPrune build invariant test.
diskann-pipnn/src/lsh.rs New LSH sketch computation (seeded random hyperplanes) and errors.
diskann-pipnn/src/lib.rs Adds HashPrune config/types and integrates candidate-merge selection into build_graph.
diskann-pipnn/src/leaf_build/tests.rs Adds CSR construction tests used by HashPrune leaf streaming.
diskann-pipnn/src/leaf_build.rs Refactors leaf computation; adds CSR edge streaming into HashPrune reservoirs.
diskann-pipnn/src/hash_prune.rs New HashPrune implementation (hot/cold slabs, per-row locking, SIMD hash ops, extraction).
diskann-pipnn/src/hash_prune/tests.rs Adds unit and concurrency tests for HashPrune kernels and reservoir behavior.
diskann-pipnn/src/bf16.rs Adds bf16 packing helpers for compact distance keys.
diskann-pipnn/Cargo.toml Adds new dependencies and a HashPrune benchmark target.
diskann-pipnn/benches/hash_prune.rs Adds criterion benchmark comparing direct vs HashPrune merge paths.
diskann-disk/src/lib.rs Re-exports HashPruneParameters when pipnn feature is enabled.
diskann-disk/src/build/mod.rs Re-exports HashPruneParameters from configuration.
diskann-disk/src/build/configuration/mod.rs Exposes HashPrune parameters in configuration module exports.
diskann-disk/src/build/configuration/build_algorithm.rs Extends PiPNNParameters with hash_prune and serde defaults.
diskann-disk/src/build/configuration/disk_index_build_parameter.rs Switches to returning borrowed PiPNN parameters for build selection.
diskann-disk/src/build/builder/build/pipnn.rs Wires optional HashPrune parameters into PiPNNBuildContext.
diskann-disk/src/build/builder/build/pipnn/tests.rs Updates PiPNN disk builder tests for new parameter passing.
diskann-disk/src/build/builder/build.rs Validates PiPNN + HashPrune config when pipnn is selected.
diskann-benchmark/src/index/build.rs Wires optional HashPrune parameters into benchmark PiPNN builds.
Cargo.lock Adds new transitive dependencies for diskann-pipnn changes.
.github/workflows/nightly.yml Improves feature quoting/formatting and adds Miri strict-provenance coverage for HashPrune kernels.
Comments suppressed due to low confidence (1)

diskann-disk/src/build/configuration/build_algorithm.rs:157

  • If PiPNNParameters::default() is changed to keep hash_prune opt-in, this serde-defaults test should be updated to expect None instead of Some(HashPruneParameters::default()).
        assert_eq!(config.hash_prune, Some(HashPruneParameters::default()));

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread diskann-pipnn/Cargo.toml Resolved
Copilot AI review requested due to automatic review settings July 30, 2026 13:25
@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from f8f27bf to 661365c Compare July 30, 2026 13:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 31, 2026 04:24
@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from 661365c to 25ab02b Compare July 31, 2026 04:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated no new comments.

Suppressed comments (1)

diskann-disk/src/build/configuration/build_algorithm.rs:82

  • PiPNNParameters is #[serde(default)], so deserializing PiPNN configs that omit the new hash_prune field will inherit this Default value. Setting hash_prune: Some(HashPruneParameters::default()) therefore enables HashPrune by default and can change behavior for existing JSON configs that previously used direct candidate merging. If HashPrune is meant to be opt-in (as described), make the default None and require explicit configuration to enable it.
impl Default for PiPNNParameters {
    fn default() -> Self {
        Self {
            c_max: 256,
            c_min: 16,
            p_samp: 0.005,
            fanout: vec![8, 3],
            k: 2,
            replicas: 1,
            hash_prune: Some(HashPruneParameters::default()),
        }

Copilot AI review requested due to automatic review settings August 3, 2026 02:18
@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from 25ab02b to 10bec9e Compare August 3, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (2)

diskann/src/graph/pipnn/leaf_build.rs:524

  • build_symmetric_edge_csr uses neighbors.chunks_exact(leaf_k), which silently ignores any remainder if the slice length is not an exact multiple of leaf_k. Adding an explicit assertion (or error) here would make it harder for future refactors/callers to accidentally drop edges without noticing.
    for (source, neighbors) in neighbors.chunks_exact(leaf_k).enumerate() {

diskann/src/graph/pipnn/lsh.rs:155

  • buffer.resize(ndims, 0.0) does not clear existing elements when buffer.len() == ndims, so if fill_point only writes a prefix (or has an early-return bug) sketch computation can reuse stale values from a previous point. Either document that fill_point must fully initialize the slice or clear it before calling fill_point to make the API robust.
                if buffer.len() < ndims {
                    let additional = ndims - buffer.len();
                    buffer
                        .try_reserve(additional)
                        .map_err(LshSketchError::Allocation)?;
                }
                buffer.resize(ndims, 0.0);
                fill_point(i, &mut buffer[..ndims]).map_err(LshSketchError::Fill)?;
                for j in 0..num_planes {

Copilot AI review requested due to automatic review settings August 5, 2026 09:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (1)

diskann-disk/src/build/configuration/build_algorithm.rs:82

  • PiPNNParameters is documented as allowing hash_prune: None for exact/direct candidate accumulation, but the Default impl sets hash_prune: Some(HashPruneParameters::default()). Because the struct is #[serde(default)], omitting hash_prune in JSON will now silently opt into HashPrune instead of keeping direct merging as the default, which contradicts the PR description and the field’s doc comment. Consider defaulting hash_prune to None (or updating the docs/PR description if HashPrune is intended to become the default).
            fanout: vec![8, 3],
            k: 2,
            replicas: 1,
            hash_prune: Some(HashPruneParameters::default()),
        }

Copilot AI review requested due to automatic review settings August 5, 2026 11:04
@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from 7935c84 to cbbe4f5 Compare August 5, 2026 11:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (3)

diskann-disk/src/build/configuration/build_algorithm.rs:157

  • This serde-defaults test currently asserts that omitted hash_prune deserializes to Some(default), which would force-enable HashPrune by default. If direct merge is intended to remain the default, this assertion should instead verify that hash_prune is absent unless explicitly set.
        assert_eq!(config.hash_prune, Some(HashPruneParameters::default()));

diskann/src/graph/pipnn/hash_prune.rs:717

  • collect_sorted_neighbors/collect_neighbor_ids use Vec::reserve and Vec::with_capacity, which can panic on allocation failure. Elsewhere in the PiPNN pipeline allocations are handled fallibly (try_reserve*) and surfaced as ANNError; this new path introduces an OOM panic in a library build/extraction phase.
    let n = hot.len as usize;
    scratch.clear();
    scratch.reserve(n);
    for i in 0..n {
        // SAFETY: guaranteed by this function's contract.
        scratch.push(unsafe { (*neighbors.add(i), *distances.add(i)) });
    }
    scratch.sort_unstable_by_key(|&(id, distance)| (distance, id));
    let out_len = n.min(cap);
    let mut out = Vec::with_capacity(out_len);
    for &(id, d) in &scratch[..out_len] {
        out.push((id, bf16_to_f32(key_to_bf16(d))));
    }
    out
}

/// Collect the reservoir's neighbor ids, truncated to `cap`, WITHOUT sorting.
/// Reservoir order is intentionally not preserved. Reading only `neighbors`
/// lets the caller drop the hashes and distances slabs before extraction; any
/// ordering required by a later graph-finalization policy belongs to that caller.
///
/// SAFETY: caller holds the slot lock (or owns the reservoir); `neighbors` is
/// valid for `hot.len` elements.
#[inline]
unsafe fn collect_neighbor_ids(hot: &HotSlot, neighbors: *const u32, cap: usize) -> Vec<u32> {
    let out_len = (hot.len as usize).min(cap);
    let mut out = Vec::with_capacity(out_len);
    for i in 0..out_len {
        // SAFETY: guaranteed by this function's contract.
        out.push(unsafe { *neighbors.add(i) });
    }
    out
}

diskann-disk/src/build/configuration/build_algorithm.rs:81

  • PiPNNParameters now defaults hash_prune to Some(...), which enables HashPrune even when callers don’t specify it in JSON. That changes the default candidate-merge behavior and contradicts the PR description that direct merging remains the default unless explicitly opted in.

This issue also appears on line 157 of the same file.

            hash_prune: Some(HashPruneParameters::default()),

Copilot AI review requested due to automatic review settings August 5, 2026 12:10
@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch from cbbe4f5 to 91bc385 Compare August 5, 2026 12:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (1)

diskann-disk/src/build/configuration/build_algorithm.rs:83

  • PiPNNParameters::default() now enables HashPrune by default (hash_prune: Some(...)). That contradicts the PR description (“Direct merging remains the default”) and changes behavior for JSON configs that omit hash_prune, because #[serde(default)] uses the struct Default.

Consider making the default hash_prune: None so HashPrune is only enabled when explicitly requested (e.g., via "hash_prune": { ... } in JSON).

#[cfg(feature = "pipnn")]
impl Default for PiPNNParameters {
    fn default() -> Self {
        Self {
            c_max: 256,
            c_min: 16,
            p_samp: 0.005,
            fanout: vec![8, 3],
            k: 2,
            replicas: 1,
            hash_prune: Some(HashPruneParameters::default()),
        }
    }

@SeliMeli
weiyaoluo (SeliMeli) force-pushed the pipnn-stack/06-hash-prune branch 2 times, most recently from 8225da6 to f611b04 Compare August 6, 2026 05:40
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Aug 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.08139% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.29%. Comparing base (f650cbf) to head (3ff4d45).

Files with missing lines Patch % Lines
diskann/src/graph/pipnn/hash_prune.rs 95.91% 35 Missing ⚠️
diskann/src/graph/pipnn/lsh.rs 94.02% 8 Missing ⚠️
diskann/src/graph/pipnn/mod.rs 95.93% 5 Missing ⚠️
diskann-benchmark/src/inputs/graph_index.rs 0.00% 1 Missing ⚠️
diskann-disk/src/build/builder/build/pipnn.rs 96.00% 1 Missing ⚠️
...nn-disk/src/build/configuration/build_algorithm.rs 96.15% 1 Missing ⚠️
diskann/src/graph/pipnn/leaf_build.rs 99.20% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                      @@
##           pipnn-stack/05-benchmark    #1295      +/-   ##
============================================================
+ Coverage                     91.23%   91.29%   +0.05%     
============================================================
  Files                           621      624       +3     
  Lines                        130255   131564    +1309     
============================================================
+ Hits                         118841   120109    +1268     
- Misses                        11414    11455      +41     
Flag Coverage Δ
miri 91.29% <96.08%> (+0.05%) ⬆️
unittests 91.01% <96.08%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
diskann-wide/src/arch/x86_64/v3/i16x16_.rs 100.00% <100.00%> (ø)
diskann-wide/src/doubled.rs 85.89% <100.00%> (+0.30%) ⬆️
diskann/src/graph/adjacencylist.rs 99.71% <100.00%> (+0.01%) ⬆️
diskann/src/graph/pipnn/bf16.rs 100.00% <100.00%> (ø)
diskann-benchmark/src/inputs/graph_index.rs 65.96% <0.00%> (ø)
diskann-disk/src/build/builder/build/pipnn.rs 97.75% <96.00%> (-0.23%) ⬇️
...nn-disk/src/build/configuration/build_algorithm.rs 97.18% <96.15%> (-0.69%) ⬇️
diskann/src/graph/pipnn/leaf_build.rs 99.72% <99.20%> (+0.13%) ⬆️
diskann/src/graph/pipnn/mod.rs 95.02% <95.93%> (+0.20%) ⬆️
diskann/src/graph/pipnn/lsh.rs 94.02% <94.02%> (ø)
... and 1 more

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@SeliMeli

Copy link
Copy Markdown
Contributor Author

Simplification follow-up in 404d68e7 removes 191 net lines from this PR:

  • optional HashPrune state is now Option<HashPruneConfig>;
  • private LSH consumes MatrixView<T>/VectorRepr directly;
  • MmapSlab no longer exposes an unused safe Deref;
  • nearest extraction materializes IDs once instead of building and discarding (ID, distance) output;
  • single-call wrappers and unnecessary visibility were removed.

Validation: 442 diskann tests, workspace all-feature Clippy, AArch64/Windows checks, and four strict-provenance Miri cases pass. Local IAI-Callgrind HashPrune fixture: instructions -0.20788%, estimated cycles -0.20511% versus a0fc45d4.

@SeliMeli

Copy link
Copy Markdown
Contributor Author

Validation count after deleting unsupported dynamic-k and unordered-leaf test paths: 440 diskann library tests pass with pipnn,testing. No .cargo/mutants.toml exists in any stack layer.

@SeliMeli

Copy link
Copy Markdown
Contributor Author

The outdated HashPrune threads are addressed in the current layout: libc is Linux-target-specific, HashPrune defaults to None, and list extraction destructures ownership before early buffer release. I am resolving those superseded threads.

@SeliMeli

Copy link
Copy Markdown
Contributor Author

Latest cross-matrix Clippy exposed two boundary issues after removing dead-code suppression. AdjacencyList::from_vec_trusted is now compiled only with its PiPNN callers, and the slab test no longer calls the Linux-only byte helper on Windows. Exact default-workspace, AArch64-default, and Windows all-feature Clippy commands pass locally with -Dwarnings; no allow(dead_code) was added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement PiPNN HashPrune candidate merging

3 participants