Skip to main content

velesdb_core/
simd_dispatch.rs

1#![allow(
2    clippy::cast_precision_loss,
3    clippy::cast_possible_truncation,
4    clippy::cast_sign_loss,
5    clippy::float_cmp
6)]
7//! Zero-overhead SIMD function dispatch.
8//!
9//! This module provides a thin wrapper around `simd_native` functions,
10//! offering a stable public API while `simd_native` handles the
11//! architecture-specific SIMD implementations internally.
12//!
13//! # EPIC-C.2: TS-SIMD-002
14
15// Reason: Numeric casts in SIMD dispatch are intentional:
16// - usize->u32 for Hamming distance: vector dimensions bounded by implementation
17// - Maximum dimension is 65536, result fits in u32
18
19// =============================================================================
20// Public dispatch API - Direct calls to simd_native
21// =============================================================================
22
23/// Compute dot product with automatic SIMD dispatch.
24#[inline]
25#[must_use]
26pub fn dot_product_dispatched(a: &[f32], b: &[f32]) -> f32 {
27    crate::simd_native::dot_product_native(a, b)
28}
29
30/// Compute Euclidean distance with automatic SIMD dispatch.
31#[inline]
32#[must_use]
33pub fn euclidean_dispatched(a: &[f32], b: &[f32]) -> f32 {
34    crate::simd_native::euclidean_native(a, b)
35}
36
37/// Compute cosine similarity with automatic SIMD dispatch.
38#[inline]
39#[must_use]
40pub fn cosine_dispatched(a: &[f32], b: &[f32]) -> f32 {
41    crate::simd_native::cosine_similarity_native(a, b)
42}
43
44/// Compute cosine similarity for pre-normalized vectors.
45#[inline]
46#[must_use]
47pub fn cosine_normalized_dispatched(a: &[f32], b: &[f32]) -> f32 {
48    crate::simd_native::cosine_normalized_native(a, b)
49}
50
51/// Compute Hamming distance with automatic SIMD dispatch.
52#[inline]
53#[must_use]
54pub fn hamming_dispatched(a: &[f32], b: &[f32]) -> u32 {
55    #[allow(clippy::cast_sign_loss)]
56    // Reason: hamming_distance_native returns count of differing bits (non-negative),
57    // and vector dimensions are bounded by u32::MAX, so result always fits in u32
58    {
59        crate::simd_native::hamming_distance_native(a, b) as u32
60    }
61}
62
63/// Returns information about which SIMD features are available.
64#[must_use]
65pub fn simd_features_info() -> SimdFeatures {
66    SimdFeatures::detect()
67}
68
69/// Information about available SIMD features.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[allow(clippy::struct_excessive_bools)]
72pub struct SimdFeatures {
73    /// AVX-512 foundation instructions available.
74    pub avx512f: bool,
75    /// AVX-512 VPOPCNTDQ (population count) available.
76    pub avx512_popcnt: bool,
77    /// AVX2 instructions available.
78    pub avx2: bool,
79    /// POPCNT instruction available.
80    pub popcnt: bool,
81}
82
83impl SimdFeatures {
84    /// Detects available SIMD features on the current CPU.
85    #[must_use]
86    pub fn detect() -> Self {
87        #[cfg(target_arch = "x86_64")]
88        {
89            Self {
90                avx512f: is_x86_feature_detected!("avx512f"),
91                avx512_popcnt: is_x86_feature_detected!("avx512vpopcntdq"),
92                avx2: is_x86_feature_detected!("avx2"),
93                popcnt: is_x86_feature_detected!("popcnt"),
94            }
95        }
96
97        #[cfg(not(target_arch = "x86_64"))]
98        {
99            Self {
100                avx512f: false,
101                avx512_popcnt: false,
102                avx2: false,
103                popcnt: false,
104            }
105        }
106    }
107
108    /// Returns the best available instruction set name.
109    #[must_use]
110    pub const fn best_instruction_set(&self) -> &'static str {
111        if self.avx512f {
112            "AVX-512"
113        } else if self.avx2 {
114            "AVX2"
115        } else {
116            "Scalar"
117        }
118    }
119}
120
121// =============================================================================
122// Prefetch constants - EPIC-C.1
123// =============================================================================
124
125// Scalar implementations for tests
126#[cfg(test)]
127fn dot_product_scalar(a: &[f32], b: &[f32]) -> f32 {
128    assert_eq!(a.len(), b.len(), "Vector length mismatch");
129    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
130}
131
132#[cfg(test)]
133fn euclidean_scalar(a: &[f32], b: &[f32]) -> f32 {
134    assert_eq!(a.len(), b.len(), "Vector length mismatch");
135    a.iter()
136        .zip(b.iter())
137        .map(|(x, y)| {
138            let d = x - y;
139            d * d
140        })
141        .sum::<f32>()
142        .sqrt()
143}
144
145#[cfg(test)]
146fn cosine_scalar(a: &[f32], b: &[f32]) -> f32 {
147    assert_eq!(a.len(), b.len(), "Vector length mismatch");
148    let mut dot = 0.0f32;
149    let mut norm_a = 0.0f32;
150    let mut norm_b = 0.0f32;
151
152    for (x, y) in a.iter().zip(b.iter()) {
153        dot += x * y;
154        norm_a += x * x;
155        norm_b += y * y;
156    }
157
158    let denom = (norm_a * norm_b).sqrt();
159    if denom > 0.0 {
160        dot / denom
161    } else {
162        0.0
163    }
164}
165
166#[cfg(test)]
167fn hamming_scalar(a: &[f32], b: &[f32]) -> u32 {
168    assert_eq!(a.len(), b.len(), "Vector length mismatch");
169    #[allow(clippy::cast_possible_truncation)]
170    let count = a
171        .iter()
172        .zip(b.iter())
173        .filter(|(&x, &y)| (x > 0.5) != (y > 0.5))
174        .count() as u32;
175    count
176}
177
178#[cfg(test)]
179fn cosine_normalized_scalar(a: &[f32], b: &[f32]) -> f32 {
180    // For normalized vectors, cosine = dot product
181    dot_product_scalar(a, b)
182}
183
184/// Cache line size in bytes (standard for modern x86/ARM CPUs).
185pub const CACHE_LINE_SIZE: usize = 64;
186
187/// Prefetch distance for 768-dimensional vectors (3072 bytes).
188/// Calculated at compile time: `768 * 4 / 64 = 48` cache lines.
189pub const PREFETCH_DISTANCE_768D: usize = 768 * std::mem::size_of::<f32>() / CACHE_LINE_SIZE;
190
191/// Prefetch distance for 384-dimensional vectors.
192pub const PREFETCH_DISTANCE_384D: usize = 384 * std::mem::size_of::<f32>() / CACHE_LINE_SIZE;
193
194/// Prefetch distance for 1536-dimensional vectors.
195pub const PREFETCH_DISTANCE_1536D: usize = 1536 * std::mem::size_of::<f32>() / CACHE_LINE_SIZE;
196
197/// Calculates prefetch distance for a given dimension at compile time.
198#[inline]
199#[must_use]
200pub const fn prefetch_distance(dimension: usize) -> usize {
201    (dimension * std::mem::size_of::<f32>()) / CACHE_LINE_SIZE
202}
203
204// =============================================================================
205// Tests
206// =============================================================================
207
208#[cfg(test)]
209#[path = "simd_dispatch_unit_tests.rs"]
210mod tests;