usearch 2.17.0 → 2.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/c/lib.cpp +5 -0
- package/c/module.modulemap +4 -0
- package/c/usearch.h +13 -7
- package/cpp/README.md +1 -1
- package/cpp/bench.cpp +1 -1
- package/cpp/test.cpp +110 -16
- package/include/usearch/index.hpp +48 -35
- package/include/usearch/index_dense.hpp +34 -19
- package/include/usearch/index_plugins.hpp +29 -7
- package/javascript/README.md +8 -6
- package/javascript/dist/cjs/usearch.d.ts +9 -4
- package/javascript/dist/cjs/usearch.js +23 -8
- package/javascript/dist/esm/usearch.d.ts +9 -4
- package/javascript/dist/esm/usearch.js +21 -7
- package/javascript/lib.cpp +137 -59
- package/javascript/usearch.test.js +359 -192
- package/javascript/usearch.ts +35 -11
- package/package.json +1 -1
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
- package/simsimd/CONTRIBUTING.md +23 -8
- package/simsimd/Cargo.lock +1 -1
- package/simsimd/Cargo.toml +1 -1
- package/simsimd/README.md +49 -9
- package/simsimd/build.rs +7 -3
- package/simsimd/c/lib.c +6 -1
- package/simsimd/include/simsimd/binary.h +3 -3
- package/simsimd/include/simsimd/dot.h +2 -2
- package/simsimd/include/simsimd/elementwise.h +26 -17
- package/simsimd/include/simsimd/simsimd.h +125 -49
- package/simsimd/include/simsimd/sparse.h +30 -29
- package/simsimd/include/simsimd/spatial.h +4 -3
- package/simsimd/include/simsimd/types.h +19 -8
- package/simsimd/javascript/lib.c +0 -1
- package/simsimd/package-lock.json +411 -362
- package/simsimd/package.json +9 -9
- package/simsimd/rust/lib.rs +63 -29
- package/simsimd/scripts/bench.cxx +13 -0
- package/simsimd/scripts/bench_cosine.rs +8 -0
- package/simsimd/scripts/bench_native.rs +4 -0
- package/simsimd/scripts/bench_sqeuclidean.rs +8 -0
- package/simsimd/scripts/test.c +22 -0
- package/simsimd/scripts/test.py +80 -4
- package/simsimd/swift/SimSIMD.swift +17 -0
- package/simsimd/swift/Test.swift +36 -0
package/simsimd/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simsimd",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.10",
|
|
4
4
|
"description": "Portable mixed-precision BLAS-like vector math library for x86 and ARM",
|
|
5
5
|
"homepage": "https://github.com/ashvardanian/simsimd",
|
|
6
6
|
"author": "Ash Vardanian",
|
|
@@ -43,19 +43,19 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"bindings": "^1.5.0",
|
|
46
|
-
"node-addon-api": "^
|
|
47
|
-
"node-gyp-build": "^4.8.
|
|
46
|
+
"node-addon-api": "^8.4.0",
|
|
47
|
+
"node-gyp-build": "^4.8.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/bindings": "^1.5.5",
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"node-gyp": "^
|
|
53
|
-
"prebuildify": "^6.0.
|
|
54
|
-
"typescript": "^5.
|
|
51
|
+
"@types/node": "^24.0.10",
|
|
52
|
+
"node-gyp": "^11.2.0",
|
|
53
|
+
"prebuildify": "^6.0.1",
|
|
54
|
+
"typescript": "^5.8.3"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
57
|
"benchmark": "^2.1.4",
|
|
58
|
-
"mathjs": "^
|
|
59
|
-
"usearch": "^2.
|
|
58
|
+
"mathjs": "^14.5.3",
|
|
59
|
+
"usearch": "^2.17.1"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/simsimd/rust/lib.rs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! * Targets ARM NEON, SVE, x86 AVX2, AVX-512 (VNNI, FP16) hardware backends.
|
|
4
4
|
//! * Handles `f64` double-, `f32` single-, and `f16` half-precision, `i8` integral, and binary vectors.
|
|
5
|
-
//! * Zero-dependency header-only C 99 library with bindings for Rust and other
|
|
5
|
+
//! * Zero-dependency header-only C 99 library with bindings for Rust and other languages.
|
|
6
6
|
//!
|
|
7
7
|
//! ## Implemented distance functions include:
|
|
8
8
|
//!
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
//!
|
|
27
27
|
//! // Compute squared Euclidean distance
|
|
28
28
|
//! let l2sq_dist = i8::l2sq(a, b);
|
|
29
|
+
//!
|
|
30
|
+
//! // Optimize performance by flushing denormals
|
|
31
|
+
//! simsimd::capabilities::flush_denormals();
|
|
29
32
|
//! ```
|
|
30
33
|
//!
|
|
31
34
|
//! ## Traits
|
|
@@ -53,6 +56,7 @@ type ComplexProduct = (f64, f64);
|
|
|
53
56
|
|
|
54
57
|
extern "C" {
|
|
55
58
|
|
|
59
|
+
fn simsimd_dot_i8(a: *const i8, b: *const i8, c: usize, d: *mut Distance);
|
|
56
60
|
fn simsimd_dot_f16(a: *const u16, b: *const u16, c: usize, d: *mut Distance);
|
|
57
61
|
fn simsimd_dot_bf16(a: *const u16, b: *const u16, c: usize, d: *mut Distance);
|
|
58
62
|
fn simsimd_dot_f32(a: *const f32, b: *const f32, c: usize, d: *mut Distance);
|
|
@@ -99,8 +103,20 @@ extern "C" {
|
|
|
99
103
|
fn simsimd_kl_f32(a: *const f32, b: *const f32, c: usize, d: *mut Distance);
|
|
100
104
|
fn simsimd_kl_f64(a: *const f64, b: *const f64, c: usize, d: *mut Distance);
|
|
101
105
|
|
|
102
|
-
fn simsimd_intersect_u16(
|
|
103
|
-
|
|
106
|
+
fn simsimd_intersect_u16(
|
|
107
|
+
a: *const u16,
|
|
108
|
+
b: *const u16,
|
|
109
|
+
a_length: usize,
|
|
110
|
+
b_length: usize,
|
|
111
|
+
d: *mut Distance,
|
|
112
|
+
);
|
|
113
|
+
fn simsimd_intersect_u32(
|
|
114
|
+
a: *const u32,
|
|
115
|
+
b: *const u32,
|
|
116
|
+
a_length: usize,
|
|
117
|
+
b_length: usize,
|
|
118
|
+
d: *mut Distance,
|
|
119
|
+
);
|
|
104
120
|
|
|
105
121
|
fn simsimd_uses_neon() -> i32;
|
|
106
122
|
fn simsimd_uses_neon_f16() -> i32;
|
|
@@ -117,6 +133,9 @@ extern "C" {
|
|
|
117
133
|
fn simsimd_uses_sapphire() -> i32;
|
|
118
134
|
fn simsimd_uses_turin() -> i32;
|
|
119
135
|
fn simsimd_uses_sierra() -> i32;
|
|
136
|
+
|
|
137
|
+
fn simsimd_flush_denormals() -> i32;
|
|
138
|
+
fn simsimd_uses_dynamic_dispatch() -> i32;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
/// A half-precision floating point number.
|
|
@@ -194,6 +213,29 @@ pub mod capabilities {
|
|
|
194
213
|
pub fn uses_sierra() -> bool {
|
|
195
214
|
unsafe { crate::simsimd_uses_sierra() != 0 }
|
|
196
215
|
}
|
|
216
|
+
|
|
217
|
+
/// Flushes denormalized numbers to zero on the current CPU architecture.
|
|
218
|
+
///
|
|
219
|
+
/// This function should be called on each thread before any SIMD operations
|
|
220
|
+
/// to avoid performance penalties. When facing denormalized values,
|
|
221
|
+
/// Fused-Multiply-Add (FMA) operations can be up to 30x slower.
|
|
222
|
+
///
|
|
223
|
+
/// # Returns
|
|
224
|
+
///
|
|
225
|
+
/// Returns `true` if the operation was successful, `false` otherwise.
|
|
226
|
+
pub fn flush_denormals() -> bool {
|
|
227
|
+
unsafe { crate::simsimd_flush_denormals() != 0 }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Checks if the library is using dynamic dispatch for function selection.
|
|
231
|
+
///
|
|
232
|
+
/// # Returns
|
|
233
|
+
///
|
|
234
|
+
/// Returns `true` if dynamic dispatch is enabled, `false` otherwise.
|
|
235
|
+
/// Currently always returns `false` as dynamic dispatch is not implemented.
|
|
236
|
+
pub fn uses_dynamic_dispatch() -> bool {
|
|
237
|
+
unsafe { crate::simsimd_uses_dynamic_dispatch() != 0 }
|
|
238
|
+
}
|
|
197
239
|
}
|
|
198
240
|
|
|
199
241
|
/// `SpatialSimilarity` provides a set of trait methods for computing similarity
|
|
@@ -225,8 +267,8 @@ where
|
|
|
225
267
|
fn l2sq(a: &[Self], b: &[Self]) -> Option<Distance>;
|
|
226
268
|
|
|
227
269
|
/// Computes the Euclidean distance between two slices.
|
|
228
|
-
/// The Euclidean distance is the square root of
|
|
229
|
-
// sum of the squared differences between corresponding
|
|
270
|
+
/// The Euclidean distance is the square root of
|
|
271
|
+
// sum of the squared differences between corresponding
|
|
230
272
|
/// elements of the two slices.
|
|
231
273
|
fn l2(a: &[Self], b: &[Self]) -> Option<Distance>;
|
|
232
274
|
|
|
@@ -238,8 +280,8 @@ where
|
|
|
238
280
|
}
|
|
239
281
|
|
|
240
282
|
/// Computes the Euclidean distance between two slices.
|
|
241
|
-
/// The Euclidean distance is the square root of the
|
|
242
|
-
/// sum of the squared differences between corresponding
|
|
283
|
+
/// The Euclidean distance is the square root of the
|
|
284
|
+
/// sum of the squared differences between corresponding
|
|
243
285
|
/// elements of the two slices.
|
|
244
286
|
fn euclidean(a: &[Self], b: &[Self]) -> Option<Distance> {
|
|
245
287
|
SpatialSimilarity::l2(a, b)
|
|
@@ -367,7 +409,7 @@ impl SpatialSimilarity for i8 {
|
|
|
367
409
|
}
|
|
368
410
|
let mut distance_value: Distance = 0.0;
|
|
369
411
|
let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
|
|
370
|
-
unsafe {
|
|
412
|
+
unsafe { simsimd_dot_i8(a.as_ptr(), b.as_ptr(), a.len(), distance_ptr) };
|
|
371
413
|
Some(distance_value)
|
|
372
414
|
}
|
|
373
415
|
|
|
@@ -393,25 +435,21 @@ impl SpatialSimilarity for i8 {
|
|
|
393
435
|
}
|
|
394
436
|
|
|
395
437
|
impl Sparse for u16 {
|
|
396
|
-
|
|
397
438
|
fn intersect(a: &[Self], b: &[Self]) -> Option<Distance> {
|
|
398
439
|
let mut distance_value: Distance = 0.0;
|
|
399
440
|
let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
|
|
400
441
|
unsafe { simsimd_intersect_u16(a.as_ptr(), b.as_ptr(), a.len(), b.len(), distance_ptr) };
|
|
401
442
|
Some(distance_value)
|
|
402
443
|
}
|
|
403
|
-
|
|
404
444
|
}
|
|
405
445
|
|
|
406
446
|
impl Sparse for u32 {
|
|
407
|
-
|
|
408
447
|
fn intersect(a: &[Self], b: &[Self]) -> Option<Distance> {
|
|
409
448
|
let mut distance_value: Distance = 0.0;
|
|
410
449
|
let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
|
|
411
450
|
unsafe { simsimd_intersect_u32(a.as_ptr(), b.as_ptr(), a.len(), b.len(), distance_ptr) };
|
|
412
451
|
Some(distance_value)
|
|
413
452
|
}
|
|
414
|
-
|
|
415
453
|
}
|
|
416
454
|
|
|
417
455
|
impl SpatialSimilarity for f16 {
|
|
@@ -458,7 +496,6 @@ impl SpatialSimilarity for f16 {
|
|
|
458
496
|
}
|
|
459
497
|
|
|
460
498
|
fn l2(a: &[Self], b: &[Self]) -> Option<Distance> {
|
|
461
|
-
|
|
462
499
|
if a.len() != b.len() {
|
|
463
500
|
return None;
|
|
464
501
|
}
|
|
@@ -524,7 +561,7 @@ impl SpatialSimilarity for bf16 {
|
|
|
524
561
|
let b_ptr = b.as_ptr() as *const u16;
|
|
525
562
|
let mut distance_value: Distance = 0.0;
|
|
526
563
|
let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
|
|
527
|
-
unsafe {
|
|
564
|
+
unsafe { simsimd_l2_bf16(a_ptr, b_ptr, a.len(), distance_ptr) };
|
|
528
565
|
Some(distance_value)
|
|
529
566
|
}
|
|
530
567
|
}
|
|
@@ -891,7 +928,7 @@ mod tests {
|
|
|
891
928
|
|
|
892
929
|
if let Some(result) = SpatialSimilarity::dot(a, b) {
|
|
893
930
|
println!("The result of dot_i8 is {:.8}", result);
|
|
894
|
-
assert_almost_equal(0
|
|
931
|
+
assert_almost_equal(32.0, result, 0.01);
|
|
895
932
|
}
|
|
896
933
|
}
|
|
897
934
|
|
|
@@ -993,7 +1030,6 @@ mod tests {
|
|
|
993
1030
|
.map(|&x| HalfF16::from_f32(x))
|
|
994
1031
|
.collect();
|
|
995
1032
|
|
|
996
|
-
|
|
997
1033
|
let a_simsimd: &[f16] =
|
|
998
1034
|
unsafe { std::slice::from_raw_parts(a_half.as_ptr() as *const f16, a_half.len()) };
|
|
999
1035
|
let b_simsimd: &[f16] =
|
|
@@ -1003,7 +1039,6 @@ mod tests {
|
|
|
1003
1039
|
println!("The result of l2_f16 is {:.8}", result);
|
|
1004
1040
|
assert_almost_equal(5.2, result, 0.01);
|
|
1005
1041
|
}
|
|
1006
|
-
|
|
1007
1042
|
}
|
|
1008
1043
|
|
|
1009
1044
|
#[test]
|
|
@@ -1160,8 +1195,8 @@ mod tests {
|
|
|
1160
1195
|
#[test]
|
|
1161
1196
|
fn test_intersect_u16() {
|
|
1162
1197
|
{
|
|
1163
|
-
let a_u16: &[u16] = &[153, 16384, 17408];
|
|
1164
|
-
let b_u16: &[u16] = &[15360, 16384, 7408];
|
|
1198
|
+
let a_u16: &[u16] = &[153, 16384, 17408];
|
|
1199
|
+
let b_u16: &[u16] = &[15360, 16384, 7408];
|
|
1165
1200
|
|
|
1166
1201
|
if let Some(result) = Sparse::intersect(a_u16, b_u16) {
|
|
1167
1202
|
println!("The result of intersect_u16 is {:.8}", result);
|
|
@@ -1170,37 +1205,36 @@ mod tests {
|
|
|
1170
1205
|
}
|
|
1171
1206
|
|
|
1172
1207
|
{
|
|
1173
|
-
let a_u16: &[u16] = &[153, 11638, 08];
|
|
1174
|
-
let b_u16: &[u16] = &[15360, 16384, 7408];
|
|
1208
|
+
let a_u16: &[u16] = &[153, 11638, 08];
|
|
1209
|
+
let b_u16: &[u16] = &[15360, 16384, 7408];
|
|
1175
1210
|
|
|
1176
1211
|
if let Some(result) = Sparse::intersect(a_u16, b_u16) {
|
|
1177
1212
|
println!("The result of intersect_u16 is {:.8}", result);
|
|
1178
1213
|
assert_almost_equal(0.0, result, 0.0001);
|
|
1179
|
-
}
|
|
1214
|
+
}
|
|
1180
1215
|
}
|
|
1181
1216
|
}
|
|
1182
1217
|
|
|
1183
1218
|
#[test]
|
|
1184
1219
|
fn test_intersect_u32() {
|
|
1185
1220
|
{
|
|
1186
|
-
let a_u32: &[u32] = &[11, 153];
|
|
1187
|
-
let b_u32: &[u32] = &[11, 153, 7408, 16384];
|
|
1221
|
+
let a_u32: &[u32] = &[11, 153];
|
|
1222
|
+
let b_u32: &[u32] = &[11, 153, 7408, 16384];
|
|
1188
1223
|
|
|
1189
1224
|
if let Some(result) = Sparse::intersect(a_u32, b_u32) {
|
|
1190
1225
|
println!("The result of intersect_u32 is {:.8}", result);
|
|
1191
1226
|
assert_almost_equal(2.0, result, 0.0001);
|
|
1192
1227
|
}
|
|
1193
1228
|
}
|
|
1194
|
-
|
|
1229
|
+
|
|
1195
1230
|
{
|
|
1196
|
-
let a_u32: &[u32] = &[153, 7408, 11638];
|
|
1197
|
-
let b_u32: &[u32] = &[153, 7408, 11638];
|
|
1231
|
+
let a_u32: &[u32] = &[153, 7408, 11638];
|
|
1232
|
+
let b_u32: &[u32] = &[153, 7408, 11638];
|
|
1198
1233
|
|
|
1199
1234
|
if let Some(result) = Sparse::intersect(a_u32, b_u32) {
|
|
1200
1235
|
println!("The result of intersect_u32 is {:.8}", result);
|
|
1201
1236
|
assert_almost_equal(3.0, result, 0.0001);
|
|
1202
|
-
}
|
|
1237
|
+
}
|
|
1203
1238
|
}
|
|
1204
|
-
|
|
1205
1239
|
}
|
|
1206
1240
|
}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SimSIMD C++ Benchmark Suite
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive benchmarks comparing SimSIMD SIMD-optimized functions against
|
|
5
|
+
* baseline implementations using Google Benchmark framework. Run with:
|
|
6
|
+
*
|
|
7
|
+
* ```bash
|
|
8
|
+
* cmake -B build_release -D SIMSIMD_BUILD_BENCHMARKS=1
|
|
9
|
+
* cmake --build build_release
|
|
10
|
+
* build_release/simsimd_bench
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
#include <array> // `std::array`
|
|
2
15
|
#include <cmath> // `std::sqrt`
|
|
3
16
|
#include <cstdlib> // `std::aligned_alloc`
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
//! Cosine Similarity Benchmark
|
|
2
|
+
//!
|
|
3
|
+
//! Compares SimSIMD vs native Rust implementations using Criterion.
|
|
4
|
+
//! Run with:
|
|
5
|
+
//!
|
|
6
|
+
//! ```bash
|
|
7
|
+
//! cargo bench --bench bench_cosine
|
|
8
|
+
//! ```
|
|
1
9
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
2
10
|
use simsimd::SpatialSimilarity as SimSIMD;
|
|
3
11
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
//! Squared Euclidean Distance Benchmark
|
|
2
|
+
//!
|
|
3
|
+
//! Compares SimSIMD vs native Rust implementations using Criterion.
|
|
4
|
+
//! Run with:
|
|
5
|
+
//!
|
|
6
|
+
//! ```bash
|
|
7
|
+
//! cargo bench --bench bench_sqeuclidean
|
|
8
|
+
//! ```
|
|
1
9
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
2
10
|
use simsimd::SpatialSimilarity as SimSIMD;
|
|
3
11
|
|
package/simsimd/scripts/test.c
CHANGED
|
@@ -156,10 +156,32 @@ void test_distance_from_itself(void) {
|
|
|
156
156
|
simsimd_kl_f64(f64s, f64s, 1536, &distance[0]);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* @brief Test whether denormals are being flushed to zero or not.
|
|
161
|
+
*
|
|
162
|
+
* We create subnormal float values, do a small computation (multiplication),
|
|
163
|
+
* and classify the result. If flush-to-zero @b (FTZ) is enabled, the result is
|
|
164
|
+
* likely zero. Otherwise, you may see another subnormal or normal number.
|
|
165
|
+
*/
|
|
166
|
+
static void test_denormals(void) {
|
|
167
|
+
|
|
168
|
+
// Create two subnormal floats:
|
|
169
|
+
// 1e-40 ~ 1.0 * 10^-40 is typically a subnormal in IEEE-754 single precision
|
|
170
|
+
float subnorm1 = 1e-40f;
|
|
171
|
+
float subnorm2 = 2e-40f;
|
|
172
|
+
float result = subnorm1 * subnorm2; // This might be subnormal, zero, or normal
|
|
173
|
+
int classification = fpclassify(result);
|
|
174
|
+
if (classification == FP_SUBNORMAL) { printf("Denormal test: result is subnormal: %.8g\n", result); }
|
|
175
|
+
else if (result == 0.0f) { printf("Denormal test: result is zero (denormals likely flushed).\n"); }
|
|
176
|
+
else if (classification == FP_NORMAL) { printf("Denormal test: result is normal: %.8g\n", result); }
|
|
177
|
+
else { printf("Denormal test: result has unexpected classification.\n"); }
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
int main(int argc, char **argv) {
|
|
160
181
|
|
|
161
182
|
print_capabilities();
|
|
162
183
|
test_utilities();
|
|
163
184
|
test_distance_from_itself();
|
|
185
|
+
test_denormals();
|
|
164
186
|
return 0;
|
|
165
187
|
}
|
package/simsimd/scripts/test.py
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
Module: test.py
|
|
5
5
|
|
|
6
6
|
This module contains a suite of tests for the `simsimd` package.
|
|
7
|
-
It compares various SIMD kernels (like Dot-products, squared Euclidean, and Cosine distances)
|
|
8
|
-
with their NumPy or baseline counterparts, testing accuracy for different data types including
|
|
7
|
+
It compares various SIMD kernels (like Dot-products, squared Euclidean, and Cosine distances)
|
|
8
|
+
with their NumPy or baseline counterparts, testing accuracy for different data types including
|
|
9
9
|
floating-point, integer, and complex numbers.
|
|
10
10
|
|
|
11
11
|
The tests cover:
|
|
@@ -1245,7 +1245,7 @@ def test_batch(ndim, dtype, capability):
|
|
|
1245
1245
|
result_simd = np.array(simd.sqeuclidean(A, B)).astype(np.float64)
|
|
1246
1246
|
assert np.allclose(result_simd, result_np, atol=SIMSIMD_ATOL, rtol=SIMSIMD_RTOL)
|
|
1247
1247
|
|
|
1248
|
-
# Distance between matrixes A (N x D scalars) and B (N x D scalars) with a
|
|
1248
|
+
# Distance between matrixes A (N x D scalars) and B (N x D scalars) with a different output type.
|
|
1249
1249
|
A = np.random.randn(10, ndim).astype(dtype)
|
|
1250
1250
|
B = np.random.randn(10, ndim).astype(dtype)
|
|
1251
1251
|
result_np = np.array([spd.sqeuclidean(A[i], B[i]) for i in range(10)]).astype(np.float32)
|
|
@@ -1365,7 +1365,7 @@ def test_cdist_complex(ndim, input_dtype, out_dtype, metric, capability):
|
|
|
1365
1365
|
C = C_extended[:, :N]
|
|
1366
1366
|
|
|
1367
1367
|
#! Unlike the `np.dot`, the `np.vdot` flattens multi-dimensional inputs into 1D arrays.
|
|
1368
|
-
#! So to
|
|
1368
|
+
#! So to compare the results we need to manually compute all the dot-products.
|
|
1369
1369
|
expected = np.zeros((M, N), dtype=out_dtype if out_dtype else np.complex128)
|
|
1370
1370
|
baseline_kernel = np.dot if metric == "dot" else np.vdot
|
|
1371
1371
|
for i in range(M):
|
|
@@ -1418,6 +1418,82 @@ def test_cdist_hamming(ndim, out_dtype, capability):
|
|
|
1418
1418
|
np.testing.assert_allclose(result, expected, atol=SIMSIMD_ATOL, rtol=SIMSIMD_RTOL)
|
|
1419
1419
|
|
|
1420
1420
|
|
|
1421
|
+
@pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
|
|
1422
|
+
def test_gil_free_threading():
|
|
1423
|
+
"""Test SimSIMD in Python 3.13t free-threaded mode if available."""
|
|
1424
|
+
import sys
|
|
1425
|
+
import sysconfig
|
|
1426
|
+
|
|
1427
|
+
# Check if we're in a GIL-free environment
|
|
1428
|
+
# https://py-free-threading.github.io/running-gil-disabled/
|
|
1429
|
+
version = sys.version_info
|
|
1430
|
+
if version.major == 3 and version.minor >= 13:
|
|
1431
|
+
is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
|
|
1432
|
+
if not is_free_threaded:
|
|
1433
|
+
pytest.skip("Uses non-free-threaded Python, skipping GIL-related tests")
|
|
1434
|
+
if sys._is_gil_enabled():
|
|
1435
|
+
pytest.skip("GIL is enabled, skipping GIL-related tests")
|
|
1436
|
+
else:
|
|
1437
|
+
pytest.skip("Python < 3.13t, skipping GIL-related tests")
|
|
1438
|
+
|
|
1439
|
+
import multiprocessing
|
|
1440
|
+
import concurrent.futures
|
|
1441
|
+
|
|
1442
|
+
num_threads = multiprocessing.cpu_count()
|
|
1443
|
+
vectors_a = np.random.rand(32 * 1024 * num_threads, 1024).astype(np.float32)
|
|
1444
|
+
vectors_b = np.random.rand(32 * 1024 * num_threads, 1024).astype(np.float32)
|
|
1445
|
+
distances = np.zeros(vectors_a.shape[0], dtype=np.float32)
|
|
1446
|
+
|
|
1447
|
+
def compute_batch(start_idx, end_idx) -> float:
|
|
1448
|
+
"""Compute cosine distances for a batch."""
|
|
1449
|
+
slice_a = vectors_a[start_idx:end_idx]
|
|
1450
|
+
slice_b = vectors_b[start_idx:end_idx]
|
|
1451
|
+
slice_distances = distances[start_idx:end_idx]
|
|
1452
|
+
simd.cosine(slice_a, slice_b, out=slice_distances)
|
|
1453
|
+
return sum(slice_distances)
|
|
1454
|
+
|
|
1455
|
+
def compute_with_threads(threads: int) -> float:
|
|
1456
|
+
"""Compute cosine distances using multiple threads."""
|
|
1457
|
+
chunk_size = len(vectors_a) // threads
|
|
1458
|
+
futures = []
|
|
1459
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
|
|
1460
|
+
for i in range(threads):
|
|
1461
|
+
start_idx = i * chunk_size
|
|
1462
|
+
end_idx = (i + 1) * chunk_size if i < threads - 1 else len(vectors_a)
|
|
1463
|
+
futures.append(executor.submit(compute_batch, start_idx, end_idx))
|
|
1464
|
+
|
|
1465
|
+
total_sum = 0.0
|
|
1466
|
+
for future in concurrent.futures.as_completed(futures):
|
|
1467
|
+
total_sum += future.result()
|
|
1468
|
+
|
|
1469
|
+
return total_sum
|
|
1470
|
+
|
|
1471
|
+
# Dual-threaded baseline is better than single-threaded,
|
|
1472
|
+
# as it will include the overhead of thread management.
|
|
1473
|
+
start_time = time.time()
|
|
1474
|
+
baseline_sum = compute_with_threads(2)
|
|
1475
|
+
end_time = time.time()
|
|
1476
|
+
baseline_duration = end_time - start_time
|
|
1477
|
+
|
|
1478
|
+
# Multi-threaded execution, using all available threads
|
|
1479
|
+
start_time = time.time()
|
|
1480
|
+
multi_sum = compute_with_threads(num_threads)
|
|
1481
|
+
end_time = time.time()
|
|
1482
|
+
multi_duration = end_time - start_time
|
|
1483
|
+
|
|
1484
|
+
# Verify results are the same length and reasonable
|
|
1485
|
+
assert np.allclose(
|
|
1486
|
+
baseline_sum, multi_sum, atol=SIMSIMD_ATOL, rtol=SIMSIMD_RTOL
|
|
1487
|
+
), f"Results differ: baseline {baseline_sum} vs multi-threaded {multi_sum}"
|
|
1488
|
+
|
|
1489
|
+
# Warn if multi-threaded execution is slower than the baseline
|
|
1490
|
+
if baseline_duration < multi_duration:
|
|
1491
|
+
pytest.warns(
|
|
1492
|
+
UserWarning,
|
|
1493
|
+
f"{num_threads}-threaded execution took longer than 2-threaded baseline: {multi_duration:.2f}s vs {baseline_duration:.2f}s",
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
|
|
1421
1497
|
if __name__ == "__main__":
|
|
1422
1498
|
pytest.main(
|
|
1423
1499
|
[
|
|
@@ -4,6 +4,7 @@ public protocol SimSIMD {
|
|
|
4
4
|
static var dataType: simsimd_datatype_t { get }
|
|
5
5
|
static var cosine: simsimd_metric_dense_punned_t { get }
|
|
6
6
|
static var dotProduct: simsimd_metric_dense_punned_t { get }
|
|
7
|
+
static var euclidean: simsimd_metric_dense_punned_t { get }
|
|
7
8
|
static var squaredEuclidean: simsimd_metric_dense_punned_t { get }
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -11,21 +12,26 @@ extension Int8: SimSIMD {
|
|
|
11
12
|
public static let dataType = simsimd_datatype_i8_k
|
|
12
13
|
public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
|
|
13
14
|
public static let dotProduct = find(kind: simsimd_metric_dot_k, dataType: dataType)
|
|
15
|
+
public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
|
|
14
16
|
public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
#if !arch(x86_64)
|
|
17
20
|
@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
|
|
18
21
|
extension Float16: SimSIMD {
|
|
19
22
|
public static let dataType = simsimd_datatype_f16_k
|
|
20
23
|
public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
|
|
21
24
|
public static let dotProduct = find(kind: simsimd_metric_dot_k, dataType: dataType)
|
|
25
|
+
public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
|
|
22
26
|
public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
|
|
23
27
|
}
|
|
28
|
+
#endif
|
|
24
29
|
|
|
25
30
|
extension Float32: SimSIMD {
|
|
26
31
|
public static let dataType = simsimd_datatype_f32_k
|
|
27
32
|
public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
|
|
28
33
|
public static let dotProduct = find(kind: simsimd_metric_inner_k, dataType: dataType)
|
|
34
|
+
public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
|
|
29
35
|
public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
|
|
30
36
|
}
|
|
31
37
|
|
|
@@ -33,6 +39,7 @@ extension Float64: SimSIMD {
|
|
|
33
39
|
public static let dataType = simsimd_datatype_f64_k
|
|
34
40
|
public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
|
|
35
41
|
public static let dotProduct = find(kind: simsimd_metric_dot_k, dataType: dataType)
|
|
42
|
+
public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
|
|
36
43
|
public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -47,6 +54,11 @@ extension SimSIMD {
|
|
|
47
54
|
perform(dotProduct, a: a, b: b)
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
@inlinable @inline(__always)
|
|
58
|
+
public static func euclidean<A, B>(_ a: A, _ b: B) -> Double? where A: Sequence, B: Sequence, A.Element == Self, B.Element == Self {
|
|
59
|
+
perform(euclidean, a: a, b: b)
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
@inlinable @inline(__always)
|
|
51
63
|
public static func sqeuclidean<A, B>(_ a: A, _ b: B) -> Double? where A: Sequence, B: Sequence, A.Element == Self, B.Element == Self {
|
|
52
64
|
perform(squaredEuclidean, a: a, b: b)
|
|
@@ -64,6 +76,11 @@ extension RandomAccessCollection where Element: SimSIMD {
|
|
|
64
76
|
Element.dot(self, b)
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
@inlinable @inline(__always)
|
|
80
|
+
public func euclidean<B>(_ b: B) -> Double? where B: Sequence, B.Element == Element {
|
|
81
|
+
Element.euclidean(self, b)
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
@inlinable @inline(__always)
|
|
68
85
|
public func sqeuclidean<B>(_ b: B) -> Double? where B: Sequence, B.Element == Element {
|
|
69
86
|
Element.sqeuclidean(self, b)
|
package/simsimd/swift/Test.swift
CHANGED
|
@@ -13,12 +13,14 @@ class SimSIMDTests: XCTestCase {
|
|
|
13
13
|
XCTAssertEqual(result, 0.00012027938, accuracy: 0.01)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
#if !arch(x86_64)
|
|
16
17
|
func testCosineFloat16() throws {
|
|
17
18
|
let a: [Float16] = [1.0, 2.0, 3.0]
|
|
18
19
|
let b: [Float16] = [1.0, 2.0, 3.0]
|
|
19
20
|
let result = try XCTUnwrap(a.cosine(b))
|
|
20
21
|
XCTAssertEqual(result, 0.004930496, accuracy: 0.01)
|
|
21
22
|
}
|
|
23
|
+
#endif
|
|
22
24
|
|
|
23
25
|
func testCosineFloat32() throws {
|
|
24
26
|
let a: [Float32] = [1.0, 2.0, 3.0]
|
|
@@ -41,12 +43,14 @@ class SimSIMDTests: XCTestCase {
|
|
|
41
43
|
XCTAssertEqual(result, 32.0, accuracy: 0.01)
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
#if !arch(x86_64)
|
|
44
47
|
func testDotFloat16() throws {
|
|
45
48
|
let a: [Float16] = [1.0, 2.0, 3.0]
|
|
46
49
|
let b: [Float16] = [4.0, 5.0, 6.0]
|
|
47
50
|
let result = try XCTUnwrap(a.dot(b))
|
|
48
51
|
XCTAssertEqual(result, 32.0, accuracy: 0.01)
|
|
49
52
|
}
|
|
53
|
+
#endif
|
|
50
54
|
|
|
51
55
|
func testDotFloat32() throws {
|
|
52
56
|
let a: [Float32] = [1.0, 2.0, 3.0]
|
|
@@ -62,6 +66,36 @@ class SimSIMDTests: XCTestCase {
|
|
|
62
66
|
XCTAssertEqual(result, 32.0, accuracy: 0.01)
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
func testEuclideanInt8() throws {
|
|
70
|
+
let a: [Int8] = [1, 2, 3]
|
|
71
|
+
let b: [Int8] = [4, 5, 6]
|
|
72
|
+
let result = try XCTUnwrap(a.euclidean(b))
|
|
73
|
+
XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#if !arch(x86_64)
|
|
77
|
+
func testEuclideanFloat16() throws {
|
|
78
|
+
let a: [Float16] = [1.0, 2.0, 3.0]
|
|
79
|
+
let b: [Float16] = [4.0, 5.0, 6.0]
|
|
80
|
+
let result = try XCTUnwrap(a.euclidean(b))
|
|
81
|
+
XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
|
|
82
|
+
}
|
|
83
|
+
#endif
|
|
84
|
+
|
|
85
|
+
func testEuclideanFloat32() throws {
|
|
86
|
+
let a: [Float32] = [1.0, 2.0, 3.0]
|
|
87
|
+
let b: [Float32] = [4.0, 5.0, 6.0]
|
|
88
|
+
let result = try XCTUnwrap(a.euclidean(b))
|
|
89
|
+
XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func testEuclideanFloat64() throws {
|
|
93
|
+
let a: [Float64] = [1.0, 2.0, 3.0]
|
|
94
|
+
let b: [Float64] = [4.0, 5.0, 6.0]
|
|
95
|
+
let result = try XCTUnwrap(a.euclidean(b))
|
|
96
|
+
XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
func testSqeuclideanInt8() throws {
|
|
66
100
|
let a: [Int8] = [1, 2, 3]
|
|
67
101
|
let b: [Int8] = [4, 5, 6]
|
|
@@ -69,12 +103,14 @@ class SimSIMDTests: XCTestCase {
|
|
|
69
103
|
XCTAssertEqual(result, 27.0, accuracy: 0.01)
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
#if !arch(x86_64)
|
|
72
107
|
func testSqeuclideanFloat16() throws {
|
|
73
108
|
let a: [Float16] = [1.0, 2.0, 3.0]
|
|
74
109
|
let b: [Float16] = [4.0, 5.0, 6.0]
|
|
75
110
|
let result = try XCTUnwrap(a.sqeuclidean(b))
|
|
76
111
|
XCTAssertEqual(result, 27.0, accuracy: 0.01)
|
|
77
112
|
}
|
|
113
|
+
#endif
|
|
78
114
|
|
|
79
115
|
func testSqeuclideanFloat32() throws {
|
|
80
116
|
let a: [Float32] = [1.0, 2.0, 3.0]
|