mirror of
https://github.com/ruvnet/RuView
synced 2026-07-22 17:23:19 +00:00
1f05456588
* feat(ADR-261): multi-bit (b∈{1,2,4}) quantized HNSW traversal + scaling harness
Generalize the SymphonyQG-style quantized-traversal HNSW from 1-bit Hamming to a
b-bit-per-dimension code (b ∈ {1,2,4}), mirroring ADR-156 §10's multi-bit RaBitQ
scheme (rotate via FHT Pass-2, uniform mid-rise scalar quantizer over [-3,3],
ranked by per-dim L1). b=1 is byte-for-byte the original construction (codes in
{0,1} ⇒ L1 == Hamming), pinned by one_bit_build_bits_matches_legacy_build.
Bytes/node scales linearly: 128-d → 16/32/64 B for b=1/2/4.
- hnsw_quantized.rs: QuantizedHnswIndex::build_bits(...,bits,...), bits()/
bytes_per_node() accessors, code-L1 greedy+beam traversal. build(...) kept as
the b=1 backward-compatible entry point. +4 tests (multi-bit recall regression,
bits clamp, bytes/node, legacy parity).
- ann_measure.rs: build_indices_bits / build_quant_bits / run_scaling_study +
best_float_op / best_quant_op; scaling_report (#[ignore], --release) and a
CI-safe scaling_study_small_is_consistent.
- ann_bench.rs: 2-bit and 4-bit quant criterion benches over the shared graph.
ruvector lib 151 → 156 passed, 0 failed, 1 ignored (scaling_report).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-261): record M2 multi-bit scaling study — measured, no crossover (refutes M1 prediction)
Multi-bit (b∈{1,2,4}) quantized HNSW traversal + N∈{10k,100k,250k} scaling study,
measured on this box. No crossover at any (N,b): at 10k more bits help (ratio
0.19→0.48×, b≥2 reaches 0.90 recall) but quant stays slower than float HNSW at
equal recall; at 100k/250k quant recall collapses (b=4: 1.0→0.788→0.624, never
≥0.90) while float holds ≥0.92. The predicted large-N crossover moved the wrong
way. Published negative with the mechanism explained. ADR-261 §11.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: ruv <ruvnet@gmail.com>
99 lines
3.5 KiB
Rust
99 lines
3.5 KiB
Rust
//! Criterion bench for the ADR-261 graph-ANN index: linear scan vs float HNSW
|
|
//! vs quantized HNSW, on the shared `ann_measure` fixture.
|
|
//!
|
|
//! The authoritative recall/QPS numbers in ADR-261 come from the
|
|
//! `--no-default-features --release` test report
|
|
//! (`ann_bench_report` in `src/ann_measure.rs`), which is deterministic and
|
|
//! gate-runnable. This criterion bench times the same operations through the
|
|
//! criterion harness for stable per-op medians:
|
|
//!
|
|
//! ```text
|
|
//! cargo bench -p wifi-densepose-ruvector --bench ann_bench
|
|
//! ```
|
|
//!
|
|
//! Build is excluded from the timed region (done once in setup); only the query
|
|
//! path is measured. The fixture and both indices are identical to the report's,
|
|
//! so the bench and the report can never measure different graphs.
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
use wifi_densepose_ruvector::ann_measure::{
|
|
build_indices, build_quant_bits, queries, AnnBenchParams,
|
|
};
|
|
|
|
fn bench_ann(c: &mut Criterion) {
|
|
// Modest N so the bench builds quickly; the report covers the larger N.
|
|
let p = AnnBenchParams::default_fixture(10_000);
|
|
let (float_idx, quant_idx, vectors) = build_indices(p);
|
|
// Multi-bit quant variants over the SAME graph/fixture (ADR-261 §11).
|
|
let quant_2bit = build_quant_bits(p, &vectors, 2);
|
|
let quant_4bit = build_quant_bits(p, &vectors, 4);
|
|
let qs = queries(p);
|
|
let k = p.k;
|
|
|
|
let mut group = c.benchmark_group("ann_query");
|
|
group.sample_size(20);
|
|
|
|
// Linear scan (brute force) — the no-index baseline.
|
|
group.bench_function("linear_scan", |b| {
|
|
b.iter(|| {
|
|
let mut sink = 0u64;
|
|
for q in &qs {
|
|
sink = sink.wrapping_add(float_idx.brute_force(black_box(q), k).len() as u64);
|
|
}
|
|
black_box(sink)
|
|
})
|
|
});
|
|
|
|
// Float HNSW at a mid beam width.
|
|
for &ef in &[64usize, 128] {
|
|
group.bench_function(format!("float_hnsw_ef{ef}"), |b| {
|
|
b.iter(|| {
|
|
let mut sink = 0u64;
|
|
for q in &qs {
|
|
sink = sink.wrapping_add(float_idx.search(black_box(q), k, ef).len() as u64);
|
|
}
|
|
black_box(sink)
|
|
})
|
|
});
|
|
}
|
|
|
|
// Quantized HNSW (1-bit) at matched beam widths + rerank.
|
|
for &ef in &[64usize, 128] {
|
|
let rr = k * 5;
|
|
group.bench_function(format!("quant_hnsw_1bit_ef{ef}_rr{rr}"), |b| {
|
|
b.iter(|| {
|
|
let mut sink = 0u64;
|
|
for q in &qs {
|
|
sink = sink
|
|
.wrapping_add(quant_idx.search_quantized(black_box(q), k, ef, rr).len() as u64);
|
|
}
|
|
black_box(sink)
|
|
})
|
|
});
|
|
}
|
|
|
|
// Multi-bit quant HNSW (ADR-261 §11): 2-bit and 4-bit traversal codes at a
|
|
// mid beam width, so the criterion medians show the per-bit QPS cost the
|
|
// scaling study reports against recall.
|
|
for (label, idx) in [("2bit", &quant_2bit), ("4bit", &quant_4bit)] {
|
|
for &ef in &[64usize, 128] {
|
|
let rr = k * 5;
|
|
group.bench_function(format!("quant_hnsw_{label}_ef{ef}_rr{rr}"), |b| {
|
|
b.iter(|| {
|
|
let mut sink = 0u64;
|
|
for q in &qs {
|
|
sink = sink
|
|
.wrapping_add(idx.search_quantized(black_box(q), k, ef, rr).len() as u64);
|
|
}
|
|
black_box(sink)
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
criterion_group!(benches, bench_ann);
|
|
criterion_main!(benches);
|