Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ jobs:
run: cargo nextest run --no-default-features --features hnsw
# MSRV check
msrv:
name: MSRV (1.89)
name: MSRV (1.94)
runs-on: ubuntu-latest
needs: basics
steps:
Expand All @@ -196,7 +196,7 @@ jobs:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: "1.89.0"
toolchain: "1.94.0"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Check MSRV
run: |
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ These source changes are not assigned to a package release here.

### Changed

- Development-only SIMD ports use an immutable Pulp Git revision and require
Rust 1.94 for its x86 intrinsics. Forbid unsafe
code in all Vicinity targets, including benchmarks. Allocation diagnostics now
use `stats_alloc`; `acquired_bytes` counts allocations plus positive realloc
growth, rather than full realloc destination sizes. Counts remain process-wide
and are not live-byte measurements. Dependency internals may use unsafe code.
- Require `qntz` 0.2.2 or later in the 0.2 series for its signed RaBitQ edge
distance correction, allowing negative edge terms in IVF-RaBitQ scoring
when a vector is nearer the query than its cluster centroid. IVF-RaBitQ
Expand Down
14 changes: 11 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "vicinity"
version = "0.11.1"
authors = ["Arc <attobop@gmail.com>"]
edition = "2021"
rust-version = "1.89"
rust-version = "1.94"
description = "Approximate nearest-neighbor search"
repository = "https://github.com/arclabs561/vicinity"
documentation = "https://docs.rs/vicinity"
Expand All @@ -14,6 +14,9 @@ exclude = ["data/", "benches/", "docs/", "scripts/", ".github/"]

[workspace]

[patch.crates-io]
innr = { git = "https://github.com/arclabs561/innr.git", rev = "392736772688262ce5f89f51db6b08a8167b8262" }

[lib]
# `rlib` for normal Rust consumers; `cdylib` so maturin can build the PyO3
# extension module. maturin does not inject the cdylib target itself (it reports
Expand Down Expand Up @@ -41,7 +44,7 @@ pyo3 = { version = "0.29.0", optional = true, features = ["abi3-py310"] }
numpy = { version = "0.29.0", optional = true }

# SIMD primitives (ecosystem crate)
innr = { version = "0.6", optional = true }
innr = { git = "https://github.com/arclabs561/innr.git", rev = "392736772688262ce5f89f51db6b08a8167b8262", optional = true }

# Linear algebra for OPQ Procrustes solver (SVD)
nalgebra = { version = "0.33", optional = true, default-features = false, features = [
Expand Down Expand Up @@ -79,6 +82,10 @@ segstore = { version = "0.6", optional = true }
fs4 = { version = "0.13", optional = true }


# NEON `tbl` for the 4-bit FastScan kernel without `unsafe` in this crate.
[target.'cfg(target_arch = "aarch64")'.dependencies]
pulp = { git = "https://github.com/sarah-quinones/pulp", rev = "601de0c071ba32df3fd7ff33fc4218c9bd712aba", default-features = false, features = ["std"] }

# RNG backend for wasm32 (rand -> getrandom needs a JS backend on wasm).
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
getrandom = { version = "0.3", features = ["wasm_js"] }
Expand Down Expand Up @@ -149,6 +156,7 @@ qntz = ["dep:qntz"]
binary_index = ["dep:qntz", "qntz/binary", "dep:serde", "dep:serde_json"]

[dev-dependencies]
stats_alloc = "0.1.10"
criterion = { version = "0.5", features = ["html_reports"] }
proptest = "1.5"
tempfile = "3"
Expand Down Expand Up @@ -370,7 +378,7 @@ unexpected_cfgs = "warn"
# Documentation lints: temporarily relaxed, enable incrementally
# missing_docs = "warn"
# missing_doc_code_examples = "warn"
unsafe_code = "deny"
unsafe_code = "forbid"
unstable_features = "deny"
unused_import_braces = "warn"
# unused_qualifications = "warn" # Relaxed: explicit paths often improve clarity
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Approximate nearest-neighbor search.

`vicinity` provides Rust indexes and Python bindings for vector search.

This development branch requires Rust 1.94 for its Git-pinned SIMD backend and
is not a registry release. The root-only Innr patch also unifies Qntz and benchmark dependencies for
development validation, not published-artifact equivalence.
Vicinity-authored Rust forbids unsafe code across its targets. Dependencies,
including Pulp and benchmark allocator instrumentation, use unsafe
internally. This is not a transitive unsafe-free guarantee.

## Rust

```toml
Expand Down
49 changes: 12 additions & 37 deletions benches/hnsw_search.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,22 @@
#![allow(clippy::expect_used, clippy::unwrap_used, unsafe_code)]
#![allow(clippy::expect_used, clippy::unwrap_used)]
//! Search-only HNSW benchmarks.
//!
//! Keep this target separate from `benches/hnsw.rs` so profiling filtered
//! search runs does not include setup for construction benchmarks.

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use rand::prelude::*;
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use stats_alloc::{Region, StatsAlloc, INSTRUMENTED_SYSTEM};
use std::alloc::System;
#[cfg(feature = "benchmark")]
use vicinity::hnsw::{reset_search_counters, take_search_counters, HnswSearchCounters};
use vicinity::hnsw::{HNSWIndex, HNSWParams};

#[cfg(feature = "parallel")]
use vicinity::hnsw::AdaptiveConfig;

static ALLOC_CALLS: AtomicUsize = AtomicUsize::new(0);
static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);

struct CountingAlloc;

// SAFETY: This wrapper delegates allocation behavior to `System` unchanged and
// only records relaxed diagnostic counters for this benchmark binary.
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
// SAFETY: `layout` is forwarded unchanged to the system allocator.
unsafe { System.alloc(layout) }
}

unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// SAFETY: `ptr` and `layout` are forwarded unchanged to the system allocator.
unsafe { System.dealloc(ptr, layout) }
}

unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(new_size, Ordering::Relaxed);
// SAFETY: `ptr`, `layout`, and `new_size` are forwarded unchanged.
unsafe { System.realloc(ptr, layout, new_size) }
}
}

#[global_allocator]
static GLOBAL: CountingAlloc = CountingAlloc;
static GLOBAL: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;

#[derive(Clone, Copy, Debug, Default)]
struct AllocationProfile {
Expand All @@ -54,11 +26,14 @@ struct AllocationProfile {

impl AllocationProfile {
fn record_search<T>(f: impl FnOnce() -> T) -> (T, Self) {
ALLOC_CALLS.store(0, Ordering::Relaxed);
ALLOC_BYTES.store(0, Ordering::Relaxed);
// Process-wide deltas include concurrent allocations, not just this
// closure. Acquired bytes count allocations plus positive realloc growth,
// not live bytes or the old sum of full realloc destination sizes.
let region = Region::new(GLOBAL);
let out = f();
let calls = ALLOC_CALLS.load(Ordering::Relaxed);
let bytes = ALLOC_BYTES.load(Ordering::Relaxed);
let stats = region.change();
let calls = stats.allocations + stats.reallocations;
let bytes = stats.bytes_allocated;
(out, Self { calls, bytes })
}

Expand Down Expand Up @@ -190,7 +165,7 @@ fn print_search_summary(
hits as f64 / (queries.len() * k) as f64,
);
eprintln!(
"hnsw alloc {label}: ef={ef} alloc_calls={:.1}/query alloc_bytes={:.1}/query results={result_count}",
"hnsw alloc {label}: ef={ef} alloc_calls={:.1}/query acquired_bytes={:.1}/query results={result_count}",
alloc_total.calls as f64 / queries_len,
alloc_total.bytes as f64 / queries_len,
);
Expand Down
56 changes: 12 additions & 44 deletions benches/ivfpq_search.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(clippy::expect_used, clippy::unwrap_used, unsafe_code)]
#![allow(clippy::expect_used, clippy::unwrap_used)]
//! Search-only IVF-PQ benchmarks.
//!
//! Keep this target separate from the ANN dataset example so profiling search
Expand All @@ -9,54 +9,19 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughpu
#[cfg(feature = "ivf_pq")]
use rand::prelude::*;
#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
use std::alloc::{GlobalAlloc, Layout, System};
use stats_alloc::{Region, StatsAlloc, INSTRUMENTED_SYSTEM};
#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::alloc::System;
#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
use vicinity::ivf_pq::IVFPQSearchProfile;
#[cfg(feature = "ivf_pq")]
use vicinity::ivf_pq::{IVFPQFileSearcher, IVFPQIndex, IVFPQParams};
#[cfg(feature = "ivf_pq")]
use vicinity::FileCacheConfig;

#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
static ALLOC_CALLS: AtomicUsize = AtomicUsize::new(0);
#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);

#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
struct CountingAlloc;

#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
// SAFETY: This wrapper delegates allocation behavior to `System` unchanged and
// only records relaxed diagnostic counters for this benchmark binary.
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
// SAFETY: `layout` is provided by the allocator caller and is forwarded
// unchanged to the system allocator.
unsafe { System.alloc(layout) }
}

unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// SAFETY: `ptr` and `layout` are the pair previously returned to the
// caller; this wrapper does not alter either value.
unsafe { System.dealloc(ptr, layout) }
}

unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(new_size, Ordering::Relaxed);
// SAFETY: `ptr`, `layout`, and `new_size` are forwarded unchanged to
// the system allocator.
unsafe { System.realloc(ptr, layout, new_size) }
}
}

#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
#[global_allocator]
static GLOBAL: CountingAlloc = CountingAlloc;
static GLOBAL: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;

#[cfg(feature = "ivf_pq")]
fn random_vectors(n: usize, dim: usize, seed: u64) -> Vec<Vec<f32>> {
Expand Down Expand Up @@ -108,11 +73,14 @@ struct AllocationProfile {
#[cfg(all(feature = "ivf_pq", feature = "benchmark"))]
impl AllocationProfile {
fn record_search<T>(f: impl FnOnce() -> T) -> (T, Self) {
ALLOC_CALLS.store(0, Ordering::Relaxed);
ALLOC_BYTES.store(0, Ordering::Relaxed);
// Process-wide deltas include concurrent allocations, not just this
// closure. Acquired bytes count allocations plus positive realloc growth,
// not live bytes or the old sum of full realloc destination sizes.
let region = Region::new(GLOBAL);
let out = f();
let calls = ALLOC_CALLS.load(Ordering::Relaxed);
let bytes = ALLOC_BYTES.load(Ordering::Relaxed);
let stats = region.change();
let calls = stats.allocations + stats.reallocations;
let bytes = stats.bytes_allocated;
(out, Self { calls, bytes })
}

Expand Down Expand Up @@ -153,7 +121,7 @@ fn print_profile_summary(label: &str, index: &IVFPQIndex, queries: &[Vec<f32>],
"candidates={candidates:.1}/query ",
"copy_bytes={copy_bytes:.1}/query ",
"alloc_calls={alloc_calls:.1}/query ",
"alloc_bytes={alloc_bytes:.1}/query ",
"acquired_bytes={alloc_bytes:.1}/query ",
"results={results}"
),
label = label,
Expand Down
2 changes: 1 addition & 1 deletion examples/lid_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ fn generate_sphere_points(
let mut points = Vec::with_capacity(n);

// Use golden ratio based quasi-random sequence for better uniformity
let phi = 1.618_034_f32;
let phi = std::f32::consts::GOLDEN_RATIO;

for i in 0..n {
let mut point = vec![0.0f32; ambient_dim];
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Crate-level lint configuration
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![cfg_attr(
test,
Expand Down
Loading
Loading