usearch 0.21.0 → 0.22.1
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 +19 -0
- package/.github/workflows/release.yml +32 -1
- package/.github/workflows/update_version.sh +2 -1
- package/CMakeLists.txt +4 -0
- package/Cargo.toml +1 -1
- package/README.md +117 -626
- 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/README.md +88 -0
- package/cpp/bench.cpp +32 -41
- package/cpp/test.cpp +13 -7
- package/docs/golang/index.rst +6 -0
- package/docs/index.rst +6 -2
- package/docs/javascript/index.rst +1 -1
- package/docs/python/index.rst +1 -1
- 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 +204 -78
- package/include/usearch/index_punned_dense.hpp +239 -85
- package/java/README.md +22 -0
- package/javascript/README.md +36 -0
- package/javascript/lib.cpp +1 -1
- package/package.json +1 -1
- package/python/README.md +283 -0
- package/python/lib.cpp +61 -11
- package/python/scripts/join.py +7 -3
- package/python/scripts/test.py +28 -243
- package/python/scripts/test_jit.py +244 -0
- package/python/usearch/index.py +97 -25
- package/rust/README.md +67 -0
- package/swift/README.md +20 -0
- package/wasm/CMakeLists.txt +15 -0
- package/wasm/README.md +13 -0
- package/wasm/lib.cpp +5 -0
- package/wasmer.toml +8 -0
- package/wolfram/README.md +13 -0
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
|
|
@@ -118,11 +120,24 @@ def test_index(
|
|
|
118
120
|
assert index.max_level >= 0
|
|
119
121
|
assert index.levels_stats.nodes >= 1
|
|
120
122
|
assert index.level_stats(0).nodes == 1
|
|
121
|
-
assert str(index).startswith("usearch.
|
|
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
|
|
122
131
|
|
|
123
132
|
index.save("tmp.usearch")
|
|
133
|
+
|
|
134
|
+
# Re-populate cleared index
|
|
124
135
|
index.clear()
|
|
125
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
|
|
126
141
|
|
|
127
142
|
index.load("tmp.usearch")
|
|
128
143
|
assert len(index) == 1
|
|
@@ -157,11 +172,11 @@ def test_index_batch(
|
|
|
157
172
|
labels = np.arange(batch_size)
|
|
158
173
|
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=numpy_type)
|
|
159
174
|
|
|
160
|
-
index.add(labels, vectors)
|
|
175
|
+
index.add(labels, vectors, threads=2)
|
|
161
176
|
assert len(index) == batch_size
|
|
162
177
|
assert np.allclose(index.get_vectors(labels).astype(numpy_type), vectors, atol=0.1)
|
|
163
178
|
|
|
164
|
-
matches: Matches = index.search(vectors, 10)
|
|
179
|
+
matches: Matches = index.search(vectors, 10, threads=2)
|
|
165
180
|
assert matches.labels.shape[0] == matches.distances.shape[0]
|
|
166
181
|
assert matches.counts.shape[0] == batch_size
|
|
167
182
|
assert np.all(np.sort(index.labels) == np.sort(labels))
|
|
@@ -225,236 +240,6 @@ def test_exact_recall(
|
|
|
225
240
|
assert man == woman, "Stable marriage failed"
|
|
226
241
|
|
|
227
242
|
|
|
228
|
-
@pytest.mark.parametrize("ndim", dimensions)
|
|
229
|
-
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
230
|
-
def test_index_numba(ndim: int, batch_size: int):
|
|
231
|
-
try:
|
|
232
|
-
from numba import cfunc, types, carray
|
|
233
|
-
except ImportError:
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
237
|
-
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
238
|
-
signature_two_args = types.float32(
|
|
239
|
-
types.CPointer(types.float32),
|
|
240
|
-
types.CPointer(types.float32),
|
|
241
|
-
)
|
|
242
|
-
signature_three_args = types.float32(
|
|
243
|
-
types.CPointer(types.float32),
|
|
244
|
-
types.CPointer(types.float32),
|
|
245
|
-
types.uint64,
|
|
246
|
-
)
|
|
247
|
-
signature_four_args = types.float32(
|
|
248
|
-
types.CPointer(types.float32),
|
|
249
|
-
types.uint64,
|
|
250
|
-
types.CPointer(types.float32),
|
|
251
|
-
types.uint64,
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
@cfunc(signature_two_args)
|
|
255
|
-
def python_inner_product_two_args(a, b):
|
|
256
|
-
a_array = carray(a, ndim)
|
|
257
|
-
b_array = carray(b, ndim)
|
|
258
|
-
c = 0.0
|
|
259
|
-
for i in range(ndim):
|
|
260
|
-
c += a_array[i] * b_array[i]
|
|
261
|
-
return 1 - c
|
|
262
|
-
|
|
263
|
-
@cfunc(signature_three_args)
|
|
264
|
-
def python_inner_product_three_args(a, b, ndim):
|
|
265
|
-
a_array = carray(a, ndim)
|
|
266
|
-
b_array = carray(b, ndim)
|
|
267
|
-
c = 0.0
|
|
268
|
-
for i in range(ndim):
|
|
269
|
-
c += a_array[i] * b_array[i]
|
|
270
|
-
return 1 - c
|
|
271
|
-
|
|
272
|
-
@cfunc(signature_four_args)
|
|
273
|
-
def python_inner_product_four_args(a, a_ndim, b, b_ndim):
|
|
274
|
-
a_array = carray(a, a_ndim)
|
|
275
|
-
b_array = carray(b, b_ndim)
|
|
276
|
-
c = 0.0
|
|
277
|
-
for i in range(a_ndim):
|
|
278
|
-
c += a_array[i] * b_array[i]
|
|
279
|
-
return 1 - c
|
|
280
|
-
|
|
281
|
-
functions = [
|
|
282
|
-
python_inner_product_two_args,
|
|
283
|
-
python_inner_product_three_args,
|
|
284
|
-
python_inner_product_four_args,
|
|
285
|
-
]
|
|
286
|
-
signatures = [
|
|
287
|
-
MetricSignature.ArrayArray,
|
|
288
|
-
MetricSignature.ArrayArraySize,
|
|
289
|
-
MetricSignature.ArraySizeArraySize,
|
|
290
|
-
]
|
|
291
|
-
for function, signature in zip(functions, signatures):
|
|
292
|
-
metric = CompiledMetric(
|
|
293
|
-
pointer=function.address,
|
|
294
|
-
kind=MetricKind.IP,
|
|
295
|
-
signature=signature,
|
|
296
|
-
)
|
|
297
|
-
index = Index(ndim=ndim, metric=metric)
|
|
298
|
-
|
|
299
|
-
labels = np.arange(batch_size)
|
|
300
|
-
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
301
|
-
|
|
302
|
-
index.add(labels, vectors)
|
|
303
|
-
matches, distances, count = index.search(vectors, 10)
|
|
304
|
-
assert matches.shape[0] == distances.shape[0]
|
|
305
|
-
assert count.shape[0] == batch_size
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
@pytest.mark.parametrize("ndim", dimensions[-1:])
|
|
309
|
-
@pytest.mark.parametrize("batch_size", batch_sizes[-1:])
|
|
310
|
-
def test_index_cppyy(ndim: int, batch_size: int):
|
|
311
|
-
try:
|
|
312
|
-
import cppyy
|
|
313
|
-
import cppyy.ll
|
|
314
|
-
except ImportError:
|
|
315
|
-
return
|
|
316
|
-
|
|
317
|
-
cppyy.cppdef(
|
|
318
|
-
"""
|
|
319
|
-
float inner_product_two_args(float *a, float *b) {
|
|
320
|
-
float result = 0;
|
|
321
|
-
#pragma unroll
|
|
322
|
-
for (size_t i = 0; i != ndim; ++i)
|
|
323
|
-
result += a[i] * b[i];
|
|
324
|
-
return 1 - result;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
float inner_product_three_args(float *a, float *b, size_t n) {
|
|
328
|
-
float result = 0;
|
|
329
|
-
for (size_t i = 0; i != n; ++i)
|
|
330
|
-
result += a[i] * b[i];
|
|
331
|
-
return 1 - result;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
float inner_product_four_args(float *a, size_t an, float *b, size_t) {
|
|
335
|
-
float result = 0;
|
|
336
|
-
for (size_t i = 0; i != an; ++i)
|
|
337
|
-
result += a[i] * b[i];
|
|
338
|
-
return 1 - result;
|
|
339
|
-
}
|
|
340
|
-
""".replace(
|
|
341
|
-
"ndim", str(ndim)
|
|
342
|
-
)
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
functions = [
|
|
346
|
-
cppyy.gbl.inner_product_two_args,
|
|
347
|
-
cppyy.gbl.inner_product_three_args,
|
|
348
|
-
cppyy.gbl.inner_product_four_args,
|
|
349
|
-
]
|
|
350
|
-
signatures = [
|
|
351
|
-
MetricSignature.ArrayArray,
|
|
352
|
-
MetricSignature.ArrayArraySize,
|
|
353
|
-
MetricSignature.ArraySizeArraySize,
|
|
354
|
-
]
|
|
355
|
-
for function, signature in zip(functions, signatures):
|
|
356
|
-
metric = CompiledMetric(
|
|
357
|
-
pointer=cppyy.ll.addressof(function),
|
|
358
|
-
kind=MetricKind.IP,
|
|
359
|
-
signature=signature,
|
|
360
|
-
)
|
|
361
|
-
index = Index(ndim=ndim, metric=metric)
|
|
362
|
-
|
|
363
|
-
labels = np.arange(batch_size)
|
|
364
|
-
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
365
|
-
|
|
366
|
-
index.add(labels, vectors)
|
|
367
|
-
matches, distances, count = index.search(vectors, 10)
|
|
368
|
-
assert matches.shape[0] == distances.shape[0]
|
|
369
|
-
assert count.shape[0] == batch_size
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
@pytest.mark.parametrize("ndim", [8])
|
|
373
|
-
@pytest.mark.parametrize("batch_size", batch_sizes)
|
|
374
|
-
def test_index_peachpy(ndim: int, batch_size: int):
|
|
375
|
-
try:
|
|
376
|
-
from peachpy import (
|
|
377
|
-
Argument,
|
|
378
|
-
ptr,
|
|
379
|
-
float_,
|
|
380
|
-
const_float_,
|
|
381
|
-
)
|
|
382
|
-
from peachpy.x86_64 import (
|
|
383
|
-
abi,
|
|
384
|
-
Function,
|
|
385
|
-
uarch,
|
|
386
|
-
isa,
|
|
387
|
-
GeneralPurposeRegister64,
|
|
388
|
-
LOAD,
|
|
389
|
-
YMMRegister,
|
|
390
|
-
VSUBPS,
|
|
391
|
-
VADDPS,
|
|
392
|
-
VHADDPS,
|
|
393
|
-
VMOVUPS,
|
|
394
|
-
VFMADD231PS,
|
|
395
|
-
VPERM2F128,
|
|
396
|
-
VXORPS,
|
|
397
|
-
RETURN,
|
|
398
|
-
)
|
|
399
|
-
except ImportError:
|
|
400
|
-
return
|
|
401
|
-
|
|
402
|
-
a = Argument(ptr(const_float_), name="a")
|
|
403
|
-
b = Argument(ptr(const_float_), name="b")
|
|
404
|
-
|
|
405
|
-
with Function(
|
|
406
|
-
"InnerProduct", (a, b), float_, target=uarch.default + isa.avx + isa.avx2
|
|
407
|
-
) as asm_function:
|
|
408
|
-
# Request two 64-bit general-purpose registers for addresses
|
|
409
|
-
reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
|
|
410
|
-
LOAD.ARGUMENT(reg_a, a)
|
|
411
|
-
LOAD.ARGUMENT(reg_b, b)
|
|
412
|
-
|
|
413
|
-
# Load the vectors
|
|
414
|
-
ymm_a = YMMRegister()
|
|
415
|
-
ymm_b = YMMRegister()
|
|
416
|
-
VMOVUPS(ymm_a, [reg_a])
|
|
417
|
-
VMOVUPS(ymm_b, [reg_b])
|
|
418
|
-
|
|
419
|
-
# Prepare the accumulator
|
|
420
|
-
ymm_c = YMMRegister()
|
|
421
|
-
ymm_one = YMMRegister()
|
|
422
|
-
VXORPS(ymm_c, ymm_c, ymm_c)
|
|
423
|
-
VXORPS(ymm_one, ymm_one, ymm_one)
|
|
424
|
-
|
|
425
|
-
# Accumulate A and B product into C
|
|
426
|
-
VFMADD231PS(ymm_c, ymm_a, ymm_b)
|
|
427
|
-
|
|
428
|
-
# Reduce the contents of a YMM register
|
|
429
|
-
ymm_c_permuted = YMMRegister()
|
|
430
|
-
VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
|
|
431
|
-
VADDPS(ymm_c, ymm_c, ymm_c_permuted)
|
|
432
|
-
VHADDPS(ymm_c, ymm_c, ymm_c)
|
|
433
|
-
VHADDPS(ymm_c, ymm_c, ymm_c)
|
|
434
|
-
|
|
435
|
-
# Negate the values, to go from "similarity" to "distance"
|
|
436
|
-
VSUBPS(ymm_c, ymm_one, ymm_c)
|
|
437
|
-
|
|
438
|
-
# A common convention is to return floats in XMM registers
|
|
439
|
-
RETURN(ymm_c.as_xmm)
|
|
440
|
-
|
|
441
|
-
python_function = asm_function.finalize(abi.detect()).encode().load()
|
|
442
|
-
metric = CompiledMetric(
|
|
443
|
-
pointer=python_function.loader.code_address,
|
|
444
|
-
kind=MetricKind.IP,
|
|
445
|
-
signature=MetricSignature.ArrayArray,
|
|
446
|
-
)
|
|
447
|
-
index = Index(ndim=ndim, metric=metric)
|
|
448
|
-
|
|
449
|
-
labels = np.arange(batch_size)
|
|
450
|
-
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
451
|
-
|
|
452
|
-
index.add(labels, vectors)
|
|
453
|
-
matches, distances, count = index.search(vectors, 10)
|
|
454
|
-
assert matches.shape[0] == distances.shape[0]
|
|
455
|
-
assert count.shape[0] == batch_size
|
|
456
|
-
|
|
457
|
-
|
|
458
243
|
@pytest.mark.parametrize("bits", dimensions)
|
|
459
244
|
@pytest.mark.parametrize("metric", hash_metrics)
|
|
460
245
|
@pytest.mark.parametrize("connectivity", connectivity_options)
|
|
@@ -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
|