usearch 0.2.3 → 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 +40 -16
- 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 +236 -218
- package/VERSION +1 -1
- package/conanfile.py +1 -1
- 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 +37 -7
- package/python/lib.cpp +23 -23
- package/python/test.py +78 -36
- package/simsimd/include/simsimd/simsimd.h +31 -41
- package/src/advanced.hpp +45 -21
- package/src/bench.cpp +10 -14
- package/.github/workflows/package.json +0 -82
- package/CMakeListsInstructions.md +0 -185
package/python/bench.py
CHANGED
|
@@ -12,30 +12,50 @@ from usearch.io import load_matrix
|
|
|
12
12
|
|
|
13
13
|
def measure(f) -> float:
|
|
14
14
|
a = time.time_ns()
|
|
15
|
-
f()
|
|
15
|
+
result = f()
|
|
16
16
|
b = time.time_ns()
|
|
17
17
|
c = b - a
|
|
18
18
|
secs = c / (10 ** 9)
|
|
19
19
|
print(f'- Took: {secs:.2f} seconds')
|
|
20
|
-
return secs
|
|
20
|
+
return secs, result
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
|
|
24
|
-
dt = measure(lambda: index.add(vectors))
|
|
25
|
-
print(f'- Performance: {vectors.shape[0]/dt:.2f}
|
|
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} %')
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
|
|
29
39
|
labels = np.arange(vectors.shape[0], dtype=np.longlong)
|
|
30
40
|
assert len(index) == 0
|
|
31
|
-
dt = measure(lambda: index.add(labels, vectors))
|
|
41
|
+
dt, _ = measure(lambda: index.add(labels, vectors))
|
|
32
42
|
assert len(index) == vectors.shape[0]
|
|
33
|
-
print(f'- Performance: {vectors.shape[0]/dt:.2f}
|
|
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')
|
|
34
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} %')
|
|
35
54
|
|
|
36
55
|
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
37
56
|
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
38
57
|
|
|
58
|
+
|
|
39
59
|
signature_f32 = types.float32(
|
|
40
60
|
types.CPointer(types.float32),
|
|
41
61
|
types.CPointer(types.float32),
|
|
@@ -85,7 +105,7 @@ def main(
|
|
|
85
105
|
)
|
|
86
106
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
87
107
|
|
|
88
|
-
print('USearch: HNSW +
|
|
108
|
+
print('USearch: HNSW + Half Precision')
|
|
89
109
|
index = Index(
|
|
90
110
|
ndim=dim,
|
|
91
111
|
dtype='f16',
|
|
@@ -95,6 +115,16 @@ def main(
|
|
|
95
115
|
)
|
|
96
116
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
97
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
|
+
|
|
98
128
|
print('FAISS: HNSW')
|
|
99
129
|
index = IndexHNSWFlat(dim, connectivity)
|
|
100
130
|
index.hnsw.efSearch = expansion_search
|
package/python/lib.cpp
CHANGED
|
@@ -145,9 +145,9 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
|
|
|
145
145
|
if (vector_info.ndim != 1)
|
|
146
146
|
throw std::invalid_argument("Expects a vector, not a higher-rank tensor!");
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
149
149
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
150
|
-
if (vector_dimensions != static_cast<
|
|
150
|
+
if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
151
151
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
152
152
|
|
|
153
153
|
if (index.size() + 1 >= index.capacity())
|
|
@@ -180,10 +180,10 @@ static void add_many_to_index( //
|
|
|
180
180
|
if (vectors_info.ndim != 2)
|
|
181
181
|
throw std::invalid_argument("Expects a matrix of vectors to add!");
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
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()))
|
|
187
187
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
188
188
|
|
|
189
189
|
if (labels_count != vectors_count)
|
|
@@ -222,13 +222,13 @@ static void add_many_to_index( //
|
|
|
222
222
|
static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, std::size_t wanted) {
|
|
223
223
|
|
|
224
224
|
py::buffer_info vector_info = vector.request();
|
|
225
|
-
|
|
225
|
+
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
226
226
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
227
|
-
if (vector_dimensions != static_cast<
|
|
227
|
+
if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
228
228
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
229
229
|
|
|
230
|
-
py::array_t<label_t> labels_py(static_cast<
|
|
231
|
-
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));
|
|
232
232
|
std::size_t count{};
|
|
233
233
|
auto labels_py1d = labels_py.mutable_unchecked<1>();
|
|
234
234
|
auto distances_py1d = distances_py.mutable_unchecked<1>();
|
|
@@ -246,8 +246,8 @@ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, s
|
|
|
246
246
|
else
|
|
247
247
|
throw std::invalid_argument("Incompatible scalars in the query vector!");
|
|
248
248
|
|
|
249
|
-
labels_py.resize(py_shape_t{static_cast<
|
|
250
|
-
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)});
|
|
251
251
|
|
|
252
252
|
py::tuple results(3);
|
|
253
253
|
results[0] = labels_py;
|
|
@@ -276,14 +276,14 @@ static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors,
|
|
|
276
276
|
if (vectors_info.ndim != 2)
|
|
277
277
|
throw std::invalid_argument("Expects a matrix of vectors to add!");
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
Py_ssize_t vectors_count = vectors_info.shape[0];
|
|
280
|
+
Py_ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
281
281
|
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
282
|
-
if (vectors_dimensions != static_cast<
|
|
282
|
+
if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
|
|
283
283
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
284
284
|
|
|
285
|
-
py::array_t<label_t> labels_py({vectors_count, static_cast<
|
|
286
|
-
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)});
|
|
287
287
|
py::array_t<Py_ssize_t> counts_py(vectors_count);
|
|
288
288
|
auto labels_py2d = labels_py.mutable_unchecked<2>();
|
|
289
289
|
auto distances_py2d = distances_py.mutable_unchecked<2>();
|
|
@@ -364,8 +364,8 @@ inline std::uint64_t hash(std::uint64_t v) noexcept {
|
|
|
364
364
|
template <typename scalar_at>
|
|
365
365
|
inline void hash_typed_buffer(hash_index_w_meta_t& index, py::buffer_info const& vector_info) noexcept {
|
|
366
366
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
368
|
+
Py_ssize_t vector_stride = vector_info.strides[0];
|
|
369
369
|
std::memset(index.buffer_.data(), 0, index.words_ * sizeof(hash_word_t));
|
|
370
370
|
|
|
371
371
|
for (std::size_t i = 0; i != static_cast<std::size_t>(vector_dimensions); ++i) {
|
|
@@ -477,10 +477,10 @@ PYBIND11_MODULE(index, m) {
|
|
|
477
477
|
validate_set(set);
|
|
478
478
|
auto proxy = set.unchecked<1>();
|
|
479
479
|
auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
480
|
-
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)});
|
|
481
481
|
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
482
482
|
auto found = index.search(view, count, &labels_proxy(0), nullptr, 0);
|
|
483
|
-
labels_py.resize(py_shape_t{static_cast<
|
|
483
|
+
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
|
|
484
484
|
return labels_py;
|
|
485
485
|
},
|
|
486
486
|
py::arg("set"), //
|
|
@@ -527,10 +527,10 @@ PYBIND11_MODULE(index, m) {
|
|
|
527
527
|
if (index.size() + 1 >= index.capacity())
|
|
528
528
|
index.reserve(ceil2(index.size() + 1));
|
|
529
529
|
hash_buffer(index, array);
|
|
530
|
-
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)});
|
|
531
531
|
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
532
532
|
auto found = index.search(index.buffer(), count, &labels_proxy(0), nullptr, 0);
|
|
533
|
-
labels_py.resize(py_shape_t{static_cast<
|
|
533
|
+
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
|
|
534
534
|
return labels_py;
|
|
535
535
|
},
|
|
536
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
|
|
@@ -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;
|
package/src/advanced.hpp
CHANGED
|
@@ -174,6 +174,29 @@ class f8_bits_t {
|
|
|
174
174
|
|
|
175
175
|
inline f16_bits_t::f16_bits_t(f8_bits_t v) noexcept : f16_bits_t(float(v)) {}
|
|
176
176
|
|
|
177
|
+
struct cos_f8_t {
|
|
178
|
+
punned_distance_t operator()(f8_bits_t const* a, f8_bits_t const* b, std::size_t n) const noexcept {
|
|
179
|
+
std::int32_t ab{}, a2{}, b2{};
|
|
180
|
+
for (std::size_t i = 0; i != n; i++) {
|
|
181
|
+
std::int16_t ai{a[i]};
|
|
182
|
+
std::int16_t bi{b[i]};
|
|
183
|
+
ab += ai * bi;
|
|
184
|
+
a2 += square(ai);
|
|
185
|
+
b2 += square(bi);
|
|
186
|
+
}
|
|
187
|
+
return 1.f - ab / (std::sqrt(a2) * std::sqrt(b2));
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
struct l2sq_f8_t {
|
|
192
|
+
punned_distance_t operator()(f8_bits_t const* a, f8_bits_t const* b, std::size_t n) const noexcept {
|
|
193
|
+
std::int32_t ab_deltas_sq{};
|
|
194
|
+
for (std::size_t i = 0; i != n; i++)
|
|
195
|
+
ab_deltas_sq += square(std::int16_t(a[i]) - std::int16_t(b[i]));
|
|
196
|
+
return ab_deltas_sq;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
177
200
|
struct uuid_t {
|
|
178
201
|
std::uint8_t octets[16];
|
|
179
202
|
};
|
|
@@ -495,7 +518,11 @@ class auto_index_gt {
|
|
|
495
518
|
|
|
496
519
|
private:
|
|
497
520
|
static index_t* aligned_index_alloc() noexcept {
|
|
521
|
+
#if defined(WINDOWS)
|
|
522
|
+
return (index_t*)_aligned_malloc(64, 64 * divide_round_up<64>(sizeof(index_t)));
|
|
523
|
+
#else
|
|
498
524
|
return (index_t*)aligned_alloc(64, 64 * divide_round_up<64>(sizeof(index_t)));
|
|
525
|
+
#endif
|
|
499
526
|
}
|
|
500
527
|
|
|
501
528
|
static void aligned_index_free(index_t* raw) noexcept { free(raw); }
|
|
@@ -652,43 +679,40 @@ class auto_index_gt {
|
|
|
652
679
|
return {pun_metric<f32_t>(ip_gt<f32_t>{}), isa_t::auto_k};
|
|
653
680
|
}
|
|
654
681
|
|
|
655
|
-
static metric_and_meta_t
|
|
682
|
+
static metric_and_meta_t cos_metric_f16(std::size_t dimensions) {
|
|
656
683
|
(void)dimensions;
|
|
657
684
|
#if defined(USEARCH_USE_SIMD)
|
|
658
685
|
#if defined(__x86_64__)
|
|
659
|
-
if (dimensions %
|
|
686
|
+
if (dimensions % 16 == 0)
|
|
660
687
|
return {
|
|
661
688
|
pun_metric<simsimd_f16_t>([](simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) noexcept {
|
|
662
|
-
return 1.f -
|
|
689
|
+
return 1.f - simsimd_cos_f16x16avx512(a, b, d);
|
|
663
690
|
}),
|
|
664
691
|
isa_t::avx512_k,
|
|
665
692
|
};
|
|
666
693
|
#elif defined(__aarch64__)
|
|
667
|
-
if (
|
|
668
|
-
return {
|
|
669
|
-
pun_metric<simsimd_f16_t>([](simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) noexcept {
|
|
670
|
-
return 1.f - simsimd_dot_f16sve(a, b, d);
|
|
671
|
-
}),
|
|
672
|
-
isa_t::sve_k,
|
|
673
|
-
};
|
|
674
|
-
if (dimensions % 8 == 0)
|
|
694
|
+
if (dimensions % 4 == 0)
|
|
675
695
|
return {
|
|
676
696
|
pun_metric<simsimd_f16_t>([](simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) noexcept {
|
|
677
|
-
return 1.f -
|
|
697
|
+
return 1.f - simsimd_cos_f16x4neon(a, b, d);
|
|
678
698
|
}),
|
|
679
699
|
isa_t::neon_k,
|
|
680
700
|
};
|
|
681
701
|
#endif
|
|
682
702
|
#endif
|
|
683
|
-
return {pun_metric<f16_bits_t>(
|
|
703
|
+
return {pun_metric<f16_bits_t>(cos_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
|
|
684
704
|
}
|
|
685
705
|
|
|
686
706
|
static metric_and_meta_t ip_metric(std::size_t dimensions, accuracy_t accuracy) {
|
|
687
707
|
switch (accuracy) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
case accuracy_t::
|
|
708
|
+
case accuracy_t::f32_k:
|
|
709
|
+
// The two most common numeric types for the most common metric have optimized versions
|
|
710
|
+
return ip_metric_f32(dimensions);
|
|
711
|
+
case accuracy_t::f16_k:
|
|
712
|
+
// Dot-product accumulates error, Cosine-distance normalizes it
|
|
713
|
+
return cos_metric_f16(dimensions);
|
|
714
|
+
|
|
715
|
+
case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(cos_f8_t{}), isa_t::auto_k};
|
|
692
716
|
case accuracy_t::f64_k: return {pun_metric<f64_t>(ip_gt<f64_t>{}), isa_t::auto_k};
|
|
693
717
|
default: return {};
|
|
694
718
|
}
|
|
@@ -696,7 +720,7 @@ class auto_index_gt {
|
|
|
696
720
|
|
|
697
721
|
static metric_and_meta_t l2_metric(std::size_t, accuracy_t accuracy) {
|
|
698
722
|
switch (accuracy) {
|
|
699
|
-
case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(
|
|
723
|
+
case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(l2sq_f8_t{}), isa_t::auto_k};
|
|
700
724
|
case accuracy_t::f16_k: return {pun_metric<f16_bits_t>(l2sq_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
|
|
701
725
|
case accuracy_t::f32_k: return {pun_metric<f32_t>(l2sq_gt<f32_t>{}), isa_t::auto_k};
|
|
702
726
|
case accuracy_t::f64_k: return {pun_metric<f64_t>(l2sq_gt<f64_t>{}), isa_t::auto_k};
|
|
@@ -704,10 +728,10 @@ class auto_index_gt {
|
|
|
704
728
|
}
|
|
705
729
|
}
|
|
706
730
|
|
|
707
|
-
static metric_and_meta_t cos_metric(std::size_t, accuracy_t accuracy) {
|
|
731
|
+
static metric_and_meta_t cos_metric(std::size_t dimensions, accuracy_t accuracy) {
|
|
708
732
|
switch (accuracy) {
|
|
709
|
-
case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(
|
|
710
|
-
case accuracy_t::f16_k: return
|
|
733
|
+
case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(cos_f8_t{}), isa_t::auto_k};
|
|
734
|
+
case accuracy_t::f16_k: return cos_metric_f16(dimensions);
|
|
711
735
|
case accuracy_t::f32_k: return {pun_metric<f32_t>(cos_gt<f32_t>{}), isa_t::auto_k};
|
|
712
736
|
case accuracy_t::f64_k: return {pun_metric<f64_t>(cos_gt<f64_t>{}), isa_t::auto_k};
|
|
713
737
|
default: return {};
|
package/src/bench.cpp
CHANGED
|
@@ -400,7 +400,7 @@ struct args_t {
|
|
|
400
400
|
bool quantize_f16 = false;
|
|
401
401
|
bool quantize_f8 = false;
|
|
402
402
|
|
|
403
|
-
bool metric_ip =
|
|
403
|
+
bool metric_ip = false;
|
|
404
404
|
bool metric_l2 = false;
|
|
405
405
|
bool metric_cos = false;
|
|
406
406
|
bool metric_haversine = false;
|
|
@@ -408,23 +408,19 @@ struct args_t {
|
|
|
408
408
|
|
|
409
409
|
template <typename index_at, typename dataset_at> //
|
|
410
410
|
index_at type_punned_index_for_metric(dataset_at& dataset, args_t const& args, config_t config, accuracy_t accuracy) {
|
|
411
|
-
if (args.metric_ip) {
|
|
412
|
-
std::printf("-- Metric: Inner Product\n");
|
|
413
|
-
return index_at::ip(dataset.dimensions(), accuracy, config);
|
|
414
|
-
}
|
|
415
411
|
if (args.metric_l2) {
|
|
416
412
|
std::printf("-- Metric: Euclidean\n");
|
|
417
413
|
return index_at::l2(dataset.dimensions(), accuracy, config);
|
|
418
|
-
}
|
|
419
|
-
if (args.metric_cos) {
|
|
414
|
+
} else if (args.metric_cos) {
|
|
420
415
|
std::printf("-- Metric: Angular\n");
|
|
421
416
|
return index_at::cos(dataset.dimensions(), accuracy, config);
|
|
422
|
-
}
|
|
423
|
-
if (args.metric_haversine) {
|
|
417
|
+
} else if (args.metric_haversine) {
|
|
424
418
|
std::printf("-- Metric: Haversine\n");
|
|
425
419
|
return index_at::haversine(accuracy, config);
|
|
420
|
+
} else {
|
|
421
|
+
std::printf("-- Metric: Inner Product\n");
|
|
422
|
+
return index_at::ip(dataset.dimensions(), accuracy, config);
|
|
426
423
|
}
|
|
427
|
-
throw std::invalid_argument("Unknown metric!");
|
|
428
424
|
}
|
|
429
425
|
|
|
430
426
|
template <typename index_at, typename dataset_at> //
|
|
@@ -472,16 +468,16 @@ template <typename neighbor_id_at, typename dataset_at> //
|
|
|
472
468
|
void run_big_or_small(dataset_at& dataset, args_t const& args, config_t config) {
|
|
473
469
|
if (args.native) {
|
|
474
470
|
if (args.metric_cos) {
|
|
475
|
-
std::printf("-- Metric:
|
|
476
|
-
run_typed<index_gt<
|
|
471
|
+
std::printf("-- Metric: Angular\n");
|
|
472
|
+
run_typed<index_gt<cos_gt<float>, std::size_t, neighbor_id_at>>(dataset, args, config);
|
|
477
473
|
} else if (args.metric_l2) {
|
|
478
474
|
std::printf("-- Metric: Euclidean\n");
|
|
479
475
|
run_typed<index_gt<l2sq_gt<float>, std::size_t, neighbor_id_at>>(dataset, args, config);
|
|
480
476
|
} else if (args.metric_haversine) {
|
|
481
|
-
std::printf("-- Metric:
|
|
477
|
+
std::printf("-- Metric: Haversine\n");
|
|
482
478
|
run_typed<index_gt<haversine_gt<float>, std::size_t, neighbor_id_at>>(dataset, args, config);
|
|
483
479
|
} else {
|
|
484
|
-
std::printf("-- Metric:
|
|
480
|
+
std::printf("-- Metric: Inner Product\n");
|
|
485
481
|
run_typed<index_gt<ip_gt<float>, std::size_t, neighbor_id_at>>(dataset, args, config);
|
|
486
482
|
}
|
|
487
483
|
} else
|