usearch 0.1.9 → 0.1.10

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,60 +1,13 @@
1
1
  import datetime
2
2
  import numpy as np
3
+ import fire
4
+
5
+ from usearchh.io import load_matrix
3
6
 
4
7
  import faiss
5
8
  import usearch
6
9
 
7
10
 
8
- print('Welcome to basic kANN benchmark!')
9
-
10
-
11
- def read_matrix(filename: str, start_row: int = 0, count_rows: int = None):
12
- """
13
- Read *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
14
- Args:
15
- :param filename (str): path to the matrix file
16
- :param start_row (int): start reading vectors from this index
17
- :param count_rows (int): number of vectors to read. If None, read all vectors
18
- Returns:
19
- Parsed matrix (numpy.ndarray)
20
- """
21
- dtype = np.float32
22
- scalar_size = 4
23
- if filename.endswith('.fbin'):
24
- dtype = np.float32
25
- scalar_size = 4
26
- elif filename.endswith('.dbin'):
27
- dtype = np.float64
28
- scalar_size = 8
29
- elif filename.endswith('.hbin'):
30
- dtype = np.float16
31
- scalar_size = 2
32
- elif filename.endswith('.ibin'):
33
- dtype = np.int32
34
- scalar_size = 4
35
- else:
36
- raise Exception('Unknown file type')
37
- with open(filename, 'rb') as f:
38
- rows, cols = np.fromfile(f, count=2, dtype=np.int32)
39
- rows = (rows - start_row) if count_rows is None else count_rows
40
- arr = np.fromfile(
41
- f, count=rows * cols, dtype=dtype,
42
- offset=start_row * scalar_size * cols)
43
- return arr.reshape(rows, cols)
44
-
45
-
46
- print('Will read datasets!')
47
- xb = read_matrix('datasets/wiki_1M/base.1M.fbin')
48
- xq = read_matrix('datasets/wiki_1M/query.public.100K.fbin')
49
-
50
- d = 256 # vector size
51
- M = 16
52
- efSearch = 32 # number of entry points (neighbors) we use on each layer
53
- efConstruction = 128 # number of entry points used on each layer
54
- labels = np.arange(xb.shape[0], dtype=np.longlong)
55
- print('Will benchmark algorithms!')
56
-
57
-
58
11
  def measure(f) -> float:
59
12
  a = datetime.datetime.now()
60
13
  f()
@@ -64,29 +17,46 @@ def measure(f) -> float:
64
17
  return c.seconds
65
18
 
66
19
 
67
- def construct_both() -> tuple:
20
+ 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')
68
23
 
69
- index_meta = faiss.IndexHNSWFlat(d, M)
70
- index_meta.hnsw.efSearch = efSearch
71
- index_meta.hnsw.efConstruction = efConstruction
72
- dt_meta = measure(lambda: index_meta.add(xb))
73
24
 
74
- index_unum = usearch.Index(
75
- dim=d,
76
- expansion_add=efConstruction,
77
- expansion_search=efSearch,
78
- connectivity=M,
79
- )
80
- assert len(index_unum) == 0
81
- dt_unum = measure(lambda: index_unum.add(labels, xb))
82
- assert len(index_unum) == xb.shape[0]
25
+ def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
26
+ labels = np.arange(vectors.shape[0], dtype=np.longlong)
27
+ assert len(index) == 0
28
+ dt = measure(lambda: index.add(labels, vectors))
29
+ assert len(index) == vectors.shape[0]
30
+ print(f'- USearch: {vectors.shape[0]/dt:.2f} vectors/s')
31
+
32
+
33
+ def main(
34
+ vectors: str,
35
+ queries: str,
36
+ neighbors: str,
37
+ connectivity: int = 16,
38
+ expansion_add: int = 128,
39
+ expansion_search: int = 64,
40
+ ):
83
41
 
84
- return dt_meta, dt_unum
42
+ vectors_mat = load_matrix(vectors)
43
+ queries_mat = load_matrix(queries)
44
+ neighbors_mat = load_matrix(neighbors)
45
+ dim = vectors_mat.shape[1]
46
+
47
+ index = faiss.IndexHNSWFlat(dim, connectivity)
48
+ index.hnsw.efSearch = expansion_search
49
+ index.hnsw.efConstruction = expansion_add
50
+ bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
51
+
52
+ index = usearch.Index(
53
+ ndim=dim,
54
+ expansion_add=expansion_add,
55
+ expansion_search=expansion_search,
56
+ connectivity=connectivity,
57
+ )
58
+ bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
85
59
 
86
60
 
87
- experiments = 10
88
- durations = [construct_both() for _ in range(experiments)]
89
- dt_meta = sum(x[0] for x in durations)
90
- dt_unum = sum(x[1] for x in durations)
91
- print(f'- FAISS vectors per second: {xb.shape[0]*experiments/dt_meta:.2f}')
92
- print(f'- USearch vectors per second: {xb.shape[0]*experiments/dt_unum:.2f}')
61
+ if __name__ == '__main__':
62
+ fire.Fire(main)
package/python/lib.cpp CHANGED
@@ -36,7 +36,8 @@ static native_index_t make_index( //
36
36
  std::string const& metric, //
37
37
  std::size_t connectivity, //
38
38
  std::size_t expansion_add, //
39
- std::size_t expansion_search //
39
+ std::size_t expansion_search, //
40
+ std::size_t metric_uintptr //
40
41
  ) {
41
42
 
42
43
  config_t config;
@@ -48,7 +49,11 @@ static native_index_t make_index( //
48
49
  config.max_threads_search = std::thread::hardware_concurrency();
49
50
 
50
51
  accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
51
- return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
52
+ punned_metric_t metric_ptr = reinterpret_cast<punned_metric_t>(metric_uintptr);
53
+ if (metric_ptr)
54
+ return native_index_t::udf(dimensions, metric_ptr, accuracy, config);
55
+ else
56
+ return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
52
57
  }
53
58
 
54
59
  static void add_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
@@ -173,15 +178,16 @@ PYBIND11_MODULE(usearch, m) {
173
178
 
174
179
  auto i = py::class_<native_index_t>(m, "Index");
175
180
 
176
- i.def(py::init(&make_index), //
177
- py::kw_only(), //
178
- py::arg("dim"), //
179
- py::arg("capacity") = 0, //
180
- py::arg("dtype") = std::string("f32"), //
181
- py::arg("metric") = std::string("ip"), //
182
- py::arg("connectivity") = config_t::connectivity_default_k, //
183
- py::arg("expansion_add") = config_t::expansion_add_default_k, //
184
- py::arg("expansion_search") = config_t::expansion_search_default_k //
181
+ i.def(py::init(&make_index), //
182
+ py::kw_only(), //
183
+ py::arg("ndim"), //
184
+ py::arg("capacity") = 0, //
185
+ py::arg("dtype") = std::string("f32"), //
186
+ py::arg("metric") = std::string("ip"), //
187
+ py::arg("connectivity") = config_t::connectivity_default_k, //
188
+ py::arg("expansion_add") = config_t::expansion_add_default_k, //
189
+ py::arg("expansion_search") = config_t::expansion_search_default_k, //
190
+ py::arg("metric_pointer") = 0 //
185
191
  );
186
192
 
187
193
  i.def( //
package/python/test.py CHANGED
@@ -2,14 +2,40 @@ import numpy as np
2
2
 
3
3
  import usearch
4
4
 
5
+ from numba import cfunc, types, carray
6
+
7
+ # Showcases how to use Numba to JIT-compile similarity measures for USearch.
8
+ # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
9
+
10
+ signature = types.float32(
11
+ types.CPointer(types.float32),
12
+ types.CPointer(types.float32),
13
+ types.size_t, types.size_t)
14
+
15
+
16
+ @cfunc(signature)
17
+ def python_dot(a, b, n, m):
18
+ a_array = carray(a, n)
19
+ b_array = carray(b, n)
20
+ c = 0.0
21
+ for i in range(n):
22
+ c += a_array[i] * b_array[i]
23
+ return c
24
+
25
+
5
26
  count_vectors = 100
6
27
  count_dimensions = 96
7
-
8
- index = usearch.Index(dim=96)
9
28
  vectors = np.random.uniform(
10
29
  0, 0.3, (count_vectors, count_dimensions)).astype(np.float32)
11
30
  labels = np.array(range(count_vectors), dtype=np.longlong)
12
31
 
32
+ index_udf = usearch.Index(ndim=96, metric_pointer=python_dot.address)
33
+ index_udf.add(labels, vectors, copy=True)
34
+ results = index_udf.search(vectors, 10)
35
+ print('found', results[0].shape, results[1].shape, results[2].shape)
36
+
37
+
38
+ index = usearch.Index(ndim=96)
13
39
  index.add(labels, vectors, copy=True)
14
40
  assert index.ndim == count_dimensions
15
41
  assert index.capacity >= count_vectors
@@ -0,0 +1,34 @@
1
+ import numpy as np
2
+ from ucall.client import Client
3
+
4
+
5
+ class IndexClient:
6
+
7
+ def __init__(self, uri: str = '127.0.0.1', port: int = 8545, use_http: bool = True) -> None:
8
+ self.client = Client(uri=uri, port=port, use_http=use_http)
9
+
10
+ def add(self, labels: np.array, vectors: np.array):
11
+ if isinstance(labels, int):
12
+ return self.client.add_one(label=labels, vectors=vectors)
13
+ else:
14
+ return self.client.add_many(labels=labels, vectors=vectors)
15
+
16
+ def search(self, vectors: np.array, count: int) -> tuple[np.array, np.array, np.array]:
17
+ matches = []
18
+ distances = []
19
+ counts = []
20
+ # return self.client.search_one(vectors=vectors, count=count)
21
+ return matches, distances, counts
22
+
23
+ def __len__(self):
24
+ return self.client.size()
25
+
26
+ @property
27
+ def ndim(self):
28
+ return self.client.ndim()
29
+
30
+ def capacity(self):
31
+ return self.client.capacity()
32
+
33
+ def connectivity(self):
34
+ return self.client.connectivity()
@@ -0,0 +1,67 @@
1
+ import struct
2
+ import numpy as np
3
+
4
+
5
+ def load_matrix(filename: str, start_row: int = 0, count_rows: int = None, view: bool = False):
6
+ """
7
+ Read *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
8
+ Args:
9
+ :param filename (str): path to the matrix file
10
+ :param start_row (int): start reading vectors from this index
11
+ :param count_rows (int): number of vectors to read. If None, read all vectors
12
+ Returns:
13
+ Parsed matrix (numpy.ndarray)
14
+ """
15
+ dtype = np.float32
16
+ scalar_size = 4
17
+ if filename.endswith('.fbin'):
18
+ dtype = np.float32
19
+ scalar_size = 4
20
+ elif filename.endswith('.dbin'):
21
+ dtype = np.float64
22
+ scalar_size = 8
23
+ elif filename.endswith('.hbin'):
24
+ dtype = np.float16
25
+ scalar_size = 2
26
+ elif filename.endswith('.ibin'):
27
+ dtype = np.int32
28
+ scalar_size = 4
29
+ else:
30
+ raise Exception('Unknown file type')
31
+
32
+ with open(filename, 'rb') as f:
33
+ rows, cols = np.fromfile(f, count=2, dtype=np.int32)
34
+ rows = (rows - start_row) if count_rows is None else count_rows
35
+ row_offset = start_row * scalar_size * cols
36
+
37
+ if view:
38
+ return np.memmap(f, dtype=dtype, mode='r', offset=8+row_offset, shape=(rows, cols))
39
+ else:
40
+ return np.fromfile(f, count=rows * cols, dtype=dtype, offset=row_offset).reshape(rows, cols)
41
+
42
+
43
+ def save_matrix(vecs: np.array, filename: str):
44
+ """
45
+ Write *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
46
+ Args:
47
+ :param vecs (numpy.array): the matrix to serialize
48
+ :param filename (str): path to the matrix file
49
+ """
50
+ dtype = np.float32
51
+ if filename.endswith('.fbin'):
52
+ dtype = np.float32
53
+ elif filename.endswith('.dbin'):
54
+ dtype = np.float64
55
+ elif filename.endswith('.hbin'):
56
+ dtype = np.float16
57
+ elif filename.endswith('.ibin'):
58
+ dtype = np.int32
59
+ else:
60
+ raise Exception('Unknown file type')
61
+
62
+ assert len(vecs.shape) == 2, 'Input array must have 2 dimensions'
63
+ with open(filename, 'wb') as f:
64
+ nvecs, dim = vecs.shape
65
+ f.write(struct.pack('<i', nvecs))
66
+ f.write(struct.pack('<i', dim))
67
+ vecs.astype(dtype).flatten().tofile(f)
@@ -0,0 +1,51 @@
1
+ import ucall.rich_posix as ucall
2
+ import usearch
3
+
4
+ import fire
5
+ import numpy as np
6
+ from PIL import Image
7
+
8
+
9
+ def serve(ndim: int, metric: str = 'ip'):
10
+
11
+ server = ucall.Server()
12
+ index = usearch.Index(ndim=ndim, metric=metric)
13
+
14
+ @server
15
+ def add_one(label: int, vector: np.array):
16
+ labels = np.array([label], dtype=np.longlong)
17
+ vectors = vector.reshape(vector.shape[0], 1)
18
+ index.add(labels, vectors, copy=True)
19
+
20
+ @server
21
+ def add_many(labels: np.array, vectors: np.array):
22
+ labels = labels.astype(np.longlong)
23
+ index.add(labels, vectors, copy=True)
24
+
25
+ @server
26
+ def search_one(vector: np.array, count: int) -> np.ndarray:
27
+ vectors = vector.reshape(vector.shape[0], 1)
28
+ results = index.search(vectors, 3)
29
+ return results[0][:results[2][0]]
30
+
31
+ @server
32
+ def size() -> int:
33
+ return len(index)
34
+
35
+ @server
36
+ def ndim() -> int:
37
+ return index.ndim
38
+
39
+ @server
40
+ def capacity() -> int:
41
+ return index.capacity()
42
+
43
+ @server
44
+ def connectivity() -> int:
45
+ return index.connectivity()
46
+
47
+ server.run()
48
+
49
+
50
+ if __name__ == '__main__':
51
+ fire(serve)
package/setup.py CHANGED
@@ -32,7 +32,7 @@ setup(
32
32
  version=__version__,
33
33
  packages=find_packages(),
34
34
 
35
- description='The Mighty Tiny Vector Search Engine with Automatic Quantization and Hardware Acceleration',
35
+ description='Smaller & Faster Single-File Vector Search Engine from Unum',
36
36
  long_description=long_description,
37
37
  long_description_content_type='text/markdown',
38
38
  license='Apache-2.0',
@@ -62,12 +62,14 @@
62
62
  "stop_token": "cpp",
63
63
  "streambuf": "cpp",
64
64
  "thread": "cpp",
65
- "typeinfo": "cpp"
65
+ "typeinfo": "cpp",
66
+ "complex": "cpp"
66
67
  },
67
68
  "cSpell.words": [
68
69
  "dtype",
69
70
  "dtypes",
70
71
  "kann",
72
+ "simsimd",
71
73
  "unsw"
72
74
  ]
73
75
  }
@@ -70,7 +70,10 @@ inline static simsimd_f16_t simsimd_dot_f16sve(simsimd_f16_t const* a_enum, sims
70
70
  i += svcnth();
71
71
  pg_vec = svwhilelt_b16(i, d);
72
72
  } while (svptest_any(svptrue_b16(), pg_vec));
73
- return static_cast<simsimd_f16_t>(svaddv_f16(svptrue_b16(), ab_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;
74
77
  #else
75
78
  (void)a_enum, (void)b_enum, (void)d;
76
79
  return 0;
@@ -103,7 +106,7 @@ inline static simsimd_f32_t simsimd_cos_f32sve(simsimd_f32_t const* a, simsimd_f
103
106
  #endif
104
107
  }
105
108
 
106
- inline static simsimd_f32_t simsimd_euclidean_f32sve(simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) {
109
+ inline static simsimd_f32_t simsimd_l2sq_f32sve(simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) {
107
110
  #if defined(__ARM_FEATURE_SVE)
108
111
  size_t i = 0;
109
112
  svfloat32_t d2_vec = svdupq_n_f32(0.f, 0.f, 0.f, 0.f);
@@ -124,8 +127,7 @@ inline static simsimd_f32_t simsimd_euclidean_f32sve(simsimd_f32_t const* a, sim
124
127
  #endif
125
128
  }
126
129
 
127
- inline static simsimd_f16_t simsimd_euclidean_f16sve( //
128
- simsimd_f16_t const* a_enum, simsimd_f16_t const* b_enum, size_t d) {
130
+ inline static simsimd_f16_t simsimd_l2sq_f16sve(simsimd_f16_t const* a_enum, simsimd_f16_t const* b_enum, size_t d) {
129
131
  #if defined(__ARM_FEATURE_SVE)
130
132
  size_t i = 0;
131
133
  svfloat16_t d2_vec = svdupq_n_f16(0, 0, 0, 0, 0, 0, 0, 0);
@@ -140,8 +142,10 @@ inline static simsimd_f16_t simsimd_euclidean_f16sve( //
140
142
  i += svcnth();
141
143
  pg_vec = svwhilelt_b16(i, d);
142
144
  } while (svptest_any(svptrue_b16(), pg_vec));
143
- simsimd_f16_t d2 = svaddv_f16(svptrue_b16(), d2_vec);
144
- return static_cast<simsimd_f16_t>(sqrt(d2));
145
+ float16_t f16 = svaddv_f16(svptrue_b16(), d2_vec);
146
+ simsimd_f16_t i16;
147
+ memcpy(&i16, &f16, sizeof(i16));
148
+ return i16;
145
149
  #else
146
150
  (void)a_enum, (void)b_enum, (void)d;
147
151
  return 0;
@@ -186,7 +190,10 @@ inline static simsimd_f16_t simsimd_dot_f16x8neon(simsimd_f16_t const* a, simsim
186
190
  float16x8_t ab_vec = vdupq_n_f16(0);
187
191
  for (size_t i = 0; i != d; i += 8)
188
192
  ab_vec = vaddq_f16(ab_vec, vmulq_f16(vld1q_f16((float16_t const*)a + i), vld1q_f16((float16_t const*)b + i)));
189
- return vaddvq_f32(vcvt_f32_f16(vget_high_f16(ab_vec))) + vaddvq_f32(vcvt_f32_f16(vget_low_f16(ab_vec)));
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;
190
197
  #else
191
198
  (void)a, (void)b, (void)d;
192
199
  return 0;
package/src/advanced.hpp CHANGED
@@ -7,8 +7,14 @@
7
7
  #include <sys/auxv.h>
8
8
  #endif
9
9
 
10
+ #if defined(__aarch64__)
11
+ #include <arm_fp16.h>
12
+ #endif
13
+
10
14
  #include <fp16/fp16.h>
15
+ #if defined(USEARCH_USE_SIMD)
11
16
  #include <simsimd/simsimd.h>
17
+ #endif
12
18
 
13
19
  #include <usearch/usearch.hpp>
14
20
 
@@ -39,6 +45,27 @@ template <typename at> inline at clamp(at v, at lo, at hi) noexcept {
39
45
  class i8q100_converted_t;
40
46
  class f16_converted_t;
41
47
 
48
+ inline float f16_to_f32(std::uint16_t u16) noexcept {
49
+ #if defined(__aarch64__)
50
+ __fp16 f16;
51
+ std::memcpy(&f16, &u16, sizeof(std::uint16_t));
52
+ return float(f16);
53
+ #else
54
+ return fp16_ieee_to_fp32_value(u16);
55
+ #endif
56
+ }
57
+
58
+ inline std::uint16_t f32_to_f16(float f32) noexcept {
59
+ #if defined(__aarch64__)
60
+ __fp16 f16 = __fp16(f32);
61
+ std::uint16_t u16;
62
+ std::memcpy(&u16, &f16, sizeof(std::uint16_t));
63
+ return u16;
64
+ #else
65
+ return fp16_ieee_from_fp32_value(f32);
66
+ #endif
67
+ }
68
+
42
69
  class f16_converted_t {
43
70
  std::uint16_t uint16_{};
44
71
 
@@ -49,11 +76,11 @@ class f16_converted_t {
49
76
  inline f16_converted_t(f16_converted_t const&) = default;
50
77
  inline f16_converted_t& operator=(f16_converted_t const&) = default;
51
78
 
52
- inline operator float() const noexcept { return fp16_ieee_to_fp32_value(uint16_); }
79
+ inline operator float() const noexcept { return f16_to_f32(uint16_); }
53
80
 
54
81
  inline f16_converted_t(i8q100_converted_t) noexcept;
55
- inline f16_converted_t(float v) noexcept : uint16_(fp16_ieee_from_fp32_value(v)) {}
56
- inline f16_converted_t(double v) noexcept : uint16_(fp16_ieee_from_fp32_value(v)) {}
82
+ inline f16_converted_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
83
+ inline f16_converted_t(double v) noexcept : uint16_(f32_to_f16(v)) {}
57
84
 
58
85
  inline f16_converted_t operator+(f16_converted_t other) const noexcept { return {float(*this) + float(other)}; }
59
86
  inline f16_converted_t operator-(f16_converted_t other) const noexcept { return {float(*this) - float(other)}; }
@@ -69,22 +96,22 @@ class f16_converted_t {
69
96
  inline f16_converted_t operator/(double other) const noexcept { return {float(*this) / other}; }
70
97
 
71
98
  inline f16_converted_t& operator+=(float v) noexcept {
72
- uint16_ = fp16_ieee_from_fp32_value(v + fp16_ieee_to_fp32_value(uint16_));
99
+ uint16_ = f32_to_f16(v + f16_to_f32(uint16_));
73
100
  return *this;
74
101
  }
75
102
 
76
103
  inline f16_converted_t& operator-=(float v) noexcept {
77
- uint16_ = fp16_ieee_from_fp32_value(v - fp16_ieee_to_fp32_value(uint16_));
104
+ uint16_ = f32_to_f16(v - f16_to_f32(uint16_));
78
105
  return *this;
79
106
  }
80
107
 
81
108
  inline f16_converted_t& operator*=(float v) noexcept {
82
- uint16_ = fp16_ieee_from_fp32_value(v * fp16_ieee_to_fp32_value(uint16_));
109
+ uint16_ = f32_to_f16(v * f16_to_f32(uint16_));
83
110
  return *this;
84
111
  }
85
112
 
86
113
  inline f16_converted_t& operator/=(float v) noexcept {
87
- uint16_ = fp16_ieee_from_fp32_value(v / fp16_ieee_to_fp32_value(uint16_));
114
+ uint16_ = f32_to_f16(v / f16_to_f32(uint16_));
88
115
  return *this;
89
116
  }
90
117
  };
@@ -102,6 +129,10 @@ class i8q100_converted_t {
102
129
  inline operator float() const noexcept { return float(int8_) / 100.f; }
103
130
  inline operator f16_converted_t() const noexcept { return float(int8_) / 100.f; }
104
131
  inline operator double() const noexcept { return double(int8_) / 100.0; }
132
+ inline operator std::int8_t() const noexcept { return int8_; }
133
+ inline operator std::int16_t() const noexcept { return int8_; }
134
+ inline operator std::int32_t() const noexcept { return int8_; }
135
+ inline operator std::int64_t() const noexcept { return int8_; }
105
136
 
106
137
  inline i8q100_converted_t(float v) noexcept
107
138
  : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * 100.f), -100, 100)) {}
@@ -127,7 +158,7 @@ void multithreaded(std::size_t threads, std::size_t tasks, callback_at&& callbac
127
158
  }
128
159
 
129
160
  std::vector<std::thread> threads_pool;
130
- std::size_t tasks_per_thread = tasks / threads + (tasks % threads) != 0;
161
+ std::size_t tasks_per_thread = (tasks / threads) + ((tasks % threads) != 0);
131
162
  for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx) {
132
163
  threads_pool.emplace_back([=]() {
133
164
  for (std::size_t task_idx = thread_idx * tasks_per_thread;
@@ -282,9 +313,25 @@ template <> struct cast_gt<f16_converted_t, f16_converted_t> {
282
313
  bool operator()(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
283
314
  };
284
315
 
316
+ struct ip_i8q100_converted_t {
317
+ float operator()(i8q100_converted_t const* a, i8q100_converted_t const* b, std::size_t d) const noexcept {
318
+ std::int64_t ab = 0;
319
+ #if defined(__GNUC__)
320
+ #pragma GCC ivdep
321
+ #elif defined(__clang__)
322
+ #pragma clang loop vectorize(enable)
323
+ #elif defined(_OPENMP)
324
+ #pragma omp simd reduction(+ : ab)
325
+ #endif
326
+ for (std::size_t i = 0; i < d; ++i)
327
+ ab += std::int16_t(a[i]) * std::int16_t(b[i]);
328
+ return 1.f - ab / 10000.f;
329
+ }
330
+ };
331
+
285
332
  /**
286
333
  * @brief Oversimplified type-punned index for equidimensional floating-point
287
- * vectors with automatic down-casting and isa acceleration.
334
+ * vectors with automatic down-casting and hardware acceleration.
288
335
  */
289
336
  template <typename label_at = std::int64_t, typename id_at = std::uint32_t> //
290
337
  class auto_index_gt {
@@ -362,6 +409,12 @@ class auto_index_gt {
362
409
  static auto_index_gt haversine(accuracy_t accuracy = accuracy_t::f32_k, config_t config = {}) { return make(2, accuracy, haversine_metric(accuracy), make_casts(accuracy), config); }
363
410
  // clang-format on
364
411
 
412
+ static auto_index_gt udf( //
413
+ std::size_t dimensions, punned_stateful_metric_t metric, //
414
+ accuracy_t accuracy = accuracy_t::f32_k, config_t config = {}) {
415
+ return make(dimensions, accuracy, {metric, isa_t::auto_k}, make_casts(accuracy), config);
416
+ }
417
+
365
418
  auto_index_gt fork() const {
366
419
  auto_index_gt result;
367
420
 
@@ -456,6 +509,8 @@ class auto_index_gt {
456
509
  }
457
510
 
458
511
  static metric_and_meta_t ip_metric_f32(std::size_t dimensions) {
512
+ (void)dimensions;
513
+ #if defined(USEARCH_USE_SIMD)
459
514
  #if defined(__x86_64__)
460
515
  if (dimensions % 4 == 0)
461
516
  return {
@@ -479,11 +534,14 @@ class auto_index_gt {
479
534
  }),
480
535
  isa_t::neon_k,
481
536
  };
537
+ #endif
482
538
  #endif
483
539
  return {pun_metric<f32_t>(ip_gt<f32_t>{}), isa_t::auto_k};
484
540
  }
485
541
 
486
542
  static metric_and_meta_t ip_metric_f16(std::size_t dimensions) {
543
+ (void)dimensions;
544
+ #if defined(USEARCH_USE_SIMD)
487
545
  #if defined(__x86_64__)
488
546
  #elif defined(__aarch64__)
489
547
  if (supports_arm_sve())
@@ -500,13 +558,14 @@ class auto_index_gt {
500
558
  }),
501
559
  isa_t::neon_k,
502
560
  };
561
+ #endif
503
562
  #endif
504
563
  return {pun_metric<f16_converted_t>(ip_gt<f16_converted_t>{}), isa_t::auto_k};
505
564
  }
506
565
 
507
566
  static metric_and_meta_t ip_metric(std::size_t dimensions, accuracy_t accuracy) {
508
567
  switch (accuracy) {
509
- case accuracy_t::i8q100_k: return {};
568
+ case accuracy_t::i8q100_k: return {pun_metric<i8q100_converted_t>(ip_i8q100_converted_t{}), isa_t::auto_k};
510
569
  case accuracy_t::f16_k: return ip_metric_f16(dimensions);
511
570
  case accuracy_t::f32_k: return ip_metric_f32(dimensions);
512
571
  case accuracy_t::f64_k: return {pun_metric<f64_t>(ip_gt<f64_t>{}), isa_t::auto_k};