Skip to main content

velesdb_core/simd_native/dispatch/
mod.rs

1//! Runtime SIMD level detection and dispatch wiring.
2
3mod cosine;
4mod dot;
5mod euclidean;
6mod hamming;
7
8pub use cosine::{batch_cosine_native, cosine_normalized_native, cosine_similarity_native};
9pub use dot::{batch_dot_product_native, dot_product_native};
10pub use euclidean::{
11    batch_euclidean_native, batch_squared_l2_native, euclidean_native, norm_native,
12    normalize_inplace_native, squared_l2_native,
13};
14pub use hamming::{
15    batch_hamming_native, batch_jaccard_native, hamming_binary_native, hamming_distance_native,
16    jaccard_similarity_native,
17};
18
19/// SIMD capability level detected at runtime.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum SimdLevel {
23    /// AVX-512F available (x86_64 only).
24    Avx512,
25    /// AVX2 + FMA available (x86_64 only).
26    Avx2,
27    /// NEON available (aarch64, always true).
28    Neon,
29    /// Scalar fallback.
30    Scalar,
31}
32
33static SIMD_LEVEL: std::sync::OnceLock<SimdLevel> = std::sync::OnceLock::new();
34
35#[inline]
36pub(super) fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
37    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
38}
39
40#[inline]
41pub(super) fn squared_l2_scalar(a: &[f32], b: &[f32]) -> f32 {
42    a.iter()
43        .zip(b.iter())
44        .map(|(x, y)| {
45            let d = x - y;
46            d * d
47        })
48        .sum()
49}
50
51fn detect_simd_level() -> SimdLevel {
52    let level;
53
54    #[cfg(target_arch = "x86_64")]
55    {
56        if is_x86_feature_detected!("avx512f") {
57            level = SimdLevel::Avx512;
58        } else if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
59            level = SimdLevel::Avx2;
60        } else {
61            level = SimdLevel::Scalar;
62        }
63    }
64
65    #[cfg(target_arch = "aarch64")]
66    {
67        level = SimdLevel::Neon;
68    }
69
70    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
71    {
72        level = SimdLevel::Scalar;
73    }
74
75    level
76}
77
78#[inline]
79#[must_use]
80/// Returns the cached SIMD level for the current process.
81pub fn simd_level() -> SimdLevel {
82    *SIMD_LEVEL.get_or_init(detect_simd_level)
83}
84
85#[inline]
86#[must_use]
87pub fn has_avx512vl() -> bool {
88    #[cfg(target_arch = "x86_64")]
89    {
90        return is_x86_feature_detected!("avx512vl");
91    }
92    #[allow(unreachable_code)]
93    false
94}
95
96#[inline]
97#[must_use]
98pub fn has_avx512bw() -> bool {
99    #[cfg(target_arch = "x86_64")]
100    {
101        return is_x86_feature_detected!("avx512bw");
102    }
103    #[allow(unreachable_code)]
104    false
105}
106
107#[inline]
108#[must_use]
109pub fn has_avx512vnni() -> bool {
110    #[cfg(target_arch = "x86_64")]
111    {
112        return is_x86_feature_detected!("avx512vnni");
113    }
114    #[allow(unreachable_code)]
115    false
116}
117
118/// Returns `true` if the CPU supports AVX-512 VPOPCNTDQ (native 64-bit popcount).
119///
120/// Available on Ice Lake (client), Cascade Lake (server), and Zen4+ CPUs.
121/// Enables `_mm512_popcnt_epi64` for hardware-accelerated binary Hamming distance.
122#[inline]
123#[must_use]
124pub fn has_avx512vpopcntdq() -> bool {
125    #[cfg(target_arch = "x86_64")]
126    {
127        return is_x86_feature_detected!("avx512vpopcntdq");
128    }
129    #[allow(unreachable_code)]
130    false
131}
132
133/// Batch distance computation with multi-level prefetch hints.
134///
135/// Applies a single-pair distance function to each candidate against the query,
136/// using software prefetching to hide memory latency for upcoming candidates.
137/// This eliminates the identical batch loop pattern duplicated across all metrics.
138#[inline]
139#[must_use]
140pub(super) fn batch_with_prefetch(
141    candidates: &[&[f32]],
142    query: &[f32],
143    distance_fn: fn(&[f32], &[f32]) -> f32,
144) -> Vec<f32> {
145    let mut results = Vec::with_capacity(candidates.len());
146    for (i, candidate) in candidates.iter().enumerate() {
147        dot::batch_prefetch_candidate(candidates, i);
148        results.push(distance_fn(candidate, query));
149    }
150    results
151}
152
153#[inline]
154/// Warms up runtime SIMD dispatch cache and CPU caches for common dimensions.
155///
156/// Logs the detected SIMD level on non-WASM targets for diagnostics.
157pub fn warmup_simd_cache() {
158    // Log detected SIMD level for diagnostics (skipped in WASM)
159    #[cfg(feature = "persistence")]
160    tracing::info!("SIMD dispatch: {:?} detected", simd_level());
161
162    let warmup_size = 768;
163    let a: Vec<f32> = vec![0.01; warmup_size];
164    let b: Vec<f32> = vec![0.01; warmup_size];
165    for _ in 0..3 {
166        let _ = dot_product_native(&a, &b);
167        let _ = cosine_similarity_native(&a, &b);
168    }
169}
170
171/// Pre-resolved SIMD kernels for repeated distance operations at a fixed dimension.
172#[derive(Clone, Copy)]
173pub struct DistanceEngine {
174    dot_product_fn: fn(&[f32], &[f32]) -> f32,
175    squared_l2_fn: fn(&[f32], &[f32]) -> f32,
176    cosine_fn: fn(&[f32], &[f32]) -> f32,
177    hamming_fn: fn(&[f32], &[f32]) -> f32,
178    jaccard_fn: fn(&[f32], &[f32]) -> f32,
179    dimension: usize,
180}
181
182impl std::fmt::Debug for DistanceEngine {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        let avx512_ext = (
185            has_avx512vl(),
186            has_avx512bw(),
187            has_avx512vnni(),
188            has_avx512vpopcntdq(),
189        );
190        f.debug_struct("DistanceEngine")
191            .field("dimension", &self.dimension)
192            .field("simd_level", &simd_level())
193            .field("avx512_ext(vl,bw,vnni,vpopcntdq)", &avx512_ext)
194            .finish_non_exhaustive()
195    }
196}
197
198// SAFETY: `DistanceEngine` stores only plain function pointers and a `usize`.
199// - Condition 1: All fields are `fn(...)` pointers and `usize`, both inherently `Send`.
200// - Condition 2: No interior mutability or non-`Send` types like `Rc`, raw pointers, or thread-local refs.
201// SAFETY: Function pointers are safe to transfer across threads.
202unsafe impl Send for DistanceEngine {}
203// SAFETY: Function pointers are immutable references to static code.
204// - Condition 1: All fields are `fn(...)` pointers and `usize`, both inherently `Sync`.
205// - Condition 2: No mutable shared state; the struct is read-only after construction.
206// SAFETY: Multiple threads can safely share a `&DistanceEngine` for distance computation.
207unsafe impl Sync for DistanceEngine {}
208
209impl DistanceEngine {
210    /// Creates a distance engine and resolves SIMD kernels once for `dimension`.
211    #[must_use]
212    pub fn new(dimension: usize) -> Self {
213        let level = simd_level();
214        Self {
215            dot_product_fn: dot::resolve_dot_product(level, dimension),
216            squared_l2_fn: euclidean::resolve_squared_l2(level, dimension),
217            cosine_fn: cosine::resolve_cosine(level, dimension),
218            hamming_fn: hamming::resolve_hamming(level, dimension),
219            jaccard_fn: hamming::resolve_jaccard(level, dimension),
220            dimension,
221        }
222    }
223
224    /// Dispatches to a pre-resolved kernel with release-mode dimension checks.
225    ///
226    /// The kernels were resolved for `self.dimension` at construction time;
227    /// calling them with a different length would read out of bounds, so the
228    /// asserts must hold in release builds (same policy as `dot_product_native`).
229    #[allow(clippy::inline_always)]
230    #[inline(always)]
231    fn dispatch(&self, kernel: fn(&[f32], &[f32]) -> f32, a: &[f32], b: &[f32]) -> f32 {
232        assert_eq!(a.len(), b.len(), "Vector dimensions must match");
233        assert_eq!(
234            a.len(),
235            self.dimension,
236            "Vector dimension mismatch with engine"
237        );
238        kernel(a, b)
239    }
240
241    /// Computes dot product with the pre-resolved kernel.
242    #[allow(clippy::inline_always)]
243    #[inline(always)]
244    #[must_use]
245    pub fn dot_product(&self, a: &[f32], b: &[f32]) -> f32 {
246        self.dispatch(self.dot_product_fn, a, b)
247    }
248
249    /// Computes squared L2 with the pre-resolved kernel.
250    #[allow(clippy::inline_always)]
251    #[inline(always)]
252    #[must_use]
253    pub fn squared_l2(&self, a: &[f32], b: &[f32]) -> f32 {
254        self.dispatch(self.squared_l2_fn, a, b)
255    }
256
257    /// Computes Euclidean distance (includes sqrt).
258    ///
259    /// Returns `sqrt(sum((a_i - b_i)^2))`. Used by brute-force search and
260    /// public API paths where the actual Euclidean distance is needed.
261    #[allow(clippy::inline_always)]
262    #[inline(always)]
263    #[must_use]
264    pub fn euclidean(&self, a: &[f32], b: &[f32]) -> f32 {
265        self.squared_l2(a, b).sqrt()
266    }
267
268    /// Computes squared Euclidean distance (no sqrt).
269    ///
270    /// Equivalent to [`squared_l2_native()`] -- exists for semantic clarity
271    /// at call sites that emphasize the "Euclidean without sqrt" intent.
272    ///
273    /// Returns `sum((a_i - b_i)^2)`. Used by HNSW graph traversal where
274    /// only ordering matters and sqrt can be deferred to the final results,
275    /// saving one `f32::sqrt()` per distance computation in the hot loop.
276    ///
277    /// [`squared_l2_native()`]: super::dispatch::squared_l2_native
278    #[allow(clippy::inline_always)]
279    #[inline(always)]
280    #[must_use]
281    pub fn euclidean_squared(&self, a: &[f32], b: &[f32]) -> f32 {
282        self.dispatch(self.squared_l2_fn, a, b)
283    }
284
285    /// Computes cosine similarity with the pre-resolved kernel.
286    #[allow(clippy::inline_always)]
287    #[inline(always)]
288    #[must_use]
289    pub fn cosine_similarity(&self, a: &[f32], b: &[f32]) -> f32 {
290        self.dispatch(self.cosine_fn, a, b)
291    }
292
293    /// Computes Hamming distance with the pre-resolved kernel.
294    #[allow(clippy::inline_always)]
295    #[inline(always)]
296    #[must_use]
297    pub fn hamming(&self, a: &[f32], b: &[f32]) -> f32 {
298        self.dispatch(self.hamming_fn, a, b)
299    }
300
301    /// Computes Jaccard similarity with the pre-resolved kernel.
302    #[allow(clippy::inline_always)]
303    #[inline(always)]
304    #[must_use]
305    pub fn jaccard(&self, a: &[f32], b: &[f32]) -> f32 {
306        self.dispatch(self.jaccard_fn, a, b)
307    }
308
309    /// Returns the dimension this engine is specialized for.
310    #[inline]
311    #[must_use]
312    pub fn dimension(&self) -> usize {
313        self.dimension
314    }
315}