Files
ruvnet--RuView/v2/crates/wifi-densepose-ruvector/benches/sketch_bench.rs
T
rUv 91248536bc feat(beyond-sota): ADR-156 M2 — RaBitQ unbiased distance estimator (rigorous published negative on strict-K) (#1056)
* feat(ruvector): RaBitQ unbiased distance estimator (ADR-156 M2)

Implement the real Gao & Long (SIGMOD 2024) RaBitQ contribution on top of
the existing Pass-2 rotation: an unbiased estimator of the inner product /
squared distance recovered from the 1-bit code plus 8 B/vec per-vector side
info (residual_norm + x_dot_o), used to rerank the candidate set instead of
raw Hamming.

- src/estimator.rs (new): EstimatorSketch, SideInfo, EstimatorQuery,
  DistanceEstimator (estimate_inner_product / estimate_sq_distance /
  ranking_key / cosine_ranking_key), EstimatorBank (topk_estimated[_cosine],
  with_centroid). Zero-centroid simplification documented; paper-faithful
  centroid path also built.
- src/rotation.rs: extract apply_padded() (full padded FHT frame the code
  lives in); apply() now truncates apply_padded(). No behaviour change.
- lib.rs: export estimator types.

Additive + backward-compatible: Pass-1 Sketch / Pass-2 SketchBank / WireSketch
wire format unchanged; all external callers use Pass-1 and are unaffected.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(ruvector): estimator strict-K coverage harness (ADR-156 M2)

Add measure_estimator (cosine rerank) + measure_estimator_euclidean to the
coverage harness, on the BIT-IDENTICAL fixture / cluster centres / query
stream / cosine ground truth as measure_pass1/measure_pass2 — apples-to-apples
sign-Hamming vs unbiased-estimator-rerank.

Regression tests:
- estimator_rerank_not_worse_than_sign (>= sign-only Pass-2 on a fixed fixture)
- estimator_coverage_is_deterministic
- estimator_coverage_report (--nocapture prints the strict-K table)

MEASURED strict-K (candidate_k=K=8): Pass-1 36.13% -> Pass-2-sign 46.39% ->
estimator-cosine 49.71%. Still short of the ADR-084 90% strict bar; estimator
reaches 95.12% at candidate_k=24 (vs sign 91.60%). Published negative.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(ruvector): record RaBitQ estimator measured negative (ADR-156 §11, ADR-084)

- sketch_bench: estimator cosine/euclid columns in the coverage table.
- ADR-156 §11 (new): estimator formula + zero-centroid simplification stated
  honestly; strict-K coverage table; RESOLVED-NEGATIVE verdict (49.71% strict,
  short of 90%); pinning test names. §5 #2 + §10.5 updated.
- ADR-084 'Pass 2b' (new): estimator landed + measured strict-K vs the bar.
- CHANGELOG [Unreleased]: ADR-156 §11 Milestone-2 entry.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 18:24:40 -04:00

250 lines
8.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! ADR-084 acceptance criterion benchmark: sketch-vs-float compare cost.
//!
//! Acceptance threshold from `docs/adr/ADR-084-rabitq-similarity-sensor.md`:
//! > Sketch compare cost reduction: **8×–30×** vs full-float compare.
//!
//! This bench measures the per-pair compare cost at the embedding sizes
//! actually used in RuView:
//!
//! - 128-d (AETHER re-ID embeddings, ADR-024)
//! - 256-d (CSI spectrogram embeddings, ADR-076)
//! - 512-d (forward-looking, in case of post-rotation projection)
//!
//! For each dimension, three benches compare:
//!
//! 1. **`float_l2`** — squared-euclidean over `&[f32]` (the baseline; what
//! AETHER actually computes today via the centroid path in
//! `tracker_bridge.rs`).
//! 2. **`float_cosine`** — cosine distance over `&[f32]` (alternative
//! baseline; what some pipeline sites prefer).
//! 3. **`sketch_hamming`** — hamming distance over the 1-bit sketch.
//!
//! Run with:
//! ```bash
//! cargo bench -p wifi-densepose-ruvector --bench sketch_bench
//! ```
//!
//! Pass criterion: `sketch_hamming` is at least **8×** faster than the
//! cheaper of `float_l2` / `float_cosine` at every measured dimension.
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::hint;
use wifi_densepose_ruvector::Sketch;
const SKETCH_VERSION: u16 = 1;
/// Squared-euclidean over `&[f32]` — baseline AETHER path.
#[inline]
fn float_l2_squared(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| {
let d = x - y;
d * d
})
.sum()
}
/// Cosine distance (1.0 - cosine similarity) over `&[f32]`.
/// Alternative baseline — used by some pipeline sites that need
/// magnitude-invariant similarity.
#[inline]
fn float_cosine(a: &[f32], b: &[f32]) -> f32 {
let mut dot = 0.0f32;
let mut na = 0.0f32;
let mut nb = 0.0f32;
for (&x, &y) in a.iter().zip(b.iter()) {
dot += x * y;
na += x * x;
nb += y * y;
}
let denom = (na * nb).sqrt();
if denom < f32::EPSILON {
1.0
} else {
1.0 - dot / denom
}
}
/// Generate a deterministic pseudo-random embedding of the given dimension.
/// Uses a simple LCG so benches are repeatable across runs and machines
/// without pulling in a `rand` dev-dep just for fixture generation.
fn make_embedding(dim: usize, seed: u32) -> Vec<f32> {
let mut state = seed.wrapping_mul(2654435761).wrapping_add(1);
(0..dim)
.map(|_| {
// Iterate LCG (Numerical Recipes constants — for fixture only,
// not for cryptographic use).
state = state.wrapping_mul(1664525).wrapping_add(1013904223);
// Map to [-1.0, 1.0] approximately.
let u = (state >> 8) as f32 / (1u32 << 24) as f32;
u * 2.0 - 1.0
})
.collect()
}
fn bench_compare_cost(c: &mut Criterion) {
for &dim in &[128usize, 256, 512] {
let a_vec = make_embedding(dim, 0xAAAA_AAAA);
let b_vec = make_embedding(dim, 0xBBBB_BBBB);
let a_sketch = Sketch::from_embedding(&a_vec, SKETCH_VERSION);
let b_sketch = Sketch::from_embedding(&b_vec, SKETCH_VERSION);
let mut group = c.benchmark_group(format!("compare_d{dim}"));
group.throughput(Throughput::Elements(1));
group.bench_with_input(BenchmarkId::new("float_l2", dim), &dim, |bencher, _| {
bencher.iter(|| {
let d = float_l2_squared(black_box(&a_vec), black_box(&b_vec));
hint::black_box(d)
});
});
group.bench_with_input(BenchmarkId::new("float_cosine", dim), &dim, |bencher, _| {
bencher.iter(|| {
let d = float_cosine(black_box(&a_vec), black_box(&b_vec));
hint::black_box(d)
});
});
group.bench_with_input(
BenchmarkId::new("sketch_hamming", dim),
&dim,
|bencher, _| {
bencher.iter(|| {
let d = black_box(&a_sketch).distance_unchecked(black_box(&b_sketch));
hint::black_box(d)
});
},
);
group.finish();
}
}
/// Top-K @ K=8 over a 1024-sketch bank — the realistic AETHER use case
/// (a few thousand re-ID candidates, K small).
fn bench_topk(c: &mut Criterion) {
use wifi_densepose_ruvector::SketchBank;
let dim = 128usize;
let bank_size = 1024usize;
let k = 8usize;
let mut bank = SketchBank::new();
for i in 0..bank_size {
let v = make_embedding(dim, i as u32);
bank.insert(i as u32, Sketch::from_embedding(&v, SKETCH_VERSION))
.expect("schema-locked insert");
}
let query_vec = make_embedding(dim, 0xCAFE_BABE);
let query_sketch = Sketch::from_embedding(&query_vec, SKETCH_VERSION);
// Build a parallel float bank for the baseline.
let float_bank: Vec<Vec<f32>> = (0..bank_size)
.map(|i| make_embedding(dim, i as u32))
.collect();
let mut group = c.benchmark_group(format!("topk_d{dim}_n{bank_size}_k{k}"));
group.throughput(Throughput::Elements(bank_size as u64));
group.bench_function("float_l2_topk", |bencher| {
bencher.iter(|| {
let mut scored: Vec<(u32, f32)> = float_bank
.iter()
.enumerate()
.map(|(i, v)| (i as u32, float_l2_squared(black_box(&query_vec), v)))
.collect();
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
scored.truncate(k);
hint::black_box(scored)
});
});
group.bench_function("sketch_hamming_topk", |bencher| {
bencher.iter(|| {
let result = black_box(&bank)
.topk(black_box(&query_sketch), k)
.expect("schema match");
hint::black_box(result)
});
});
group.finish();
}
/// ADR-156 §8 RaBitQ Pass-2 coverage measurement.
///
/// Not a timing bench — it prints the **measured top-K coverage** (Pass-1 vs
/// Pass-2 rotation) on the deterministic anisotropic planted-cluster fixture
/// from `wifi_densepose_ruvector::coverage`, so `cargo bench` surfaces the
/// numbers quoted in ADR-156 §8 / ADR-084. The same harness backs the
/// `pass2_coverage_report` unit test (single source of truth). Each criterion
/// "benchmark" body computes the coverage once (cached) and the bench loop just
/// reads it back, so the criterion timing is meaningless here on purpose — the
/// value is the `println!` summary.
fn bench_pass2_coverage(c: &mut Criterion) {
use wifi_densepose_ruvector::coverage::{
measure_estimator, measure_estimator_euclidean, measure_pass1, measure_pass2,
CoverageParams,
};
let base = CoverageParams::aether_default(0xAD00_0084);
let rot_seed = 0x5EED_C0DE_1234_5678u64;
println!("\n=== ADR-156 §8/§11 RaBitQ coverage (anisotropic planted clusters) ===");
println!(
"dim={} N={} K={} clusters={} noise={} queries={} master_seed=0x{:X} rot_seed=0x{:X}",
base.dim, base.n, base.k, base.n_clusters, base.noise, base.n_queries, base.seed, rot_seed
);
println!("(coverage = |sketch_topK ∩ float_cosine_topK| / K, ADR-084 bar = 90%)");
println!("estimator side info = 8 B/vec (residual_norm + x_dot_o, 2x f32)");
println!(
" {:<12} {:>8} {:>8} {:>11} {:>11}",
"candidate_k", "P1-sign", "P2-sign", "Est-cosine", "Est-euclid"
);
for &cand in &[8usize, 16, 24, 32, 64] {
let p = CoverageParams {
candidate_k: cand,
..base
};
let p1 = measure_pass1(p).coverage;
let p2 = measure_pass2(p, rot_seed).coverage;
let est_cos = measure_estimator(p, rot_seed).coverage;
let est_euc = measure_estimator_euclidean(p, rot_seed).coverage;
let flag = if est_cos >= 0.90 { "EST≥90%" } else { "" };
let strict = if cand == base.k { " STRICT" } else { "" };
println!(
" {:<12} {:>7.2}% {:>7.2}% {:>10.2}% {:>10.2}% {flag}{strict}",
cand,
p1 * 100.0,
p2 * 100.0,
est_cos * 100.0,
est_euc * 100.0
);
}
println!("========================================================================\n");
// A minimal criterion group so `cargo bench` exercises the path under the
// harness (timing is not the point; the printed table above is).
let mut group = c.benchmark_group("pass2_coverage");
group.sample_size(10);
let p = CoverageParams {
n: 256,
n_queries: 16,
n_clusters: 16,
..base
};
group.bench_function("measure_pass2_small", |b| {
b.iter(|| {
let r = measure_pass2(black_box(p), black_box(rot_seed));
hint::black_box(r.coverage)
});
});
group.finish();
}
criterion_group!(benches, bench_compare_cost, bench_topk, bench_pass2_coverage);
criterion_main!(benches);