Skip to content

feat(router-core): add an optional lattice-simd distance backend - #765

Open
ohdearquant wants to merge 4 commits into
ruvnet:mainfrom
ohdearquant:feat/router-core-lattice-simd
Open

feat(router-core): add an optional lattice-simd distance backend#765
ohdearquant wants to merge 4 commits into
ruvnet:mainfrom
ohdearquant:feat/router-core-lattice-simd

Conversation

@ohdearquant

@ohdearquant ohdearquant commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What I found first

crates/ruvector-router-core/src/distance.rs opened with

//! SIMD-optimized distance calculations using SimSIMD

and inside euclidean_distance, // Use SimSIMD for optimal performance.

Nothing in this crate calls SimSIMD. grep -rn simsimd crates/ruvector-router-core/src
returns 0 across its 8 source files; the same grep against ruvector-core/src
returns 4, so the pattern does find real uses where they exist. The dependency
is declared in Cargo.toml and 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-checked
indexing 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:

  1. Corrects the module header to describe what the code does. This is a
    factual change to a doc comment, not a style edit, which is why it is called
    out here rather than buried.
  2. Adds the SIMD the header claimed, behind an opt-in feature.

I have left the unused simsimd dependency and the README line alone. Removing
the dependency is a reasonable follow-up and it is your call, not mine.

The backend

An opt-in lattice-simd feature routes all four metrics through
lattice-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-4 with an absolute floor of 1e-5
near 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_distance does 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 than
being 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.0 is the same value the existing
zero 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) or 0.0 (dot). Guarding means turning the feature on
cannot convert a panic into a silent value. calculate_distance already
rejects mismatched dimensions ahead of every metric, and a new test pins that
for all four.

MSRV

lattice-embed requires Rust >= 1.93 (edition 2024) and Cargo cannot express a
per-feature rust-version, so enabling lattice-simd raises the effective
MSRV for whoever enables it. Same trade-off ruvector-core documents for
lattice-embeddings and the same shape as simd-avx512's documented bump
(#438). The pin uses default-features = false, which excludes lattice-embed's
model, tokenizer, and download stack and leaves only the kernels.

Verification

Both feature settings, --locked:

build result
cargo test -p ruvector-router-core --lib distance 7 passed, 0 failed
... --features lattice-simd 7 passed, 0 failed

New backends_match_reference compares whichever backend is compiled against
an 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_distance and
calculate_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:

mutation --features lattice-simd default
Euclidean route returns squared distance (no sqrt) 2 tests FAIL 7 pass
cosine route drops the 1.0 - conversion 3 tests FAIL 7 pass
dot route drops the negation 2 tests FAIL 7 pass
Manhattan route doubles the returned distance 2 tests FAIL 7 pass

The 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 cfg
block. All reverted; both settings green as pushed.

cargo fmt --all -- --check exits 0. cargo clippy -p ruvector-router-core --all-targets --features lattice-simd exits 0 across 139 lines of output, none
naming distance.rs.

Cargo.lock

Gains the lattice-embed 0.7.1 entry and disambiguates ruvector-core's
existing edge to 0.6.1. Re-resolution separately wanted to move tempfile's
getrandom edge from 0.3.4 to 0.4.3; that is unrelated to this change and was
reverted, 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-simd exists for.

Measurement

Apple silicon Mac mini, macOS, aarch64, rustc 1.93.0. The crate's own
vector_search bench target, Criterion --measurement-time 10, filtered to the
three benchmarks that call the distance module directly.

This crate's default feature set is empty, so the off side here is the scalar
path, not another SIMD backend. Both sides are the same commit; the only
difference is the feature flag, because main has no such feature and a
base-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.

benchmark (384-dim f32) off (scalar) on (lattice-embed) change (95% CI)
euclidean_distance 186.55 ns 18.359 ns -90.22% .. -90.06%
cosine_similarity 208.82 ns 26.460 ns -87.35% .. -87.31%
dot_product 175.63 ns 16.761 ns -90.49% .. -90.44%

All three p = 0.00.

Two limits worth stating rather than leaving to be discovered.

The bench builds its operands as vec![0.5; 384] and vec![0.6; 384], so every
element 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 insert and search groups 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.

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
ohdearquant marked this pull request as draft August 2, 2026 14:37
ohdearquant and others added 3 commits August 2, 2026 12:23
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
ohdearquant marked this pull request as ready for review August 3, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant