fix(accel): restore f32::EPSILON near-zero-denom guard in cosine_similarity
CI / test (pull_request) Canceled after 0s

The SIMD migration weakened the near-zero-norm guard in all four
clawhdf5-accel cosine_similarity backends (scalar/avx2/avx512/neon)
from `denom < f32::EPSILON` to `denom == 0.0`. Vectors with a tiny
but nonzero norm (denom in (0, 1.19e-7)) fell through to dot/denom
and scored as identical instead of maximally dissimilar, diverging
from the pre-SIMD scalar loop's documented fallback behavior.

Restores the epsilon threshold in all four backends so
`1.0 - cosine_similarity(...)` in hnsw.rs::compute_distance
reproduces the old fallback exactly. Adds regression tests in
clawhdf5-accel and clawhdf5-ann locking in the near-zero-norm case.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
clawhdf5 committer agent
2026-08-17 13:28:43 +00:00
co-authored by Claude Sonnet 5
parent 07b7301ded
commit 377c8b6f17
7 changed files with 229 additions and 4 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ pub unsafe fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
}
let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom }
if denom < f32::EPSILON { 0.0 } else { dot / denom }
}
}
+1 -1
View File
@@ -89,7 +89,7 @@ pub unsafe fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
}
let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom }
if denom < f32::EPSILON { 0.0 } else { dot / denom }
}
}
+12
View File
@@ -361,6 +361,18 @@ mod tests {
assert!(approx_eq(cosine_similarity(&a, &b), 0.0, EPSILON));
}
#[test]
fn test_cosine_near_zero_norm_clamped() {
// denom = 1e-4 * 1e-4 = 1e-8, comfortably below f32::EPSILON
// (~1.19e-7) but not exactly 0.0 — must still clamp to 0.0 so
// callers computing `1.0 - cosine_similarity(...)` treat these
// as maximally dissimilar, matching the pre-SIMD scalar guard.
let a = [1e-4f32];
let b = [1e-4f32];
assert_eq!(cosine_similarity(&a, &b), 0.0);
assert_eq!(scalar::cosine_similarity(&a, &b), 0.0);
}
#[test]
fn test_cosine_scalar_vs_dispatch() {
let a: Vec<f32> = (0..384).map(|i| (i as f32).sin()).collect();
+1 -1
View File
@@ -94,7 +94,7 @@ pub unsafe fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
}
let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom }
if denom < f32::EPSILON { 0.0 } else { dot / denom }
}
/// NEON L2 distance.
+1 -1
View File
@@ -21,7 +21,7 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
norm_b += y * y;
}
let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom }
if denom < f32::EPSILON { 0.0 } else { dot / denom }
}
pub fn batch_cosine(query: &[f32], vectors: &[&[f32]], results: &mut [(usize, f32)]) {