usearch 0.2.2 → 0.2.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/.github/workflows/release.yml +39 -15
- package/.github/workflows/update_version.sh +6 -0
- package/.releaserc +77 -0
- package/.vscode/settings.json +10 -1
- package/Cargo.toml +1 -1
- package/README.md +281 -230
- package/VERSION +1 -1
- package/conanfile.py +3 -2
- package/docs/Benchmarking.md +182 -0
- package/docs/Compilation.md +112 -0
- package/include/usearch/usearch.hpp +212 -116
- package/objc/include/USearchObjective.h +10 -9
- package/package.json +7 -1
- package/python/bench.py +100 -28
- package/python/lib.cpp +51 -28
- package/python/test.py +78 -36
- package/setup.py +11 -3
- package/simsimd/include/simsimd/simsimd.h +31 -41
- package/src/advanced.hpp +58 -23
- package/src/bench.cpp +11 -15
- package/.github/workflows/package.json +0 -82
- package/CMakeListsInstructions.md +0 -185
package/python/bench.py
CHANGED
|
@@ -1,33 +1,75 @@
|
|
|
1
|
-
import
|
|
1
|
+
import time
|
|
2
2
|
import numpy as np
|
|
3
3
|
import fire
|
|
4
4
|
|
|
5
|
-
import faiss
|
|
6
|
-
import usearch
|
|
7
|
-
|
|
8
|
-
import struct
|
|
9
5
|
import numpy as np
|
|
6
|
+
from numba import cfunc, types, carray
|
|
7
|
+
|
|
8
|
+
from faiss import IndexHNSWFlat, IndexIVFPQ
|
|
9
|
+
from usearch.index import Index
|
|
10
|
+
from usearch.io import load_matrix
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def measure(f) -> float:
|
|
12
|
-
a =
|
|
13
|
-
f()
|
|
14
|
-
b =
|
|
14
|
+
a = time.time_ns()
|
|
15
|
+
result = f()
|
|
16
|
+
b = time.time_ns()
|
|
15
17
|
c = b - a
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
secs = c / (10 ** 9)
|
|
19
|
+
print(f'- Took: {secs:.2f} seconds')
|
|
20
|
+
return secs, result
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
|
|
21
|
-
dt = measure(lambda: index.add(vectors))
|
|
22
|
-
print(f'-
|
|
24
|
+
dt, _ = measure(lambda: index.add(vectors))
|
|
25
|
+
print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
|
|
26
|
+
|
|
27
|
+
dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
|
|
28
|
+
print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
|
|
29
|
+
|
|
30
|
+
recall_at_one = 0.0
|
|
31
|
+
matches = results[1]
|
|
32
|
+
for i in range(queries.shape[0]):
|
|
33
|
+
recall_at_one += neighbors[i, 0] in matches[i, :]
|
|
34
|
+
recall_at_one /= queries.shape[0]
|
|
35
|
+
print(f'- Recall@1: {recall_at_one * 100:.2f} %')
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
|
|
26
39
|
labels = np.arange(vectors.shape[0], dtype=np.longlong)
|
|
27
40
|
assert len(index) == 0
|
|
28
|
-
dt = measure(lambda: index.add(labels, vectors))
|
|
41
|
+
dt, _ = measure(lambda: index.add(labels, vectors))
|
|
29
42
|
assert len(index) == vectors.shape[0]
|
|
30
|
-
print(f'-
|
|
43
|
+
print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
|
|
44
|
+
|
|
45
|
+
dt, results = measure(lambda: index.search(queries, neighbors.shape[1]))
|
|
46
|
+
print(f'- Performance: {queries.shape[0]/dt:.2f} queries/s')
|
|
47
|
+
|
|
48
|
+
recall_at_one = 0.0
|
|
49
|
+
matches = results[0]
|
|
50
|
+
for i in range(queries.shape[0]):
|
|
51
|
+
recall_at_one += neighbors[i, 0] in matches[i, :]
|
|
52
|
+
recall_at_one /= queries.shape[0]
|
|
53
|
+
print(f'- Recall@1: {recall_at_one * 100:.2f} %')
|
|
54
|
+
|
|
55
|
+
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
56
|
+
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
signature_f32 = types.float32(
|
|
60
|
+
types.CPointer(types.float32),
|
|
61
|
+
types.CPointer(types.float32),
|
|
62
|
+
types.size_t, types.size_t)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@cfunc(signature_f32)
|
|
66
|
+
def inner_product_f32(a, b, n, m):
|
|
67
|
+
a_array = carray(a, n)
|
|
68
|
+
b_array = carray(b, n)
|
|
69
|
+
c = types.float32(0)
|
|
70
|
+
for i in range(n):
|
|
71
|
+
c += a_array[i] * b_array[i]
|
|
72
|
+
return types.float32(1 - c)
|
|
31
73
|
|
|
32
74
|
|
|
33
75
|
def main(
|
|
@@ -44,31 +86,61 @@ def main(
|
|
|
44
86
|
neighbors_mat = load_matrix(neighbors)
|
|
45
87
|
dim = vectors_mat.shape[1]
|
|
46
88
|
|
|
47
|
-
|
|
48
|
-
index =
|
|
89
|
+
print('USearch: HNSW')
|
|
90
|
+
index = Index(
|
|
91
|
+
ndim=dim,
|
|
92
|
+
expansion_add=expansion_add,
|
|
93
|
+
expansion_search=expansion_search,
|
|
94
|
+
connectivity=connectivity,
|
|
95
|
+
)
|
|
96
|
+
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
97
|
+
|
|
98
|
+
print('USearch: HNSW + Numba JIT')
|
|
99
|
+
index = Index(
|
|
100
|
+
ndim=dim,
|
|
101
|
+
expansion_add=expansion_add,
|
|
102
|
+
expansion_search=expansion_search,
|
|
103
|
+
connectivity=connectivity,
|
|
104
|
+
metric_pointer=inner_product_f32.address,
|
|
105
|
+
)
|
|
106
|
+
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
107
|
+
|
|
108
|
+
print('USearch: HNSW + Half Precision')
|
|
109
|
+
index = Index(
|
|
110
|
+
ndim=dim,
|
|
111
|
+
dtype='f16',
|
|
112
|
+
expansion_add=expansion_add,
|
|
113
|
+
expansion_search=expansion_search,
|
|
114
|
+
connectivity=connectivity,
|
|
115
|
+
)
|
|
116
|
+
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
117
|
+
|
|
118
|
+
print('USearch: HNSW + Quarter Precision')
|
|
119
|
+
index = Index(
|
|
120
|
+
ndim=dim,
|
|
121
|
+
dtype='f8',
|
|
122
|
+
expansion_add=expansion_add,
|
|
123
|
+
expansion_search=expansion_search,
|
|
124
|
+
connectivity=connectivity,
|
|
125
|
+
)
|
|
126
|
+
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
127
|
+
|
|
128
|
+
print('FAISS: HNSW')
|
|
129
|
+
index = IndexHNSWFlat(dim, connectivity)
|
|
49
130
|
index.hnsw.efSearch = expansion_search
|
|
50
131
|
index.hnsw.efConstruction = expansion_add
|
|
51
132
|
bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
|
|
52
133
|
|
|
53
|
-
|
|
134
|
+
print('FAISS: HNSW + IVFPQ')
|
|
54
135
|
nlist = 10000 # Number of inverted lists (number of partitions or cells).
|
|
55
136
|
nsegment = 16 # Number of segments for PQ (number of subquantizers).
|
|
56
137
|
nbit = 8 # Number of bits to encode each segment.
|
|
57
|
-
coarse_quantizer =
|
|
58
|
-
index =
|
|
138
|
+
coarse_quantizer = IndexHNSWFlat(dim, connectivity)
|
|
139
|
+
index = IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
|
|
59
140
|
index.train(vectors_mat)
|
|
60
141
|
index.nprobe = 10
|
|
61
142
|
bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
|
|
62
143
|
|
|
63
|
-
# Test USearch `Index`
|
|
64
|
-
index = usearch.Index(
|
|
65
|
-
ndim=dim,
|
|
66
|
-
expansion_add=expansion_add,
|
|
67
|
-
expansion_search=expansion_search,
|
|
68
|
-
connectivity=connectivity,
|
|
69
|
-
)
|
|
70
|
-
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
71
|
-
|
|
72
144
|
|
|
73
145
|
if __name__ == '__main__':
|
|
74
146
|
fire.Fire(main)
|
package/python/lib.cpp
CHANGED
|
@@ -59,6 +59,25 @@ struct hash_index_w_meta_t : public hash_index_t {
|
|
|
59
59
|
span_gt<hash_word_t const> buffer() const noexcept { return {buffer_.data(), buffer_.size()}; }
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
template <typename scalar_at> punned_stateful_metric_t udf(std::size_t metric_uintptr) {
|
|
63
|
+
return [metric_uintptr](byte_t const* a_bytes, byte_t const* b_bytes, std::size_t n, std::size_t m) -> distance_t {
|
|
64
|
+
using metric_t = punned_distance_t (*)(scalar_at const*, scalar_at const*, std::size_t, std::size_t);
|
|
65
|
+
metric_t metric_ptr = reinterpret_cast<metric_t>(metric_uintptr);
|
|
66
|
+
scalar_at const* a = reinterpret_cast<scalar_at const*>(a_bytes);
|
|
67
|
+
scalar_at const* b = reinterpret_cast<scalar_at const*>(b_bytes);
|
|
68
|
+
return metric_ptr(a, b, n / sizeof(scalar_at), m / sizeof(scalar_at));
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
punned_stateful_metric_t udf(std::size_t metric_uintptr, accuracy_t accuracy) {
|
|
73
|
+
switch (accuracy) {
|
|
74
|
+
case accuracy_t::f8_k: return udf<f8_bits_t>(metric_uintptr);
|
|
75
|
+
case accuracy_t::f16_k: return udf<f16_bits_t>(metric_uintptr);
|
|
76
|
+
case accuracy_t::f32_k: return udf<f32_t>(metric_uintptr);
|
|
77
|
+
case accuracy_t::f64_k: return udf<f64_t>(metric_uintptr);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
static native_index_t make_index( //
|
|
63
82
|
std::size_t dimensions, //
|
|
64
83
|
std::size_t capacity, //
|
|
@@ -79,9 +98,8 @@ static native_index_t make_index( //
|
|
|
79
98
|
config.max_threads_search = std::thread::hardware_concurrency();
|
|
80
99
|
|
|
81
100
|
accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return native_index_t::udf(dimensions, metric_ptr, accuracy, config);
|
|
101
|
+
if (metric_uintptr)
|
|
102
|
+
return native_index_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
|
|
85
103
|
else
|
|
86
104
|
return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
|
|
87
105
|
}
|
|
@@ -127,9 +145,9 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
|
|
|
127
145
|
if (vector_info.ndim != 1)
|
|
128
146
|
throw std::invalid_argument("Expects a vector, not a higher-rank tensor!");
|
|
129
147
|
|
|
130
|
-
|
|
148
|
+
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
131
149
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
132
|
-
if (vector_dimensions != static_cast<
|
|
150
|
+
if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
133
151
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
134
152
|
|
|
135
153
|
if (index.size() + 1 >= index.capacity())
|
|
@@ -146,7 +164,9 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
|
|
|
146
164
|
throw std::invalid_argument("Incompatible scalars in the vector!");
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
static void add_many_to_index(
|
|
167
|
+
static void add_many_to_index( //
|
|
168
|
+
native_index_t& index, py::buffer labels, py::buffer vectors, //
|
|
169
|
+
bool copy, std::size_t threads = 0) {
|
|
150
170
|
|
|
151
171
|
py::buffer_info labels_info = labels.request();
|
|
152
172
|
py::buffer_info vectors_info = vectors.request();
|
|
@@ -160,10 +180,10 @@ static void add_many_to_index(native_index_t& index, py::buffer labels, py::buff
|
|
|
160
180
|
if (vectors_info.ndim != 2)
|
|
161
181
|
throw std::invalid_argument("Expects a matrix of vectors to add!");
|
|
162
182
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (vectors_dimensions != static_cast<
|
|
183
|
+
Py_ssize_t labels_count = labels_info.shape[0];
|
|
184
|
+
Py_ssize_t vectors_count = vectors_info.shape[0];
|
|
185
|
+
Py_ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
186
|
+
if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
167
187
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
168
188
|
|
|
169
189
|
if (labels_count != vectors_count)
|
|
@@ -202,13 +222,13 @@ static void add_many_to_index(native_index_t& index, py::buffer labels, py::buff
|
|
|
202
222
|
static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, std::size_t wanted) {
|
|
203
223
|
|
|
204
224
|
py::buffer_info vector_info = vector.request();
|
|
205
|
-
|
|
225
|
+
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
206
226
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
207
|
-
if (vector_dimensions != static_cast<
|
|
227
|
+
if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
208
228
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
209
229
|
|
|
210
|
-
py::array_t<label_t> labels_py(static_cast<
|
|
211
|
-
py::array_t<distance_t> distances_py(static_cast<
|
|
230
|
+
py::array_t<label_t> labels_py(static_cast<Py_ssize_t>(wanted));
|
|
231
|
+
py::array_t<distance_t> distances_py(static_cast<Py_ssize_t>(wanted));
|
|
212
232
|
std::size_t count{};
|
|
213
233
|
auto labels_py1d = labels_py.mutable_unchecked<1>();
|
|
214
234
|
auto distances_py1d = distances_py.mutable_unchecked<1>();
|
|
@@ -226,8 +246,8 @@ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, s
|
|
|
226
246
|
else
|
|
227
247
|
throw std::invalid_argument("Incompatible scalars in the query vector!");
|
|
228
248
|
|
|
229
|
-
labels_py.resize(py_shape_t{static_cast<
|
|
230
|
-
distances_py.resize(py_shape_t{static_cast<
|
|
249
|
+
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
250
|
+
distances_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
231
251
|
|
|
232
252
|
py::tuple results(3);
|
|
233
253
|
results[0] = labels_py;
|
|
@@ -256,14 +276,14 @@ static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors,
|
|
|
256
276
|
if (vectors_info.ndim != 2)
|
|
257
277
|
throw std::invalid_argument("Expects a matrix of vectors to add!");
|
|
258
278
|
|
|
259
|
-
|
|
260
|
-
|
|
279
|
+
Py_ssize_t vectors_count = vectors_info.shape[0];
|
|
280
|
+
Py_ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
261
281
|
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
262
|
-
if (vectors_dimensions != static_cast<
|
|
282
|
+
if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
263
283
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
264
284
|
|
|
265
|
-
py::array_t<label_t> labels_py({vectors_count, static_cast<
|
|
266
|
-
py::array_t<distance_t> distances_py({vectors_count, static_cast<
|
|
285
|
+
py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
286
|
+
py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
267
287
|
py::array_t<Py_ssize_t> counts_py(vectors_count);
|
|
268
288
|
auto labels_py2d = labels_py.mutable_unchecked<2>();
|
|
269
289
|
auto distances_py2d = distances_py.mutable_unchecked<2>();
|
|
@@ -344,8 +364,8 @@ inline std::uint64_t hash(std::uint64_t v) noexcept {
|
|
|
344
364
|
template <typename scalar_at>
|
|
345
365
|
inline void hash_typed_buffer(hash_index_w_meta_t& index, py::buffer_info const& vector_info) noexcept {
|
|
346
366
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
347
|
-
|
|
348
|
-
|
|
367
|
+
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
368
|
+
Py_ssize_t vector_stride = vector_info.strides[0];
|
|
349
369
|
std::memset(index.buffer_.data(), 0, index.words_ * sizeof(hash_word_t));
|
|
350
370
|
|
|
351
371
|
for (std::size_t i = 0; i != static_cast<std::size_t>(vector_dimensions); ++i) {
|
|
@@ -393,7 +413,8 @@ PYBIND11_MODULE(index, m) {
|
|
|
393
413
|
py::arg("labels"), //
|
|
394
414
|
py::arg("vectors"), //
|
|
395
415
|
py::kw_only(), //
|
|
396
|
-
py::arg("copy") = true
|
|
416
|
+
py::arg("copy") = true, //
|
|
417
|
+
py::arg("threads") = 0 //
|
|
397
418
|
);
|
|
398
419
|
|
|
399
420
|
i.def( //
|
|
@@ -415,6 +436,8 @@ PYBIND11_MODULE(index, m) {
|
|
|
415
436
|
i.def_property_readonly("ndim", &native_index_t::dimensions);
|
|
416
437
|
i.def_property_readonly("connectivity", &native_index_t::connectivity);
|
|
417
438
|
i.def_property_readonly("capacity", &native_index_t::capacity);
|
|
439
|
+
i.def_property_readonly( //
|
|
440
|
+
"dtype", [](native_index_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
|
|
418
441
|
|
|
419
442
|
i.def("save", &save_index<native_index_t>, py::arg("path"));
|
|
420
443
|
i.def("load", &load_index<native_index_t>, py::arg("path"));
|
|
@@ -454,10 +477,10 @@ PYBIND11_MODULE(index, m) {
|
|
|
454
477
|
validate_set(set);
|
|
455
478
|
auto proxy = set.unchecked<1>();
|
|
456
479
|
auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
457
|
-
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<
|
|
480
|
+
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
458
481
|
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
459
482
|
auto found = index.search(view, count, &labels_proxy(0), nullptr, 0);
|
|
460
|
-
labels_py.resize(py_shape_t{static_cast<
|
|
483
|
+
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
|
|
461
484
|
return labels_py;
|
|
462
485
|
},
|
|
463
486
|
py::arg("set"), //
|
|
@@ -504,10 +527,10 @@ PYBIND11_MODULE(index, m) {
|
|
|
504
527
|
if (index.size() + 1 >= index.capacity())
|
|
505
528
|
index.reserve(ceil2(index.size() + 1));
|
|
506
529
|
hash_buffer(index, array);
|
|
507
|
-
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<
|
|
530
|
+
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
508
531
|
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
509
532
|
auto found = index.search(index.buffer(), count, &labels_proxy(0), nullptr, 0);
|
|
510
|
-
labels_py.resize(py_shape_t{static_cast<
|
|
533
|
+
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
|
|
511
534
|
return labels_py;
|
|
512
535
|
},
|
|
513
536
|
py::arg("array"), //
|
package/python/test.py
CHANGED
|
@@ -1,51 +1,93 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
import numpy as np
|
|
2
3
|
|
|
4
|
+
|
|
3
5
|
from usearch.index import Index, SetsIndex
|
|
4
|
-
from numba import cfunc, types, carray
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
dimensions = [13, 99, 100, 256]
|
|
8
|
+
sizes = [1, 2, 100]
|
|
9
|
+
types = ['f32', 'f16', 'f8', 'f64']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize('ndim', dimensions)
|
|
13
|
+
@pytest.mark.parametrize('dtype', types)
|
|
14
|
+
def test_cos(ndim: int, dtype: str):
|
|
15
|
+
index = Index(ndim=ndim, metric='cos', dtype=dtype)
|
|
16
|
+
vector = np.random.uniform(0, 0.7, (index.ndim)).astype(np.float32)
|
|
17
|
+
index.add(42, vector)
|
|
18
|
+
matches, distances, count = index.search(vector, 10)
|
|
19
|
+
|
|
20
|
+
assert len(matches) == count
|
|
21
|
+
assert len(distances) == count
|
|
22
|
+
assert count == 1
|
|
23
|
+
assert matches[0] == 42
|
|
24
|
+
assert distances[0] == pytest.approx(0, abs=1e-3)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize('ndim', dimensions)
|
|
28
|
+
@pytest.mark.parametrize('size', sizes)
|
|
29
|
+
@pytest.mark.parametrize('dtype', types)
|
|
30
|
+
def test_cos_batch(ndim: int, size: int, dtype: str):
|
|
31
|
+
index = Index(ndim=ndim, metric='cos', dtype=dtype)
|
|
32
|
+
|
|
33
|
+
vectors = np.random.uniform(
|
|
34
|
+
0, 0.3, (size, index.ndim)).astype(np.float32)
|
|
35
|
+
labels = np.array(range(size), dtype=np.longlong)
|
|
36
|
+
|
|
37
|
+
index.add(labels, vectors)
|
|
38
|
+
matches, distances, count = index.search(vectors, 10)
|
|
39
|
+
|
|
40
|
+
assert matches.shape[0] == distances.shape[0]
|
|
41
|
+
assert count.shape[0] == size
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize('ndim', dimensions)
|
|
45
|
+
@pytest.mark.parametrize('size', sizes)
|
|
46
|
+
def test_user_defined_function(ndim: int, size: int):
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from numba import cfunc, types, carray
|
|
50
|
+
except ImportError:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
54
|
+
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
55
|
+
signature = types.float32(
|
|
56
|
+
types.CPointer(types.float32),
|
|
57
|
+
types.CPointer(types.float32),
|
|
58
|
+
types.size_t, types.size_t)
|
|
8
59
|
|
|
9
|
-
signature
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
60
|
+
@cfunc(signature)
|
|
61
|
+
def python_dot(a, b, n, m):
|
|
62
|
+
a_array = carray(a, n)
|
|
63
|
+
b_array = carray(b, n)
|
|
64
|
+
c = 0.0
|
|
65
|
+
for i in range(n):
|
|
66
|
+
c += a_array[i] * b_array[i]
|
|
67
|
+
return c
|
|
13
68
|
|
|
69
|
+
index = Index(ndim=ndim, metric_pointer=python_dot.address)
|
|
14
70
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
b_array = carray(b, n)
|
|
19
|
-
c = 0.0
|
|
20
|
-
for i in range(n):
|
|
21
|
-
c += a_array[i] * b_array[i]
|
|
22
|
-
return c
|
|
71
|
+
vectors = np.random.uniform(
|
|
72
|
+
0, 0.3, (size, index.ndim)).astype(np.float32)
|
|
73
|
+
labels = np.array(range(size), dtype=np.longlong)
|
|
23
74
|
|
|
75
|
+
index.add(labels, vectors)
|
|
76
|
+
matches, distances, count = index.search(vectors, 10)
|
|
24
77
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
vectors = np.random.uniform(
|
|
28
|
-
0, 0.3, (count_vectors, count_dimensions)).astype(np.float32)
|
|
29
|
-
labels = np.array(range(count_vectors), dtype=np.longlong)
|
|
78
|
+
assert matches.shape[0] == distances.shape[0]
|
|
79
|
+
assert count.shape[0] == size
|
|
30
80
|
|
|
31
|
-
index_udf = Index(ndim=96, metric_pointer=python_dot.address)
|
|
32
|
-
index_udf.add(labels, vectors, copy=True)
|
|
33
|
-
results = index_udf.search(vectors, 10)
|
|
34
|
-
print('found', results[0].shape, results[1].shape, results[2].shape)
|
|
35
81
|
|
|
82
|
+
def test_sets():
|
|
36
83
|
|
|
37
|
-
index =
|
|
38
|
-
index.add(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
assert len(index) == count_vectors
|
|
84
|
+
index = SetsIndex()
|
|
85
|
+
index.add(10, np.array([10, 12, 15], dtype=np.uint32))
|
|
86
|
+
index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
87
|
+
results = index.search(np.array([12, 15], dtype=np.uint32), 20)
|
|
42
88
|
|
|
43
|
-
results
|
|
44
|
-
print('found', results[0].shape, results[1].shape, results[2].shape)
|
|
89
|
+
# assert list(results) == [10, 11]
|
|
45
90
|
|
|
46
91
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
index_sets.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
50
|
-
results = index_sets.search(np.array([12, 15], dtype=np.uint32), 20)
|
|
51
|
-
print(results)
|
|
92
|
+
if __name__ == '__main__':
|
|
93
|
+
pass
|
package/setup.py
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
-
from setuptools import setup
|
|
3
|
+
from setuptools import setup
|
|
4
4
|
from pybind11.setup_helpers import Pybind11Extension
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
compile_args = ['-std=c++11', '-O3']
|
|
8
|
+
link_args = []
|
|
9
|
+
|
|
10
|
+
if sys.platform == 'linux':
|
|
11
|
+
compile_args.append('-fopenmp')
|
|
12
|
+
link_args.append('-lgomp')
|
|
8
13
|
|
|
9
14
|
if sys.platform == 'darwin':
|
|
10
|
-
|
|
15
|
+
# MacOS 10.15 orhigher is needed for `aligned_alloc` support.
|
|
16
|
+
# https://github.com/unum-cloud/usearch/actions/runs/4975434891/jobs/8902603392
|
|
17
|
+
compile_args.append('-mmacosx-version-min=10.15')
|
|
11
18
|
|
|
12
19
|
if sys.platform == 'win32':
|
|
13
20
|
compile_args = ['/std:c++14', '/O2']
|
|
@@ -17,7 +24,8 @@ ext_modules = [
|
|
|
17
24
|
Pybind11Extension(
|
|
18
25
|
'usearch.index',
|
|
19
26
|
['python/lib.cpp'],
|
|
20
|
-
extra_compile_args=compile_args
|
|
27
|
+
extra_compile_args=compile_args,
|
|
28
|
+
extra_link_args=link_args,
|
|
21
29
|
),
|
|
22
30
|
]
|
|
23
31
|
|
|
@@ -56,30 +56,6 @@ inline static simsimd_f32_t simsimd_dot_f32sve(simsimd_f32_t const* a, simsimd_f
|
|
|
56
56
|
#endif
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
inline static simsimd_f16_t simsimd_dot_f16sve(simsimd_f16_t const* a_enum, simsimd_f16_t const* b_enum, size_t d) {
|
|
60
|
-
#if defined(__ARM_FEATURE_SVE)
|
|
61
|
-
size_t i = 0;
|
|
62
|
-
svfloat16_t ab_vec = svdupq_n_f16(0, 0, 0, 0, 0, 0, 0, 0);
|
|
63
|
-
svbool_t pg_vec = svwhilelt_b16(i, d);
|
|
64
|
-
simsimd_f16_t const* a = (simsimd_f16_t const*)(a_enum);
|
|
65
|
-
simsimd_f16_t const* b = (simsimd_f16_t const*)(b_enum);
|
|
66
|
-
do {
|
|
67
|
-
svfloat16_t a_vec = svld1_f16(pg_vec, (float16_t const*)a + i);
|
|
68
|
-
svfloat16_t b_vec = svld1_f16(pg_vec, (float16_t const*)b + i);
|
|
69
|
-
ab_vec = svmla_f16_x(pg_vec, ab_vec, a_vec, b_vec);
|
|
70
|
-
i += svcnth();
|
|
71
|
-
pg_vec = svwhilelt_b16(i, d);
|
|
72
|
-
} while (svptest_any(svptrue_b16(), pg_vec));
|
|
73
|
-
float16_t f16 = svaddv_f16(svptrue_b16(), ab_vec);
|
|
74
|
-
simsimd_f16_t i16;
|
|
75
|
-
memcpy(&i16, &f16, sizeof(i16));
|
|
76
|
-
return i16;
|
|
77
|
-
#else
|
|
78
|
-
(void)a_enum, (void)b_enum, (void)d;
|
|
79
|
-
return 0;
|
|
80
|
-
#endif
|
|
81
|
-
}
|
|
82
|
-
|
|
83
59
|
inline static simsimd_f32_t simsimd_cos_f32sve(simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) {
|
|
84
60
|
#if defined(__ARM_FEATURE_SVE)
|
|
85
61
|
size_t i = 0;
|
|
@@ -176,7 +152,7 @@ inline static simsimd_f32_t simsimd_dot_f32x4neon(simsimd_f32_t const* a, simsim
|
|
|
176
152
|
#if defined(__ARM_NEON)
|
|
177
153
|
float32x4_t ab_vec = vdupq_n_f32(0);
|
|
178
154
|
for (size_t i = 0; i != d; i += 4)
|
|
179
|
-
ab_vec =
|
|
155
|
+
ab_vec = vfmaq_f32(ab_vec, vld1q_f32(a + i), vld1q_f32(b + i));
|
|
180
156
|
return vaddvq_f32(ab_vec);
|
|
181
157
|
#else
|
|
182
158
|
(void)a, (void)b, (void)d;
|
|
@@ -184,16 +160,23 @@ inline static simsimd_f32_t simsimd_dot_f32x4neon(simsimd_f32_t const* a, simsim
|
|
|
184
160
|
#endif
|
|
185
161
|
}
|
|
186
162
|
|
|
187
|
-
inline static
|
|
163
|
+
inline static simsimd_f32_t simsimd_cos_f16x4neon(simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) {
|
|
188
164
|
#if defined(__ARM_NEON)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
165
|
+
float32x4_t ab_vec = vdupq_n_f32(0);
|
|
166
|
+
float32x4_t a2_vec = vdupq_n_f32(0);
|
|
167
|
+
float32x4_t b2_vec = vdupq_n_f32(0);
|
|
168
|
+
for (size_t i = 0; i != d; i += 4) {
|
|
169
|
+
float32x4_t a_vec = vcvt_f32_f16(vld1_f16((float16_t const*)a + i));
|
|
170
|
+
float32x4_t b_vec = vcvt_f32_f16(vld1_f16((float16_t const*)b + i));
|
|
171
|
+
// vfmaq_f32(a, b, c) == vaddq_f32(vmulq_f32(b, c), a)
|
|
172
|
+
ab_vec = vfmaq_f32(ab_vec, a_vec, b_vec);
|
|
173
|
+
a2_vec = vfmaq_f32(a2_vec, a_vec, a_vec);
|
|
174
|
+
b2_vec = vfmaq_f32(b2_vec, b_vec, b_vec);
|
|
175
|
+
}
|
|
176
|
+
simsimd_f32_t ab = vaddvq_f32(ab_vec);
|
|
177
|
+
simsimd_f32_t a2 = vaddvq_f32(a2_vec);
|
|
178
|
+
simsimd_f32_t b2 = vaddvq_f32(b2_vec);
|
|
179
|
+
return ab / (sqrt(a2) * sqrt(b2));
|
|
197
180
|
#else
|
|
198
181
|
(void)a, (void)b, (void)d;
|
|
199
182
|
return 0;
|
|
@@ -255,15 +238,22 @@ inline static simsimd_f32_t simsimd_dot_f32x4avx2(simsimd_f32_t const* a, simsim
|
|
|
255
238
|
#endif
|
|
256
239
|
}
|
|
257
240
|
|
|
258
|
-
inline static simsimd_f32_t
|
|
241
|
+
inline static simsimd_f32_t simsimd_cos_f16x16avx512(simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) {
|
|
259
242
|
#if defined(__AVX512F__)
|
|
260
243
|
__m512 ab_vec = _mm512_set1_ps(0);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
244
|
+
__m512 a2_vec = _mm512_set1_ps(0);
|
|
245
|
+
__m512 b2_vec = _mm512_set1_ps(0);
|
|
246
|
+
for (size_t i = 0; i != d; i += 16) {
|
|
247
|
+
__m512 a_vec = _mm512_cvtxph_ps(_mm256_loadu_ph(a + i));
|
|
248
|
+
__m512 b_vec = _mm512_cvtxph_ps(_mm256_loadu_ph(b + i));
|
|
249
|
+
ab_vec = _mm512_fmadd_ps(a_vec, b_vec, ab_vec);
|
|
250
|
+
a2_vec = _mm512_fmadd_ps(a_vec, a_vec, a2_vec);
|
|
251
|
+
b2_vec = _mm512_fmadd_ps(b_vec, b_vec, b2_vec);
|
|
252
|
+
}
|
|
253
|
+
simsimd_f32_t ab = _mm512_reduce_add_ps(ab_vec);
|
|
254
|
+
simsimd_f32_t a2 = _mm512_reduce_add_ps(a2_vec);
|
|
255
|
+
simsimd_f32_t b2 = _mm512_reduce_add_ps(b2_vec);
|
|
256
|
+
return ab / (sqrt(a2) * sqrt(b2));
|
|
267
257
|
#else
|
|
268
258
|
(void)a, (void)b, (void)d;
|
|
269
259
|
return 0;
|