Skip to main content

holographic_memory/core/qgraph/
mod.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3//! Quantized graph index for approximate nearest-neighbour search on angular data, and (the
4//! [`Graph`] and [`VGraph`] only, see [`Metric`]) on Euclidean data.
5//!
6//! Vectors are normalized and rotated (random sign flips and fast Walsh-Hadamard transforms),
7//! and a Vamana-style graph with alpha pruning and a fixed out-degree is built on exact inner
8//! products, inserting in deterministic batches. Each vertex owns one contiguous block holding
9//! its rotated vector, its neighbours' ids, and for every edge a 1-bit RaBitQ code of the edge
10//! residual (neighbour minus vertex) with per-edge correction factors.
11//!
12//! Search keeps a pool of `ef` candidates ordered by estimated distance. Expanding a vertex
13//! scores it exactly (this is the re-rank) and estimates all of its neighbours in one batch from
14//! the edge codes against a 4-bit query code. The result is the top-k of the exactly scored
15//! vertices. `max_exact` caps the number of expansions (0 = no cap).
16//!
17//! Estimator (RaBitQ, Gao & Long 2024, applied per edge as in SymphonyQG, Gou et al. 2025):
18//! with `w = u - v` and `s = sign(w)`, `<q, u> ~= <q, v> + <v, w> + M (<s, q> - <s, v>)` where
19//! `M = |w|^2 / |w|_1`. `<q, v>` is exact at expansion time; `<s, q>` comes from the bit planes.
20//!
21//! [`VGraph`] is the compact alternative on the same [`Graph`]: one 8-bit code per vertex instead
22//! of one 1-bit code per edge (see `vertex.rs`).
23
24mod adjacency;
25mod build_kernels;
26mod fastscan;
27pub(crate) mod kernels;
28#[cfg(not(target_arch = "wasm32"))]
29mod update;
30mod vertex;
31#[cfg(not(target_arch = "wasm32"))]
32pub use update::{ConsolidationPolicy, UpdatableVGraph, UpdateError};
33
34#[cfg(feature = "experimental")]
35pub mod diagnostics;
36
37#[cfg(feature = "experimental")]
38pub mod ivf;
39
40use std::cell::RefCell;
41
42use rayon::prelude::*;
43
44use crate::core::error::HmsError;
45use adjacency::Adjacency;
46
47use kernels::{
48    as_f32, as_f32_mut, as_u32, as_u32_mut, dot, dot_i8_kernel, DotI8, QueryCode, Rotation,
49    SplitMix,
50};
51pub use vertex::{Residency, SearchKnobs, VGraph, VSearchParams, VSearcher};
52
53/// Default out-degree of every vertex; one batch of edge codes per expansion.
54pub const DEFAULT_DEGREE: usize = 32;
55const NONE: u32 = u32::MAX;
56
57/// Every failure of a build or search names its cause as an invalid parameter.
58fn invalid(details: String) -> HmsError {
59    HmsError::InvalidParam { details }
60}
61const UPPER_DEGREE: usize = 16;
62const LAYER_RATIO: usize = 16;
63const MIN_LAYER: usize = 64;
64/// Smallest dimension at which construction screens exact distances with [`BuildCodes`]: a
65/// skipped distance then saves a row of at least 512 bytes, which pays for the extra code
66/// access (the build measured 1.8x faster at 256 dimensions, and not faster at 100).
67const BUILD_CODES_MIN_DIM: usize = 128;
68/// Largest candidate list of the first construction pass. That pass only has to make the
69/// graph navigable for the second, whose search and prune set the final lists. At degree 64
70/// and `build_ef` 500 the cap cut the first pass's construction estimates 2.2x on SIFT-1M and
71/// 2.7x on glove-100, and searches needed within 1.1% of the estimates per query for recall
72/// 0.90 and 0.95 and 1.4-1.7% more for 0.99 (benchmarks/results/qgraph_build_schedule_wl02_v1.json).
73const FIRST_PASS_EF: usize = 200;
74
75/// Distance a graph is built and searched for.
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
77pub enum Metric {
78    /// Cosine: rows are normalized at build, queries need not be.
79    #[default]
80    Angular,
81    /// Squared Euclidean distance on the rows as given. Construction scores
82    /// `|a|^2 + |b|^2 - 2 <a, b>` with exact per-row squared norms; the vertex index orders
83    /// candidates by `|x|^2 / 2 - <q, x - mean>`, its code estimate of the same order. The edge
84    /// index ([`QGraph`]), the screen and the neighbour blocks are angular only.
85    Euclidean,
86}
87
88impl Metric {
89    fn code(self) -> u64 {
90        match self {
91            Self::Angular => 0,
92            Self::Euclidean => 1,
93        }
94    }
95}
96
97#[derive(Clone, Debug)]
98pub struct BuildParams {
99    /// Distance the graph is built for.
100    pub metric: Metric,
101    /// Candidate list size of the construction-time greedy search (the first of the two
102    /// insertion passes uses at most 200).
103    pub build_ef: usize,
104    /// Out-degree of the bottom layer (even).
105    pub degree: usize,
106    /// Pruning parameter of the second pass (the first pass uses 1.0).
107    pub alpha: f32,
108    /// Length of the 1-bit edge codes; rounded up to a power of two of at least 64 and at
109    /// least the dimension. 0 picks the smallest such length.
110    pub code_bits: usize,
111    pub seed: u64,
112    /// Score the construction search's candidates with the 8-bit codes (one SDOT over a
113    /// `dim`-byte row) instead of exact float distances, at every dimension; the expanded
114    /// set is re-scored exactly before the prune, which stays exact. The cache header does
115    /// not record this yet: build with a fresh cache path when it changes.
116    pub quantized_search: bool,
117}
118
119impl Default for BuildParams {
120    fn default() -> Self {
121        Self {
122            metric: Metric::Angular,
123            build_ef: 128,
124            degree: DEFAULT_DEGREE,
125            alpha: 1.0,
126            code_bits: 0,
127            seed: 0x5EED,
128            quantized_search: false,
129        }
130    }
131}
132
133#[derive(Clone, Copy, Debug)]
134pub struct SearchParams {
135    /// Candidate pool size (beam width).
136    pub ef: usize,
137    /// Maximum exactly scored vertices per query; 0 means no cap.
138    pub max_exact: usize,
139}
140
141pub struct QGraph {
142    n: usize,
143    dim: usize,
144    degree: usize,
145    words: usize,
146    rotation: Rotation,
147    stride: usize,
148    codes_off: usize,
149    factors_off: usize,
150    ids_off: usize,
151    blocks: Vec<u64>,
152    entry: u32,
153    /// Global ids of the vertices in the upper layers, by local id.
154    upper_ids: Vec<u32>,
155    /// Upper-layer adjacency in local ids, largest layer first.
156    layers: Vec<Vec<Vec<u32>>>,
157}
158
159#[derive(Clone, Copy)]
160struct Candidate {
161    dist: f32,
162    id: u32,
163    done: bool,
164}
165
166struct Visited {
167    marks: Vec<u32>,
168    epoch: u32,
169}
170
171impl Visited {
172    fn new(n: usize) -> Self {
173        Self {
174            marks: vec![0; n],
175            epoch: 0,
176        }
177    }
178
179    fn next(&mut self) {
180        self.epoch = self.epoch.wrapping_add(1);
181        if self.epoch == 0 {
182            self.marks.fill(0);
183            self.epoch = 1;
184        }
185    }
186
187    /// True if `id` was not yet visited in this epoch.
188    fn insert(&mut self, id: u32) -> bool {
189        let m = &mut self.marks[id as usize];
190        let fresh = *m != self.epoch;
191        *m = self.epoch;
192        fresh
193    }
194}
195
196/// Visited set of the construction search: one bit per vertex, so it stays in the L1/L2 cache
197/// where the epoch marks of [`Visited`] would not; clearing it is cheap next to a search.
198#[derive(Default)]
199struct BuildVisited {
200    bits: Vec<u64>,
201}
202
203impl BuildVisited {
204    fn reset(&mut self, n: usize) {
205        self.bits.clear();
206        self.bits.resize(n.div_ceil(64), 0);
207    }
208
209    /// True if `id` was not yet visited since the last reset.
210    fn insert(&mut self, id: u32) -> bool {
211        let (w, b) = (id as usize / 64, 1u64 << (id % 64));
212        let fresh = self.bits[w] & b == 0;
213        self.bits[w] |= b;
214        fresh
215    }
216}
217
218/// Opt-in construction profile (`HMS_QGRAPH_BUILD_PROFILE` set): per-phase wall and CPU
219/// seconds, construction-search filter counts and a hash of the built graph, on stderr.
220mod prof {
221    use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
222    use std::sync::OnceLock;
223    use std::time::Instant;
224
225    pub(super) static GREEDY: AtomicU64 = AtomicU64::new(0);
226    pub(super) static PRUNE: AtomicU64 = AtomicU64::new(0);
227    pub(super) static REV: AtomicU64 = AtomicU64::new(0);
228    pub(super) static FRESH: AtomicU64 = AtomicU64::new(0);
229    pub(super) static EXACT: AtomicU64 = AtomicU64::new(0);
230
231    pub(super) fn on() -> bool {
232        static ON: OnceLock<bool> = OnceLock::new();
233        *ON.get_or_init(|| std::env::var_os("HMS_QGRAPH_BUILD_PROFILE").is_some())
234    }
235
236    pub(super) fn start() -> Option<Instant> {
237        on().then(Instant::now)
238    }
239
240    pub(super) fn add(c: &AtomicU64, t: Option<Instant>) {
241        if let Some(t) = t {
242            c.fetch_add(t.elapsed().as_nanos() as u64, Relaxed);
243        }
244    }
245
246    pub(super) fn count(c: &AtomicU64, n: u64) {
247        if on() {
248            c.fetch_add(n, Relaxed);
249        }
250    }
251
252    pub(super) fn report(phase: &str, t: Option<Instant>) {
253        let Some(t) = t else { return };
254        let secs = |c: &AtomicU64| c.swap(0, Relaxed) as f64 * 1e-9;
255        eprintln!(
256            "PHASE {phase} wall={:.2}s greedy_cpu={:.1}s prune_cpu={:.1}s rev_wall={:.2}s fresh={} exact={}",
257            t.elapsed().as_secs_f64(),
258            secs(&GREEDY),
259            secs(&PRUNE),
260            secs(&REV),
261            FRESH.swap(0, Relaxed),
262            EXACT.swap(0, Relaxed),
263        );
264    }
265
266    pub(super) fn graph_hash(h: impl std::hash::Hash) {
267        if on() {
268            use std::hash::Hasher;
269            let mut s = std::collections::hash_map::DefaultHasher::new();
270            h.hash(&mut s);
271            eprintln!("GRAPH_HASH {:016x}", s.finish());
272        }
273    }
274}
275thread_local! {
276    static BUILD_VISITED: RefCell<BuildVisited> = RefCell::new(BuildVisited::default());
277}
278
279struct Rows<'a> {
280    data: &'a [f32],
281    dim: usize,
282    /// Squared norm of every row (Euclidean); `None` for unit rows (angular).
283    norms: Option<&'a [f32]>,
284    /// Bounds that let construction skip exact distances whose outcome they already decide,
285    /// or, with `quantized`, the estimates that order the construction search's pool.
286    codes: Option<&'a BuildCodes>,
287    quantized: bool,
288}
289
290/// One 8-bit code per row (`x = scale q + e` with `|e| <= err`), giving bounds on the
291/// computed `d2` of two rows. The construction search, the prune and the fill use them only to
292/// settle comparisons the bounds decide; the rest are scored exactly, so the graph is the one
293/// exact scoring alone would build.
294struct BuildCodes {
295    width: usize,
296    q: Vec<i8>,
297    /// Per-row factors, side by side so that one cache line load serves a row.
298    meta: Vec<CodeMeta>,
299    dot: DotI8,
300    /// Euclidean rows: `meta` carries the squared norm the exact distance uses and the norm.
301    euclidean: bool,
302    /// Euclidean rows: relative f32 rounding slack of the computed distance.
303    rel_slack: f32,
304}
305
306/// The factors of one [`BuildCodes`] row; the norms are 0 for unit rows.
307#[derive(Clone, Copy, Default)]
308#[repr(C, align(16))]
309struct CodeMeta {
310    scale: f32,
311    err: f32,
312    n2: f32,
313    n: f32,
314}
315
316impl BuildCodes {
317    fn new(data: &[f32], dim: usize, norms: Option<&[f32]>) -> Self {
318        let n = data.len() / dim;
319        let width = dim.next_multiple_of(16);
320        let mut q = vec![0i8; n * width];
321        let mut meta = vec![CodeMeta::default(); n];
322        q.par_chunks_mut(width)
323            .zip(meta.par_iter_mut())
324            .zip(data.par_chunks(dim))
325            .for_each(|((q, m), x)| {
326                let (s, e) = (&mut m.scale, &mut m.err);
327                let max = x.iter().fold(0f32, |m, v| m.max(v.abs()));
328                *s = if max > 0.0 { max / 127.0 } else { 0.0 };
329                let mut e2 = 0f64;
330                for (o, &v) in q.iter_mut().zip(x) {
331                    let c = if *s > 0.0 {
332                        (v / *s).round().clamp(-127.0, 127.0)
333                    } else {
334                        0.0
335                    };
336                    *o = c as i8;
337                    let r = f64::from(v) - f64::from(*s) * f64::from(c);
338                    e2 += r * r;
339                }
340                // Rounded up so that `err` bounds the true residual norm.
341                *e = (e2.sqrt() * (1.0 + 1e-6)) as f32 + 1e-7;
342            });
343        if let Some(norms) = norms {
344            for (m, &n2) in meta.iter_mut().zip(norms) {
345                (m.n2, m.n) = (n2, n2.sqrt());
346            }
347        }
348        Self {
349            width,
350            q,
351            meta,
352            dot: dot_i8_kernel(),
353            euclidean: norms.is_some(),
354            // The computed dot product and squared norms are each within `dim * 2^-24` of the
355            // magnitudes summed; twice that, and at least the unit-row slack, covers them.
356            rel_slack: 1e-4f32.max((dim as f32 + 4.0) * 2.0 * f32::EPSILON),
357        }
358    }
359
360    fn code(&self, i: u32) -> &[i8] {
361        let i = i as usize;
362        &self.q[i * self.width..(i + 1) * self.width]
363    }
364
365    /// Hint the cache to load the code and the factors of row `i`.
366    #[inline]
367    fn prefetch(&self, i: u32) {
368        build_kernels::prefetch_code(self.code(i));
369        build_kernels::prefetch_meta(&self.meta[i as usize]);
370    }
371
372    /// A lower bound on the computed `d2` of the unit rows `p` and `u`. With
373    /// `x = s_x q_x + e_x`: `<p, u> <= s_p s_u <q_p, q_u> + |e_p| + |s_p q_p| |e_u|`, and
374    /// `|s_p q_p| <= 1 + |e_p|`. `SLACK` covers the f32 rounding of this bound and of the exact
375    /// dot product (at most `dim * 2^-24` for unit vectors).
376    fn d2_lower(&self, p: u32, u: u32) -> f32 {
377        self.d2_bounds(p, u).0
378    }
379
380    /// `(lower, upper)` bounds on the computed `d2` of the unit rows `p` and `u`, from the
381    /// two-sided form of the inequality in [`Self::d2_lower`].
382    /// The 8-bit estimate of the computed `d2` of the unit rows `p` and `u`.
383    #[inline]
384    fn d2_est(&self, p: u32, u: u32) -> f32 {
385        let raw = (self.dot)(self.code(p), self.code(u)) as f32;
386        let (mp, mu) = (&self.meta[p as usize], &self.meta[u as usize]);
387        if self.euclidean {
388            (mp.n2 + mu.n2 - 2.0 * mp.scale * mu.scale * raw).max(0.0)
389        } else {
390            (2.0 - 2.0 * mp.scale * mu.scale * raw).max(0.0)
391        }
392    }
393
394    /// For rows of any norm the same inequality reads
395    /// `|<p, u> - s_p s_u <q_p, q_u>| <= |e_p| |u| + (|p| + |e_p|) |e_u|`, and the computed
396    /// distance `|p|^2 + |u|^2 - 2 <p, u>` gets a slack relative to the magnitudes it sums.
397    fn d2_bounds(&self, p: u32, u: u32) -> (f32, f32) {
398        const SLACK: f32 = 1e-4;
399        let (mp, mu) = (&self.meta[p as usize], &self.meta[u as usize]);
400        let raw = (self.dot)(self.code(p), self.code(u)) as f32;
401        let ep = mp.err;
402        let est = mp.scale * mu.scale * raw;
403        if !self.euclidean {
404            let margin = ep + (1.0 + ep) * mu.err + SLACK;
405            return (2.0 - 2.0 * (est + margin), 2.0 - 2.0 * (est - margin));
406        }
407        let ((np2, np), (nu2, nu)) = ((mp.n2, mp.n), (mu.n2, mu.n));
408        let margin = ep * nu + (np + ep) * mu.err;
409        let slack = self.rel_slack * (np2 + nu2 + 2.0 * np * nu);
410        let base = np2 + nu2;
411        (
412            base - 2.0 * (est + margin) - slack,
413            base - 2.0 * (est - margin) + slack,
414        )
415    }
416}
417
418impl Rows<'_> {
419    fn row(&self, i: u32) -> &[f32] {
420        let i = i as usize;
421        &self.data[i * self.dim..(i + 1) * self.dim]
422    }
423
424    /// Hint the cache to load row `u` and, for Euclidean rows, its squared norm.
425    #[inline]
426    fn prefetch(&self, u: u32) {
427        build_kernels::prefetch_row(self.row(u));
428        if let Some(n) = self.norms {
429            build_kernels::prefetch_meta(&n[u as usize]);
430        }
431    }
432
433    /// Squared distance of rows `a` and `b` from their computed dot product: `2 - 2 dot` for
434    /// unit rows, `|a|^2 + |b|^2 - 2 dot` with the stored squared norms otherwise.
435    #[inline]
436    fn dist(&self, a: u32, b: u32, dot: f32) -> f32 {
437        match self.norms {
438            None => (2.0 - 2.0 * dot).max(0.0),
439            Some(n) => (n[a as usize] + n[b as usize] - 2.0 * dot).max(0.0),
440        }
441    }
442
443    /// Squared distance of rows `a` and `b`.
444    fn d2(&self, a: u32, b: u32) -> f32 {
445        self.dist(a, b, dot(self.row(a), self.row(b)))
446    }
447
448    /// `d2(p, u)` for every `u` in `ids`, written to `out`.
449    fn d2_many(&self, p: u32, ids: &[u32], out: &mut Vec<f32>) {
450        out.clear();
451        let target = self.row(p);
452        let (quads, rest) = ids.as_chunks::<4>();
453        for q in quads {
454            let d = build_kernels::dot4(target, q.map(|u| self.row(u)));
455            out.extend(d.iter().zip(q).map(|(&x, &u)| self.dist(p, u, x)));
456        }
457        out.extend(rest.iter().map(|&u| self.d2(p, u)));
458    }
459
460    /// `d2_many` over a long list of rows not yet in cache, prefetching a few rows ahead.
461    fn d2_stream(&self, p: u32, ids: &[u32], out: &mut Vec<f32>) {
462        const AHEAD: usize = 16;
463        out.clear();
464        let target = self.row(p);
465        for &u in ids.iter().take(AHEAD) {
466            self.prefetch(u);
467        }
468        let (quads, rest) = ids.as_chunks::<4>();
469        for (i, q) in quads.iter().enumerate() {
470            for &u in ids.iter().skip(AHEAD + 4 * i).take(4) {
471                self.prefetch(u);
472            }
473            let d = build_kernels::dot4(target, q.map(|u| self.row(u)));
474            out.extend(d.iter().zip(q).map(|(&x, &u)| self.dist(p, u, x)));
475        }
476        out.extend(rest.iter().map(|&u| self.d2(p, u)));
477    }
478
479    /// True if some `s` in `kept` has `a2 d2(s, c) <= dc`.
480    fn occluded(&self, c: u32, kept: &[u32], dc: f32, a2: f32) -> bool {
481        let rc = self.row(c);
482        if let Some(codes) = self.codes {
483            // The codes settle most pairs; the rest are scored exactly, four at a time, so the
484            // answer is the one exact scoring alone gives (it does not depend on the order).
485            let mut open = [0u32; 4];
486            let mut n_open = 0;
487            for &s in kept {
488                let (lo, hi) = codes.d2_bounds(c, s);
489                if a2 * hi <= dc {
490                    return true;
491                }
492                if a2 * lo > dc {
493                    continue;
494                }
495                open[n_open] = s;
496                n_open += 1;
497                if n_open == 4 {
498                    n_open = 0;
499                    let d = build_kernels::dot4(rc, open.map(|u| self.row(u)));
500                    if d.iter()
501                        .zip(&open)
502                        .any(|(&x, &s)| a2 * self.dist(c, s, x) <= dc)
503                    {
504                        return true;
505                    }
506                }
507            }
508            return open[..n_open]
509                .iter()
510                .any(|&s| a2 * self.dist(s, c, dot(self.row(s), rc)) <= dc);
511        }
512        let (quads, rest) = kept.as_chunks::<4>();
513        for q in quads {
514            let d = build_kernels::dot4(rc, q.map(|u| self.row(u)));
515            if d.iter()
516                .zip(q)
517                .any(|(&x, &s)| a2 * self.dist(c, s, x) <= dc)
518            {
519                return true;
520            }
521        }
522        rest.iter()
523            .any(|&s| a2 * self.dist(s, c, dot(self.row(s), rc)) <= dc)
524    }
525
526    /// Greedy search from `start`; returns every expanded vertex with its distance to row `p`.
527    fn greedy(
528        &self,
529        graph: &Adjacency,
530        start: u32,
531        p: u32,
532        l: usize,
533        visited: &mut BuildVisited,
534    ) -> Vec<(f32, u32)> {
535        let (mut n_fresh, mut n_exact) = (0u64, 0u64);
536        visited.reset(graph.len());
537        visited.insert(start);
538        let mut pool = vec![Candidate {
539            dist: self.d2(p, start),
540            id: start,
541            done: false,
542        }];
543        let mut expanded = Vec::new();
544        let mut fresh = Vec::new();
545        let mut dists = Vec::new();
546        let mut cur = 0;
547        while cur < pool.len() {
548            pool[cur].done = true;
549            let v = pool[cur].id;
550            expanded.push((pool[cur].dist, v));
551            // Mark and prefetch every new neighbour first so their rows load concurrently.
552            fresh.clear();
553            // With a full pool, a neighbour at or beyond the pool bound is discarded below; the
554            // codes rule most of them out without loading their rows.
555            match self.codes.filter(|_| self.quantized || pool.len() >= l) {
556                Some(codes) if self.quantized => {
557                    for &u in graph.get(v as usize) {
558                        if visited.insert(u) {
559                            codes.prefetch(u);
560                            fresh.push(u);
561                        }
562                    }
563                    n_fresh += fresh.len() as u64;
564                }
565                Some(codes) => {
566                    for &u in graph.get(v as usize) {
567                        if visited.insert(u) {
568                            codes.prefetch(u);
569                            fresh.push(u);
570                        }
571                    }
572                    let bound = pool[l - 1].dist;
573                    n_fresh += fresh.len() as u64;
574                    fresh.retain(|&u| codes.d2_lower(p, u) < bound);
575                    for &u in &fresh {
576                        build_kernels::prefetch_row(self.row(u));
577                    }
578                }
579                None => {
580                    for &u in graph.get(v as usize) {
581                        if visited.insert(u) {
582                            build_kernels::prefetch_row(self.row(u));
583                            fresh.push(u);
584                        }
585                    }
586                    n_fresh += fresh.len() as u64;
587                }
588            }
589            match self.codes.filter(|_| self.quantized) {
590                Some(codes) => {
591                    dists.clear();
592                    dists.extend(fresh.iter().map(|&u| codes.d2_est(p, u)));
593                }
594                None => {
595                    n_exact += fresh.len() as u64;
596                    self.d2_many(p, &fresh, &mut dists);
597                }
598            }
599            let mut best = usize::MAX;
600            for (&u, &du) in fresh.iter().zip(&dists) {
601                if pool.len() >= l && du >= pool[l - 1].dist {
602                    continue;
603                }
604                let pos = pool.partition_point(|c| c.dist <= du);
605                pool.insert(
606                    pos,
607                    Candidate {
608                        dist: du,
609                        id: u,
610                        done: false,
611                    },
612                );
613                pool.truncate(l);
614                best = best.min(pos);
615                // The list header of a pooled vertex is read when it is expanded.
616                build_kernels::prefetch_meta(graph.len_word(u as usize));
617            }
618            cur = best.min(cur + 1);
619            while cur < pool.len() && pool[cur].done {
620                cur += 1;
621            }
622            if let Some(c) = pool.get(cur) {
623                build_kernels::prefetch_ids(graph.get(c.id as usize));
624            }
625        }
626        if self.quantized {
627            // Exact distances of the expanded set, which the prune scores exactly.
628            let ids: Vec<u32> = expanded.iter().map(|e| e.1).collect();
629            let mut d = Vec::with_capacity(ids.len());
630            self.d2_stream(p, &ids, &mut d);
631            n_exact += ids.len() as u64;
632            for (e, d) in expanded.iter_mut().zip(d) {
633                e.0 = d;
634            }
635        }
636        prof::count(&prof::FRESH, n_fresh);
637        prof::count(&prof::EXACT, n_exact);
638        expanded
639    }
640
641    /// Vamana robust prune: keep `c` unless an already kept `s` has `alpha^2 d2(s, c) <= d2(p, c)`.
642    fn prune(&self, p: u32, mut cands: Vec<(f32, u32)>, alpha: f32, r: usize) -> Vec<u32> {
643        cands.retain(|c| c.1 != p);
644        cands.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
645        cands.dedup_by_key(|c| c.1);
646        let a2 = alpha * alpha;
647        let mut out: Vec<u32> = Vec::with_capacity(r);
648        for &(dc, c) in &cands {
649            if out.len() == r {
650                break;
651            }
652            if !self.occluded(c, &out, dc, a2) {
653                out.push(c);
654            }
655        }
656        out
657    }
658
659    /// `prune` of `list` (distinct ids, not `t`) when its first `closed` entries are an
660    /// unchanged `prune` output for `t` under the same `alpha`. Such an entry was not occluded
661    /// by the closed entries nearer than it, so it is checked only against kept entries from
662    /// the rest of the list; the result equals `prune` of the whole list.
663    fn prune_merged(&self, t: u32, list: &[u32], closed: usize, alpha: f32, r: usize) -> Vec<u32> {
664        let mut d = Vec::with_capacity(list.len());
665        self.d2_stream(t, list, &mut d);
666        let mut cands: Vec<(f32, u32, bool)> = d
667            .into_iter()
668            .zip(list)
669            .enumerate()
670            .map(|(i, (d, &u))| (d, u, i < closed))
671            .collect();
672        cands.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
673        let a2 = alpha * alpha;
674        let mut out: Vec<u32> = Vec::with_capacity(r);
675        let mut open: Vec<u32> = Vec::new();
676        for &(dc, c, was_kept) in &cands {
677            if out.len() == r {
678                break;
679            }
680            if !self.occluded(c, if was_kept { &open } else { &out }, dc, a2) {
681                out.push(c);
682                if !was_kept {
683                    open.push(c);
684                }
685            }
686        }
687        out
688    }
689
690    /// One insertion pass in batches of doubling size; each batch searches a frozen graph, so
691    /// the result does not depend on thread scheduling.
692    #[allow(clippy::too_many_arguments)]
693    fn insert_pass(
694        &self,
695        graph: &mut Adjacency,
696        order: &[u32],
697        entry: u32,
698        l: usize,
699        alpha: f32,
700        r: usize,
701    ) {
702        let n = graph.len();
703        // Length of the prefix of each list that is an unchanged output of `prune` in this pass.
704        let mut closed = vec![0u32; n];
705        let max_batch = (n / 50).max(64);
706        let mut outs = Adjacency::new(max_batch.min(order.len()), r);
707        let (mut start, mut size) = (0, 1);
708        while start < order.len() {
709            let end = (start + size).min(order.len());
710            let batch = &order[start..end];
711            let g: &Adjacency = graph;
712            outs.par_set(batch.len(), |i| {
713                let p = batch[i];
714                let t0 = prof::start();
715                let mut cands = BUILD_VISITED.with(|cell| {
716                    let mut vis = cell.borrow_mut();
717                    self.greedy(g, entry, p, l, &mut vis)
718                });
719                prof::add(&prof::GREEDY, t0);
720                let t0 = prof::start();
721                let adj = g.get(p as usize);
722                let mut d = Vec::with_capacity(adj.len());
723                self.d2_stream(p, adj, &mut d);
724                cands.extend(d.into_iter().zip(adj.iter().copied()));
725                let out = self.prune(p, cands, alpha, r);
726                prof::add(&prof::PRUNE, t0);
727                out
728            });
729            let trev = prof::start();
730            let mut rev: Vec<(u32, u32)> = batch
731                .iter()
732                .enumerate()
733                .flat_map(|(i, &p)| outs.get(i).iter().map(move |&u| (u, p)))
734                .collect();
735            for (i, &p) in batch.iter().enumerate() {
736                let out = outs.get(i);
737                closed[p as usize] = out.len() as u32;
738                graph.set(p as usize, out);
739            }
740            rev.par_sort_unstable();
741            // Each group updates only its own target's list, so the groups run in place.
742            let groups: Vec<_> = rev.chunk_by(|a, b| a.0 == b.0).collect();
743            let targets = || groups.iter().map(|grp| grp[0].0 as usize);
744            let closed_of = pick_mut(&mut closed, targets());
745            let jobs: Vec<_> = graph
746                .rows_mut(targets())
747                .zip(closed_of)
748                .zip(&groups)
749                .collect();
750            jobs.into_par_iter()
751                .for_each(|(((row, len), closed), grp)| {
752                    let t = grp[0].0;
753                    let mut merged = row[..*len as usize].to_vec();
754                    for &(_, s) in grp.iter() {
755                        if !merged.contains(&s) {
756                            merged.push(s);
757                        }
758                    }
759                    if merged.len() > r {
760                        merged = self.prune_merged(t, &merged, *closed as usize, alpha, r);
761                        *closed = merged.len() as u32;
762                    }
763                    row[..merged.len()].copy_from_slice(&merged);
764                    *len = merged.len() as u32;
765                });
766            prof::add(&prof::REV, trev);
767            start = end;
768            size = (size * 2).min(max_batch);
769        }
770    }
771
772    /// Fill every adjacency list up to `r` with the nearest two-hop neighbours; the
773    /// search estimates a full batch per expansion anyway, so the extra edges are free.
774    fn fill(&self, graph: &mut Adjacency, r: usize) {
775        graph.par_append(|g, v| {
776            let have = g.get(v);
777            if have.len() >= r {
778                return Vec::new();
779            }
780            let mut ids: Vec<u32> = have
781                .iter()
782                .flat_map(|&u| g.get(u as usize).iter().copied())
783                .collect();
784            ids.sort_unstable();
785            ids.dedup();
786            // `ids` is sorted, so one merge with the sorted list drops the ids in it.
787            let mut skip = have.to_vec();
788            skip.push(v as u32);
789            skip.sort_unstable();
790            let mut at = 0;
791            ids.retain(|&u| {
792                while at < skip.len() && skip[at] < u {
793                    at += 1;
794                }
795                skip.get(at) != Some(&u)
796            });
797            let k = r - have.len();
798            if let Some(codes) = self.codes.filter(|_| ids.len() > k) {
799                // At least `k` ids score at most the k-th smallest upper bound, so an id
800                // whose lower bound exceeds it cannot be among the `k` nearest.
801                const AHEAD: usize = 8;
802                ids.iter().take(AHEAD).for_each(|&u| codes.prefetch(u));
803                let bounds: Vec<(f32, f32)> = ids
804                    .iter()
805                    .enumerate()
806                    .map(|(i, &u)| {
807                        if let Some(&w) = ids.get(i + AHEAD) {
808                            codes.prefetch(w);
809                        }
810                        codes.d2_bounds(v as u32, u)
811                    })
812                    .collect();
813                let mut upper: Vec<f32> = bounds.iter().map(|b| b.1).collect();
814                let cut = *upper.select_nth_unstable_by(k - 1, f32::total_cmp).1;
815                let mut b = bounds.iter();
816                ids.retain(|_| b.next().is_some_and(|b| b.0 <= cut));
817            }
818            let mut d = Vec::new();
819            self.d2_stream(v as u32, &ids, &mut d);
820            let mut c: Vec<(f32, u32)> = d.into_iter().zip(ids).collect();
821            c.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
822            c.into_iter().take(r - have.len()).map(|x| x.1).collect()
823        });
824    }
825}
826
827/// The entries of `values` at `ids` (strictly increasing), as disjoint mutable references.
828fn pick_mut<T>(values: &mut [T], ids: impl Iterator<Item = usize>) -> impl Iterator<Item = &mut T> {
829    let mut rest = values;
830    let mut next = 0;
831    ids.map(move |v| {
832        assert!(v >= next, "ids must be strictly increasing");
833        let (x, tail) = std::mem::take(&mut rest)[v - next..]
834            .split_first_mut()
835            .expect("id in range");
836        rest = tail;
837        next = v + 1;
838        x
839    })
840}
841
842/// Per-edge code and factors for the edge v -> u (rotated unit vectors).
843/// Returns `(K, M, popcount)` with `K = <v, w> - M <s, v>`.
844fn edge_factors(pv: &[f32], pu: &[f32], code: &mut [u64]) -> (f32, f32, f32) {
845    let (mut r2, mut l1, mut sv, mut vw) = (0f32, 0f32, 0f32, 0f32);
846    // The padded length is a multiple of 64, so every code word is written.
847    for ((a64, b64), word) in pv
848        .as_chunks::<64>()
849        .0
850        .iter()
851        .zip(pu.as_chunks::<64>().0)
852        .zip(code.iter_mut())
853    {
854        let mut bits = 0u64;
855        for (i, (&a, &b)) in a64.iter().zip(b64).enumerate() {
856            let w = b - a;
857            r2 += w * w;
858            l1 += w.abs();
859            vw += a * w;
860            let up = w > 0.0;
861            sv += if up { a } else { -a };
862            bits |= u64::from(up) << i;
863        }
864        *word = bits;
865    }
866    let m = if l1 > 0.0 { r2 / l1 } else { 0.0 };
867    let pop = code.iter().map(|c| c.count_ones()).sum::<u32>() as f32;
868    (vw - m * sv, m, pop)
869}
870
871impl QGraph {
872    /// Build over `data` (row-major, `dim` floats per vector). Rows are normalized; zero rows
873    /// stay zero. Uses the rayon pool; the result depends only on the data and `params`.
874    pub fn build(data: &[f32], dim: usize, params: &BuildParams) -> Result<Self, HmsError> {
875        Self::from_graph(&Graph::build(data, dim, params)?, params)
876    }
877
878    /// Encodes a built graph; `params` must be the ones the graph was built with.
879    pub fn from_graph(g: &Graph, params: &BuildParams) -> Result<Self, HmsError> {
880        let (n, dim, r) = (g.n, g.dim, g.degree);
881        if r != params.degree {
882            return Err(invalid(format!(
883                "graph built with degree {r}, parameters say {}",
884                params.degree
885            )));
886        }
887        if g.metric != Metric::Angular {
888            return Err(invalid(
889                "the edge index is angular only; use VGraph for Euclidean data".into(),
890            ));
891        }
892        let rotation = Rotation::new(dim, params.code_bits, params.seed);
893        let words = rotation.padded() / 64;
894        let rows = g.rows();
895        let graph = &g.adj;
896        let codes_off = dim.div_ceil(2);
897        let factors_off = codes_off + r * words;
898        let ids_off = factors_off + 3 * r / 2;
899        let stride = ids_off + r / 2;
900        // Every row is rotated once here instead of once per incident edge.
901        let padded = rotation.padded();
902        let mut rotated = vec![0f32; n * padded];
903        rotated
904            .par_chunks_mut(padded)
905            .enumerate()
906            .for_each(|(v, out)| rotation.apply(rows.row(v as u32), out));
907        let rot_row = |v: u32| &rotated[v as usize * padded..(v as usize + 1) * padded];
908        let mut blocks = vec![0u64; n * stride];
909        blocks
910            .par_chunks_mut(stride)
911            .enumerate()
912            .for_each(|(v, b)| {
913                let adj = graph.get(v);
914                let (vec_w, rest) = b.split_at_mut(codes_off);
915                let (codes, rest) = rest.split_at_mut(r * words);
916                let (fac, ids) = rest.split_at_mut(3 * r / 2);
917                as_f32_mut(vec_w)[..dim].copy_from_slice(rows.row(v as u32));
918                let fac = as_f32_mut(fac);
919                let ids = as_u32_mut(ids);
920                ids.fill(NONE);
921                let pv = rot_row(v as u32);
922                for &u in adj.iter().take(4) {
923                    build_kernels::prefetch_row(rot_row(u));
924                }
925                for (j, &u) in adj.iter().enumerate() {
926                    if let Some(&next) = adj.get(j + 4) {
927                        build_kernels::prefetch_row(rot_row(next));
928                    }
929                    ids[j] = u;
930                    let (k, m, pop) =
931                        edge_factors(pv, rot_row(u), &mut codes[j * words..(j + 1) * words]);
932                    fac[j] = k;
933                    fac[r + j] = m;
934                    fac[2 * r + j] = pop;
935                }
936            });
937        Ok(Self {
938            n,
939            dim,
940            degree: r,
941            words,
942            rotation,
943            stride,
944            codes_off,
945            factors_off,
946            ids_off,
947            blocks,
948            entry: g.entry,
949            upper_ids: g.upper_ids.clone(),
950            layers: g.layers.clone(),
951        })
952    }
953}
954
955/// A built proximity graph, before any encoding: over normalized vectors (angular) or the
956/// vectors as given (Euclidean). Holds a full-precision copy of the data, so it is a
957/// build-time object; the indexes keep only their encodings.
958pub struct Graph {
959    n: usize,
960    dim: usize,
961    degree: usize,
962    metric: Metric,
963    /// Unit rows (angular) or the rows as given (Euclidean).
964    vectors: Vec<f32>,
965    /// Squared norm of every row (Euclidean); empty for angular.
966    norms: Vec<f32>,
967    adj: Adjacency,
968    entry: u32,
969    upper_ids: Vec<u32>,
970    layers: Vec<Vec<Vec<u32>>>,
971    /// The parameters the graph was built with; the on-disk cache records and checks them.
972    build: BuildParams,
973}
974
975/// Prepares rows for `metric` in place: normalizes them for angular (zero rows stay zero) and
976/// returns the squared norms for Euclidean (empty for angular).
977fn prepare_rows(data: &mut [f32], dim: usize, metric: Metric) -> Vec<f32> {
978    match metric {
979        Metric::Angular => {
980            data.par_chunks_mut(dim).for_each(|x| {
981                let norm = dot(x, x).sqrt();
982                if norm > 0.0 {
983                    x.iter_mut().for_each(|o| *o /= norm);
984                }
985            });
986            Vec::new()
987        }
988        Metric::Euclidean => data.par_chunks(dim).map(|x| dot(x, x)).collect(),
989    }
990}
991
992/// The squared norms as [`Rows`] takes them: `None` when empty (angular).
993fn norms_opt(norms: &[f32]) -> Option<&[f32]> {
994    (!norms.is_empty()).then_some(norms)
995}
996
997impl Graph {
998    fn rows(&self) -> Rows<'_> {
999        Rows {
1000            data: &self.vectors,
1001            dim: self.dim,
1002            norms: norms_opt(&self.norms),
1003            codes: None,
1004            quantized: false,
1005        }
1006    }
1007
1008    /// The metric the graph was built for.
1009    pub fn metric(&self) -> Metric {
1010        self.metric
1011    }
1012
1013    /// The out-neighbours of vertex `v` in the base layer.
1014    pub(crate) fn neighbors(&self, v: usize) -> &[u32] {
1015        self.adj.get(v)
1016    }
1017
1018    /// Stable 64-bit hash of the structure (adjacency, upper layers, entry): equal values mean
1019    /// a bit-identical graph, across builds, binaries and cache round trips.
1020    pub fn structure_hash(&self) -> u64 {
1021        let mut h = Fnv::new();
1022        h.word(self.entry);
1023        for list in self.adj.iter() {
1024            h.word(list.len() as u32);
1025            list.iter().for_each(|&u| h.word(u));
1026        }
1027        for layer in &self.layers {
1028            h.word(layer.len() as u32);
1029            for list in layer {
1030                h.word(list.len() as u32);
1031                list.iter().for_each(|&u| h.word(u));
1032            }
1033        }
1034        h.finish()
1035    }
1036
1037    /// Build over `data` (row-major, `dim` floats per vector). Angular rows are normalized;
1038    /// zero rows stay zero. Uses the rayon pool; the result depends only on the data and
1039    /// `params` (`code_bits` is not used).
1040    pub fn build(data: &[f32], dim: usize, params: &BuildParams) -> Result<Self, HmsError> {
1041        Self::build_owned(data.to_vec(), dim, params)
1042    }
1043
1044    /// [`Graph::build`] over an owned matrix, prepared in place: the caller's copy is the
1045    /// one the graph keeps, so the vectors are resident once during construction instead of
1046    /// twice (the build's own copy next to the caller's).
1047    pub fn build_owned(
1048        mut data: Vec<f32>,
1049        dim: usize,
1050        params: &BuildParams,
1051    ) -> Result<Self, HmsError> {
1052        if dim == 0 || !data.len().is_multiple_of(dim) {
1053            return Err(invalid(format!(
1054                "ragged data: {} values are not a multiple of dimension {dim}",
1055                data.len()
1056            )));
1057        }
1058        let n = data.len() / dim;
1059        if n == 0 {
1060            return Err(invalid("empty index: no vectors to build from".into()));
1061        }
1062        if n >= NONE as usize {
1063            return Err(invalid(format!(
1064                "index too large: {n} vectors, the limit is {}",
1065                NONE - 1
1066            )));
1067        }
1068        if let Some(at) = data.iter().position(|v| !v.is_finite()) {
1069            return Err(invalid(format!(
1070                "non-finite value in row {} (coordinate {})",
1071                at / dim,
1072                at % dim
1073            )));
1074        }
1075        if params.build_ef == 0 {
1076            return Err(invalid("build_ef must be positive".into()));
1077        }
1078        let r = params.degree;
1079        if r == 0 || !r.is_multiple_of(2) {
1080            return Err(invalid(format!(
1081                "degree must be positive and even, not {r}"
1082            )));
1083        }
1084        let metric = params.metric;
1085        let norms = prepare_rows(&mut data, dim, metric);
1086        if let Some(at) = norms.iter().position(|v| !v.is_finite()) {
1087            return Err(invalid(format!("row {at} overflows its squared norm")));
1088        }
1089        let vectors = data;
1090        let codes = (params.quantized_search || dim >= BUILD_CODES_MIN_DIM)
1091            .then(|| BuildCodes::new(&vectors, dim, norms_opt(&norms)));
1092        let rows = Rows {
1093            data: &vectors,
1094            dim,
1095            norms: norms_opt(&norms),
1096            codes: codes.as_ref(),
1097            quantized: params.quantized_search,
1098        };
1099
1100        let mut centroid = vec![0f32; dim];
1101        for x in vectors.chunks_exact(dim) {
1102            centroid.iter_mut().zip(x).for_each(|(c, v)| *c += v);
1103        }
1104        let entry = match metric {
1105            Metric::Angular => {
1106                (0..n as u32)
1107                    .into_par_iter()
1108                    .map(|i| (dot(rows.row(i), &centroid), i))
1109                    .reduce(
1110                        || (f32::NEG_INFINITY, NONE),
1111                        |a, b| {
1112                            if b.0 > a.0 || (b.0 == a.0 && b.1 < a.1) {
1113                                b
1114                            } else {
1115                                a
1116                            }
1117                        },
1118                    )
1119                    .1
1120            }
1121            Metric::Euclidean => {
1122                // The row nearest the mean: `|x|^2 - 2 <x, mean>` is `|x - mean|^2` less a
1123                // constant.
1124                centroid.iter_mut().for_each(|c| *c /= n as f32);
1125                (0..n as u32)
1126                    .into_par_iter()
1127                    .map(|i| (norms[i as usize] - 2.0 * dot(rows.row(i), &centroid), i))
1128                    .reduce(
1129                        || (f32::INFINITY, NONE),
1130                        |a, b| {
1131                            if b.0 < a.0 || (b.0 == a.0 && b.1 < a.1) {
1132                                b
1133                            } else {
1134                                a
1135                            }
1136                        },
1137                    )
1138                    .1
1139            }
1140        };
1141
1142        let mut rng = SplitMix(params.seed ^ 0xA5A5_A5A5);
1143        let mut order: Vec<u32> = (0..n as u32).collect();
1144        for i in (1..n).rev() {
1145            order.swap(i, (rng.next_u64() % (i as u64 + 1)) as usize);
1146        }
1147        let mut graph = Adjacency::new(n, r);
1148        let first_ef = params.build_ef.min(FIRST_PASS_EF);
1149        for (l, alpha) in [(first_ef, 1.0), (params.build_ef, params.alpha)] {
1150            let t = prof::start();
1151            rows.insert_pass(&mut graph, &order, entry, l, alpha, r);
1152            prof::report("pass", t);
1153        }
1154        let t = prof::start();
1155        rows.fill(&mut graph, r);
1156        prof::report("fill", t);
1157        let tup = prof::start();
1158
1159        // Upper layers (HNSW-like) give each query a nearby entry vertex: layer j holds the
1160        // first n / 16^(j+1) vertices of `order`, so a vertex keeps its local id in every layer.
1161        let mut layers = Vec::new();
1162        let mut size = n / LAYER_RATIO;
1163        while size >= MIN_LAYER {
1164            let sub: Vec<f32> = order[..size]
1165                .iter()
1166                .flat_map(|&g| rows.row(g).iter().copied())
1167                .collect();
1168            let sub_norms: Vec<f32> = match rows.norms {
1169                Some(n) => order[..size].iter().map(|&g| n[g as usize]).collect(),
1170                None => Vec::new(),
1171            };
1172            let sub_codes = (params.quantized_search || dim >= BUILD_CODES_MIN_DIM)
1173                .then(|| BuildCodes::new(&sub, dim, norms_opt(&sub_norms)));
1174            let sub_rows = Rows {
1175                data: &sub,
1176                dim,
1177                norms: norms_opt(&sub_norms),
1178                codes: sub_codes.as_ref(),
1179                quantized: params.quantized_search,
1180            };
1181            let local: Vec<u32> = (0..size as u32).collect();
1182            let mut g = Adjacency::new(size, UPPER_DEGREE);
1183            sub_rows.insert_pass(
1184                &mut g,
1185                &local,
1186                0,
1187                params.build_ef,
1188                params.alpha,
1189                UPPER_DEGREE,
1190            );
1191            layers.push(g.to_lists());
1192            size /= LAYER_RATIO;
1193        }
1194        prof::report("upper", tup);
1195        prof::graph_hash((&graph, &layers, entry));
1196        let upper_ids = order[..n / LAYER_RATIO].to_vec();
1197        Ok(Self {
1198            n,
1199            dim,
1200            degree: r,
1201            metric,
1202            vectors,
1203            norms,
1204            adj: graph,
1205            entry,
1206            upper_ids,
1207            layers,
1208            build: params.clone(),
1209        })
1210    }
1211}
1212
1213/// FNV-1a over 32-bit words: a stable fingerprint, not a cryptographic hash.
1214struct Fnv(u64);
1215
1216impl Fnv {
1217    fn new() -> Self {
1218        Self(0xcbf2_9ce4_8422_2325)
1219    }
1220
1221    fn word(&mut self, w: u32) {
1222        self.0 = (self.0 ^ u64::from(w)).wrapping_mul(0x0000_0100_0000_01b3);
1223    }
1224
1225    fn finish(&self) -> u64 {
1226        self.0
1227    }
1228}
1229
1230/// Bytes of the upper layers and their id map.
1231fn upper_bytes(upper_ids: &[u32], layers: &[Vec<Vec<u32>>]) -> usize {
1232    let adj: usize = layers.iter().flatten().map(|a| a.len() * 4).sum();
1233    upper_ids.len() * 4 + adj
1234}
1235
1236impl QGraph {
1237    pub fn len(&self) -> usize {
1238        self.n
1239    }
1240
1241    pub fn is_empty(&self) -> bool {
1242        self.n == 0
1243    }
1244
1245    /// Bytes held by the index (vertex blocks; the rotation is negligible).
1246    pub fn index_bytes(&self) -> usize {
1247        self.blocks.len() * 8 + upper_bytes(&self.upper_ids, &self.layers)
1248    }
1249
1250    fn vector(&self, v: u32) -> &[f32] {
1251        &as_f32(&self.block(v)[..self.codes_off])[..self.dim]
1252    }
1253
1254    /// Greedy descent through the upper layers with exact distances; returns the bottom-layer
1255    /// entry vertex and the number of exact evaluations.
1256    fn descend(&self, pq: &[f32]) -> (u32, usize) {
1257        if self.layers.is_empty() {
1258            return (self.entry, 0);
1259        }
1260        let mut p = 0u32;
1261        let mut best = -dot(pq, self.vector(self.upper_ids[0]));
1262        let mut evals = 1;
1263        for layer in self.layers.iter().rev() {
1264            loop {
1265                let mut moved = false;
1266                for &u in &layer[p as usize] {
1267                    let d = -dot(pq, self.vector(self.upper_ids[u as usize]));
1268                    evals += 1;
1269                    if d < best {
1270                        best = d;
1271                        p = u;
1272                        moved = true;
1273                    }
1274                }
1275                if !moved {
1276                    break;
1277                }
1278            }
1279        }
1280        (self.upper_ids[p as usize], evals)
1281    }
1282
1283    pub fn searcher(&self) -> Searcher<'_> {
1284        Searcher {
1285            index: self,
1286            visited: Visited::new(self.n),
1287            pq: vec![0.0; self.rotation.padded()],
1288            code: QueryCode::new(self.words),
1289            raw: vec![0; self.degree],
1290            est: vec![0.0; self.degree],
1291            pool: Vec::new(),
1292            results: Vec::new(),
1293        }
1294    }
1295
1296    fn block(&self, v: u32) -> &[u64] {
1297        let v = v as usize;
1298        &self.blocks[v * self.stride..(v + 1) * self.stride]
1299    }
1300}
1301
1302/// Per-thread search state for one index.
1303pub struct Searcher<'a> {
1304    index: &'a QGraph,
1305    visited: Visited,
1306    pq: Vec<f32>,
1307    code: QueryCode,
1308    raw: Vec<u32>,
1309    est: Vec<f32>,
1310    pool: Vec<Candidate>,
1311    results: Vec<(f32, u32)>,
1312}
1313
1314impl Searcher<'_> {
1315    /// Writes the ids of the (approximate) `k` nearest vectors by cosine into `out`, nearest
1316    /// first, and returns the number of exactly scored vertices.
1317    pub fn search(
1318        &mut self,
1319        query: &[f32],
1320        k: usize,
1321        params: SearchParams,
1322        out: &mut Vec<u32>,
1323    ) -> usize {
1324        let idx = self.index;
1325        let ef = params.ef.max(1);
1326        idx.rotation.apply(query, &mut self.pq);
1327        self.code.encode(&self.pq);
1328        let (lo2, d2q, sq) = (2.0 * self.code.lo, 2.0 * self.code.delta, self.code.sum);
1329
1330        self.visited.next();
1331        let (entry, mut exact) = idx.descend(query);
1332        let mut expanded = 0;
1333        self.visited.insert(entry);
1334        self.pool.clear();
1335        self.results.clear();
1336        self.pool.push(Candidate {
1337            dist: 0.0,
1338            id: entry,
1339            done: false,
1340        });
1341        let mut cur = 0;
1342        while cur < self.pool.len() {
1343            if params.max_exact > 0 && expanded >= params.max_exact {
1344                break;
1345            }
1346            let v = self.pool.remove(cur).id;
1347            if let Some(next) = self.pool[cur..].iter().find(|c| !c.done) {
1348                kernels::prefetch(idx.block(next.id));
1349            }
1350            let b = idx.block(v);
1351            let ipv = dot(query, &as_f32(&b[..idx.codes_off])[..idx.dim]);
1352            exact += 1;
1353            expanded += 1;
1354            push_top(&mut self.results, k, -ipv, v);
1355            // The expanded vertex re-enters the pool with its exact distance, so an optimistic
1356            // estimate cannot hold a pool slot and stall the search.
1357            let pos = self.pool.partition_point(|c| c.dist <= -ipv);
1358            if pos < ef {
1359                self.pool.insert(
1360                    pos,
1361                    Candidate {
1362                        dist: -ipv,
1363                        id: v,
1364                        done: true,
1365                    },
1366                );
1367                self.pool.truncate(ef);
1368            }
1369
1370            kernels::raw_estimates(
1371                &b[idx.codes_off..idx.factors_off],
1372                &self.code.planes,
1373                idx.words,
1374                &mut self.raw,
1375            );
1376            let fac = as_f32(&b[idx.factors_off..idx.ids_off]);
1377            let (kf, rest) = fac.split_at(idx.degree);
1378            let (mf, pf) = rest.split_at(idx.degree);
1379            for ((((e, &kj), &mj), &pj), &rj) in
1380                self.est.iter_mut().zip(kf).zip(mf).zip(pf).zip(&self.raw)
1381            {
1382                *e = -(ipv + kj + mj * (lo2 * pj - sq + d2q * rj as f32));
1383            }
1384
1385            let mut best = usize::MAX;
1386            for (&u, &d) in as_u32(&b[idx.ids_off..]).iter().zip(&self.est) {
1387                if u == NONE || !self.visited.insert(u) {
1388                    continue;
1389                }
1390                if self.pool.len() >= ef && d >= self.pool[ef - 1].dist {
1391                    continue;
1392                }
1393                let pos = self.pool.partition_point(|c| c.dist <= d);
1394                self.pool.insert(
1395                    pos,
1396                    Candidate {
1397                        dist: d,
1398                        id: u,
1399                        done: false,
1400                    },
1401                );
1402                self.pool.truncate(ef);
1403                best = best.min(pos);
1404            }
1405            cur = best.min(cur);
1406            while cur < self.pool.len() && self.pool[cur].done {
1407                cur += 1;
1408            }
1409        }
1410        out.clear();
1411        out.extend(self.results.iter().map(|r| r.1));
1412        exact
1413    }
1414}
1415
1416fn push_top(top: &mut Vec<(f32, u32)>, k: usize, dist: f32, id: u32) {
1417    if top.len() >= k && top.last().is_none_or(|l| dist >= l.0) {
1418        return;
1419    }
1420    let pos = top.partition_point(|c| c.0 <= dist);
1421    top.insert(pos, (dist, id));
1422    top.truncate(k);
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427    use super::*;
1428    use kernels::raw_estimates;
1429
1430    pub(super) fn gaussian(rng: &mut SplitMix) -> f32 {
1431        let u = rng.next_f32().max(1e-7);
1432        let v = rng.next_f32();
1433        (-2.0 * u.ln()).sqrt() * (std::f32::consts::TAU * v).cos()
1434    }
1435
1436    fn unit(mut x: Vec<f32>) -> Vec<f32> {
1437        let n = dot(&x, &x).sqrt();
1438        x.iter_mut().for_each(|v| *v /= n);
1439        x
1440    }
1441
1442    /// Mean of the shipped per-edge estimator over rotation seeds must match `<q, u>`, for the
1443    /// automatic code length (128 bits at d = 100) and for 512-bit codes.
1444    /// The WHT-with-signs rotation is not Haar, so the bound is empirical: 400 seeds put the
1445    /// standard error near 0.003; tolerances are 0.02 (float query) and 0.03 (4-bit query).
1446    #[test]
1447    fn edge_estimator_is_unbiased_over_rotations() {
1448        let d = 100;
1449        let mut rng = SplitMix(42);
1450        for bits in [0, 0, 0, 512, 512, 512] {
1451            let q = unit((0..d).map(|_| gaussian(&mut rng)).collect());
1452            let v = unit((0..d).map(|_| gaussian(&mut rng)).collect());
1453            let u = unit(v.iter().map(|x| x + 0.15 * gaussian(&mut rng)).collect());
1454            let truth = dot(&q, &u);
1455            let (mut float_sum, mut quant_sum) = (0f64, 0f64);
1456            let seeds = 400;
1457            for seed in 0..seeds {
1458                let rot = Rotation::new(d, bits, seed);
1459                let p = rot.padded();
1460                let (mut pq, mut pv, mut pu) = (vec![0.0; p], vec![0.0; p], vec![0.0; p]);
1461                rot.apply(&q, &mut pq);
1462                rot.apply(&v, &mut pv);
1463                rot.apply(&u, &mut pu);
1464                let mut code = vec![0u64; p / 64];
1465                let (k, m, pop) = edge_factors(&pv, &pu, &mut code);
1466                let ipv = dot(&pq, &pv);
1467                let sq: f32 = pq
1468                    .iter()
1469                    .enumerate()
1470                    .map(|(i, x)| {
1471                        if code[i / 64] >> (i % 64) & 1 == 1 {
1472                            *x
1473                        } else {
1474                            -*x
1475                        }
1476                    })
1477                    .sum();
1478                float_sum += f64::from(ipv + k + m * sq);
1479                let mut qc = QueryCode::new(p / 64);
1480                qc.encode(&pq);
1481                let mut raw = [0u32];
1482                raw_estimates(&code, &qc.planes, p / 64, &mut raw);
1483                let est =
1484                    ipv + k + m * (2.0 * qc.lo * pop - qc.sum + 2.0 * qc.delta * raw[0] as f32);
1485                quant_sum += f64::from(est);
1486            }
1487            let float_mean = float_sum / f64::from(seeds as u32);
1488            let quant_mean = quant_sum / f64::from(seeds as u32);
1489            assert!(
1490                (float_mean - f64::from(truth)).abs() < 0.02,
1491                "{float_mean} vs {truth}"
1492            );
1493            assert!(
1494                (quant_mean - f64::from(truth)).abs() < 0.03,
1495                "{quant_mean} vs {truth}"
1496            );
1497        }
1498    }
1499
1500    pub(super) fn clustered(n: usize, d: usize, seed: u64) -> Vec<f32> {
1501        let mut rng = SplitMix(seed);
1502        let centers: Vec<Vec<f32>> = (0..20)
1503            .map(|_| (0..d).map(|_| gaussian(&mut rng)).collect())
1504            .collect();
1505        (0..n)
1506            .flat_map(|i| {
1507                let c = &centers[i % centers.len()];
1508                c.iter()
1509                    .map(|x| x + 0.4 * gaussian(&mut rng))
1510                    .collect::<Vec<_>>()
1511            })
1512            .collect()
1513    }
1514
1515    pub(super) fn brute_force(data: &[f32], d: usize, q: &[f32], k: usize) -> Vec<u32> {
1516        let mut s: Vec<(f32, u32)> = data
1517            .chunks_exact(d)
1518            .enumerate()
1519            .map(|(i, x)| (-dot(x, q) / dot(x, x).sqrt(), i as u32))
1520            .collect();
1521        s.sort_by(|a, b| a.0.total_cmp(&b.0));
1522        s.into_iter().take(k).map(|x| x.1).collect()
1523    }
1524
1525    #[test]
1526    fn recall_on_clustered_data() {
1527        let d = 24;
1528        let data = clustered(4000, d, 1);
1529        let queries = clustered(100, d, 2);
1530        let index = QGraph::build(&data, d, &BuildParams::default()).expect("build");
1531        let mut s = index.searcher();
1532        let mut out = Vec::new();
1533        let mut hits = 0;
1534        for q in queries.chunks_exact(d) {
1535            let truth = brute_force(&data, d, q, 10);
1536            s.search(
1537                q,
1538                10,
1539                SearchParams {
1540                    ef: 64,
1541                    max_exact: 0,
1542                },
1543                &mut out,
1544            );
1545            hits += out.iter().filter(|x| truth.contains(x)).count();
1546        }
1547        let recall = hits as f64 / 1000.0;
1548        // Regression guard on a deliberately hard set (24 dimensions of noise per cluster).
1549        assert!(recall >= 0.9, "recall {recall}");
1550    }
1551
1552    /// The reverse-edge prune that skips re-checks inside a closed prefix must equal a full
1553    /// prune of the merged list.
1554    #[test]
1555    fn prune_merged_matches_prune() {
1556        let d = 12;
1557        let mut data = clustered(600, d, 5);
1558        data.chunks_exact_mut(d).for_each(|x| {
1559            let n = dot(x, x).sqrt();
1560            x.iter_mut().for_each(|v| *v /= n);
1561        });
1562        let rows = Rows {
1563            data: &data,
1564            dim: d,
1565            norms: None,
1566            quantized: false,
1567            codes: None,
1568        };
1569        let mut rng = SplitMix(11);
1570        for trial in 0..200 {
1571            let t = (rng.next_u64() % 600) as u32;
1572            let (r, alpha) = (4 + trial % 13, [1.0, 1.2][trial % 2]);
1573            let all = |ids: &[u32]| -> Vec<(f32, u32)> {
1574                ids.iter().map(|&u| (rows.d2(t, u), u)).collect()
1575            };
1576            let pool: Vec<u32> = (0..60)
1577                .map(|_| (rng.next_u64() % 600) as u32)
1578                .filter(|&u| u != t)
1579                .collect();
1580            let mut list = rows.prune(t, all(&pool), alpha, r);
1581            let closed = list.len();
1582            for _ in 0..1 + trial % 9 {
1583                let u = (rng.next_u64() % 600) as u32;
1584                if u != t && !list.contains(&u) {
1585                    list.push(u);
1586                }
1587            }
1588            assert_eq!(
1589                rows.prune_merged(t, &list, closed, alpha, r),
1590                rows.prune(t, all(&list), alpha, r),
1591                "trial {trial}"
1592            );
1593        }
1594    }
1595
1596    /// The code filters of the construction search, the prune and the fill only skip exact
1597    /// work, so they build the very graph exact scoring builds, including on near-duplicate
1598    /// rows (bounds at their tightest) and a zero row; for Euclidean rows too, with norms
1599    /// spread over two decades and an offset from the origin (bounds scaled by the norms).
1600    #[test]
1601    fn build_codes_leave_the_graph_unchanged() {
1602        for metric in [Metric::Angular, Metric::Euclidean] {
1603            for d in [100, 160] {
1604                let n = 1200;
1605                let mut data = clustered(n, d, 3);
1606                let mut rng = SplitMix(5);
1607                if metric == Metric::Euclidean {
1608                    for (i, x) in data.chunks_exact_mut(d).enumerate() {
1609                        let s = 0.1 * 100f32.powf((i * 7919 % n) as f32 / n as f32);
1610                        x.iter_mut().for_each(|v| *v = s * (*v + 3.0));
1611                    }
1612                }
1613                for i in (0..n).step_by(7) {
1614                    let j = (rng.next_u64() % n as u64) as usize;
1615                    for k in 0..d {
1616                        data[i * d + k] = data[j * d + k] * (1.0 + 1e-6 * gaussian(&mut rng));
1617                    }
1618                }
1619                data[5 * d..6 * d].fill(0.0);
1620                let norms = prepare_rows(&mut data, d, metric);
1621                let codes = BuildCodes::new(&data, d, norms_opt(&norms));
1622                let build = |codes: Option<&BuildCodes>| {
1623                    let rows = Rows {
1624                        data: &data,
1625                        dim: d,
1626                        norms: norms_opt(&norms),
1627                        codes,
1628                        quantized: false,
1629                    };
1630                    let order: Vec<u32> = (0..n as u32).rev().collect();
1631                    let mut graph = Adjacency::new(n, 12);
1632                    for alpha in [1.0, 1.2] {
1633                        rows.insert_pass(&mut graph, &order, 0, 40, alpha, 12);
1634                    }
1635                    rows.fill(&mut graph, 12);
1636                    graph
1637                };
1638                assert_eq!(build(Some(&codes)), build(None), "{metric:?}, d = {d}");
1639            }
1640        }
1641    }
1642
1643    #[test]
1644    fn build_and_search_are_deterministic() {
1645        let d = 16;
1646        let data = clustered(3000, d, 3);
1647        let params = BuildParams {
1648            seed: 9,
1649            ..BuildParams::default()
1650        };
1651        let a = QGraph::build(&data, d, &params).expect("build");
1652        let b = QGraph::build(&data, d, &params).expect("build");
1653        assert_eq!(a.entry, b.entry);
1654        assert!(a.blocks == b.blocks);
1655        let (mut sa, mut sb) = (a.searcher(), b.searcher());
1656        let (mut oa, mut ob) = (Vec::new(), Vec::new());
1657        let p = SearchParams {
1658            ef: 32,
1659            max_exact: 0,
1660        };
1661        for q in data.chunks_exact(d).take(50) {
1662            sa.search(q, 10, p, &mut oa);
1663            sb.search(q, 10, p, &mut ob);
1664            assert_eq!(oa, ob);
1665        }
1666    }
1667
1668    /// The graphs a build produces, pinned by structure hash: a change to how construction
1669    /// stores or schedules its work must leave every list, layer and the entry unchanged.
1670    #[test]
1671    fn build_reproduces_the_pinned_graphs() {
1672        let cases = [
1673            (Metric::Angular, false, 16, 0x2b16_3721_02ba_f39b),
1674            (Metric::Euclidean, true, 128, 0xe014_c782_9bed_e9aa),
1675        ];
1676        for (metric, quantized_search, d, want) in cases {
1677            let data = clustered(2500, d, 11);
1678            let params = BuildParams {
1679                metric,
1680                quantized_search,
1681                degree: 16,
1682                build_ef: 48,
1683                seed: 7,
1684                ..BuildParams::default()
1685            };
1686            let g = Graph::build(&data, d, &params).expect("build");
1687            assert_eq!(g.structure_hash(), want, "{metric:?}");
1688        }
1689    }
1690
1691    #[test]
1692    fn exact_budget_caps_work() {
1693        let d = 16;
1694        let data = clustered(2000, d, 4);
1695        let index = QGraph::build(&data, d, &BuildParams::default()).expect("build");
1696        let mut s = index.searcher();
1697        let mut out = Vec::new();
1698        let q = &data[..d];
1699        let capped = s.search(
1700            q,
1701            10,
1702            SearchParams {
1703                ef: 128,
1704                max_exact: 20,
1705            },
1706            &mut out,
1707        );
1708        assert_eq!(out.len(), 10);
1709        let free = s.search(
1710            q,
1711            10,
1712            SearchParams {
1713                ef: 128,
1714                max_exact: 0,
1715            },
1716            &mut out,
1717        );
1718        assert!(capped < free);
1719        assert_eq!(capped - index.descend(q).1, 20);
1720    }
1721}