feat(spann): add an optional lattice-simd distance backend - #763
Open
ohdearquant wants to merge 4 commits into
Open
feat(spann): add an optional lattice-simd distance backend#763ohdearquant wants to merge 4 commits into
ohdearquant wants to merge 4 commits into
Conversation
The SPANN partition index computes l2_squared in twelve places across index.rs and kmeans.rs, and every one of them ran a scalar iterator sum. Adds an opt-in `lattice-simd` feature that routes the inner products through lattice-embed's runtime-dispatched SIMD kernels (AVX-512, AVX2, NEON, wasm32 SIMD128, each with a scalar fallback). Default builds are byte-for-byte the same code as before and this crate's default dependency set stays empty. Only the accumulation changes. Length handling and the 1e-9 small-norm guard stay outside the backend split, so both backends take identical branches. cosine_distance is composed from three inner products rather than calling lattice's cosine_similarity, because that function applies its own zero-norm rule and this module's contract is the 1e-9 threshold. The lattice route is taken only for equal-length inputs: lattice returns f32::MAX (l2) or 0.0 (dot) on a mismatch where the scalar path truncates to the shorter slice, and routing only the equal-length case keeps the two backends from disagreeing on an input the debug assertion already treats as a caller bug. The dependency is pinned with default-features = false, which excludes lattice-embed's model, tokenizer, and download stack and leaves only the SIMD kernels.
ohdearquant
marked this pull request as draft
August 2, 2026 14:37
0.7.1 is the current release. Keeping 0.7.0 here would land the lockfile one patch behind on the day this merges and diverge from the sibling backend PRs for no reason; 0.7.1 is additive over 0.7.0, so no code change is needed. Re-resolution again wanted to move tempfile's getrandom edge from 0.3.4 to 0.4.3, unrelated to this change and reverted as before, so the lock diff is only the lattice entry.
ohdearquant
marked this pull request as ready for review
August 3, 2026 18:08
Add a lattice-simd-gated test that fails if the SIMD routing in distance.rs is silently reverted to the scalar loops, and correct the edge/WASM doc section that claimed the manifest has no dependencies at all (the optional lattice-simd feature now adds lattice-embed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the bit-inequality selection test, which rejected a valid lattice-embed scalar fallback on hosts without an accelerated SIMD path, with a per-thread call witness set after each lattice_embed::simd::* call returns. Also drop the README's no_std/wasm-pack overclaim: the crate has no #![no_std] attribute and uses std::cmp::Ordering in production code, and has no tested WASM build configuration.
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
ruvector-spann/src/distance.rshad four scalar iterator sums and no SIMD path.l2_squaredis called from eleven places acrossindex.rsandkmeans.rs, soit is the inner loop of partition assignment, spill selection, and search.
This adds an opt-in
lattice-simdfeature that routes the inner productsthrough
lattice-embed's runtime-dispatched SIMD kernels (AVX-512F, AVX2,NEON, wasm32 SIMD128, each with a scalar fallback). It is the same backend
split as #762, so a reader who has seen that one already knows this shape.
Default builds are unchanged. With the feature off, the crate's resolved
normal dependency tree is still empty (
cargo tree -e normalprints the singleruvector-spannnode); the manifest gains one optional dependency that adefault build never compiles, and the code compiled is the code that was here
before.
What is deliberately not behind the feature flag
Only the accumulation moved. Length handling and the
1e-9small-norm guardstay outside the backend split, so both backends take identical branches and
can only differ in floating-point association.
Two specifics worth a reviewer's attention:
cosine_distanceis composed from three inner products rather than callinglattice_embed::simd::cosine_similarity. That function applies its ownzero-norm rule, while this module's contract is "return 1.0 below 1e-9". Using
dot_productthree times keeps the threshold the single place either backenddecides it.
a.len() == b.len(). lattice returnsf32::MAX(squared L2) or0.0(dot) on a mismatch, where the existingscalar path truncates to the shorter slice. The
debug_assert_eq!alreadycalls a mismatch a caller bug; guarding here means enabling the feature cannot
silently change what a release build does with one.
MSRV
lattice-embedrequires Rust >= 1.93 (edition 2024) and Cargo cannot express aper-feature
rust-version, so turninglattice-simdon raises the effectiveMSRV for whoever turns it on. This is the same trade-off
ruvector-coredocuments for
lattice-embeddings, and the same shape as the existingsimd-avx512feature's documented per-feature bump (#438). Default builds areunaffected.
The pin uses
default-features = false, which excludes lattice-embed's model,tokenizer, and download stack; what remains enabled is the SIMD kernel path
plus its small support dependencies, not a dependency-free graph.
Verification
Both feature settings,
--locked, on the same machine:cargo test -p ruvector-spann --libcargo test -p ruvector-spann --lib --features lattice-simdThe default count includes the crate's existing k-means and index recall
acceptance tests, which exercise
l2_squaredthrough its real call sitesrather than directly. The feature build's extra test is a routing witness
asserting the lattice backend is actually the one called on the equal-length
path.
New
backend_matches_referencecompares whichever backend is compiled againstan f64 reference over 17 dimensions chosen to straddle the 4/8/16-lane widths
and the unrolled chunk sizes a SIMD backend uses, so remainder handling is
exercised rather than assumed. New
cosine_zero_vector_is_not_nanpins thesmall-norm branch.
Reachability, checked rather than assumed. A cfg-gated backend can be dead
and still let every test pass, so each arm was mutated individually and both
outcomes recorded:
--features lattice-simdsquared_euclidean_distance->euclidean_distancedot_product(b, b)->dot_product(a, a)in the cosine pathThe failing column proves the lattice route is genuinely taken and the tests
detect it; the passing column proves each mutation stayed inside the
cfgblock and the default path is untouched. Both were reverted; both settings are
green as pushed.
cargo fmt --all -- --checkexits 0.cargo clippy -p ruvector-spann --all-targetsproduces zero diagnostics attributable todistance.rsundereither feature setting (139 lines of clippy output on the feature build, none
naming the file).
Cargo.lock
The lock gains the
lattice-embed 0.7.1entry and disambiguatesruvector-core's existinglattice-embededge to0.6.1, since two versionsnow appear. Re-resolution also wanted to drift
tempfile'sgetrandomedgefrom 0.3.4 to 0.4.3; that is unrelated to this change and was reverted, so the
diff is only the lattice entry.
cargo check --lockedpasses on both featuresettings.
Both lattice-embed pins are behind off-by-default optional features, so a
default build compiles neither and the two versions only ever coexist in a
build that enables
ruvector-core/lattice-embeddingsandruvector-spann/lattice-simdat once. #758 moves the workspace pins to 0.7.1,after which the question disappears; happy to rebase onto it if you would
rather land them in that order.
Benchmarks
When this PR was first opened, no speed claim was made: the measurement host
was too noisy at the time to certify an A/B (ambient idle sampled at 50-63%,
under the floor the harness enforces), and the position taken was to open with
correctness evidence only. A quiet window later became available; the
Measurement section below supersedes this paragraph and records the numbers
with their conditions.
Measurement
When this section was first written the crate had no benchmark to run — no
[[bench]]target, nobenches/directory, nocriteriondev-dependency —and the position taken was that a harness should arrive as its own reviewable
change rather than ride under a backend flag. That harness now exists as #782.
The numbers below were produced by cherry-picking #782's commit onto this PR's
head locally; if #782 lands first, this branch measures cleanly as-is.
Apple silicon Mac mini, macOS, aarch64, rustc 1.93.0,
cargo bench -p ruvector-spann --bench distance, Criterion--measurement-time 10. Thiscrate's
defaultfeature set is empty, so the off side is the scalar path.Same commit both phases; only the feature flag differs. CPU idle sampled every
20s inside each measured phase: off phase minimum 84%, on phase minimum 83% —
both phases quiet and symmetric, which is the cleanest window any measurement
in this PR series has had.
l2_squared/128l2_squared/384l2_squared/768l2_squared/1536cosine_distance/128cosine_distance/384cosine_distance/768cosine_distance/1536All p = 0.00. Inputs are seeded-random, not constant fills.
What this does and does not claim: these are kernel-level numbers on the two
functions this PR routes, measured through the crate's own new harness. Nothing
here measures the SPANN index end to end, and the sibling PRs show the
kernel-to-end-to-end ratio varies widely between crates. The feature is off by
default; the default build keeps the scalar path bit-for-bit.
Note on this PR's CI.
Tests (core-and-rest)is red on every branch in this repository,including
main: the job is cancelled at its 240-minute cap while still compiling and neverreaches the test phase. #786 restores the exclusion list that the shard's
packages:value losesto a shell comment, #784 unblocks the
ruvector-filtertest target that the compiler cannotfinish, and #787 fixes a deadlock waiting behind both. That failure is not caused by this branch.