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/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} 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} %')
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} 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')
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 + Numba JIT + Half Precision')
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
- ssize_t vector_dimensions = vector_info.shape[0];
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<ssize_t>(index.dimensions()))
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
- ssize_t labels_count = labels_info.shape[0];
184
- ssize_t vectors_count = vectors_info.shape[0];
185
- ssize_t vectors_dimensions = vectors_info.shape[1];
186
- 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()))
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
- ssize_t vector_dimensions = vector_info.shape[0];
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<ssize_t>(index.dimensions()))
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<ssize_t>(wanted));
231
- 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));
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<ssize_t>(count)});
250
- 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)});
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
- ssize_t vectors_count = vectors_info.shape[0];
280
- 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];
281
281
  char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
282
- if (vectors_dimensions != static_cast<ssize_t>(index.dimensions()))
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<ssize_t>(wanted)});
286
- 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)});
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
- ssize_t vector_dimensions = vector_info.shape[0];
368
- 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];
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<ssize_t>(count)});
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<ssize_t>(found)});
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<ssize_t>(count)});
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<ssize_t>(found)});
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
- # 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
@@ -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;
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 ip_metric_f16(std::size_t dimensions) {
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 % 32 == 0)
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 - simsimd_dot_f16x16avx512(a, b, d);
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 (supports_arm_sve())
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 - simsimd_dot_f16x8neon(a, b, d);
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>(ip_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
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
- // The tow most common numeric types for the most common metric have afew optimized versions:
689
- case accuracy_t::f16_k: return ip_metric_f16(dimensions);
690
- case accuracy_t::f32_k: return ip_metric_f32(dimensions);
691
- case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(ip_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
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>(l2sq_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
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>(cos_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
710
- case accuracy_t::f16_k: return {pun_metric<f16_bits_t>(cos_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
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 = true;
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: Inner Product\n");
476
- run_typed<index_gt<ip_gt<float>, std::size_t, neighbor_id_at>>(dataset, args, config);
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: Angular\n");
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: Haversine\n");
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