feat(router-core): add an optional lattice-simd distance backend - #765
Open
ohdearquant wants to merge 4 commits into
Open
feat(router-core): add an optional lattice-simd distance backend#765ohdearquant wants to merge 4 commits into
ohdearquant wants to merge 4 commits into
Conversation
distance.rs was headed "SIMD-optimized distance calculations using SimSIMD" and never called SimSIMD. The four metrics are scalar loops, manually unrolled eight elements at a time with bounds-checked indexing. The header is corrected to say what the module does. Adds an opt-in `lattice-simd` feature routing Euclidean, cosine, and dot product through lattice-embed's runtime-dispatched kernels (AVX-512F, AVX2, NEON, wasm32 SIMD128, each with a scalar fallback). Manhattan stays scalar: lattice-embed 0.7.0 has no L1 kernel. Default builds compile the same code as before. The sign and similarity-to-distance conversions stay in this module rather than moving into the backend, so both paths agree on the convention each metric already defined: cosine returns 1 - similarity, dot product is negated, and a zero-magnitude cosine operand still gives 1.0. Each route is guarded on equal lengths. The scalar paths index the second slice by the first slice's length and panic on a short one, where lattice returns f32::MAX or 0.0, so guarding keeps enabling the feature from converting a panic into a silent value. calculate_distance already rejects mismatched dimensions ahead of every metric. The dependency is pinned with default-features = false, which excludes lattice-embed's model, tokenizer, and download stack.
ohdearquant
marked this pull request as draft
August 2, 2026 14:37
lattice-embed 0.7.1 adds a runtime-dispatched L1 kernel (AVX-512F, AVX2, NEON, wasm32 SIMD128, each with a scalar fallback), so Manhattan no longer has to stay on the scalar loop while the other three metrics route through lattice. All four metrics now share one backend under `lattice-simd`. The equal-length guard matches `euclidean_distance` for the same reason: the scalar path indexes `b` by `a`'s length and panics on a short `b`, where lattice returns f32::MAX, so only equal lengths are routed and enabling the feature cannot turn a panic into a silent value. Default builds are unchanged and still scalar.
Doc comments claimed the lattice-simd path returns identical values to the scalar path. It does not: SIMD reduction order differs from the scalar loops, producing bounded floating-point rounding differences. Each routed function now states the bound its tests enforce (relative error 1e-4, absolute floor 1e-5 near zero), and the parity test checks exactly that bound against an independent f64 reference at dimensions including 8, 384, 385, and 1536. Adds table-driven tests for NaN, +-inf, signed zero, and empty slices across all four public distance functions, comparing the compiled backend (scalar or lattice-simd) against this module's own scalar implementation bit-for-bit (NaN-aware). Also adds a direct length-mismatch test per public function, since lattice-simd only routes equal-length inputs and both build configurations otherwise share the scalar loop's truncate/panic behavior.
…ity test Each routed distance function documents a bound against the scalar path (relative 1e-4, absolute floor 1e-5), but the parity test only compared each compiled backend against an f64 reference. Two paths each within tolerance of a reference can still differ from each other by up to 2x that tolerance, so the documented routed-vs-scalar bound was unenforced. Add a direct pairwise assertion per metric inside the existing dim/seed loop, comparing the public function against this module's private scalar implementation with the same tolerance helper. Under the default build this compares a function with itself; under lattice-simd it enforces the contract described in the rustdoc. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ohdearquant
marked this pull request as ready for review
August 3, 2026 16:05
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.
What I found first
crates/ruvector-router-core/src/distance.rsopened with//! SIMD-optimized distance calculations using SimSIMDand inside
euclidean_distance,// Use SimSIMD for optimal performance.Nothing in this crate calls SimSIMD.
grep -rn simsimd crates/ruvector-router-core/srcreturns 0 across its 8 source files; the same grep against
ruvector-core/srcreturns 4, so the pattern does find real uses where they exist. The dependency
is declared in
Cargo.tomland the README lists it under "SIMD Acceleration",but no code imports it.
What the four metrics actually run is scalar:
while i + 8 <= len { for j in 0..8 { ... a[i + j] ... } }. Manual eight-wide unrolling with bounds-checkedindexing helps the autovectorizer and is not the same thing as emitting vector
instructions.
So this PR does two separate things, and I want them separable in review:
factual change to a doc comment, not a style edit, which is why it is called
out here rather than buried.
I have left the unused
simsimddependency and the README line alone. Removingthe dependency is a reasonable follow-up and it is your call, not mine.
The backend
An opt-in
lattice-simdfeature routes all four metrics throughlattice-embed's runtime-dispatched kernels (AVX-512F, AVX2, NEON,wasm32 SIMD128, each with its own scalar fallback). Default builds keep the
scalar path's behaviour unchanged; the refactor routes the public functions
through private scalar helpers, so this claims preserved behaviour, not
byte-identical compiled output.
The enabled path's accuracy contract is explicit rather than implied: SIMD
reduction order differs from the scalar loops, so results may differ by
bounded rounding — relative error
1e-4with an absolute floor of1e-5near zero, stated on each routed function's doc comment. The parity test
enforces that bound directly between the routed path and the scalar helpers,
and additionally checks both paths against an independent f64 reference
(dims 8 through 1536, including odd lengths). NaN, infinities, signed zero, empty slices, and
direct length-mismatch calls are pinned to the scalar path's behaviour under
both build configurations: NaN-ness is preserved (any NaN payload accepted),
and non-NaN results match bitwise.
Manhattan was the exception when this PR was opened: lattice-embed 0.7.0
exposed no L1 kernel, so it stayed on the scalar loop. 0.7.1 adds one, and the
pin moves to 0.7.1 accordingly. That is a hard requirement rather than a tidy-up
here, since
simd::manhattan_distancedoes not exist in 0.7.0.Same backend split as #762 and #763, so it reviews the same way.
Conversions stay in this module, deliberately. Cosine returns
1 - similarity, dot product is negated, and each is applied here rather thanbeing pushed into the backend, so both paths agree on the convention this
module already defined. A zero-magnitude cosine operand still returns 1.0:
lattice yields 0.0 for that case and
1.0 - 0.0is the same value the existingzero check returns, so the degenerate branch needed no special handling and the
tests check it rather than the comment asserting it.
Every route is guarded on equal lengths. The scalar paths index the second
slice by the first slice's length and panic on a short one, where lattice
returns
f32::MAX(L2 and L1) or0.0(dot). Guarding means turning the feature oncannot convert a panic into a silent value.
calculate_distancealreadyrejects mismatched dimensions ahead of every metric, and a new test pins that
for all four.
MSRV
lattice-embedrequires Rust >= 1.93 (edition 2024) and Cargo cannot express aper-feature
rust-version, so enablinglattice-simdraises the effectiveMSRV for whoever enables it. Same trade-off
ruvector-coredocuments forlattice-embeddingsand the same shape assimd-avx512's documented bump(#438). The pin uses
default-features = false, which excludes lattice-embed'smodel, tokenizer, and download stack and leaves only the kernels.
Verification
Both feature settings,
--locked:cargo test -p ruvector-router-core --lib distance... --features lattice-simdNew
backends_match_referencecompares whichever backend is compiled againstan f64 reference across 18 dimensions, chosen to straddle both the existing
8-wide chunk boundary and the 4/8/16-lane widths a SIMD backend uses, so both
remainder paths are exercised. It covers all four metrics including sign and
the similarity-to-distance conversion, not just magnitude. Also new:
cosine_zero_vector_is_max_distanceandcalculate_distance_rejects_mismatched_dimensions.Reachability, checked rather than assumed. A cfg-gated backend can be dead
and still let every test pass, so each route was mutated on its own:
--features lattice-simdsqrt)1.0 -conversionThe failing column proves each route is genuinely taken and the tests detect a
break in it; the passing column proves each mutation stayed inside its
cfgblock. All reverted; both settings green as pushed.
cargo fmt --all -- --checkexits 0.cargo clippy -p ruvector-router-core --all-targets --features lattice-simdexits 0 across 139 lines of output, nonenaming
distance.rs.Cargo.lock
Gains the
lattice-embed 0.7.1entry and disambiguatesruvector-core'sexisting edge to
0.6.1. Re-resolution separately wanted to movetempfile'sgetrandomedge from 0.3.4 to 0.4.3; that is unrelated to this change and wasreverted, so the diff is only the lattice entry. Worth flagging on its own: the
same drift appears from a clean branch in #763, so the committed lock looks
slightly stale against current resolution independently of either PR.
Benchmarks
An earlier revision of this section declined to give numbers because the A/B
harness on my measurement host failed an A/A calibration (its null exceeded
its own detection threshold). The Measurement section below supersedes it: it
was taken on a dedicated idle machine with the crate's own bench target and
in-phase idle sampling, and those are the only numbers this PR claims.
Independent of any measurement, the structural change stands on its own: it
replaces a bounds-checked scalar loop with a runtime-dispatched vector kernel,
and on wasm32 it replaces scalar code with SIMD128, which is the case
lattice-simdexists for.Measurement
Apple silicon Mac mini, macOS, aarch64, rustc 1.93.0. The crate's own
vector_searchbench target, Criterion--measurement-time 10, filtered to thethree benchmarks that call the distance module directly.
This crate's
defaultfeature set is empty, so the off side here is the scalarpath, not another SIMD backend. Both sides are the same commit; the only
difference is the feature flag, because
mainhas no such feature and abase-vs-head comparison would therefore measure nothing about the backend.
CPU idle was sampled every 20s inside each measured phase. Off phase minimum
77%, on phase minimum 78%, three samples each. Both phases sat in the same
range, so neither side was measured under a load the other did not see.
euclidean_distancecosine_similaritydot_productAll three p = 0.00.
Two limits worth stating rather than leaving to be discovered.
The bench builds its operands as
vec![0.5; 384]andvec![0.6; 384], so everyelement is identical within a vector. That does not change the work either path
performs for these kernels, but it does mean the numbers say nothing about
data-dependent behaviour, and a harness that varied its inputs would be a
strictly better one.
The
insertandsearchgroups in this same bench target were not measured.They are index-level and dominated by graph construction rather than distance
evaluation, and at roughly 400 ms per iteration at size 100 they would have cost
far more than the three kernels did. The end-to-end effect on those paths is
therefore unknown, and a per-kernel speedup does not entitle anyone to assume an
equivalent end-to-end one.