From e8cadc792ee7d0691146a555b04a46b754854bb0 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:49:17 -0400 Subject: [PATCH 1/4] feat(spann): add an optional lattice-simd distance backend 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. --- Cargo.lock | 23 ++++- crates/ruvector-spann/Cargo.toml | 15 +++ crates/ruvector-spann/src/distance.rs | 136 +++++++++++++++++++++++++- 3 files changed, 170 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 854cd017f6..45181da9e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4938,6 +4938,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "lattice-embed" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3af50558cf953df225b68ec160f1ea49b0965564f10a9b8577d15ac8452542" +dependencies = [ + "async-trait", + "blake3", + "chrono", + "lru 0.16.4", + "parking_lot 0.12.5", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "lattice-inference" version = "0.6.1" @@ -9203,7 +9221,7 @@ dependencies = [ "dashmap 6.2.1", "hf-hub", "hnsw_rs", - "lattice-embed", + "lattice-embed 0.6.1", "memmap2", "mockall", "ndarray 0.16.1", @@ -10603,6 +10621,9 @@ dependencies = [ [[package]] name = "ruvector-spann" version = "0.1.0" +dependencies = [ + "lattice-embed 0.7.0", +] [[package]] name = "ruvector-sparse-inference" diff --git a/crates/ruvector-spann/Cargo.toml b/crates/ruvector-spann/Cargo.toml index e65ae137f8..44d1bfbea4 100644 --- a/crates/ruvector-spann/Cargo.toml +++ b/crates/ruvector-spann/Cargo.toml @@ -10,6 +10,21 @@ repository = "https://github.com/ruvnet/ruvector" name = "benchmark" path = "src/bin/benchmark.rs" +[features] +default = [] +# Route the partition index's inner products through lattice-embed's +# runtime-dispatched SIMD kernels instead of the scalar loops. +# +# Opt-in and off by default. lattice-embed requires Rust >= 1.93 (edition +# 2024) and Cargo cannot express a per-feature `rust-version`, so enabling +# this raises the effective MSRV for anyone who turns it on. The default +# build keeps this crate's dependency set empty and is unaffected. +lattice-simd = ["dep:lattice-embed"] + [dependencies] +# `default-features = false` keeps this to the SIMD kernels: the model, +# tokenizer, and download stack sit behind lattice-embed's `native` feature +# and are not pulled in here. +lattice-embed = { version = "0.7.0", optional = true, default-features = false } [dev-dependencies] diff --git a/crates/ruvector-spann/src/distance.rs b/crates/ruvector-spann/src/distance.rs index 7d71a8e232..211aaf6f92 100644 --- a/crates/ruvector-spann/src/distance.rs +++ b/crates/ruvector-spann/src/distance.rs @@ -1,9 +1,35 @@ //! Distance computation for SPANN partition index. +//! +//! Two backends compute the same quantities. The default is the scalar code +//! that has always been here. With the `lattice-simd` feature the inner +//! products come from `lattice-embed`'s runtime-dispatched SIMD kernels +//! (AVX-512 / AVX2 / NEON / wasm32 SIMD128, with its own scalar fallback). +//! +//! The backend split covers the inner products only. Length handling and the +//! small-norm guard live outside it, so both backends take the same branches +//! and differ only in how the sums are accumulated. /// Compute L2 squared distance between two f32 slices. #[inline] pub fn l2_squared(a: &[f32], b: &[f32]) -> f32 { debug_assert_eq!(a.len(), b.len()); + + #[cfg(feature = "lattice-simd")] + { + // Only the equal-length case is routed. lattice returns f32::MAX for a + // length mismatch where the scalar path below truncates to the shorter + // slice, so guarding here keeps the two backends from disagreeing on + // an input the debug assertion already calls a caller bug. + if a.len() == b.len() { + return lattice_embed::simd::squared_euclidean_distance(a, b); + } + } + + l2_squared_scalar(a, b) +} + +#[inline] +fn l2_squared_scalar(a: &[f32], b: &[f32]) -> f32 { a.iter() .zip(b.iter()) .map(|(x, y)| { @@ -18,15 +44,39 @@ pub fn l2_squared(a: &[f32], b: &[f32]) -> f32 { #[inline] pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { debug_assert_eq!(a.len(), b.len()); - let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); - let norm_a: f32 = a.iter().map(|x| x * x).sum::().sqrt(); - let norm_b: f32 = b.iter().map(|x| x * x).sum::().sqrt(); + let (dot, norm_sq_a, norm_sq_b) = inner_products(a, b); + let norm_a = norm_sq_a.sqrt(); + let norm_b = norm_sq_b.sqrt(); if norm_a < 1e-9 || norm_b < 1e-9 { return 1.0; } 1.0 - dot / (norm_a * norm_b) } +/// Returns `(dot(a, b), dot(a, a), dot(b, b))`. +/// +/// `lattice_embed::simd::cosine_similarity` is deliberately not used here: it +/// applies its own zero-norm rule, while this module's contract is a 1e-9 +/// threshold that returns 1.0. Composing the distance from three inner +/// products keeps that threshold the single place either backend decides it. +#[inline] +fn inner_products(a: &[f32], b: &[f32]) -> (f32, f32, f32) { + #[cfg(feature = "lattice-simd")] + { + // lattice's dot_product returns 0.0 on a length mismatch, which would + // read as a zero norm and short-circuit to 1.0. Route equal lengths only. + if a.len() == b.len() { + use lattice_embed::simd::dot_product; + return (dot_product(a, b), dot_product(a, a), dot_product(b, b)); + } + } + + let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); + let norm_sq_a: f32 = a.iter().map(|x| x * x).sum(); + let norm_sq_b: f32 = b.iter().map(|x| x * x).sum(); + (dot, norm_sq_a, norm_sq_b) +} + #[cfg(test)] mod tests { use super::*; @@ -56,4 +106,84 @@ mod tests { let b = vec![0.0f32, 1.0]; assert!((cosine_distance(&a, &b) - 1.0).abs() < 1e-6); } + + fn reference_l2_squared(a: &[f32], b: &[f32]) -> f32 { + let mut acc = 0.0f64; + for (x, y) in a.iter().zip(b.iter()) { + let d = f64::from(*x) - f64::from(*y); + acc += d * d; + } + acc as f32 + } + + fn reference_cosine_distance(a: &[f32], b: &[f32]) -> f32 { + let mut dot = 0.0f64; + let mut na = 0.0f64; + let mut nb = 0.0f64; + for (x, y) in a.iter().zip(b.iter()) { + dot += f64::from(*x) * f64::from(*y); + na += f64::from(*x) * f64::from(*x); + nb += f64::from(*y) * f64::from(*y); + } + let (na, nb) = (na.sqrt() as f32, nb.sqrt() as f32); + if na < 1e-9 || nb < 1e-9 { + return 1.0; + } + 1.0 - (dot as f32) / (na * nb) + } + + fn pair(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(), + ) + } + + /// Whichever backend is compiled must agree with an f64 reference. + /// + /// Dimensions straddle the 4/8/16-lane widths and the unrolled chunk sizes + /// a SIMD backend uses, so remainder handling is exercised rather than + /// assumed. Running it under both feature settings holds a default build + /// and a `lattice-simd` build to one reference. + #[test] + fn backend_matches_reference() { + for dim in [ + 1usize, 3, 4, 7, 8, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 384, 768, + ] { + for seed in 0..4u32 { + let (a, b) = pair(dim, seed); + + let got = l2_squared(&a, &b); + let want = reference_l2_squared(&a, &b); + assert!( + (got - want).abs() <= 1e-3 * want.abs().max(1.0), + "l2_squared dim={dim} seed={seed}: {got} vs {want}" + ); + + let got = cosine_distance(&a, &b); + let want = reference_cosine_distance(&a, &b); + assert!( + (got - want).abs() <= 1e-4, + "cosine_distance dim={dim} seed={seed}: {got} vs {want}" + ); + } + } + } + + /// A zero vector must take the small-norm branch, not produce a NaN. + #[test] + fn cosine_zero_vector_is_not_nan() { + let zero = vec![0.0f32; 64]; + let (v, _) = pair(64, 9); + assert_eq!(cosine_distance(&zero, &v), 1.0); + assert_eq!(cosine_distance(&v, &zero), 1.0); + assert_eq!(cosine_distance(&zero, &zero), 1.0); + } } From 6ac9301fd117cb2943dab7791bc4efa6ed6416e9 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:37:46 -0400 Subject: [PATCH 2/4] chore(deps): pin lattice-embed 0.7.1 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. --- Cargo.lock | 6 +++--- crates/ruvector-spann/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45181da9e4..789b22e4dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4940,9 +4940,9 @@ 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", @@ -10622,7 +10622,7 @@ dependencies = [ name = "ruvector-spann" version = "0.1.0" dependencies = [ - "lattice-embed 0.7.0", + "lattice-embed 0.7.1", ] [[package]] diff --git a/crates/ruvector-spann/Cargo.toml b/crates/ruvector-spann/Cargo.toml index 44d1bfbea4..faea54f6d0 100644 --- a/crates/ruvector-spann/Cargo.toml +++ b/crates/ruvector-spann/Cargo.toml @@ -25,6 +25,6 @@ lattice-simd = ["dep:lattice-embed"] # `default-features = false` keeps this to the SIMD kernels: the model, # tokenizer, and download stack sit behind lattice-embed's `native` feature # and are not pulled in here. -lattice-embed = { version = "0.7.0", optional = true, default-features = false } +lattice-embed = { version = "0.7.1", optional = true, default-features = false } [dev-dependencies] From 87f7e3a0d449c22e0fd333d228892d82a9f09be6 Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:05:56 -0400 Subject: [PATCH 3/4] test(spann): pin lattice backend selection; correct dependency docs 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 --- crates/ruvector-spann/src/distance.rs | 35 +++++++++++++++++++ .../README.md | 4 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/ruvector-spann/src/distance.rs b/crates/ruvector-spann/src/distance.rs index 211aaf6f92..a9d92ce86a 100644 --- a/crates/ruvector-spann/src/distance.rs +++ b/crates/ruvector-spann/src/distance.rs @@ -186,4 +186,39 @@ mod tests { assert_eq!(cosine_distance(&v, &zero), 1.0); assert_eq!(cosine_distance(&zero, &zero), 1.0); } + + /// Guards against a silent reversion of the `lattice-simd` routing back + /// to the scalar loops. `backend_matches_reference` above cannot catch + /// that: it would still pass if the routed calls were replaced by the + /// scalar functions, since both agree with the f64 reference within + /// tolerance. This test instead demands the routed result differ from + /// the scalar result bit-for-bit, which only holds if a distinct + /// (SIMD-ordered) summation actually ran. If the lattice calls at the + /// routing sites are ever swapped back for `l2_squared_scalar` / the + /// inline scalar sum, `l2_squared` and the dot product become the exact + /// same computation as their scalar counterparts and this test fails. + #[cfg(feature = "lattice-simd")] + #[test] + fn lattice_backend_is_actually_selected() { + let (a, b) = pair(768, 7); + + let l2_scalar = l2_squared_scalar(&a, &b); + let l2_routed = l2_squared(&a, &b); + assert_ne!( + l2_routed.to_bits(), + l2_scalar.to_bits(), + "l2_squared matched the scalar sum bit-for-bit with lattice-simd \ + enabled; the routing at distance.rs may have reverted to scalar" + ); + + let dot_scalar: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); + let (dot_routed, _, _) = inner_products(&a, &b); + assert_ne!( + dot_routed.to_bits(), + dot_scalar.to_bits(), + "cosine's dot product matched the scalar sum bit-for-bit with \ + lattice-simd enabled; the routing at distance.rs may have \ + reverted to scalar" + ); + } } diff --git a/docs/research/nightly/2026-06-24-spann-partition-spill/README.md b/docs/research/nightly/2026-06-24-spann-partition-spill/README.md index 0e60e05e74..3621132808 100644 --- a/docs/research/nightly/2026-06-24-spann-partition-spill/README.md +++ b/docs/research/nightly/2026-06-24-spann-partition-spill/README.md @@ -267,7 +267,9 @@ The witness log integration path: each spill decision (vector id, primary partit ## Edge and WASM Implications -`ruvector-spann` has zero external dependencies (`[dependencies]` is empty in `Cargo.toml`). Distance computation uses pure Rust scalar arithmetic. This makes it `no_std`-compatible with a static data source and suitable for WASM compilation via `wasm-pack`. +`ruvector-spann`'s default feature configuration resolves no normal dependencies (`cargo tree -e normal` is a single node). Distance computation uses pure Rust scalar arithmetic in this configuration, making the default build `no_std`-compatible with a static data source and suitable for WASM compilation via `wasm-pack`. + +The optional `lattice-simd` feature adds `lattice-embed` for runtime-dispatched SIMD distance kernels and requires Rust 1.93. The edge and WASM guidance above applies to the default (scalar) feature configuration; enabling `lattice-simd` pulls in that dependency's MSRV and its own WASM SIMD128 support, and should be evaluated separately for constrained deployment targets. For Cognitum Seed edge deployment: a pre-built index (centroid matrix + serialized partition lists) can be compiled into a WASM binary at deploy time and queried in the browser or on constrained hardware without any runtime index building. Build-time spilling means the edge device never needs to re-evaluate the spill condition — the partitions are pre-computed. From 02de2b39dc44a4b9a249b2147f0e63a5329b98ce Mon Sep 17 00:00:00 2001 From: OceanLi <122793010+ohdearquant@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:40:59 -0400 Subject: [PATCH 4/4] test(spann): witness the lattice distance backend after each call 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. --- crates/ruvector-spann/src/distance.rs | 82 +++++++++++++------ .../README.md | 2 +- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/crates/ruvector-spann/src/distance.rs b/crates/ruvector-spann/src/distance.rs index a9d92ce86a..d7eedceda7 100644 --- a/crates/ruvector-spann/src/distance.rs +++ b/crates/ruvector-spann/src/distance.rs @@ -9,6 +9,27 @@ //! small-norm guard live outside it, so both backends take the same branches //! and differ only in how the sums are accumulated. +#[cfg(all(test, feature = "lattice-simd"))] +thread_local! { + static LATTICE_L2_WITNESS: std::cell::Cell = const { std::cell::Cell::new(false) }; + static LATTICE_DOT_WITNESS: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// Routes to `lattice_embed`'s L2 kernel and records that the call returned. +/// +/// The witness store lives inside this wrapper, not at the call site in +/// `l2_squared`, so that reverting the call site's expression to the scalar +/// fallback (while leaving this wrapper and its store untouched) stops the +/// wrapper from being invoked at all and the witness cannot fire. +#[cfg(feature = "lattice-simd")] +#[inline] +fn l2_lattice(a: &[f32], b: &[f32]) -> f32 { + let result = lattice_embed::simd::squared_euclidean_distance(a, b); + #[cfg(test)] + LATTICE_L2_WITNESS.with(|w| w.set(true)); + result +} + /// Compute L2 squared distance between two f32 slices. #[inline] pub fn l2_squared(a: &[f32], b: &[f32]) -> f32 { @@ -21,7 +42,7 @@ pub fn l2_squared(a: &[f32], b: &[f32]) -> f32 { // slice, so guarding here keeps the two backends from disagreeing on // an input the debug assertion already calls a caller bug. if a.len() == b.len() { - return lattice_embed::simd::squared_euclidean_distance(a, b); + return l2_lattice(a, b); } } @@ -53,6 +74,20 @@ pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { 1.0 - dot / (norm_a * norm_b) } +/// Routes to `lattice_embed`'s dot-product kernel for all three inner +/// products and records that the calls returned. See `l2_lattice` for why +/// the store lives in this wrapper rather than at the `inner_products` call +/// site. +#[cfg(feature = "lattice-simd")] +#[inline] +fn dot_lattice(a: &[f32], b: &[f32]) -> (f32, f32, f32) { + use lattice_embed::simd::dot_product; + let result = (dot_product(a, b), dot_product(a, a), dot_product(b, b)); + #[cfg(test)] + LATTICE_DOT_WITNESS.with(|w| w.set(true)); + result +} + /// Returns `(dot(a, b), dot(a, a), dot(b, b))`. /// /// `lattice_embed::simd::cosine_similarity` is deliberately not used here: it @@ -66,8 +101,7 @@ fn inner_products(a: &[f32], b: &[f32]) -> (f32, f32, f32) { // lattice's dot_product returns 0.0 on a length mismatch, which would // read as a zero norm and short-circuit to 1.0. Route equal lengths only. if a.len() == b.len() { - use lattice_embed::simd::dot_product; - return (dot_product(a, b), dot_product(a, a), dot_product(b, b)); + return dot_lattice(a, b); } } @@ -191,34 +225,30 @@ mod tests { /// to the scalar loops. `backend_matches_reference` above cannot catch /// that: it would still pass if the routed calls were replaced by the /// scalar functions, since both agree with the f64 reference within - /// tolerance. This test instead demands the routed result differ from - /// the scalar result bit-for-bit, which only holds if a distinct - /// (SIMD-ordered) summation actually ran. If the lattice calls at the - /// routing sites are ever swapped back for `l2_squared_scalar` / the - /// inline scalar sum, `l2_squared` and the dot product become the exact - /// same computation as their scalar counterparts and this test fails. + /// tolerance, and a host without an accelerated path falls through to a + /// `lattice_embed` scalar loop that can equal RuVector's own scalar sum + /// bit-for-bit — a bit-inequality assertion would reject that valid + /// route. This test instead witnesses that the `lattice_embed` call + /// itself returned, independent of what bits it produced. #[cfg(feature = "lattice-simd")] #[test] - fn lattice_backend_is_actually_selected() { + fn lattice_backend_is_actually_called() { + LATTICE_L2_WITNESS.with(|w| w.set(false)); + LATTICE_DOT_WITNESS.with(|w| w.set(false)); + let (a, b) = pair(768, 7); + let _ = l2_squared(&a, &b); + let _ = inner_products(&a, &b); - let l2_scalar = l2_squared_scalar(&a, &b); - let l2_routed = l2_squared(&a, &b); - assert_ne!( - l2_routed.to_bits(), - l2_scalar.to_bits(), - "l2_squared matched the scalar sum bit-for-bit with lattice-simd \ - enabled; the routing at distance.rs may have reverted to scalar" + assert!( + LATTICE_L2_WITNESS.with(|w| w.get()), + "l2_squared did not call lattice_embed::simd::squared_euclidean_distance; \ + the routing at distance.rs may have reverted to scalar" ); - - let dot_scalar: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); - let (dot_routed, _, _) = inner_products(&a, &b); - assert_ne!( - dot_routed.to_bits(), - dot_scalar.to_bits(), - "cosine's dot product matched the scalar sum bit-for-bit with \ - lattice-simd enabled; the routing at distance.rs may have \ - reverted to scalar" + assert!( + LATTICE_DOT_WITNESS.with(|w| w.get()), + "inner_products did not call lattice_embed::simd::dot_product; \ + the routing at distance.rs may have reverted to scalar" ); } } diff --git a/docs/research/nightly/2026-06-24-spann-partition-spill/README.md b/docs/research/nightly/2026-06-24-spann-partition-spill/README.md index 3621132808..b8168555b2 100644 --- a/docs/research/nightly/2026-06-24-spann-partition-spill/README.md +++ b/docs/research/nightly/2026-06-24-spann-partition-spill/README.md @@ -267,7 +267,7 @@ The witness log integration path: each spill decision (vector id, primary partit ## Edge and WASM Implications -`ruvector-spann`'s default feature configuration resolves no normal dependencies (`cargo tree -e normal` is a single node). Distance computation uses pure Rust scalar arithmetic in this configuration, making the default build `no_std`-compatible with a static data source and suitable for WASM compilation via `wasm-pack`. +`ruvector-spann`'s default feature configuration resolves no normal dependencies (`cargo tree -e normal` is a single node). Distance computation uses pure Rust scalar arithmetic in this configuration. The crate does not declare `#![no_std]` and uses `std::cmp::Ordering` in production code (`src/lib.rs`, `src/index.rs`), so it is not `no_std`-compatible today, and it has no tested `wasm-pack`/WASM build configuration; either would need to be added and verified before relying on them for edge deployment. The optional `lattice-simd` feature adds `lattice-embed` for runtime-dispatched SIMD distance kernels and requires Rust 1.93. The edge and WASM guidance above applies to the default (scalar) feature configuration; enabling `lattice-simd` pulls in that dependency's MSRV and its own WASM SIMD128 support, and should be evaluated separately for constrained deployment targets.