vector: candidate-anchored radius gate — result-side band, native cluster radii, two-tier Skip/Terminate - #185
Closed
RuchirRaj wants to merge 4 commits into
Closed
vector: candidate-anchored radius gate — result-side band, native cluster radii, two-tier Skip/Terminate#185RuchirRaj wants to merge 4 commits into
RuchirRaj wants to merge 4 commits into
Conversation
…queries The candidate-anchored IVF probe gate needs the exact current n-th best at cluster boundaries. `threshold` can't serve that role: it is None until the first truncation and lags between truncations (it holds the (n+1)-th best at the last truncation point). kth_best() force-truncates the buffer to top_n through the existing truncation machinery and returns the buffer minimum — exact at every call, O(buffer), called only at cluster boundaries so no per-push overhead is added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a second anchor for the IVF probe loop's distance-ratio gate: the band follows the result heap's k-th best (via TopNComputer::kth_best) instead of the first-ranked centroid's distance. The gate stays unarmed until the segment heap holds top_n filter-passing candidates — the saturation precondition subsumes the starvation problem the Centroid arm's min_candidates/overfetch_margin survivor floor patches, so those knobs are ignored in Candidate mode. Termination applies patience-2 against the ranking stream's documented non-monotone yield order. Accounting contract: a pending Terminate-condition yield (patience streak 1) is withheld but neither counted as a radius skip nor budget-charged — the budget bounds pulls that do work, and a withheld pending yield does none. ProbeStats gains heap_saturated, gate_armed_at_probe, gate_armed_at_ceiling, and radius_skips (wired here; the radius gate lands in a follow-up commit), all flowing through the VectorSimilarityFruit stats path. Candidate is the default. The Centroid arm is TEMPORARY A/B scaffolding retained as the benchmark control, byte-exact with merged main (centroid_mode_unchanged pins it); its gate computation is isolated in centroid_gate_threshold so removal is one function + one enum variant. Ranked-stream consumption order is deliberately unchanged — lower-bound (d − r) reordering is a named follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One f32 per cluster: the max stored-representation displacement over the
cluster's NATIVE (rank-0) members only — replica spill is excluded so
replication cannot inflate radii into uselessness. True L2 for L2/Dot,
chord (sqrt(2·(1−cos))) for write-time-normalized Cosine; all three
reduce to L2 between stored representations, so one code path serves
every metric.
Computed in the merge's posting-write loop: the clusterer's assign()
returns only cluster ids (no distances), and the posting write already
touches every stored row's bytes post-normalization. The merge threads
provenance through AssignedVector (native: true on the primary
assignment push, false on replica_entries appends — the flag survives
the (cluster, doc) sort) and the fold skips non-native rows. One
l2_squared_bytes per native row (no decode allocation), one sqrt per
cluster. The IVF merge always re-trains and re-assigns, so merges
recompute radii fresh — no carry formula needed.
SOUNDNESS is closure through native homes, documented at the fold: any
point p with d(q,p) ≤ d_K has a native cluster c_p with d(p, μ_{c_p}) ≤
r_native(c_p) by membership, so d(q, μ_{c_p}) ≤ d_K + r_native(c_p) —
c_p can never Skip and sits inside the Terminate bound built from the
native r_max. A skipped cluster may hold qualifying REPLICA copies, but
each copy's native home clears the test; replicas are pure bonus. Same
in chord space for Cosine and via Cauchy–Schwarz for Dot.
Persisted as optional slot [3] of the .centroids composite; per-slot
presence in the composite footer is the format-compatibility mechanism
(slot [2] already works this way). A pre-radius segment loads with
all-zero radii, which reduces the radius-aware gate exactly to the
radius-less candidate gate. IvfIndex exposes cluster_radius(c) and a
load-time-cached max_radius().
Tests: serialize/open roundtrip, absent-slot zeros, length-mismatch
corruption; hand-computed radii per metric built with replicas = 2
pinning the NATIVE maxima (a replica-inclusive fold would read ~10.2);
replica_spill_does_not_inflate_radius (tight blobs + grid-gap spill →
every radius < 0.1); native invariant re-checked across a re-merge; and
an old-format end-to-end — slot [3] stripped from a real segment's
composite on disk, reloaded, queried in Candidate mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
radius_gate() — pure in (sim, kth, ε, r_c, r_max, q_norm, metric), no iterator or heap coupling — classifies each gated yield: Probe when the cluster's radius-adjusted best case (d_c − r_c)⁺ can still beat the sqrt(1+ε)-widened k-th-best band; Skip when only this cluster is ruled out (r_c-bound) — a wider cluster at the same centroid distance could still reach the band, so the terminate streak RESETS; TerminateCondition when no cluster at this distance could qualify at the segment's widest radius (r_max-bound), confirmed by patience-2. Per metric: true L2 distances (band sqrt(1+ε)·d_k keeps ε's squared-space meaning), cosine both sides chord-converted (the space radii are stored in), dot via Cauchy–Schwarz ⟨q,p⟩ ≤ sim + ‖q‖·r — a real bound, unlike the Centroid arm's linear-band heuristic. ‖q‖ is computed once per query and only for Dot. Radii are NATIVE-only; soundness is closure through native homes (see the merge's radius fold): a qualifying point's native home can never Skip and sits inside the native-r_max Terminate bound, so skipped clusters lose only replica copies whose native homes are still probed. Accounting: a Skip charges SKIPPED_CLUSTER_COST to the filter-effective budget (bounding pulls at ~1/SKIPPED_CLUSTER_COST × the ceiling), increments radius_skips, resets patience, and never enters probed_clusters; a pending Terminate yield charges and counts nothing (the streak resolves within two yields, so unpaid pulls are bounded by one per paid pull). The ceiling-tie check reuses radius_gate so gate_armed_at_ceiling keeps its meaning under radii. With r_c = r_max = 0 the verdict reduces exactly to the radius-less candidate band and Skip is unreachable — pre-radius segments behave identically to the gate without this commit (zero_radii_equals_commit2_gate sweeps this). At ε = 0 with exact radii, Skip is a per-cluster certificate against the yield stream: a skipped cluster provably holds no NATIVE member better than the current k-th best. The ε = 0 trap fixture is resurrected sound (trap_recovered_by_radius_at_eps0); its radius-less miss is preserved on a slot-stripped segment. Tests: trap miss/recovery pair; zero-radii scans report exactly zero radius_skips (pending yields are not skips); a nearer tight cluster skipped while a farther wide one probes, with termination strictly after; a Skip between two Terminate yields resets patience (hand-ordered stream); a 30-cluster Skip run trips the CEILING on budget, not the gate; Dot's bound in both directions (fires on small radii, forces the probe for a high-norm member); cosine chord gating; and the zero-radii reduction sweep across all three metrics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR