1#![doc(html_root_url = "https://docs.rs/slate-simd")]
30#![deny(unsafe_op_in_unsafe_fn)]
34#![cfg_attr(feature = "portable_simd", feature(portable_simd))]
35
36#[cfg(target_arch = "x86_64")]
37mod avx2;
38#[cfg(target_arch = "x86_64")]
39mod avx512;
40mod dispatch;
41#[cfg(target_arch = "aarch64")]
42mod neon;
43pub mod scalar;
44
45pub use dispatch::{active_tier, detect_tier, Tier};
46use slate_core::{Error, Result};
47
48#[inline]
50fn check(a: &[f32], b: &[f32]) -> Result<()> {
51 if a.len() != b.len() {
52 return Err(Error::DimensionMismatch {
53 expected: a.len(),
54 got: b.len(),
55 });
56 }
57 Ok(())
58}
59
60#[inline]
65pub fn l2_sq(a: &[f32], b: &[f32]) -> Result<f32> {
66 check(a, b)?;
67 Ok(dispatch::l2_sq_kernel()(a, b))
68}
69
70#[inline]
75pub fn inner_product(a: &[f32], b: &[f32]) -> Result<f32> {
76 check(a, b)?;
77 Ok(-dispatch::dot_kernel()(a, b))
78}
79
80#[inline]
88pub fn dot(a: &[f32], b: &[f32]) -> Result<f32> {
89 check(a, b)?;
90 Ok(dispatch::dot_kernel()(a, b))
91}
92
93#[inline]
100pub fn cosine(a: &[f32], b: &[f32]) -> Result<f32> {
101 check(a, b)?;
102 let (d, na, nb) = dispatch::cosine_parts(a, b);
103 let denom = (na * nb).sqrt();
104 if denom == 0.0 {
105 Ok(1.0)
106 } else {
107 Ok(1.0 - d / denom)
108 }
109}
110
111#[inline]
116pub fn cosine_normalized(a: &[f32], b: &[f32]) -> Result<f32> {
117 check(a, b)?;
118 Ok(1.0 - dispatch::dot_kernel()(a, b))
119}
120
121#[inline]
131pub fn distance(metric: slate_core::Metric, a: &[f32], b: &[f32]) -> Result<f32> {
132 use slate_core::Metric;
133 match metric {
134 Metric::L2 => l2_sq(a, b),
135 Metric::InnerProduct => inner_product(a, b),
136 Metric::Cosine => cosine(a, b),
137 }
138}
139
140#[inline]
153pub fn distance_f16(metric: slate_core::Metric, query: &[f32], stored: &[u8]) -> Result<f32> {
154 use slate_core::Metric;
155 if stored.len() != 2 * query.len() {
156 return Err(Error::DimensionMismatch {
157 expected: 2 * query.len(),
158 got: stored.len(),
159 });
160 }
161 match metric {
162 Metric::L2 => Ok(dispatch::l2_sq_f16(query, stored)),
163 Metric::InnerProduct => Ok(-dispatch::dot_f16(query, stored)),
164 Metric::Cosine => {
165 let (d, nq, ns) = dispatch::cosine_parts_f16(query, stored);
166 let denom = (nq * ns).sqrt();
167 if denom == 0.0 {
168 Ok(1.0)
169 } else {
170 Ok(1.0 - d / denom)
171 }
172 }
173 }
174}
175
176#[inline]
189pub fn distance_i8(
190 metric: slate_core::Metric,
191 query: &[f32],
192 scale: f32,
193 codes: &[i8],
194) -> Result<f32> {
195 use slate_core::Metric;
196 if codes.len() != query.len() {
197 return Err(Error::DimensionMismatch {
198 expected: query.len(),
199 got: codes.len(),
200 });
201 }
202 match metric {
203 Metric::L2 => Ok(dispatch::l2_sq_i8(query, scale, codes)),
204 Metric::InnerProduct => Ok(-dispatch::dot_i8(query, scale, codes)),
205 Metric::Cosine => {
206 let (d, nq, ns) = dispatch::cosine_parts_i8(query, scale, codes);
207 let denom = (nq * ns).sqrt();
208 if denom == 0.0 {
209 Ok(1.0)
210 } else {
211 Ok(1.0 - d / denom)
212 }
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn dimension_mismatch_is_reported() {
223 let a = [1.0f32, 2.0, 3.0];
224 let b = [1.0f32, 2.0];
225 assert!(matches!(
226 l2_sq(&a, &b),
227 Err(Error::DimensionMismatch { expected: 3, got: 2 })
228 ));
229 }
230
231 #[test]
232 fn l2_sq_known_value() {
233 let a = [0.0f32, 0.0, 0.0];
234 let b = [1.0f32, 2.0, 2.0];
235 assert!((l2_sq(&a, &b).unwrap() - 9.0).abs() < 1e-6);
237 }
238
239 #[test]
240 fn inner_product_is_negated() {
241 let a = [1.0f32, 2.0, 3.0];
242 let b = [1.0f32, 1.0, 1.0];
243 assert!((inner_product(&a, &b).unwrap() + 6.0).abs() < 1e-6);
245 assert!((dot(&a, &b).unwrap() - 6.0).abs() < 1e-6);
246 }
247
248 #[test]
249 fn cosine_identical_is_zero() {
250 let a = [1.0f32, 2.0, 3.0, 4.0];
251 assert!(cosine(&a, &a).unwrap().abs() < 1e-6);
252 }
253
254 #[test]
255 fn cosine_orthogonal_is_one() {
256 let a = [1.0f32, 0.0];
257 let b = [0.0f32, 1.0];
258 assert!((cosine(&a, &b).unwrap() - 1.0).abs() < 1e-6);
259 }
260
261 #[test]
262 fn cosine_zero_norm_is_one() {
263 let a = [0.0f32, 0.0, 0.0];
264 let b = [1.0f32, 2.0, 3.0];
265 assert!((cosine(&a, &b).unwrap() - 1.0).abs() < 1e-6);
266 }
267
268 #[test]
269 fn cosine_normalized_matches_cosine_on_unit_vectors() {
270 let a = [0.6f32, 0.8];
272 let b = [1.0f32, 0.0];
273 let raw = cosine(&a, &b).unwrap();
274 let norm = cosine_normalized(&a, &b).unwrap();
275 assert!((raw - norm).abs() < 1e-6);
276 }
277
278 #[test]
279 fn active_tier_is_reported() {
280 let t = active_tier();
282 assert_eq!(t, active_tier());
283 println!("active tier: {}", t.as_str());
284 }
285
286 #[test]
287 fn distance_f16_equals_decode_then_distance() {
288 use half::f16;
289 use slate_core::Metric;
290 let query = [0.5f32, -1.25, 3.0, 0.0, -2.5, 7.5, -0.125, 4.0, 1.0];
291 let raw = [0.4f32, -1.0, 3.25, 0.5, -2.0, 7.0, -0.25, 4.5, 0.75];
292 let stored: Vec<u8> = raw
293 .iter()
294 .flat_map(|&x| f16::from_f32(x).to_le_bytes())
295 .collect();
296 let decoded: Vec<f32> = raw.iter().map(|&x| f16::from_f32(x).to_f32()).collect();
298 for metric in [Metric::L2, Metric::InnerProduct, Metric::Cosine] {
299 let native = distance_f16(metric, &query, &stored).unwrap();
300 let reference = distance(metric, &query, &decoded).unwrap();
301 assert!(
302 (native - reference).abs() <= 1e-6,
303 "metric={metric:?} native={native} reference={reference}"
304 );
305 }
306 }
307
308 #[test]
309 fn distance_i8_equals_decode_then_distance() {
310 use slate_core::Metric;
311 let query = [0.5f32, -1.25, 3.0, 0.0, -2.5, 7.5, -0.125, 4.0, 1.0];
312 let scale = 0.05f32;
313 let codes = [10i8, -20, 60, 0, -50, 127, -3, 80, 15];
314 let decoded: Vec<f32> = codes.iter().map(|&c| f32::from(c) * scale).collect();
315 for metric in [Metric::L2, Metric::InnerProduct, Metric::Cosine] {
316 let native = distance_i8(metric, &query, scale, &codes).unwrap();
317 let reference = distance(metric, &query, &decoded).unwrap();
318 assert!(
319 (native - reference).abs() <= 1e-6,
320 "metric={metric:?} native={native} reference={reference}"
321 );
322 }
323 }
324
325 #[test]
326 fn narrow_distance_rejects_wrong_length() {
327 use slate_core::Metric;
328 let query = [1.0f32, 2.0, 3.0];
329 assert!(matches!(
331 distance_f16(Metric::L2, &query, &[0u8; 4]),
332 Err(Error::DimensionMismatch { expected: 6, got: 4 })
333 ));
334 assert!(matches!(
335 distance_i8(Metric::L2, &query, 1.0, &[0i8; 2]),
336 Err(Error::DimensionMismatch { expected: 3, got: 2 })
337 ));
338 }
339}
340
341#[cfg(test)]
349mod proptests {
350 use super::*;
351 use proptest::prelude::*;
352
353 fn approx_eq_scaled(got: f32, want: f32, scale: f32) -> bool {
360 let tol = 1e-4 * scale.max(1.0);
361 (got - want).abs() <= tol
362 }
363
364 fn dot_scale(a: &[f32], b: &[f32]) -> f32 {
367 a.iter().zip(b).map(|(x, y)| (x * y).abs()).sum()
368 }
369
370 fn l2_scale(a: &[f32], b: &[f32]) -> f32 {
372 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
373 }
374
375 prop_compose! {
378 fn vec_pair()(len in 0usize..=257)
379 (a in prop::collection::vec(-10.0f32..10.0, len),
380 b in prop::collection::vec(-10.0f32..10.0, len))
381 -> (Vec<f32>, Vec<f32>) {
382 (a, b)
383 }
384 }
385
386 proptest! {
387 #![proptest_config(ProptestConfig::with_cases(2000))]
388
389 #[test]
390 fn l2_sq_matches_oracle((a, b) in vec_pair()) {
391 let got = l2_sq(&a, &b).unwrap();
392 let want = scalar::l2_sq(&a, &b);
393 let scale = l2_scale(&a, &b);
394 prop_assert!(approx_eq_scaled(got, want, scale),
395 "got={got} want={want} scale={scale} len={}", a.len());
396 }
397
398 #[test]
399 fn dot_matches_oracle((a, b) in vec_pair()) {
400 let got = dot(&a, &b).unwrap();
401 let want = scalar::dot(&a, &b);
402 let scale = dot_scale(&a, &b);
403 prop_assert!(approx_eq_scaled(got, want, scale),
404 "got={got} want={want} scale={scale} len={}", a.len());
405 }
406
407 #[test]
408 fn inner_product_is_negated_dot((a, b) in vec_pair()) {
409 let got = inner_product(&a, &b).unwrap();
410 let want = -scalar::dot(&a, &b);
411 let scale = dot_scale(&a, &b);
412 prop_assert!(approx_eq_scaled(got, want, scale));
413 }
414
415 #[test]
416 fn cosine_matches_oracle((a, b) in vec_pair()) {
417 let got = cosine(&a, &b).unwrap();
418 let want = scalar::cosine_distance(&a, &b);
419 prop_assert!((got - want).abs() <= 1e-4,
422 "got={got} want={want} len={}", a.len());
423 }
424 }
425
426 use half::f16;
427 use slate_core::Metric;
428
429 fn encode_f16(v: &[f32]) -> Vec<u8> {
432 let mut out = Vec::with_capacity(2 * v.len());
433 for &x in v {
434 out.extend_from_slice(&f16::from_f32(x).to_le_bytes());
435 }
436 out
437 }
438
439 fn encode_i8(v: &[f32]) -> (f32, Vec<i8>) {
443 let max_abs = v.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
444 let scale = if max_abs == 0.0 { 0.0 } else { max_abs / 127.0 };
445 let codes = v
446 .iter()
447 .map(|&x| {
448 if scale == 0.0 {
449 0i8
450 } else {
451 (x / scale).round().clamp(-127.0, 127.0) as i8
452 }
453 })
454 .collect();
455 (scale, codes)
456 }
457
458 fn scalar_distance_f16(metric: Metric, query: &[f32], stored: &[u8]) -> f32 {
461 match metric {
462 Metric::L2 => scalar::l2_sq_f16(query, stored),
463 Metric::InnerProduct => -scalar::dot_f16(query, stored),
464 Metric::Cosine => {
465 let (d, nq, ns) = scalar::cosine_parts_f16(query, stored);
466 let denom = (nq * ns).sqrt();
467 if denom == 0.0 { 1.0 } else { 1.0 - d / denom }
468 }
469 }
470 }
471
472 fn scalar_distance_i8(metric: Metric, query: &[f32], scale: f32, codes: &[i8]) -> f32 {
473 match metric {
474 Metric::L2 => scalar::l2_sq_i8(query, scale, codes),
475 Metric::InnerProduct => -scalar::dot_i8(query, scale, codes),
476 Metric::Cosine => {
477 let (d, nq, ns) = scalar::cosine_parts_i8(query, scale, codes);
478 let denom = (nq * ns).sqrt();
479 if denom == 0.0 { 1.0 } else { 1.0 - d / denom }
480 }
481 }
482 }
483
484 fn dot_scale_decoded(query: &[f32], stored: &[f32]) -> f32 {
486 query.iter().zip(stored).map(|(x, y)| (x * y).abs()).sum()
487 }
488
489 fn l2_scale_decoded(query: &[f32], stored: &[f32]) -> f32 {
491 query.iter().zip(stored).map(|(x, y)| (x - y) * (x - y)).sum()
492 }
493
494 proptest! {
495 #![proptest_config(ProptestConfig::with_cases(2000))]
496
497 #[test]
498 fn l2_sq_f16_matches_oracle((q, v) in vec_pair()) {
499 let stored = encode_f16(&v);
500 let decoded: Vec<f32> = v.iter().map(|&x| f16::from_f32(x).to_f32()).collect();
501 let got = distance_f16(Metric::L2, &q, &stored).unwrap();
502 let want = scalar_distance_f16(Metric::L2, &q, &stored);
503 let scale = l2_scale_decoded(&q, &decoded);
504 prop_assert!(approx_eq_scaled(got, want, scale),
505 "got={got} want={want} scale={scale} len={}", q.len());
506 }
507
508 #[test]
509 fn dot_f16_matches_oracle((q, v) in vec_pair()) {
510 let stored = encode_f16(&v);
511 let decoded: Vec<f32> = v.iter().map(|&x| f16::from_f32(x).to_f32()).collect();
512 let got = distance_f16(Metric::InnerProduct, &q, &stored).unwrap();
513 let want = scalar_distance_f16(Metric::InnerProduct, &q, &stored);
514 let scale = dot_scale_decoded(&q, &decoded);
515 prop_assert!(approx_eq_scaled(got, want, scale),
516 "got={got} want={want} scale={scale} len={}", q.len());
517 }
518
519 #[test]
520 fn cosine_f16_matches_oracle((q, v) in vec_pair()) {
521 let stored = encode_f16(&v);
522 let got = distance_f16(Metric::Cosine, &q, &stored).unwrap();
523 let want = scalar_distance_f16(Metric::Cosine, &q, &stored);
524 prop_assert!((got - want).abs() <= 1e-4,
525 "got={got} want={want} len={}", q.len());
526 }
527
528 #[test]
529 fn l2_sq_i8_matches_oracle((q, v) in vec_pair()) {
530 let (scale_q, codes) = encode_i8(&v);
531 let decoded: Vec<f32> = codes.iter().map(|&c| f32::from(c) * scale_q).collect();
532 let got = distance_i8(Metric::L2, &q, scale_q, &codes).unwrap();
533 let want = scalar_distance_i8(Metric::L2, &q, scale_q, &codes);
534 let scale = l2_scale_decoded(&q, &decoded);
535 prop_assert!(approx_eq_scaled(got, want, scale),
536 "got={got} want={want} scale={scale} len={}", q.len());
537 }
538
539 #[test]
540 fn dot_i8_matches_oracle((q, v) in vec_pair()) {
541 let (scale_q, codes) = encode_i8(&v);
542 let decoded: Vec<f32> = codes.iter().map(|&c| f32::from(c) * scale_q).collect();
543 let got = distance_i8(Metric::InnerProduct, &q, scale_q, &codes).unwrap();
544 let want = scalar_distance_i8(Metric::InnerProduct, &q, scale_q, &codes);
545 let scale = dot_scale_decoded(&q, &decoded);
546 prop_assert!(approx_eq_scaled(got, want, scale),
547 "got={got} want={want} scale={scale} len={}", q.len());
548 }
549
550 #[test]
551 fn cosine_i8_matches_oracle((q, v) in vec_pair()) {
552 let (scale_q, codes) = encode_i8(&v);
553 let got = distance_i8(Metric::Cosine, &q, scale_q, &codes).unwrap();
554 let want = scalar_distance_i8(Metric::Cosine, &q, scale_q, &codes);
555 prop_assert!((got - want).abs() <= 1e-4,
556 "got={got} want={want} len={}", q.len());
557 }
558 }
559}