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/python/bench.py CHANGED
@@ -1,33 +1,75 @@
1
- import datetime
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 = datetime.datetime.now()
13
- f()
14
- b = datetime.datetime.now()
14
+ a = time.time_ns()
15
+ result = f()
16
+ b = time.time_ns()
15
17
  c = b - a
16
- print(f'Took: {c.seconds:.2f} seconds')
17
- return c.seconds
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'- FAISS: {vectors.shape[0]/dt:.2f} vectors/s')
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'- USearch: {vectors.shape[0]/dt:.2f} vectors/s')
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
- # Test FAISS
48
- index = faiss.IndexHNSWFlat(dim, connectivity)
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
- # Test FAISS with default Product Quantization
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 = faiss.IndexHNSWFlat(dim, connectivity)
58
- index = faiss.IndexIVFPQ(coarse_quantizer, dim, nlist, nsegment, nbit)
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
- punned_metric_t metric_ptr = reinterpret_cast<punned_metric_t>(metric_uintptr);
83
- if (metric_ptr)
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
- ssize_t vector_dimensions = vector_info.shape[0];
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<ssize_t>(index.dimensions()))
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(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
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
- ssize_t labels_count = labels_info.shape[0];
164
- ssize_t vectors_count = vectors_info.shape[0];
165
- ssize_t vectors_dimensions = vectors_info.shape[1];
166
- if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
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
- ssize_t vector_dimensions = vector_info.shape[0];
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<ssize_t>(index.dimensions()))
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<ssize_t>(wanted));
211
- py::array_t<distance_t> distances_py(static_cast<ssize_t>(wanted));
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<ssize_t>(count)});
230
- distances_py.resize(py_shape_t{static_cast<ssize_t>(count)});
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
- ssize_t vectors_count = vectors_info.shape[0];
260
- ssize_t vectors_dimensions = vectors_info.shape[1];
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<ssize_t>(index.dimensions()))
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<ssize_t>(wanted)});
266
- py::array_t<distance_t> distances_py({vectors_count, static_cast<ssize_t>(wanted)});
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
- ssize_t vector_dimensions = vector_info.shape[0];
348
- ssize_t vector_stride = vector_info.strides[0];
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<ssize_t>(count)});
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<ssize_t>(found)});
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<ssize_t>(count)});
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<ssize_t>(found)});
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
- # Showcases how to use Numba to JIT-compile similarity measures for USearch.
7
- # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
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 = types.float32(
10
- types.CPointer(types.float32),
11
- types.CPointer(types.float32),
12
- types.size_t, types.size_t)
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
- @cfunc(signature)
16
- def python_dot(a, b, n, m):
17
- a_array = carray(a, n)
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
- count_vectors = 100
26
- count_dimensions = 96
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 = Index(ndim=96)
38
- index.add(labels, vectors, copy=True)
39
- assert index.ndim == count_dimensions
40
- assert index.capacity >= count_vectors
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 = index.search(vectors, 10)
44
- print('found', results[0].shape, results[1].shape, results[2].shape)
89
+ # assert list(results) == [10, 11]
45
90
 
46
91
 
47
- index_sets = SetsIndex()
48
- index_sets.add(10, np.array([10, 12, 15], dtype=np.uint32))
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, find_packages
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
- compile_args.append('-mmacosx-version-min=10.13')
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 = vmlaq_f32(ab_vec, vld1q_f32(a + i), vld1q_f32(b + i));
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 simsimd_f16_t simsimd_dot_f16x8neon(simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) {
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
- // There is no half-precision FMA, only bfloat16, so we must manually multiply and add.
190
- float16x8_t ab_vec = vdupq_n_f16(0);
191
- for (size_t i = 0; i != d; i += 8)
192
- ab_vec = vaddq_f16(ab_vec, vmulq_f16(vld1q_f16((float16_t const*)a + i), vld1q_f16((float16_t const*)b + i)));
193
- float16_t f16 = vaddvq_f32(vcvt_f32_f16(vget_high_f16(ab_vec))) + vaddvq_f32(vcvt_f32_f16(vget_low_f16(ab_vec)));
194
- simsimd_f16_t i16;
195
- memcpy(&i16, &f16, sizeof(i16));
196
- return i16;
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 simsimd_dot_f16x16avx512(simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) {
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
- for (size_t i = 0; i != d; i += 16)
262
- ab_vec = _mm512_fmadd_ps( //
263
- _mm512_cvtxph_ps(_mm256_loadu_ph(a + i)), //
264
- _mm512_cvtxph_ps(_mm256_loadu_ph(b + i)), //
265
- ab_vec);
266
- return _mm512_reduce_add_ps(ab_vec);
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;