velesdb_core/simd_native/dispatch/
mod.rs1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum SimdLevel {
23 Avx512,
25 Avx2,
27 Neon,
29 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]
80pub 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#[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#[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]
154pub fn warmup_simd_cache() {
158 #[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#[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
198unsafe impl Send for DistanceEngine {}
203unsafe impl Sync for DistanceEngine {}
208
209impl DistanceEngine {
210 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
311 #[must_use]
312 pub fn dimension(&self) -> usize {
313 self.dimension
314 }
315}