usearch 0.20.0 → 0.22.0
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/prerelease.yml +46 -0
- package/.github/workflows/release.yml +28 -0
- package/.github/workflows/update_version.sh +2 -1
- package/CMakeLists.txt +4 -0
- package/Cargo.toml +1 -1
- package/README.md +93 -585
- package/c/lib.cpp +19 -15
- package/c/usearch.h +26 -23
- package/cmake/emscripten.cmake +8 -0
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +6 -2
- package/cpp/README.md +88 -0
- package/cpp/bench.cpp +72 -44
- package/cpp/test.cpp +13 -7
- package/docs/benchmarks.md +14 -12
- package/docs/golang/index.rst +6 -0
- package/docs/index.rst +5 -2
- package/docs/python/reference.rst +9 -2
- package/docs/rust/index.rst +6 -0
- package/docs/swift/index.rst +6 -0
- package/golang/README.md +28 -0
- package/include/usearch/index.hpp +668 -123
- package/include/usearch/index_punned_dense.hpp +305 -93
- package/include/usearch/index_punned_helpers.hpp +28 -3
- package/java/README.md +22 -0
- package/javascript/README.md +27 -0
- package/javascript/lib.cpp +1 -1
- package/package.json +1 -1
- package/python/README.md +283 -0
- package/python/lib.cpp +118 -16
- package/python/scripts/join.py +155 -0
- package/python/scripts/test.py +45 -241
- package/python/scripts/test_jit.py +244 -0
- package/python/usearch/eval.py +11 -3
- package/python/usearch/index.py +207 -26
- package/rust/README.md +67 -0
- package/swift/README.md +20 -0
- package/wasm/CMakeLists.txt +15 -0
- package/wasm/lib.cpp +5 -0
- package/wasmer.toml +8 -0
- package/python/usearch/join.py +0 -98
package/python/scripts/test.py
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import importlib.util
|
|
3
2
|
|
|
4
3
|
import pytest
|
|
5
4
|
import numpy as np
|
|
6
5
|
|
|
7
6
|
from usearch.io import load_matrix, save_matrix
|
|
8
7
|
from usearch.eval import random_vectors
|
|
9
|
-
from usearch.numba import jit as njit
|
|
10
8
|
|
|
11
9
|
from usearch.index import (
|
|
12
10
|
Index,
|
|
13
11
|
SparseIndex,
|
|
14
12
|
MetricKind,
|
|
15
13
|
ScalarKind,
|
|
16
|
-
MetricSignature,
|
|
17
|
-
CompiledMetric,
|
|
18
14
|
Matches,
|
|
19
15
|
)
|
|
20
16
|
from usearch.index import (
|
|
@@ -35,7 +31,6 @@ index_types = [
|
|
|
35
31
|
numpy_types = [np.float32, np.float64, np.float16]
|
|
36
32
|
|
|
37
33
|
connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
|
|
38
|
-
jit_options = [False, True] if importlib.util.find_spec("spam") is not None else [False]
|
|
39
34
|
continuous_metrics = [
|
|
40
35
|
MetricKind.Cos,
|
|
41
36
|
MetricKind.L2sq,
|
|
@@ -50,6 +45,12 @@ hash_metrics = [
|
|
|
50
45
|
@pytest.mark.parametrize("rows", batch_sizes)
|
|
51
46
|
@pytest.mark.parametrize("cols", dimensions)
|
|
52
47
|
def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
48
|
+
"""
|
|
49
|
+
Test the serialization of floating point binary matrix.
|
|
50
|
+
|
|
51
|
+
:param int rows: The number of rows in the matrix.
|
|
52
|
+
:param int cols: The number of columns in the matrix.
|
|
53
|
+
"""
|
|
53
54
|
original = np.random.rand(rows, cols).astype(np.float32)
|
|
54
55
|
save_matrix(original, "tmp.fbin")
|
|
55
56
|
reconstructed = load_matrix("tmp.fbin")
|
|
@@ -60,6 +61,12 @@ def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
|
60
61
|
@pytest.mark.parametrize("rows", batch_sizes)
|
|
61
62
|
@pytest.mark.parametrize("cols", dimensions)
|
|
62
63
|
def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
64
|
+
"""
|
|
65
|
+
Test the serialization of integer binary matrix.
|
|
66
|
+
|
|
67
|
+
:param int rows: The number of rows in the matrix.
|
|
68
|
+
:param int cols: The number of columns in the matrix.
|
|
69
|
+
"""
|
|
63
70
|
original = np.random.randint(0, rows + 1, size=(rows, cols)).astype(np.int32)
|
|
64
71
|
save_matrix(original, "tmp.ibin")
|
|
65
72
|
reconstructed = load_matrix("tmp.ibin")
|
|
@@ -72,17 +79,13 @@ def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
|
72
79
|
@pytest.mark.parametrize("index_type", index_types)
|
|
73
80
|
@pytest.mark.parametrize("numpy_type", numpy_types)
|
|
74
81
|
@pytest.mark.parametrize("connectivity", connectivity_options)
|
|
75
|
-
@pytest.mark.parametrize("jit", jit_options)
|
|
76
82
|
def test_index(
|
|
77
83
|
ndim: int,
|
|
78
84
|
metric: MetricKind,
|
|
79
85
|
index_type: ScalarKind,
|
|
80
86
|
numpy_type: str,
|
|
81
87
|
connectivity: int,
|
|
82
|
-
jit: bool,
|
|
83
88
|
):
|
|
84
|
-
if jit:
|
|
85
|
-
metric = njit(ndim, metric, index_type)
|
|
86
89
|
index = Index(
|
|
87
90
|
metric=metric,
|
|
88
91
|
ndim=ndim,
|
|
@@ -95,7 +98,6 @@ def test_index(
|
|
|
95
98
|
assert index.connectivity == connectivity
|
|
96
99
|
|
|
97
100
|
vector = random_vectors(count=1, ndim=ndim, dtype=numpy_type).flatten()
|
|
98
|
-
|
|
99
101
|
index.add(42, vector)
|
|
100
102
|
|
|
101
103
|
assert 42 in index
|
|
@@ -115,9 +117,27 @@ def test_index(
|
|
|
115
117
|
assert matches[0] == 42
|
|
116
118
|
assert distances[0] == pytest.approx(0, abs=1e-3)
|
|
117
119
|
|
|
120
|
+
assert index.max_level >= 0
|
|
121
|
+
assert index.levels_stats.nodes >= 1
|
|
122
|
+
assert index.level_stats(0).nodes == 1
|
|
123
|
+
assert str(index).startswith("usearch.")
|
|
124
|
+
|
|
125
|
+
# Try removals
|
|
126
|
+
other_vector = random_vectors(count=1, ndim=ndim, dtype=numpy_type).flatten()
|
|
127
|
+
index.add(43, other_vector)
|
|
128
|
+
assert len(index) == 2
|
|
129
|
+
index.remove(43)
|
|
130
|
+
assert len(index) == 1
|
|
131
|
+
|
|
118
132
|
index.save("tmp.usearch")
|
|
133
|
+
|
|
134
|
+
# Re-populate cleared index
|
|
119
135
|
index.clear()
|
|
120
136
|
assert len(index) == 0
|
|
137
|
+
index.add(42, vector)
|
|
138
|
+
assert len(index) == 1
|
|
139
|
+
matches, distances, count = index.search(vector, 10)
|
|
140
|
+
assert count == 1
|
|
121
141
|
|
|
122
142
|
index.load("tmp.usearch")
|
|
123
143
|
assert len(index) == 1
|
|
@@ -127,6 +147,10 @@ def test_index(
|
|
|
127
147
|
assert len(index) == 1
|
|
128
148
|
assert len(index[42]) == ndim
|
|
129
149
|
|
|
150
|
+
index_copy = index.copy()
|
|
151
|
+
assert len(index_copy) == 1
|
|
152
|
+
assert len(index_copy[42]) == ndim
|
|
153
|
+
|
|
130
154
|
# Cleanup
|
|
131
155
|
os.remove("tmp.usearch")
|
|
132
156
|
|
|
@@ -148,15 +172,19 @@ def test_index_batch(
|
|
|
148
172
|
labels = np.arange(batch_size)
|
|
149
173
|
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=numpy_type)
|
|
150
174
|
|
|
151
|
-
index.add(labels, vectors)
|
|
175
|
+
index.add(labels, vectors, threads=2)
|
|
152
176
|
assert len(index) == batch_size
|
|
153
177
|
assert np.allclose(index.get_vectors(labels).astype(numpy_type), vectors, atol=0.1)
|
|
154
178
|
|
|
155
|
-
matches: Matches = index.search(vectors, 10)
|
|
179
|
+
matches: Matches = index.search(vectors, 10, threads=2)
|
|
156
180
|
assert matches.labels.shape[0] == matches.distances.shape[0]
|
|
157
181
|
assert matches.counts.shape[0] == batch_size
|
|
158
182
|
assert np.all(np.sort(index.labels) == np.sort(labels))
|
|
159
183
|
|
|
184
|
+
assert index.max_level >= 0 # TODO: This should be 1
|
|
185
|
+
assert index.levels_stats.nodes >= batch_size
|
|
186
|
+
assert index.level_stats(0).nodes == batch_size
|
|
187
|
+
|
|
160
188
|
index.save("tmp.usearch")
|
|
161
189
|
index.clear()
|
|
162
190
|
assert len(index) == 0
|
|
@@ -205,235 +233,11 @@ def test_exact_recall(
|
|
|
205
233
|
for i in range(batch_size):
|
|
206
234
|
assert found_labels[i, 0] == i
|
|
207
235
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
from numba import cfunc, types, carray
|
|
214
|
-
except ImportError:
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
218
|
-
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
219
|
-
signature_two_args = types.float32(
|
|
220
|
-
types.CPointer(types.float32),
|
|
221
|
-
types.CPointer(types.float32),
|
|
222
|
-
)
|
|
223
|
-
signature_three_args = types.float32(
|
|
224
|
-
types.CPointer(types.float32),
|
|
225
|
-
types.CPointer(types.float32),
|
|
226
|
-
types.uint64,
|
|
227
|
-
)
|
|
228
|
-
signature_four_args = types.float32(
|
|
229
|
-
types.CPointer(types.float32),
|
|
230
|
-
types.uint64,
|
|
231
|
-
types.CPointer(types.float32),
|
|
232
|
-
types.uint64,
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
@cfunc(signature_two_args)
|
|
236
|
-
def python_inner_product_two_args(a, b):
|
|
237
|
-
a_array = carray(a, ndim)
|
|
238
|
-
b_array = carray(b, ndim)
|
|
239
|
-
c = 0.0
|
|
240
|
-
for i in range(ndim):
|
|
241
|
-
c += a_array[i] * b_array[i]
|
|
242
|
-
return 1 - c
|
|
243
|
-
|
|
244
|
-
@cfunc(signature_three_args)
|
|
245
|
-
def python_inner_product_three_args(a, b, ndim):
|
|
246
|
-
a_array = carray(a, ndim)
|
|
247
|
-
b_array = carray(b, ndim)
|
|
248
|
-
c = 0.0
|
|
249
|
-
for i in range(ndim):
|
|
250
|
-
c += a_array[i] * b_array[i]
|
|
251
|
-
return 1 - c
|
|
252
|
-
|
|
253
|
-
@cfunc(signature_four_args)
|
|
254
|
-
def python_inner_product_four_args(a, a_ndim, b, b_ndim):
|
|
255
|
-
a_array = carray(a, a_ndim)
|
|
256
|
-
b_array = carray(b, b_ndim)
|
|
257
|
-
c = 0.0
|
|
258
|
-
for i in range(a_ndim):
|
|
259
|
-
c += a_array[i] * b_array[i]
|
|
260
|
-
return 1 - c
|
|
261
|
-
|
|
262
|
-
functions = [
|
|
263
|
-
python_inner_product_two_args,
|
|
264
|
-
python_inner_product_three_args,
|
|
265
|
-
python_inner_product_four_args,
|
|
266
|
-
]
|
|
267
|
-
signatures = [
|
|
268
|
-
MetricSignature.ArrayArray,
|
|
269
|
-
MetricSignature.ArrayArraySize,
|
|
270
|
-
MetricSignature.ArraySizeArraySize,
|
|
271
|
-
]
|
|
272
|
-
for function, signature in zip(functions, signatures):
|
|
273
|
-
metric = CompiledMetric(
|
|
274
|
-
pointer=function.address,
|
|
275
|
-
kind=MetricKind.IP,
|
|
276
|
-
signature=signature,
|
|
277
|
-
)
|
|
278
|
-
index = Index(ndim=ndim, metric=metric)
|
|
279
|
-
|
|
280
|
-
labels = np.arange(batch_size)
|
|
281
|
-
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
282
|
-
|
|
283
|
-
index.add(labels, vectors)
|
|
284
|
-
matches, distances, count = index.search(vectors, 10)
|
|
285
|
-
assert matches.shape[0] == distances.shape[0]
|
|
286
|
-
assert count.shape[0] == batch_size
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
@pytest.mark.parametrize("ndim", dimensions[-1:])
|
|
290
|
-
@pytest.mark.parametrize("batch_size", batch_sizes[-1:])
|
|
291
|
-
def test_index_cppyy(ndim: int, batch_size: int):
|
|
292
|
-
try:
|
|
293
|
-
import cppyy
|
|
294
|
-
import cppyy.ll
|
|
295
|
-
except ImportError:
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
cppyy.cppdef(
|
|
299
|
-
"""
|
|
300
|
-
float inner_product_two_args(float *a, float *b) {
|
|
301
|
-
float result = 0;
|
|
302
|
-
#pragma unroll
|
|
303
|
-
for (size_t i = 0; i != ndim; ++i)
|
|
304
|
-
result += a[i] * b[i];
|
|
305
|
-
return 1 - result;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
float inner_product_three_args(float *a, float *b, size_t n) {
|
|
309
|
-
float result = 0;
|
|
310
|
-
for (size_t i = 0; i != n; ++i)
|
|
311
|
-
result += a[i] * b[i];
|
|
312
|
-
return 1 - result;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
float inner_product_four_args(float *a, size_t an, float *b, size_t) {
|
|
316
|
-
float result = 0;
|
|
317
|
-
for (size_t i = 0; i != an; ++i)
|
|
318
|
-
result += a[i] * b[i];
|
|
319
|
-
return 1 - result;
|
|
320
|
-
}
|
|
321
|
-
""".replace(
|
|
322
|
-
"ndim", str(ndim)
|
|
323
|
-
)
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
functions = [
|
|
327
|
-
cppyy.gbl.inner_product_two_args,
|
|
328
|
-
cppyy.gbl.inner_product_three_args,
|
|
329
|
-
cppyy.gbl.inner_product_four_args,
|
|
330
|
-
]
|
|
331
|
-
signatures = [
|
|
332
|
-
MetricSignature.ArrayArray,
|
|
333
|
-
MetricSignature.ArrayArraySize,
|
|
334
|
-
MetricSignature.ArraySizeArraySize,
|
|
335
|
-
]
|
|
336
|
-
for function, signature in zip(functions, signatures):
|
|
337
|
-
metric = CompiledMetric(
|
|
338
|
-
pointer=cppyy.ll.addressof(function),
|
|
339
|
-
kind=MetricKind.IP,
|
|
340
|
-
signature=signature,
|
|
341
|
-
)
|
|
342
|
-
index = Index(ndim=ndim, metric=metric)
|
|
343
|
-
|
|
344
|
-
labels = np.arange(batch_size)
|
|
345
|
-
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
346
|
-
|
|
347
|
-
index.add(labels, vectors)
|
|
348
|
-
matches, distances, count = index.search(vectors, 10)
|
|
349
|
-
assert matches.shape[0] == distances.shape[0]
|
|
350
|
-
assert count.shape[0] == batch_size
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
@pytest.mark.parametrize("ndim", [8])
|
|
354
|
-
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
355
|
-
def test_index_peachpy(ndim: int, batch_size: int):
|
|
356
|
-
try:
|
|
357
|
-
from peachpy import (
|
|
358
|
-
Argument,
|
|
359
|
-
ptr,
|
|
360
|
-
float_,
|
|
361
|
-
const_float_,
|
|
362
|
-
)
|
|
363
|
-
from peachpy.x86_64 import (
|
|
364
|
-
abi,
|
|
365
|
-
Function,
|
|
366
|
-
uarch,
|
|
367
|
-
isa,
|
|
368
|
-
GeneralPurposeRegister64,
|
|
369
|
-
LOAD,
|
|
370
|
-
YMMRegister,
|
|
371
|
-
VSUBPS,
|
|
372
|
-
VADDPS,
|
|
373
|
-
VHADDPS,
|
|
374
|
-
VMOVUPS,
|
|
375
|
-
VFMADD231PS,
|
|
376
|
-
VPERM2F128,
|
|
377
|
-
VXORPS,
|
|
378
|
-
RETURN,
|
|
379
|
-
)
|
|
380
|
-
except ImportError:
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
a = Argument(ptr(const_float_), name="a")
|
|
384
|
-
b = Argument(ptr(const_float_), name="b")
|
|
385
|
-
|
|
386
|
-
with Function(
|
|
387
|
-
"InnerProduct", (a, b), float_, target=uarch.default + isa.avx + isa.avx2
|
|
388
|
-
) as asm_function:
|
|
389
|
-
# Request two 64-bit general-purpose registers for addresses
|
|
390
|
-
reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
|
|
391
|
-
LOAD.ARGUMENT(reg_a, a)
|
|
392
|
-
LOAD.ARGUMENT(reg_b, b)
|
|
393
|
-
|
|
394
|
-
# Load the vectors
|
|
395
|
-
ymm_a = YMMRegister()
|
|
396
|
-
ymm_b = YMMRegister()
|
|
397
|
-
VMOVUPS(ymm_a, [reg_a])
|
|
398
|
-
VMOVUPS(ymm_b, [reg_b])
|
|
399
|
-
|
|
400
|
-
# Prepare the accumulator
|
|
401
|
-
ymm_c = YMMRegister()
|
|
402
|
-
ymm_one = YMMRegister()
|
|
403
|
-
VXORPS(ymm_c, ymm_c, ymm_c)
|
|
404
|
-
VXORPS(ymm_one, ymm_one, ymm_one)
|
|
405
|
-
|
|
406
|
-
# Accumulate A and B product into C
|
|
407
|
-
VFMADD231PS(ymm_c, ymm_a, ymm_b)
|
|
408
|
-
|
|
409
|
-
# Reduce the contents of a YMM register
|
|
410
|
-
ymm_c_permuted = YMMRegister()
|
|
411
|
-
VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
|
|
412
|
-
VADDPS(ymm_c, ymm_c, ymm_c_permuted)
|
|
413
|
-
VHADDPS(ymm_c, ymm_c, ymm_c)
|
|
414
|
-
VHADDPS(ymm_c, ymm_c, ymm_c)
|
|
415
|
-
|
|
416
|
-
# Negate the values, to go from "similarity" to "distance"
|
|
417
|
-
VSUBPS(ymm_c, ymm_one, ymm_c)
|
|
418
|
-
|
|
419
|
-
# A common convention is to return floats in XMM registers
|
|
420
|
-
RETURN(ymm_c.as_xmm)
|
|
421
|
-
|
|
422
|
-
python_function = asm_function.finalize(abi.detect()).encode().load()
|
|
423
|
-
metric = CompiledMetric(
|
|
424
|
-
pointer=python_function.loader.code_address,
|
|
425
|
-
kind=MetricKind.IP,
|
|
426
|
-
signature=MetricSignature.ArrayArray,
|
|
427
|
-
)
|
|
428
|
-
index = Index(ndim=ndim, metric=metric)
|
|
429
|
-
|
|
430
|
-
labels = np.arange(batch_size)
|
|
431
|
-
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
432
|
-
|
|
433
|
-
index.add(labels, vectors)
|
|
434
|
-
matches, distances, count = index.search(vectors, 10)
|
|
435
|
-
assert matches.shape[0] == distances.shape[0]
|
|
436
|
-
assert count.shape[0] == batch_size
|
|
236
|
+
# Match entries aginst themselves
|
|
237
|
+
index_copy: Index = index.copy()
|
|
238
|
+
mapping: dict = index.join(index_copy, exact=True)
|
|
239
|
+
for man, woman in mapping.items():
|
|
240
|
+
assert man == woman, "Stable marriage failed"
|
|
437
241
|
|
|
438
242
|
|
|
439
243
|
@pytest.mark.parametrize("bits", dimensions)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from usearch.eval import random_vectors
|
|
5
|
+
|
|
6
|
+
from usearch.index import (
|
|
7
|
+
Index,
|
|
8
|
+
MetricKind,
|
|
9
|
+
MetricSignature,
|
|
10
|
+
CompiledMetric,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
dimensions = [3, 97, 256]
|
|
14
|
+
batch_sizes = [1, 77]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize("ndim", dimensions)
|
|
18
|
+
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
19
|
+
def test_index_numba(ndim: int, batch_size: int):
|
|
20
|
+
try:
|
|
21
|
+
from numba import cfunc, types, carray
|
|
22
|
+
except ImportError:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
26
|
+
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
27
|
+
signature_two_args = types.float32(
|
|
28
|
+
types.CPointer(types.float32),
|
|
29
|
+
types.CPointer(types.float32),
|
|
30
|
+
)
|
|
31
|
+
signature_three_args = types.float32(
|
|
32
|
+
types.CPointer(types.float32),
|
|
33
|
+
types.CPointer(types.float32),
|
|
34
|
+
types.uint64,
|
|
35
|
+
)
|
|
36
|
+
signature_four_args = types.float32(
|
|
37
|
+
types.CPointer(types.float32),
|
|
38
|
+
types.uint64,
|
|
39
|
+
types.CPointer(types.float32),
|
|
40
|
+
types.uint64,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@cfunc(signature_two_args)
|
|
44
|
+
def python_inner_product_two_args(a, b):
|
|
45
|
+
a_array = carray(a, ndim)
|
|
46
|
+
b_array = carray(b, ndim)
|
|
47
|
+
c = 0.0
|
|
48
|
+
for i in range(ndim):
|
|
49
|
+
c += a_array[i] * b_array[i]
|
|
50
|
+
return 1 - c
|
|
51
|
+
|
|
52
|
+
@cfunc(signature_three_args)
|
|
53
|
+
def python_inner_product_three_args(a, b, ndim):
|
|
54
|
+
a_array = carray(a, ndim)
|
|
55
|
+
b_array = carray(b, ndim)
|
|
56
|
+
c = 0.0
|
|
57
|
+
for i in range(ndim):
|
|
58
|
+
c += a_array[i] * b_array[i]
|
|
59
|
+
return 1 - c
|
|
60
|
+
|
|
61
|
+
@cfunc(signature_four_args)
|
|
62
|
+
def python_inner_product_four_args(a, a_ndim, b, b_ndim):
|
|
63
|
+
a_array = carray(a, a_ndim)
|
|
64
|
+
b_array = carray(b, b_ndim)
|
|
65
|
+
c = 0.0
|
|
66
|
+
for i in range(a_ndim):
|
|
67
|
+
c += a_array[i] * b_array[i]
|
|
68
|
+
return 1 - c
|
|
69
|
+
|
|
70
|
+
functions = [
|
|
71
|
+
python_inner_product_two_args,
|
|
72
|
+
python_inner_product_three_args,
|
|
73
|
+
python_inner_product_four_args,
|
|
74
|
+
]
|
|
75
|
+
signatures = [
|
|
76
|
+
MetricSignature.ArrayArray,
|
|
77
|
+
MetricSignature.ArrayArraySize,
|
|
78
|
+
MetricSignature.ArraySizeArraySize,
|
|
79
|
+
]
|
|
80
|
+
for function, signature in zip(functions, signatures):
|
|
81
|
+
metric = CompiledMetric(
|
|
82
|
+
pointer=function.address,
|
|
83
|
+
kind=MetricKind.IP,
|
|
84
|
+
signature=signature,
|
|
85
|
+
)
|
|
86
|
+
index = Index(ndim=ndim, metric=metric)
|
|
87
|
+
|
|
88
|
+
labels = np.arange(batch_size)
|
|
89
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
90
|
+
|
|
91
|
+
index.add(labels, vectors)
|
|
92
|
+
matches, distances, count = index.search(vectors, 10)
|
|
93
|
+
assert matches.shape[0] == distances.shape[0]
|
|
94
|
+
assert count.shape[0] == batch_size
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("ndim", dimensions[-1:])
|
|
98
|
+
@pytest.mark.parametrize("batch_size", batch_sizes[-1:])
|
|
99
|
+
def test_index_cppyy(ndim: int, batch_size: int):
|
|
100
|
+
try:
|
|
101
|
+
import cppyy
|
|
102
|
+
import cppyy.ll
|
|
103
|
+
except ImportError:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
cppyy.cppdef(
|
|
107
|
+
"""
|
|
108
|
+
float inner_product_two_args(float *a, float *b) {
|
|
109
|
+
float result = 0;
|
|
110
|
+
#pragma unroll
|
|
111
|
+
for (size_t i = 0; i != ndim; ++i)
|
|
112
|
+
result += a[i] * b[i];
|
|
113
|
+
return 1 - result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
float inner_product_three_args(float *a, float *b, size_t n) {
|
|
117
|
+
float result = 0;
|
|
118
|
+
for (size_t i = 0; i != n; ++i)
|
|
119
|
+
result += a[i] * b[i];
|
|
120
|
+
return 1 - result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
float inner_product_four_args(float *a, size_t an, float *b, size_t) {
|
|
124
|
+
float result = 0;
|
|
125
|
+
for (size_t i = 0; i != an; ++i)
|
|
126
|
+
result += a[i] * b[i];
|
|
127
|
+
return 1 - result;
|
|
128
|
+
}
|
|
129
|
+
""".replace(
|
|
130
|
+
"ndim", str(ndim)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
functions = [
|
|
135
|
+
cppyy.gbl.inner_product_two_args,
|
|
136
|
+
cppyy.gbl.inner_product_three_args,
|
|
137
|
+
cppyy.gbl.inner_product_four_args,
|
|
138
|
+
]
|
|
139
|
+
signatures = [
|
|
140
|
+
MetricSignature.ArrayArray,
|
|
141
|
+
MetricSignature.ArrayArraySize,
|
|
142
|
+
MetricSignature.ArraySizeArraySize,
|
|
143
|
+
]
|
|
144
|
+
for function, signature in zip(functions, signatures):
|
|
145
|
+
metric = CompiledMetric(
|
|
146
|
+
pointer=cppyy.ll.addressof(function),
|
|
147
|
+
kind=MetricKind.IP,
|
|
148
|
+
signature=signature,
|
|
149
|
+
)
|
|
150
|
+
index = Index(ndim=ndim, metric=metric)
|
|
151
|
+
|
|
152
|
+
labels = np.arange(batch_size)
|
|
153
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
154
|
+
|
|
155
|
+
index.add(labels, vectors)
|
|
156
|
+
matches, distances, count = index.search(vectors, 10)
|
|
157
|
+
assert matches.shape[0] == distances.shape[0]
|
|
158
|
+
assert count.shape[0] == batch_size
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parametrize("ndim", [8])
|
|
162
|
+
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
163
|
+
def test_index_peachpy(ndim: int, batch_size: int):
|
|
164
|
+
try:
|
|
165
|
+
from peachpy import (
|
|
166
|
+
Argument,
|
|
167
|
+
ptr,
|
|
168
|
+
float_,
|
|
169
|
+
const_float_,
|
|
170
|
+
)
|
|
171
|
+
from peachpy.x86_64 import (
|
|
172
|
+
abi,
|
|
173
|
+
Function,
|
|
174
|
+
uarch,
|
|
175
|
+
isa,
|
|
176
|
+
GeneralPurposeRegister64,
|
|
177
|
+
LOAD,
|
|
178
|
+
YMMRegister,
|
|
179
|
+
VSUBPS,
|
|
180
|
+
VADDPS,
|
|
181
|
+
VHADDPS,
|
|
182
|
+
VMOVUPS,
|
|
183
|
+
VFMADD231PS,
|
|
184
|
+
VPERM2F128,
|
|
185
|
+
VXORPS,
|
|
186
|
+
RETURN,
|
|
187
|
+
)
|
|
188
|
+
except ImportError:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
a = Argument(ptr(const_float_), name="a")
|
|
192
|
+
b = Argument(ptr(const_float_), name="b")
|
|
193
|
+
|
|
194
|
+
with Function(
|
|
195
|
+
"InnerProduct", (a, b), float_, target=uarch.default + isa.avx + isa.avx2
|
|
196
|
+
) as asm_function:
|
|
197
|
+
# Request two 64-bit general-purpose registers for addresses
|
|
198
|
+
reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
|
|
199
|
+
LOAD.ARGUMENT(reg_a, a)
|
|
200
|
+
LOAD.ARGUMENT(reg_b, b)
|
|
201
|
+
|
|
202
|
+
# Load the vectors
|
|
203
|
+
ymm_a = YMMRegister()
|
|
204
|
+
ymm_b = YMMRegister()
|
|
205
|
+
VMOVUPS(ymm_a, [reg_a])
|
|
206
|
+
VMOVUPS(ymm_b, [reg_b])
|
|
207
|
+
|
|
208
|
+
# Prepare the accumulator
|
|
209
|
+
ymm_c = YMMRegister()
|
|
210
|
+
ymm_one = YMMRegister()
|
|
211
|
+
VXORPS(ymm_c, ymm_c, ymm_c)
|
|
212
|
+
VXORPS(ymm_one, ymm_one, ymm_one)
|
|
213
|
+
|
|
214
|
+
# Accumulate A and B product into C
|
|
215
|
+
VFMADD231PS(ymm_c, ymm_a, ymm_b)
|
|
216
|
+
|
|
217
|
+
# Reduce the contents of a YMM register
|
|
218
|
+
ymm_c_permuted = YMMRegister()
|
|
219
|
+
VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
|
|
220
|
+
VADDPS(ymm_c, ymm_c, ymm_c_permuted)
|
|
221
|
+
VHADDPS(ymm_c, ymm_c, ymm_c)
|
|
222
|
+
VHADDPS(ymm_c, ymm_c, ymm_c)
|
|
223
|
+
|
|
224
|
+
# Negate the values, to go from "similarity" to "distance"
|
|
225
|
+
VSUBPS(ymm_c, ymm_one, ymm_c)
|
|
226
|
+
|
|
227
|
+
# A common convention is to return floats in XMM registers
|
|
228
|
+
RETURN(ymm_c.as_xmm)
|
|
229
|
+
|
|
230
|
+
python_function = asm_function.finalize(abi.detect()).encode().load()
|
|
231
|
+
metric = CompiledMetric(
|
|
232
|
+
pointer=python_function.loader.code_address,
|
|
233
|
+
kind=MetricKind.IP,
|
|
234
|
+
signature=MetricSignature.ArrayArray,
|
|
235
|
+
)
|
|
236
|
+
index = Index(ndim=ndim, metric=metric)
|
|
237
|
+
|
|
238
|
+
labels = np.arange(batch_size)
|
|
239
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
240
|
+
|
|
241
|
+
index.add(labels, vectors)
|
|
242
|
+
matches, distances, count = index.search(vectors, 10)
|
|
243
|
+
assert matches.shape[0] == distances.shape[0]
|
|
244
|
+
assert count.shape[0] == batch_size
|
package/python/usearch/eval.py
CHANGED
|
@@ -3,6 +3,7 @@ from time import time_ns
|
|
|
3
3
|
from typing import Tuple, Any, Callable, Union, Optional, List
|
|
4
4
|
from dataclasses import dataclass, asdict
|
|
5
5
|
from collections import defaultdict
|
|
6
|
+
from math import ceil
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
|
|
@@ -47,7 +48,7 @@ def random_vectors(
|
|
|
47
48
|
return x
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def recall_members(index: Index,
|
|
51
|
+
def recall_members(index: Index, sample: float = 1, **kwargs) -> float:
|
|
51
52
|
"""Simplest benchmark for a quality of search, which queries every
|
|
52
53
|
existing member of the index, to make sure approximate search finds
|
|
53
54
|
the point itself.
|
|
@@ -59,9 +60,16 @@ def recall_members(index: Index, *args, **kwargs) -> float:
|
|
|
59
60
|
"""
|
|
60
61
|
if len(index) == 0:
|
|
61
62
|
return 0
|
|
63
|
+
if "k" not in kwargs:
|
|
64
|
+
kwargs["k"] = 1
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
labels = index.labels
|
|
67
|
+
if sample != 1:
|
|
68
|
+
labels = np.random.choice(labels, int(ceil(len(labels) * sample)))
|
|
69
|
+
|
|
70
|
+
queries = index.get_vectors(labels, index.dtype)
|
|
71
|
+
matches: Matches = index.search(queries, **kwargs)
|
|
72
|
+
return matches.recall_first(labels)
|
|
65
73
|
|
|
66
74
|
|
|
67
75
|
def measure_seconds(f: Callable) -> Tuple[float, Any]:
|