From b639fd74dd3d0039e21af373468b69e247a694a6 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 2 Aug 2026 08:34:55 -0400 Subject: [PATCH 01/10] feat(core,wasm): add a lattice-simd distance backend that vectorizes on wasm32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ruvector-core`'s f32 distance functions have two backends: SimSIMD on native, and a scalar fallback. SimSIMD is excluded from wasm32, so every browser build takes the scalar path — an unrolled loop for euclidean, a single-pass loop for cosine, and a plain `iter().zip().map().sum()` for dot product. That path is also on the hot line for wasm search, since `hnsw` is unavailable there (mmap), leaving brute-force scans where distance dominates query cost. This adds a third backend behind a new off-by-default `lattice-simd` feature, routing the three f32 functions through `lattice-embed`'s kernels, which do compile to `simd128` on wasm32. `ruvector-wasm` gets a matching passthrough feature next to its existing `simd` one. Precedence is explicit: `lattice-simd` wins where both it and `simd` are enabled, and the three `cfg` arms are mutually exclusive and exhaustive, so exactly one compiles for any feature/target combination. Default builds are untouched and still use SimSIMD. The dependency is now pinned `default-features = false`. That is what makes a kernels-only dependency possible: `native` is the feature that pulls `lattice-inference`, so with defaults off there is no model, tokenizer, or download stack in the tree. Verified with `cargo tree`: the `lattice-simd` build resolves `lattice-embed` and zero `lattice-inference`. `lattice-embeddings` now forwards `native` and `download` explicitly so its behaviour is unchanged. MSRV: `lattice-embed` needs Rust >= 1.93, so enabling either lattice feature raises the effective MSRV for whoever turns it on, in the same shape as the existing `simd-avx512` feature requiring >= 1.89. The default build stays on the workspace 1.77. One deliberate behavioural difference, in cosine only. The scalar path treats a denominator <= 1e-8 as degenerate and returns 1.0; the lattice kernels test the norms against exactly 0.0. Vectors with tiny but nonzero norms therefore get a computed ratio here rather than a saturated 1.0. Exact zero vectors agree, and that agreement is covered by a test. Verification: - `cargo test -p ruvector-core --features lattice-simd --lib`: 230 passed, 0 failed. Default-feature run stays green, so the SimSIMD path is unaffected. - New `test_backend_matches_scalar_reference` checks whichever backend is compiled in against naive scalar references across dimensions 1..768, chosen to straddle 4/8/16-lane widths and their remainders. It is backend-independent, so it now also covers the SimSIMD path. - Mutation-checked per adapter rather than as one patch: dropping the `1.0 -` on cosine, the negation on dot product, or swapping euclidean for its squared variant each makes that test fail, and reverting each returns it to green. - wasm32-unknown-unknown builds of `ruvector-wasm` with `RUSTFLAGS="-C target-feature=+simd128"`. Disassembling the artifact and counting vector opcodes: 474 with the feature on, 12 with it off, and 0 when the feature is on but the target-feature flag is absent. So the kernels are genuinely vectorized rather than merely compiled, and without the flag they fall back to scalar as documented. Not addressed here: `ruvector-core` does not currently build for wasm32-unknown-unknown on its own under `--no-default-features --features memory-only`, because `getrandom` needs its `js` feature. That reproduces on an unmodified checkout with this feature absent, so it is independent of this change; `ruvector-wasm` supplies the `js`-enabled `getrandom` and builds fine. Manhattan distance is left on `simd_intrinsics` and is not part of this change. --- Cargo.lock | 34 ++++-- crates/ruvector-core/Cargo.toml | 35 ++++-- crates/ruvector-core/src/distance.rs | 154 +++++++++++++++++++++++++-- crates/ruvector-wasm/Cargo.toml | 6 ++ 4 files changed, 207 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 854cd017f6..eb290c8a14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4921,14 +4921,14 @@ dependencies = [ [[package]] name = "lattice-embed" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "188e4627dabd63544da5a57daabfe3105bf4d4970840f2e5a5c7f01123835ecb" +checksum = "1f3af50558cf953df225b68ec160f1ea49b0965564f10a9b8577d15ac8452542" dependencies = [ "async-trait", "blake3", "chrono", - "lattice-inference", + "lattice-inference 0.7.0", "lru 0.16.4", "parking_lot 0.12.5", "serde", @@ -4944,9 +4944,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b113852e4c522b6607cd8e01842b5dff7de307d4cc9223dfd798b0136e2aa62b" dependencies = [ - "axum 0.8.9", "clap", - "futures", "half", "image 0.25.10", "indexmap 2.12.1", @@ -4959,7 +4957,27 @@ dependencies = [ "serde_json", "sha2 0.10.9", "thiserror 2.0.18", - "tokio", + "tracing", +] + +[[package]] +name = "lattice-inference" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a555d0b66e489e1c1291522143780fd9e0bbf0475c9f23fd299fa35df68a2" +dependencies = [ + "clap", + "half", + "image 0.25.10", + "indexmap 2.12.1", + "libc", + "memmap2", + "rayon", + "rustc-hash 2.1.2", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.18", "tracing", "ureq 2.12.1", ] @@ -11020,7 +11038,7 @@ dependencies = [ "futures-core", "half", "hf-hub", - "lattice-inference", + "lattice-inference 0.6.1", "md5", "memmap2", "metal 0.29.0", @@ -12674,7 +12692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", diff --git a/crates/ruvector-core/Cargo.toml b/crates/ruvector-core/Cargo.toml index 144c706fea..aba287a636 100644 --- a/crates/ruvector-core/Cargo.toml +++ b/crates/ruvector-core/Cargo.toml @@ -55,12 +55,20 @@ tokenizers = { version = "0.20", default-features = false, features = ["onig"], # HuggingFace Hub for model downloads hf-hub = { version = "0.4", optional = true } -# Native (pure-Rust) local embeddings via lattice-embed (not available in WASM). -# NOTE: lattice-embed 0.6 requires Rust >= 1.93 (edition 2024). Cargo cannot -# express a per-feature `rust-version`, so enabling the `lattice-embeddings` -# feature raises the effective MSRV above this crate's workspace-inherited -# 1.77 for anyone who turns it on. The default build is unaffected. -lattice-embed = { version = "0.6.1", optional = true } +# lattice-embed serves two independent features here, which is why it is pinned +# with `default-features = false`: +# * `lattice-simd` — SIMD distance kernels only. No model, no tokenizer, +# no download stack. Works on wasm32. +# * `lattice-embeddings` — local embedding models, which additionally need +# `native` (and `download` for first-use fetches). +# Turning default features off is what separates the two: `native` is what pulls +# `lattice-inference`, so the kernels-only path does not drag in the model tree. +# +# NOTE: lattice-embed requires Rust >= 1.93 (edition 2024). Cargo cannot express +# a per-feature `rust-version`, so enabling either feature raises the effective +# MSRV above this crate's workspace-inherited 1.77 for anyone who turns it on, +# the same way `simd-avx512` requires >= 1.89. The default build is unaffected. +lattice-embed = { version = "0.7.0", optional = true, default-features = false } tokio = { workspace = true, optional = true } [dev-dependencies] @@ -124,7 +132,20 @@ uuid-support = [] # Deprecated: uuid is now always included real-embeddings = [] # Feature flag for embedding provider API (use ApiEmbedding for production) api-embeddings = ["reqwest"] # API-based embeddings (not available in WASM) onnx-embeddings = ["ort", "tokenizers", "hf-hub"] # ONNX-based local embeddings (not available in WASM) -lattice-embeddings = ["dep:lattice-embed", "dep:tokio"] # Native pure-Rust local embeddings via lattice-embed (not available in WASM) +# Native pure-Rust local embeddings via lattice-embed (not available in WASM). +# Forwards `native`/`download` explicitly because the dependency is pinned with +# `default-features = false`; this reproduces the previous default feature set. +lattice-embeddings = [ + "dep:lattice-embed", + "lattice-embed/native", + "lattice-embed/download", + "dep:tokio", +] +# Route the f32 distance kernels through lattice-embed instead of SimSIMD. +# Unlike `simd`, this one covers wasm32 (via `simd128`), where SimSIMD is +# unavailable and the distance path is otherwise scalar. Takes precedence over +# `simd` where both are enabled. Off by default. +lattice-simd = ["dep:lattice-embed"] [lib] crate-type = ["rlib"] diff --git a/crates/ruvector-core/src/distance.rs b/crates/ruvector-core/src/distance.rs index ef2a10bd74..7ba9b1939e 100644 --- a/crates/ruvector-core/src/distance.rs +++ b/crates/ruvector-core/src/distance.rs @@ -1,5 +1,14 @@ //! SIMD-optimized distance metrics -//! Uses SimSIMD when available (native), falls back to pure Rust for WASM +//! +//! Three mutually exclusive backends, selected at compile time: +//! +//! - `lattice-simd`: `lattice-embed`'s kernels. Covers wasm32 (`simd128`) as well as +//! x86_64 and aarch64, so it is the only backend that vectorizes on wasm. +//! - `simd` on non-wasm: SimSIMD. +//! - otherwise: the portable scalar path. +//! +//! `lattice-simd` takes precedence where both are enabled. The scalar path stays the +//! reference implementation that the backends are checked against. use crate::error::{Result, RuvectorError}; use crate::types::DistanceMetric; @@ -25,13 +34,25 @@ pub fn distance(a: &[f32], b: &[f32], metric: DistanceMetric) -> Result { /// Euclidean (L2) distance #[inline] pub fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 { - #[cfg(all(feature = "simd", not(target_arch = "wasm32")))] + #[cfg(feature = "lattice-simd")] + { + // Already sqrt-ed, matching this function's contract. + lattice_embed::simd::euclidean_distance(a, b) + } + #[cfg(all( + not(feature = "lattice-simd"), + feature = "simd", + not(target_arch = "wasm32") + ))] { (simsimd::SpatialSimilarity::sqeuclidean(a, b) .expect("SimSIMD euclidean failed") .sqrt()) as f32 } - #[cfg(any(not(feature = "simd"), target_arch = "wasm32"))] + #[cfg(all( + not(feature = "lattice-simd"), + any(not(feature = "simd"), target_arch = "wasm32") + ))] { // Unrolled scalar fallback for WASM — 4x unroll for ILP let len = a.len(); @@ -56,11 +77,25 @@ pub fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 { /// Cosine distance (1 - cosine_similarity) #[inline] pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { - #[cfg(all(feature = "simd", not(target_arch = "wasm32")))] + #[cfg(feature = "lattice-simd")] + { + // lattice returns similarity; this function's contract is 1 - similarity. + // Its kernels return 0.0 when either norm is exactly zero, so a zero vector + // yields 1.0 here, matching the scalar path below. + 1.0 - lattice_embed::simd::cosine_similarity(a, b) + } + #[cfg(all( + not(feature = "lattice-simd"), + feature = "simd", + not(target_arch = "wasm32") + ))] { simsimd::SpatialSimilarity::cosine(a, b).expect("SimSIMD cosine failed") as f32 } - #[cfg(any(not(feature = "simd"), target_arch = "wasm32"))] + #[cfg(all( + not(feature = "lattice-simd"), + any(not(feature = "simd"), target_arch = "wasm32") + ))] { // Single-pass cosine fallback for WASM — avoids 3x iteration overhead let (mut dot, mut norm_a_sq, mut norm_b_sq) = (0.0f32, 0.0f32, 0.0f32); @@ -81,12 +116,24 @@ pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { /// Dot product distance (negative for maximization) #[inline] pub fn dot_product_distance(a: &[f32], b: &[f32]) -> f32 { - #[cfg(all(feature = "simd", not(target_arch = "wasm32")))] + #[cfg(feature = "lattice-simd")] + { + // Negated, matching this function's maximization contract. + -lattice_embed::simd::dot_product(a, b) + } + #[cfg(all( + not(feature = "lattice-simd"), + feature = "simd", + not(target_arch = "wasm32") + ))] { let dot = simsimd::SpatialSimilarity::dot(a, b).expect("SimSIMD dot product failed"); (-dot) as f32 } - #[cfg(any(not(feature = "simd"), target_arch = "wasm32"))] + #[cfg(all( + not(feature = "lattice-simd"), + any(not(feature = "simd"), target_arch = "wasm32") + ))] { // Pure Rust fallback for WASM let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); @@ -172,6 +219,99 @@ mod tests { assert!((dist - 9.0).abs() < 0.01); // |1-4| + |2-5| + |3-6| = 9 } + /// Reference implementations, deliberately naive and backend-independent. + /// Whichever backend is compiled in must agree with these. + mod reference { + pub fn euclidean(a: &[f32], b: &[f32]) -> f32 { + a.iter() + .zip(b) + .map(|(x, y)| (x - y) * (x - y)) + .sum::() + .sqrt() + } + + pub fn cosine(a: &[f32], b: &[f32]) -> f32 { + let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum(); + let na = a.iter().map(|x| x * x).sum::().sqrt(); + let nb = b.iter().map(|x| x * x).sum::().sqrt(); + if na == 0.0 || nb == 0.0 { + 1.0 + } else { + 1.0 - dot / (na * nb) + } + } + + pub fn dot(a: &[f32], b: &[f32]) -> f32 { + -a.iter().zip(b).map(|(x, y)| x * y).sum::() + } + } + + /// Deterministic pseudo-random vectors, no dev-dependency needed. + fn vecs(dim: usize, seed: u32) -> (Vec, Vec) { + let mut s = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + let mut next = || { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + (s as f32 / u32::MAX as f32) * 2.0 - 1.0 + }; + ((0..dim).map(|_| next()).collect(), (0..dim).map(|_| next()).collect()) + } + + /// The active backend must agree with the scalar reference on every metric. + /// + /// This is what catches an adapter mistake: dropping the `1.0 -` on cosine or the + /// negation on dot product still compiles and still passes the loose + /// single-case assertions above, but fails here. + #[test] + fn test_backend_matches_scalar_reference() { + // Dimensions straddling the SIMD lane widths (4/8/16) and their remainders, + // so tail handling is exercised rather than assumed. + for dim in [1usize, 3, 4, 7, 8, 15, 16, 17, 31, 64, 127, 384, 768] { + for seed in 0..4u32 { + let (a, b) = vecs(dim, seed); + + let got = euclidean_distance(&a, &b); + let want = reference::euclidean(&a, &b); + assert!( + (got - want).abs() <= 1e-3 * want.abs().max(1.0), + "euclidean mismatch at dim={dim} seed={seed}: got {got}, want {want}" + ); + + let got = cosine_distance(&a, &b); + let want = reference::cosine(&a, &b); + assert!( + (got - want).abs() <= 1e-4, + "cosine mismatch at dim={dim} seed={seed}: got {got}, want {want}" + ); + + let got = dot_product_distance(&a, &b); + let want = reference::dot(&a, &b); + assert!( + (got - want).abs() <= 1e-3 * want.abs().max(1.0), + "dot mismatch at dim={dim} seed={seed}: got {got}, want {want}" + ); + } + } + } + + /// A zero vector must not produce NaN, and cosine distance must saturate at 1.0. + #[test] + fn test_zero_vector_is_not_nan() { + let zero = vec![0.0f32; 8]; + let other = vec![1.0f32; 8]; + + let d = cosine_distance(&zero, &other); + assert!(d.is_finite(), "cosine distance went non-finite: {d}"); + assert!( + (d - 1.0).abs() < 1e-6, + "zero vector should give cosine distance 1.0, got {d}" + ); + + assert!(euclidean_distance(&zero, &other).is_finite()); + assert!(dot_product_distance(&zero, &other).is_finite()); + } + #[test] fn test_dimension_mismatch() { let a = vec![1.0, 2.0]; diff --git a/crates/ruvector-wasm/Cargo.toml b/crates/ruvector-wasm/Cargo.toml index 1901233037..9b6235e3da 100644 --- a/crates/ruvector-wasm/Cargo.toml +++ b/crates/ruvector-wasm/Cargo.toml @@ -65,6 +65,12 @@ rand = { workspace = true } [features] default = [] simd = ["ruvector-core/simd"] +# Vectorized distance kernels that actually reach wasm32. `simd` above forwards +# to SimSIMD, which ruvector-core excludes on wasm32, so a browser build gets the +# scalar path from it. This one forwards to lattice-embed, whose kernels compile +# to `simd128`. Build with `RUSTFLAGS="-C target-feature=+simd128"`; without that +# flag the kernels fall back to scalar and the feature is a no-op. +lattice-simd = ["ruvector-core/lattice-simd"] # Collections and filter features (not available in WASM due to file I/O requirements) # These features are provided for completeness but will not work in browser WASM collections = ["dep:ruvector-collections", "dep:ruvector-filter"] From 0db0d729a3f90280bbaeaa037938cff2be99d117 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:17:48 -0400 Subject: [PATCH 02/10] style(core): rustfmt the distance backend test helper Formatting only, no behaviour change. Brings the new test helper in line with the repo's rustfmt settings so the Rustfmt check passes. --- crates/ruvector-core/src/distance.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ruvector-core/src/distance.rs b/crates/ruvector-core/src/distance.rs index 7ba9b1939e..18fe83949e 100644 --- a/crates/ruvector-core/src/distance.rs +++ b/crates/ruvector-core/src/distance.rs @@ -255,7 +255,10 @@ mod tests { s ^= s << 5; (s as f32 / u32::MAX as f32) * 2.0 - 1.0 }; - ((0..dim).map(|_| next()).collect(), (0..dim).map(|_| next()).collect()) + ( + (0..dim).map(|_| next()).collect(), + (0..dim).map(|_| next()).collect(), + ) } /// The active backend must agree with the scalar reference on every metric. From 798eaf8490cc0b0756120dc721b7f9c25b926d41 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:29:41 -0400 Subject: [PATCH 03/10] feat(core): route Manhattan through the lattice-simd backend Manhattan was the one metric that bypassed the backend split entirely and called `simd_intrinsics` directly. That dispatch covers x86_64 and aarch64 and falls through to scalar everywhere else, wasm32 included, so this module's claim that `lattice-simd` is the only backend that vectorizes on wasm held for three metrics and not for this one. lattice-embed 0.7.1 adds a runtime-dispatched L1 kernel (AVX-512F, AVX2, NEON, wasm32 SIMD128, each with a scalar fallback), so Manhattan now joins the same mutually exclusive cfg split as the other three and the claim is true for all four. The pin moves to 0.7.1 because `simd::manhattan_distance` does not exist in 0.7.0. `test_backend_matches_scalar_reference` covered euclidean, cosine and dot but not Manhattan, despite documenting itself as covering every metric. It now covers all four, with a scalar L1 reference alongside the existing three. Default builds keep the `simd_intrinsics` path unchanged. --- Cargo.lock | 10 +++++----- crates/ruvector-core/Cargo.toml | 2 +- crates/ruvector-core/src/distance.rs | 22 +++++++++++++++++++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb290c8a14..bed4337c50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4921,14 +4921,14 @@ dependencies = [ [[package]] name = "lattice-embed" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3af50558cf953df225b68ec160f1ea49b0965564f10a9b8577d15ac8452542" +checksum = "3a8471670d8eb3dc5b52b7c977f1be449a4d39404ebd47bd084a1e922fa6002e" dependencies = [ "async-trait", "blake3", "chrono", - "lattice-inference 0.7.0", + "lattice-inference 0.7.1", "lru 0.16.4", "parking_lot 0.12.5", "serde", @@ -4962,9 +4962,9 @@ dependencies = [ [[package]] name = "lattice-inference" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439a555d0b66e489e1c1291522143780fd9e0bbf0475c9f23fd299fa35df68a2" +checksum = "bc6ebeb70001f572be393e646e9207b2b10fc8c7649eb0b197d027bc87018585" dependencies = [ "clap", "half", diff --git a/crates/ruvector-core/Cargo.toml b/crates/ruvector-core/Cargo.toml index aba287a636..0ca316a29b 100644 --- a/crates/ruvector-core/Cargo.toml +++ b/crates/ruvector-core/Cargo.toml @@ -68,7 +68,7 @@ hf-hub = { version = "0.4", optional = true } # a per-feature `rust-version`, so enabling either feature raises the effective # MSRV above this crate's workspace-inherited 1.77 for anyone who turns it on, # the same way `simd-avx512` requires >= 1.89. The default build is unaffected. -lattice-embed = { version = "0.7.0", optional = true, default-features = false } +lattice-embed = { version = "0.7.1", optional = true, default-features = false } tokio = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/ruvector-core/src/distance.rs b/crates/ruvector-core/src/distance.rs index 18fe83949e..ce72573414 100644 --- a/crates/ruvector-core/src/distance.rs +++ b/crates/ruvector-core/src/distance.rs @@ -144,7 +144,16 @@ pub fn dot_product_distance(a: &[f32], b: &[f32]) -> f32 { /// Manhattan (L1) distance — delegates to SIMD when available #[inline] pub fn manhattan_distance(a: &[f32], b: &[f32]) -> f32 { - crate::simd_intrinsics::manhattan_distance_simd(a, b) + #[cfg(feature = "lattice-simd")] + { + lattice_embed::simd::manhattan_distance(a, b) + } + #[cfg(not(feature = "lattice-simd"))] + { + // `simd_intrinsics` dispatches x86_64 and aarch64 and falls through to + // scalar everywhere else, wasm32 included. + crate::simd_intrinsics::manhattan_distance_simd(a, b) + } } /// Batch distance calculation optimized with Rayon (native) or sequential (WASM) @@ -244,6 +253,10 @@ mod tests { pub fn dot(a: &[f32], b: &[f32]) -> f32 { -a.iter().zip(b).map(|(x, y)| x * y).sum::() } + + pub fn manhattan(a: &[f32], b: &[f32]) -> f32 { + a.iter().zip(b).map(|(x, y)| (x - y).abs()).sum::() + } } /// Deterministic pseudo-random vectors, no dev-dependency needed. @@ -294,6 +307,13 @@ mod tests { (got - want).abs() <= 1e-3 * want.abs().max(1.0), "dot mismatch at dim={dim} seed={seed}: got {got}, want {want}" ); + + let got = manhattan_distance(&a, &b); + let want = reference::manhattan(&a, &b); + assert!( + (got - want).abs() <= 1e-3 * want.abs().max(1.0), + "manhattan mismatch at dim={dim} seed={seed}: got {got}, want {want}" + ); } } } From 6dda6c6c8510f13fe810f18fc8868bb7ece3b889 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:13:21 -0400 Subject: [PATCH 04/10] chore(deps): keep tempfile on getrandom 0.3.4 The lockfile update that added the lattice-embed pins also moved tempfile's getrandom edge from 0.3.4 to 0.4.3. That bump is unrelated to this change and is not required by it: `cargo check -p ruvector-core --features lattice-simd --locked` resolves cleanly with the edge back on 0.3.4. `rand 0.10.1` keeps its own `getrandom 0.4.3` edge, which predates this branch and is untouched here. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index bed4337c50..28a979d0d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12692,7 +12692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", From 96922fb6e2dab2966f3865294d901143495b9147 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:07:18 -0400 Subject: [PATCH 05/10] test(core): pin distance backend selection and tiny-norm cosine contract - Document which call sites use the feature-gated distance adapters (generic/FlatIndex path) versus index/hnsw.rs, which dispatches its own kernels directly regardless of the lattice-simd feature. - Add a lattice-simd-gated regression test asserting each of the four public adapters (euclidean, cosine, dot product, manhattan) is bit-exact against lattice_embed's kernels, so a silent fallback to the scalar/SimSIMD path is caught even though the existing tolerance test would still pass. - Add a test documenting the intentional tiny-norm cosine divergence: the scalar path saturates cosine distance at 1.0 below a 1e-8 norm guard, while the lattice-simd kernel only short-circuits on an exactly-zero norm. --- crates/ruvector-core/src/distance.rs | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/crates/ruvector-core/src/distance.rs b/crates/ruvector-core/src/distance.rs index ce72573414..da902df468 100644 --- a/crates/ruvector-core/src/distance.rs +++ b/crates/ruvector-core/src/distance.rs @@ -9,6 +9,14 @@ //! //! `lattice-simd` takes precedence where both are enabled. The scalar path stays the //! reference implementation that the backends are checked against. +//! +//! ## Call sites +//! +//! The generic [`distance`] function and its four metric-specific adapters below back +//! the generic distance path (used by, e.g., [`crate::index::flat`]'s `FlatIndex`). +//! `crate::index::hnsw`'s HNSW index does not go through this module: it dispatches its +//! own kernels directly via `crate::simd_intrinsics` for every metric, so enabling +//! `lattice-simd` does not change HNSW's distance evaluations. use crate::error::{Result, RuvectorError}; use crate::types::DistanceMetric; @@ -342,4 +350,97 @@ mod tests { let result = distance(&a, &b, DistanceMetric::Euclidean); assert!(result.is_err()); } + + /// Recomputes what each adapter's contract says the `lattice-simd` backend must + /// produce, straight from `lattice_embed`'s kernels. This is the seam that pins + /// backend *selection* (not just arithmetic): the loose `test_backend_matches_scalar_reference` + /// tolerance above passes even if an adapter silently fell back to the scalar path, + /// but a bit-exact comparison against this function does not, since the scalar path's + /// summation order and rounding differ from the lattice kernels'. + #[cfg(feature = "lattice-simd")] + fn lattice_kernel_result(a: &[f32], b: &[f32], metric: DistanceMetric) -> f32 { + match metric { + DistanceMetric::Euclidean => lattice_embed::simd::euclidean_distance(a, b), + DistanceMetric::Cosine => 1.0 - lattice_embed::simd::cosine_similarity(a, b), + DistanceMetric::DotProduct => -lattice_embed::simd::dot_product(a, b), + DistanceMetric::Manhattan => lattice_embed::simd::manhattan_distance(a, b), + } + } + + /// Under `lattice-simd`, every public adapter must dispatch to the lattice kernels + /// bit-for-bit. If any one of the four adapters is edited to fall through to its + /// scalar or SimSIMD branch instead, this test fails even though the value stays + /// numerically close, because the two implementations round differently. + #[cfg(feature = "lattice-simd")] + #[test] + fn test_backend_selection_uses_lattice_simd() { + for dim in [1usize, 3, 4, 7, 8, 15, 16, 17, 31, 64, 127, 384, 768] { + for seed in 0..4u32 { + let (a, b) = vecs(dim, seed); + + for metric in [ + DistanceMetric::Euclidean, + DistanceMetric::Cosine, + DistanceMetric::DotProduct, + DistanceMetric::Manhattan, + ] { + let got = distance(&a, &b, metric).unwrap(); + let want = lattice_kernel_result(&a, &b, metric); + assert_eq!( + got.to_bits(), + want.to_bits(), + "{metric:?} did not dispatch to the lattice-simd kernel at dim={dim} seed={seed}: got {got}, want {want}" + ); + } + } + } + } + + /// Test-only mirror of the scalar cosine branch (lines ~100-113 above), kept in sync + /// deliberately so the tiny-norm threshold behavior can be pinned regardless of which + /// backend (`simd` or `lattice-simd`) is actually compiled in for a given build. + fn cosine_distance_scalar(a: &[f32], b: &[f32]) -> f32 { + let (mut dot, mut norm_a_sq, mut norm_b_sq) = (0.0f32, 0.0f32, 0.0f32); + for (&ai, &bi) in a.iter().zip(b.iter()) { + dot += ai * bi; + norm_a_sq += ai * ai; + norm_b_sq += bi * bi; + } + let denom = norm_a_sq.sqrt() * norm_b_sq.sqrt(); + if denom > 1e-8 { + 1.0 - (dot / denom) + } else { + 1.0 + } + } + + /// Documents an intentional divergence: for a norm strictly between 0 and 1e-8, the + /// scalar path's `denom > 1e-8` guard saturates cosine distance at 1.0, while the + /// `lattice-simd` kernel only short-circuits on an *exactly* zero norm and otherwise + /// computes the real cosine similarity. This is a deliberate contract difference + /// between the two backends at the sub-1e-8 boundary, not a bug. + #[test] + fn test_tiny_norm_cosine_divergence_is_intentional() { + // Norm here is ~1e-9, i.e. nonzero but well under the scalar path's 1e-8 guard. + let a = vec![1e-9f32, 0.0, 0.0, 0.0]; + let b = vec![1e-9f32, 0.0, 0.0, 0.0]; + + let scalar = cosine_distance_scalar(&a, &b); + assert!( + (scalar - 1.0).abs() < 1e-6, + "scalar path should saturate at 1.0 below its 1e-8 denom guard, got {scalar}" + ); + + #[cfg(feature = "lattice-simd")] + { + // These vectors are parallel, so the true cosine distance is ~0. The lattice + // kernel does not apply the scalar path's 1e-8 guard, so it should report + // that, diverging from the scalar path's 1.0 above. + let lattice = cosine_distance(&a, &b); + assert!( + lattice.is_finite() && lattice < 0.5, + "lattice path should not saturate at 1.0 for a tiny nonzero norm, got {lattice}" + ); + } + } } From 23e12c5e9d9a2e837b6daa4f3984fa15dec7c11f Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:40:02 -0400 Subject: [PATCH 06/10] test(core): cover the production scalar tiny-norm branch directly The tiny-norm cosine coverage previously called a hand-copied mirror of the scalar branch instead of the compiled production cosine_distance, so a regression in the real branch could leave the mirror green. The scalar assertion now calls cosine_distance itself, gated on the same cfg as its scalar branch, so it only asserts when that branch is the one actually compiled in. Also corrects the comment describing the saturation threshold: it guards the product of the two vector norms (norm_a_sq.sqrt() * norm_b_sq.sqrt()), not an individual norm. Co-Authored-By: Claude Sonnet 5 --- crates/ruvector-core/src/distance.rs | 56 +++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/crates/ruvector-core/src/distance.rs b/crates/ruvector-core/src/distance.rs index da902df468..673ac202d6 100644 --- a/crates/ruvector-core/src/distance.rs +++ b/crates/ruvector-core/src/distance.rs @@ -396,46 +396,42 @@ mod tests { } } - /// Test-only mirror of the scalar cosine branch (lines ~100-113 above), kept in sync - /// deliberately so the tiny-norm threshold behavior can be pinned regardless of which - /// backend (`simd` or `lattice-simd`) is actually compiled in for a given build. - fn cosine_distance_scalar(a: &[f32], b: &[f32]) -> f32 { - let (mut dot, mut norm_a_sq, mut norm_b_sq) = (0.0f32, 0.0f32, 0.0f32); - for (&ai, &bi) in a.iter().zip(b.iter()) { - dot += ai * bi; - norm_a_sq += ai * ai; - norm_b_sq += bi * bi; - } - let denom = norm_a_sq.sqrt() * norm_b_sq.sqrt(); - if denom > 1e-8 { - 1.0 - (dot / denom) - } else { - 1.0 - } - } - - /// Documents an intentional divergence: for a norm strictly between 0 and 1e-8, the - /// scalar path's `denom > 1e-8` guard saturates cosine distance at 1.0, while the + /// Documents an intentional divergence: when the product of the two vector norms + /// (`norm_a_sq.sqrt() * norm_b_sq.sqrt()`) is strictly between 0 and 1e-8, the scalar + /// path's `denom > 1e-8` guard saturates cosine distance at 1.0, while the /// `lattice-simd` kernel only short-circuits on an *exactly* zero norm and otherwise /// computes the real cosine similarity. This is a deliberate contract difference /// between the two backends at the sub-1e-8 boundary, not a bug. + /// + /// The scalar assertion below calls the compiled production `cosine_distance` itself, + /// under the same `cfg` as its scalar branch (`distance.rs:103-121`), rather than a + /// hand-copied mirror — so a regression in that branch (e.g. dropping the saturation + /// guard) fails this test instead of leaving a separately-maintained copy green. #[test] fn test_tiny_norm_cosine_divergence_is_intentional() { - // Norm here is ~1e-9, i.e. nonzero but well under the scalar path's 1e-8 guard. - let a = vec![1e-9f32, 0.0, 0.0, 0.0]; - let b = vec![1e-9f32, 0.0, 0.0, 0.0]; - - let scalar = cosine_distance_scalar(&a, &b); - assert!( - (scalar - 1.0).abs() < 1e-6, - "scalar path should saturate at 1.0 below its 1e-8 denom guard, got {scalar}" - ); + #[cfg(all( + not(feature = "lattice-simd"), + any(not(feature = "simd"), target_arch = "wasm32") + ))] + { + // Each vector's norm is ~1e-9 (nonzero), so their product — the scalar + // path's denom — is ~1e-18, well under its 1e-8 guard threshold. + let a = vec![1e-9f32, 0.0, 0.0, 0.0]; + let b = vec![1e-9f32, 0.0, 0.0, 0.0]; + let scalar = cosine_distance(&a, &b); + assert!( + (scalar - 1.0).abs() < 1e-6, + "scalar path should saturate at 1.0 below its 1e-8 denom guard, got {scalar}" + ); + } #[cfg(feature = "lattice-simd")] { // These vectors are parallel, so the true cosine distance is ~0. The lattice // kernel does not apply the scalar path's 1e-8 guard, so it should report - // that, diverging from the scalar path's 1.0 above. + // that, diverging from the scalar path's saturation-at-1.0 behavior above. + let a = vec![1e-9f32, 0.0, 0.0, 0.0]; + let b = vec![1e-9f32, 0.0, 0.0, 0.0]; let lattice = cosine_distance(&a, &b); assert!( lattice.is_finite() && lattice < 0.5, From 76568110aa23bd4f2529bca11f3f48e618c7b854 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:13:25 -0400 Subject: [PATCH 07/10] test(wasm): add committed opcode-count check for lattice-simd on wasm32 ruvector-wasm's lattice-simd feature claims to vectorize on wasm32 via lattice-embed's simd128 kernels, but nothing in the repo reproduced that build-time. Add scripts/check_wasm_simd.sh, which builds ruvector-wasm for wasm32-unknown-unknown with and without RUSTFLAGS='-C target-feature=+simd128' (feature lattice-simd enabled in both), disassembles each .wasm with wasm-objdump, and counts SIMD128 opcodes. It fails closed: missing wasm32 target or wasm-objdump aborts before any build runs, a missing or empty .wasm artifact is treated as a failure rather than a zero count, and the with-flag build must both emit at least one SIMD128 opcode and carry strictly more than the without-flag control. On the current head this measures 1007 SIMD128 opcodes with the flag and 0 without it. --- scripts/check_wasm_simd.sh | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100755 scripts/check_wasm_simd.sh diff --git a/scripts/check_wasm_simd.sh b/scripts/check_wasm_simd.sh new file mode 100755 index 0000000000..f4631a01c5 --- /dev/null +++ b/scripts/check_wasm_simd.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Verifies that ruvector-wasm's `lattice-simd` feature actually vectorizes on +# wasm32: builds the crate with and without `-C target-feature=+simd128` and +# counts SIMD128 opcodes in each emitted .wasm artifact. Fails closed on any +# missing prerequisite, missing/empty artifact, or unmet opcode condition. +set -euo pipefail + +PACKAGE="ruvector-wasm" +RUST_TARGET="wasm32-unknown-unknown" +FEATURE="lattice-simd" +ARTIFACT="ruvector_wasm.wasm" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +BASE_TARGET_DIR="${CARGO_TARGET_DIR:-target}/wasm-simd-check" +SIMD_TARGET_DIR="$BASE_TARGET_DIR/simd128" +CONTROL_TARGET_DIR="$BASE_TARGET_DIR/control" + +echo "== Prerequisites ==" + +if ! rustup target list --installed 2>/dev/null | grep -qx "$RUST_TARGET"; then + echo "FAIL: rust target '$RUST_TARGET' is not installed." >&2 + echo " Install it with: rustup target add $RUST_TARGET" >&2 + exit 1 +fi +echo "OK: $RUST_TARGET target installed" + +if ! command -v wasm-objdump >/dev/null 2>&1; then + echo "FAIL: wasm-objdump not found on PATH (ships with WABT)." >&2 + echo " Install it with: brew install wabt (macOS) or apt-get install wabt (Linux)." >&2 + exit 1 +fi +echo "OK: wasm-objdump found at $(command -v wasm-objdump)" + +count_simd128_opcodes() { + local wasm_file="$1" + if [[ ! -s "$wasm_file" ]]; then + echo "FAIL: expected wasm artifact at '$wasm_file' but it is missing or empty." >&2 + exit 1 + fi + + local disasm + if ! disasm="$(wasm-objdump -d "$wasm_file")"; then + echo "FAIL: wasm-objdump could not disassemble '$wasm_file'." >&2 + exit 1 + fi + + # grep -c exits 1 on zero matches, which is a legitimate count here (the + # control arm below), so neutralize that with `|| true` rather than + # letting set -e/pipefail treat "no opcodes found" as a script error. + local count + count="$(grep -Ec '\b(v128|i8x16|i16x8|i32x4|i64x2|f32x4|f64x2)\.[a-z_0-9]+' <<<"$disasm" || true)" + echo "$count" +} + +build_artifact() { + local target_dir="$1" + local rustflags="$2" + CARGO_TARGET_DIR="$target_dir" RUSTFLAGS="$rustflags" \ + cargo build --release -p "$PACKAGE" --target "$RUST_TARGET" --features "$FEATURE" 1>&2 + echo "$target_dir/$RUST_TARGET/release/$ARTIFACT" +} + +echo "" +echo "== Arm A: RUSTFLAGS='-C target-feature=+simd128', --features $FEATURE ==" +SIMD_WASM="$(build_artifact "$SIMD_TARGET_DIR" "-C target-feature=+simd128")" +SIMD_COUNT="$(count_simd128_opcodes "$SIMD_WASM")" +echo "SIMD128 opcode count: $SIMD_COUNT" +if (( SIMD_COUNT <= 0 )); then + echo "FAIL: expected > 0 SIMD128 opcodes with +simd128 and --features $FEATURE, got $SIMD_COUNT." >&2 + exit 1 +fi + +echo "" +echo "== Arm B (control): no target-feature flag, --features $FEATURE ==" +CONTROL_WASM="$(build_artifact "$CONTROL_TARGET_DIR" "")" +CONTROL_COUNT="$(count_simd128_opcodes "$CONTROL_WASM")" +echo "SIMD128 opcode count: $CONTROL_COUNT" + +# Without the target-feature flag, lattice-embed's wasm32 kernels take their +# scalar fallback (crates/ruvector-core/src/distance.rs), so this arm +# currently measures 0 SIMD128 opcodes. A future dependency could +# legitimately contribute some vector code even without the flag, so this +# asserts the delta direction (control strictly below the +simd128 build) +# rather than hard-coding zero. +if (( CONTROL_COUNT >= SIMD_COUNT )); then + echo "FAIL: expected the control build to carry fewer SIMD128 opcodes than the +simd128 build (control=$CONTROL_COUNT, simd128=$SIMD_COUNT)." >&2 + exit 1 +fi + +echo "" +echo "== PASS ==" +echo "+simd128 build: $SIMD_COUNT SIMD128 opcodes" +echo "control build: $CONTROL_COUNT SIMD128 opcodes" From 77fa4f9a001115c0e73aafa329a9d4649fb10ed2 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:48:56 -0400 Subject: [PATCH 08/10] fix(wasm): close two fail-open paths in the wasm SIMD128 gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_wasm_simd.sh could exit 0 with `== PASS ==` on a genuinely broken build: `cargo build` ran inside a `$(...)` command substitution where a non-final failing command doesn't reliably abort the script under `errexit`, and because the per-arm target dirs persist across runs, a stale artifact from a prior successful build satisfied the downstream non-empty check. Fix by checking the build's exit status explicitly inside build_artifact (and again at both call sites) and by deleting the expected artifact before each build attempt. Separately, `grep -Ec ... || true` folded a real grep failure (status 2+) into the same bucket as a legitimate zero-match count (status 1), producing a blank count that bash arithmetic silently treats as zero — indistinguishable from the control arm's expected zero-opcode result. Capture grep's real exit status, fail loudly on anything other than 0/1, and validate both opcode counts are numeric before any arithmetic. Also corrects the `lattice-simd` feature comment in crates/ruvector-wasm/Cargo.toml: the simsimd dependency is not excluded on wasm32 (only its call sites are cfg-gated on `not(target_arch = "wasm32")`), and disabling the flag does not make the feature a no-op — distance calls still run through lattice-embed's own scalar fallback, whose exact-zero cosine norm guard differs from ruvector-core's own `denom <= 1e-8` scalar guard. Comment-only change; build graph is unchanged. --- crates/ruvector-wasm/Cargo.toml | 16 +++++--- scripts/check_wasm_simd.sh | 65 +++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/crates/ruvector-wasm/Cargo.toml b/crates/ruvector-wasm/Cargo.toml index 9b6235e3da..4ab1c905e5 100644 --- a/crates/ruvector-wasm/Cargo.toml +++ b/crates/ruvector-wasm/Cargo.toml @@ -65,11 +65,17 @@ rand = { workspace = true } [features] default = [] simd = ["ruvector-core/simd"] -# Vectorized distance kernels that actually reach wasm32. `simd` above forwards -# to SimSIMD, which ruvector-core excludes on wasm32, so a browser build gets the -# scalar path from it. This one forwards to lattice-embed, whose kernels compile -# to `simd128`. Build with `RUSTFLAGS="-C target-feature=+simd128"`; without that -# flag the kernels fall back to scalar and the feature is a no-op. +# Vectorized distance kernels that actually reach wasm32. `simd` above calls +# SimSIMD, but ruvector-core gates those call sites on +# `not(target_arch = "wasm32")` (the simsimd dependency itself still resolves +# on wasm32; only the call sites are excluded), so a browser build takes +# ruvector-core's own scalar fallback instead. This one forwards to +# lattice-embed, whose kernels compile to `simd128`. Build with +# `RUSTFLAGS="-C target-feature=+simd128"`; without that flag, SIMD128 +# acceleration is absent and distance calls run through lattice-embed's own +# scalar fallback — not a behavioral no-op, since its exact-zero cosine norm +# guard differs from ruvector-core's own scalar path, which saturates at +# `denom <= 1e-8` (see ruvector-core/src/distance.rs `cosine_distance`). lattice-simd = ["ruvector-core/lattice-simd"] # Collections and filter features (not available in WASM due to file I/O requirements) # These features are provided for completeness but will not work in browser WASM diff --git a/scripts/check_wasm_simd.sh b/scripts/check_wasm_simd.sh index f4631a01c5..1f1077a167 100755 --- a/scripts/check_wasm_simd.sh +++ b/scripts/check_wasm_simd.sh @@ -2,7 +2,8 @@ # Verifies that ruvector-wasm's `lattice-simd` feature actually vectorizes on # wasm32: builds the crate with and without `-C target-feature=+simd128` and # counts SIMD128 opcodes in each emitted .wasm artifact. Fails closed on any -# missing prerequisite, missing/empty artifact, or unmet opcode condition. +# missing prerequisite, missing/empty artifact, build failure, grep failure, +# or unmet opcode condition. set -euo pipefail PACKAGE="ruvector-wasm" @@ -46,26 +47,61 @@ count_simd128_opcodes() { exit 1 fi - # grep -c exits 1 on zero matches, which is a legitimate count here (the - # control arm below), so neutralize that with `|| true` rather than - # letting set -e/pipefail treat "no opcodes found" as a script error. - local count - count="$(grep -Ec '\b(v128|i8x16|i16x8|i32x4|i64x2|f32x4|f64x2)\.[a-z_0-9]+' <<<"$disasm" || true)" + # grep -c exits 1 on zero matches (a legitimate count: the control arm is + # expected to land here) and 0 on a match; capture the real status + # ourselves rather than let `|| true` fold every other status (2+: a real + # grep failure) into the same "zero" bucket a broken grep invocation would + # otherwise be indistinguishable from a genuine zero-opcode build. + local count grep_status + count="$(grep -Ec '\b(v128|i8x16|i16x8|i32x4|i64x2|f32x4|f64x2)\.[a-z_0-9]+' <<<"$disasm")" && grep_status=0 || grep_status=$? + if (( grep_status != 0 && grep_status != 1 )); then + echo "FAIL: grep exited $grep_status while counting SIMD128 opcodes for '$wasm_file'." >&2 + exit 1 + fi + if ! [[ "$count" =~ ^[0-9]+$ ]]; then + echo "FAIL: grep produced a non-numeric SIMD128 opcode count ('$count') for '$wasm_file'." >&2 + exit 1 + fi + echo "$count" } build_artifact() { local target_dir="$1" local rustflags="$2" - CARGO_TARGET_DIR="$target_dir" RUSTFLAGS="$rustflags" \ - cargo build --release -p "$PACKAGE" --target "$RUST_TARGET" --features "$FEATURE" 1>&2 - echo "$target_dir/$RUST_TARGET/release/$ARTIFACT" + local artifact_path="$target_dir/$RUST_TARGET/release/$ARTIFACT" + + # Delete any artifact left over from a previous run of this script before + # building: target dirs persist across invocations, so a failed build must + # not be able to fall through to a stale non-empty artifact satisfying the + # downstream "exists and is non-empty" check. + rm -f "$artifact_path" + + # Check the build's exit status explicitly instead of leaning on `set -e`: + # this call sits inside a function invoked via command substitution + # (`X="$(build_artifact ...)"`), and a failing non-final command in that + # position does not reliably abort the script under `errexit` — only the + # function's own final exit status, captured here, does. + if ! CARGO_TARGET_DIR="$target_dir" RUSTFLAGS="$rustflags" \ + cargo build --release -p "$PACKAGE" --target "$RUST_TARGET" --features "$FEATURE" 1>&2; then + echo "FAIL: cargo build failed (CARGO_TARGET_DIR=$target_dir, RUSTFLAGS='$rustflags')." >&2 + return 1 + fi + + echo "$artifact_path" } echo "" echo "== Arm A: RUSTFLAGS='-C target-feature=+simd128', --features $FEATURE ==" -SIMD_WASM="$(build_artifact "$SIMD_TARGET_DIR" "-C target-feature=+simd128")" +if ! SIMD_WASM="$(build_artifact "$SIMD_TARGET_DIR" "-C target-feature=+simd128")"; then + echo "FAIL: build_artifact failed for the +simd128 arm." >&2 + exit 1 +fi SIMD_COUNT="$(count_simd128_opcodes "$SIMD_WASM")" +if ! [[ "$SIMD_COUNT" =~ ^[0-9]+$ ]]; then + echo "FAIL: non-numeric SIMD128 opcode count for the +simd128 arm: '$SIMD_COUNT'." >&2 + exit 1 +fi echo "SIMD128 opcode count: $SIMD_COUNT" if (( SIMD_COUNT <= 0 )); then echo "FAIL: expected > 0 SIMD128 opcodes with +simd128 and --features $FEATURE, got $SIMD_COUNT." >&2 @@ -74,8 +110,15 @@ fi echo "" echo "== Arm B (control): no target-feature flag, --features $FEATURE ==" -CONTROL_WASM="$(build_artifact "$CONTROL_TARGET_DIR" "")" +if ! CONTROL_WASM="$(build_artifact "$CONTROL_TARGET_DIR" "")"; then + echo "FAIL: build_artifact failed for the control arm." >&2 + exit 1 +fi CONTROL_COUNT="$(count_simd128_opcodes "$CONTROL_WASM")" +if ! [[ "$CONTROL_COUNT" =~ ^[0-9]+$ ]]; then + echo "FAIL: non-numeric SIMD128 opcode count for the control arm: '$CONTROL_COUNT'." >&2 + exit 1 +fi echo "SIMD128 opcode count: $CONTROL_COUNT" # Without the target-feature flag, lattice-embed's wasm32 kernels take their From 3b181ca2066ea45d7908ae2cd1692f9f030a7bdc Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:12:54 -0400 Subject: [PATCH 09/10] fix(wasm): check rm exit status before rebuilding stale artifact check_wasm_simd.sh's build_artifact() deleted the previous artifact with an unchecked `rm -f` before invoking cargo. If rm failed (e.g. EBUSY, a permissions error, or a full/read-only filesystem), the function continued into a cache-fresh cargo build that could exit 0 without rewriting the artifact, leaving the script to disassemble the stale file and report PASS on a build that never actually produced the artifact it was checking. Check the exit status of rm and fail closed: if ! rm -f "$artifact_path"; then echo "FAIL: could not remove stale artifact '$artifact_path'." >&2 return 1 fi Co-Authored-By: Claude Sonnet 5 --- scripts/check_wasm_simd.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/check_wasm_simd.sh b/scripts/check_wasm_simd.sh index 1f1077a167..6fe97c45d0 100755 --- a/scripts/check_wasm_simd.sh +++ b/scripts/check_wasm_simd.sh @@ -75,7 +75,10 @@ build_artifact() { # building: target dirs persist across invocations, so a failed build must # not be able to fall through to a stale non-empty artifact satisfying the # downstream "exists and is non-empty" check. - rm -f "$artifact_path" + if ! rm -f "$artifact_path"; then + echo "FAIL: could not remove stale artifact '$artifact_path'." >&2 + return 1 + fi # Check the build's exit status explicitly instead of leaning on `set -e`: # this call sits inside a function invoked via command substitution From ddcce8c7dd5f2a64393f08f9910dd9ff834f58cf Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:36:10 -0400 Subject: [PATCH 10/10] docs(core): correct wasm32 SimSIMD gating and Manhattan backend docs - Cargo.toml lattice-simd feature comment: SimSIMD's call sites are gated off on wasm32, not the dependency itself (it still resolves there per cargo tree). Match the accurate phrasing in ruvector-wasm/Cargo.toml. - distance.rs module docs: scope the three-backend description (lattice-simd/simd/scalar) to Euclidean, cosine, and dot. Manhattan uses simd_intrinsics's x86_64/aarch64 dispatch by default and was incorrectly implied to follow the same three-way split. --- crates/ruvector-core/Cargo.toml | 8 +++++--- crates/ruvector-core/src/distance.rs | 10 ++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/ruvector-core/Cargo.toml b/crates/ruvector-core/Cargo.toml index 0ca316a29b..9b49e5fb4b 100644 --- a/crates/ruvector-core/Cargo.toml +++ b/crates/ruvector-core/Cargo.toml @@ -142,9 +142,11 @@ lattice-embeddings = [ "dep:tokio", ] # Route the f32 distance kernels through lattice-embed instead of SimSIMD. -# Unlike `simd`, this one covers wasm32 (via `simd128`), where SimSIMD is -# unavailable and the distance path is otherwise scalar. Takes precedence over -# `simd` where both are enabled. Off by default. +# Unlike `simd`, this one covers wasm32 (via `simd128`): SimSIMD's call sites +# are gated off on wasm32 (the simsimd dependency itself still resolves +# there), so without this feature the generic distance path falls back to +# scalar on wasm32. Takes precedence over `simd` where both are enabled. Off +# by default. lattice-simd = ["dep:lattice-embed"] [lib] diff --git a/crates/ruvector-core/src/distance.rs b/crates/ruvector-core/src/distance.rs index 673ac202d6..a386e9fa7d 100644 --- a/crates/ruvector-core/src/distance.rs +++ b/crates/ruvector-core/src/distance.rs @@ -1,15 +1,21 @@ //! SIMD-optimized distance metrics //! -//! Three mutually exclusive backends, selected at compile time: +//! For Euclidean, cosine, and dot, three mutually exclusive backends are selected at +//! compile time: //! //! - `lattice-simd`: `lattice-embed`'s kernels. Covers wasm32 (`simd128`) as well as //! x86_64 and aarch64, so it is the only backend that vectorizes on wasm. -//! - `simd` on non-wasm: SimSIMD. +//! - `simd` on non-wasm: SimSIMD. Its call sites are excluded on wasm32 (the simsimd +//! dependency itself still resolves there), so scalar is used on wasm32 unless +//! `lattice-simd` is enabled. //! - otherwise: the portable scalar path. //! //! `lattice-simd` takes precedence where both are enabled. The scalar path stays the //! reference implementation that the backends are checked against. //! +//! Manhattan is not part of this split: it uses [`crate::simd_intrinsics`]'s +//! x86_64/aarch64 dispatch by default, regardless of the `simd` feature. +//! //! ## Call sites //! //! The generic [`distance`] function and its four metric-specific adapters below back