usearch 0.1.9 → 0.2.0
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/.vscode/settings.json +6 -0
- package/CMakeLists.txt +26 -8
- package/CMakeListsInstructions.md +49 -33
- package/Cargo.toml +2 -2
- package/LICENSE +201 -0
- package/README.md +30 -19
- package/VERSION +1 -1
- package/assets/usearch-neighbor-types.png +0 -0
- package/build.gradle +1 -1
- package/include/usearch/usearch.hpp +26 -1
- package/package.json +2 -2
- package/python/bench.py +55 -73
- package/python/lib.cpp +365 -49
- package/python/test.py +35 -2
- package/python/usearch/client.py +34 -0
- package/python/usearch/io.py +67 -0
- package/python/usearch/server.py +51 -0
- package/setup.py +1 -1
- package/simsimd/.vscode/settings.json +3 -1
- package/simsimd/include/simsimd/simsimd.h +14 -7
- package/src/advanced.hpp +86 -17
- package/src/bench.cpp +45 -2
- package/wolfram/CMakeLists.txt +17 -0
- package/wolfram/lib.cpp +139 -4
- package/wolfram/test.wls +27 -0
- package/docs/Makefile +0 -20
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
|
|
@@ -17,3 +43,10 @@ assert len(index) == count_vectors
|
|
|
17
43
|
|
|
18
44
|
results = index.search(vectors, 10)
|
|
19
45
|
print('found', results[0].shape, results[1].shape, results[2].shape)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
index_sets = usearch.SetsIndex()
|
|
49
|
+
index_sets.add(10, np.array([10, 12, 15], dtype=np.uint32))
|
|
50
|
+
index_sets.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
51
|
+
results = index_sets.search(np.array([12, 15], dtype=np.uint32), 20)
|
|
52
|
+
print(results)
|
|
@@ -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='
|
|
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',
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
#include <functional>
|
|
3
|
+
#include <stdexcept> // `std::invalid_argument`
|
|
3
4
|
#include <thread>
|
|
4
5
|
#include <vector>
|
|
5
6
|
|
|
@@ -7,8 +8,14 @@
|
|
|
7
8
|
#include <sys/auxv.h>
|
|
8
9
|
#endif
|
|
9
10
|
|
|
11
|
+
#if defined(__aarch64__)
|
|
12
|
+
#include <arm_fp16.h>
|
|
13
|
+
#endif
|
|
14
|
+
|
|
10
15
|
#include <fp16/fp16.h>
|
|
16
|
+
#if defined(USEARCH_USE_SIMD)
|
|
11
17
|
#include <simsimd/simsimd.h>
|
|
18
|
+
#endif
|
|
12
19
|
|
|
13
20
|
#include <usearch/usearch.hpp>
|
|
14
21
|
|
|
@@ -39,6 +46,27 @@ template <typename at> inline at clamp(at v, at lo, at hi) noexcept {
|
|
|
39
46
|
class i8q100_converted_t;
|
|
40
47
|
class f16_converted_t;
|
|
41
48
|
|
|
49
|
+
inline float f16_to_f32(std::uint16_t u16) noexcept {
|
|
50
|
+
#if defined(__aarch64__)
|
|
51
|
+
__fp16 f16;
|
|
52
|
+
std::memcpy(&f16, &u16, sizeof(std::uint16_t));
|
|
53
|
+
return float(f16);
|
|
54
|
+
#else
|
|
55
|
+
return fp16_ieee_to_fp32_value(u16);
|
|
56
|
+
#endif
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
inline std::uint16_t f32_to_f16(float f32) noexcept {
|
|
60
|
+
#if defined(__aarch64__)
|
|
61
|
+
__fp16 f16 = __fp16(f32);
|
|
62
|
+
std::uint16_t u16;
|
|
63
|
+
std::memcpy(&u16, &f16, sizeof(std::uint16_t));
|
|
64
|
+
return u16;
|
|
65
|
+
#else
|
|
66
|
+
return fp16_ieee_from_fp32_value(f32);
|
|
67
|
+
#endif
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
class f16_converted_t {
|
|
43
71
|
std::uint16_t uint16_{};
|
|
44
72
|
|
|
@@ -49,11 +77,11 @@ class f16_converted_t {
|
|
|
49
77
|
inline f16_converted_t(f16_converted_t const&) = default;
|
|
50
78
|
inline f16_converted_t& operator=(f16_converted_t const&) = default;
|
|
51
79
|
|
|
52
|
-
inline operator float() const noexcept { return
|
|
80
|
+
inline operator float() const noexcept { return f16_to_f32(uint16_); }
|
|
53
81
|
|
|
54
82
|
inline f16_converted_t(i8q100_converted_t) noexcept;
|
|
55
|
-
inline f16_converted_t(float v) noexcept : uint16_(
|
|
56
|
-
inline f16_converted_t(double v) noexcept : uint16_(
|
|
83
|
+
inline f16_converted_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
|
|
84
|
+
inline f16_converted_t(double v) noexcept : uint16_(f32_to_f16(v)) {}
|
|
57
85
|
|
|
58
86
|
inline f16_converted_t operator+(f16_converted_t other) const noexcept { return {float(*this) + float(other)}; }
|
|
59
87
|
inline f16_converted_t operator-(f16_converted_t other) const noexcept { return {float(*this) - float(other)}; }
|
|
@@ -69,22 +97,22 @@ class f16_converted_t {
|
|
|
69
97
|
inline f16_converted_t operator/(double other) const noexcept { return {float(*this) / other}; }
|
|
70
98
|
|
|
71
99
|
inline f16_converted_t& operator+=(float v) noexcept {
|
|
72
|
-
uint16_ =
|
|
100
|
+
uint16_ = f32_to_f16(v + f16_to_f32(uint16_));
|
|
73
101
|
return *this;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
inline f16_converted_t& operator-=(float v) noexcept {
|
|
77
|
-
uint16_ =
|
|
105
|
+
uint16_ = f32_to_f16(v - f16_to_f32(uint16_));
|
|
78
106
|
return *this;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
inline f16_converted_t& operator*=(float v) noexcept {
|
|
82
|
-
uint16_ =
|
|
110
|
+
uint16_ = f32_to_f16(v * f16_to_f32(uint16_));
|
|
83
111
|
return *this;
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
inline f16_converted_t& operator/=(float v) noexcept {
|
|
87
|
-
uint16_ =
|
|
115
|
+
uint16_ = f32_to_f16(v / f16_to_f32(uint16_));
|
|
88
116
|
return *this;
|
|
89
117
|
}
|
|
90
118
|
};
|
|
@@ -102,6 +130,10 @@ class i8q100_converted_t {
|
|
|
102
130
|
inline operator float() const noexcept { return float(int8_) / 100.f; }
|
|
103
131
|
inline operator f16_converted_t() const noexcept { return float(int8_) / 100.f; }
|
|
104
132
|
inline operator double() const noexcept { return double(int8_) / 100.0; }
|
|
133
|
+
inline operator std::int8_t() const noexcept { return int8_; }
|
|
134
|
+
inline operator std::int16_t() const noexcept { return int8_; }
|
|
135
|
+
inline operator std::int32_t() const noexcept { return int8_; }
|
|
136
|
+
inline operator std::int64_t() const noexcept { return int8_; }
|
|
105
137
|
|
|
106
138
|
inline i8q100_converted_t(float v) noexcept
|
|
107
139
|
: int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * 100.f), -100, 100)) {}
|
|
@@ -127,7 +159,7 @@ void multithreaded(std::size_t threads, std::size_t tasks, callback_at&& callbac
|
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
std::vector<std::thread> threads_pool;
|
|
130
|
-
std::size_t tasks_per_thread = tasks / threads + (tasks % threads) != 0;
|
|
162
|
+
std::size_t tasks_per_thread = (tasks / threads) + ((tasks % threads) != 0);
|
|
131
163
|
for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx) {
|
|
132
164
|
threads_pool.emplace_back([=]() {
|
|
133
165
|
for (std::size_t task_idx = thread_idx * tasks_per_thread;
|
|
@@ -198,7 +230,7 @@ inline accuracy_t accuracy_from_name(char const* name, std::size_t len) {
|
|
|
198
230
|
else if (str_equals(name, len, "i8q100"))
|
|
199
231
|
accuracy = accuracy_t::i8q100_k;
|
|
200
232
|
else
|
|
201
|
-
throw std::
|
|
233
|
+
throw std::invalid_argument("Unknown type, choose: f32, f16, f64, i8q100");
|
|
202
234
|
return accuracy;
|
|
203
235
|
}
|
|
204
236
|
|
|
@@ -206,16 +238,24 @@ template <typename index_at>
|
|
|
206
238
|
inline index_at index_from_name( //
|
|
207
239
|
char const* name, std::size_t len, std::size_t dimensions, accuracy_t accuracy, config_t const& config) {
|
|
208
240
|
|
|
209
|
-
if (str_equals(name, len, "l2_sq") || str_equals(name, len, "euclidean_sq"))
|
|
241
|
+
if (str_equals(name, len, "l2_sq") || str_equals(name, len, "euclidean_sq")) {
|
|
242
|
+
if (dimensions == 0)
|
|
243
|
+
throw std::invalid_argument("The number of dimensions must be positive");
|
|
210
244
|
return index_at::l2(dimensions, accuracy, config);
|
|
211
|
-
else if (str_equals(name, len, "ip") || str_equals(name, len, "inner") || str_equals(name, len, "dot"))
|
|
245
|
+
} else if (str_equals(name, len, "ip") || str_equals(name, len, "inner") || str_equals(name, len, "dot")) {
|
|
246
|
+
if (dimensions == 0)
|
|
247
|
+
throw std::invalid_argument("The number of dimensions must be positive");
|
|
212
248
|
return index_at::ip(dimensions, accuracy, config);
|
|
213
|
-
else if (str_equals(name, len, "cos") || str_equals(name, len, "angular"))
|
|
249
|
+
} else if (str_equals(name, len, "cos") || str_equals(name, len, "angular")) {
|
|
250
|
+
if (dimensions == 0)
|
|
251
|
+
throw std::invalid_argument("The number of dimensions must be positive");
|
|
214
252
|
return index_at::cos(dimensions, accuracy, config);
|
|
215
|
-
else if (str_equals(name, len, "haversine"))
|
|
253
|
+
} else if (str_equals(name, len, "haversine")) {
|
|
254
|
+
if (dimensions != 2 && dimensions != 0)
|
|
255
|
+
throw std::invalid_argument("The number of dimensions must be equal to two");
|
|
216
256
|
return index_at::haversine(accuracy, config);
|
|
217
|
-
else
|
|
218
|
-
throw std::
|
|
257
|
+
} else
|
|
258
|
+
throw std::invalid_argument("Unknown distance, choose: l2_sq, ip, cos, hamming, jaccard");
|
|
219
259
|
return {};
|
|
220
260
|
}
|
|
221
261
|
|
|
@@ -282,9 +322,25 @@ template <> struct cast_gt<f16_converted_t, f16_converted_t> {
|
|
|
282
322
|
bool operator()(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
|
|
283
323
|
};
|
|
284
324
|
|
|
325
|
+
struct ip_i8q100_converted_t {
|
|
326
|
+
float operator()(i8q100_converted_t const* a, i8q100_converted_t const* b, std::size_t d) const noexcept {
|
|
327
|
+
std::int64_t ab = 0;
|
|
328
|
+
#if defined(__GNUC__)
|
|
329
|
+
#pragma GCC ivdep
|
|
330
|
+
#elif defined(__clang__)
|
|
331
|
+
#pragma clang loop vectorize(enable)
|
|
332
|
+
#elif defined(_OPENMP)
|
|
333
|
+
#pragma omp simd reduction(+ : ab)
|
|
334
|
+
#endif
|
|
335
|
+
for (std::size_t i = 0; i < d; ++i)
|
|
336
|
+
ab += std::int16_t(a[i]) * std::int16_t(b[i]);
|
|
337
|
+
return 1.f - ab / 10000.f;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
285
341
|
/**
|
|
286
342
|
* @brief Oversimplified type-punned index for equidimensional floating-point
|
|
287
|
-
* vectors with automatic down-casting and
|
|
343
|
+
* vectors with automatic down-casting and hardware acceleration.
|
|
288
344
|
*/
|
|
289
345
|
template <typename label_at = std::int64_t, typename id_at = std::uint32_t> //
|
|
290
346
|
class auto_index_gt {
|
|
@@ -336,6 +392,7 @@ class auto_index_gt {
|
|
|
336
392
|
std::size_t connectivity() const noexcept { return index_->connectivity(); }
|
|
337
393
|
std::size_t size() const noexcept { return index_->size(); }
|
|
338
394
|
std::size_t capacity() const noexcept { return index_->capacity(); }
|
|
395
|
+
void clear() noexcept { return index_->clear(); }
|
|
339
396
|
|
|
340
397
|
isa_t acceleration() const noexcept { return acceleration_; }
|
|
341
398
|
std::size_t concurrency() const noexcept {
|
|
@@ -362,6 +419,12 @@ class auto_index_gt {
|
|
|
362
419
|
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
420
|
// clang-format on
|
|
364
421
|
|
|
422
|
+
static auto_index_gt udf( //
|
|
423
|
+
std::size_t dimensions, punned_stateful_metric_t metric, //
|
|
424
|
+
accuracy_t accuracy = accuracy_t::f32_k, config_t config = {}) {
|
|
425
|
+
return make(dimensions, accuracy, {metric, isa_t::auto_k}, make_casts(accuracy), config);
|
|
426
|
+
}
|
|
427
|
+
|
|
365
428
|
auto_index_gt fork() const {
|
|
366
429
|
auto_index_gt result;
|
|
367
430
|
|
|
@@ -456,6 +519,8 @@ class auto_index_gt {
|
|
|
456
519
|
}
|
|
457
520
|
|
|
458
521
|
static metric_and_meta_t ip_metric_f32(std::size_t dimensions) {
|
|
522
|
+
(void)dimensions;
|
|
523
|
+
#if defined(USEARCH_USE_SIMD)
|
|
459
524
|
#if defined(__x86_64__)
|
|
460
525
|
if (dimensions % 4 == 0)
|
|
461
526
|
return {
|
|
@@ -479,11 +544,14 @@ class auto_index_gt {
|
|
|
479
544
|
}),
|
|
480
545
|
isa_t::neon_k,
|
|
481
546
|
};
|
|
547
|
+
#endif
|
|
482
548
|
#endif
|
|
483
549
|
return {pun_metric<f32_t>(ip_gt<f32_t>{}), isa_t::auto_k};
|
|
484
550
|
}
|
|
485
551
|
|
|
486
552
|
static metric_and_meta_t ip_metric_f16(std::size_t dimensions) {
|
|
553
|
+
(void)dimensions;
|
|
554
|
+
#if defined(USEARCH_USE_SIMD)
|
|
487
555
|
#if defined(__x86_64__)
|
|
488
556
|
#elif defined(__aarch64__)
|
|
489
557
|
if (supports_arm_sve())
|
|
@@ -500,13 +568,14 @@ class auto_index_gt {
|
|
|
500
568
|
}),
|
|
501
569
|
isa_t::neon_k,
|
|
502
570
|
};
|
|
571
|
+
#endif
|
|
503
572
|
#endif
|
|
504
573
|
return {pun_metric<f16_converted_t>(ip_gt<f16_converted_t>{}), isa_t::auto_k};
|
|
505
574
|
}
|
|
506
575
|
|
|
507
576
|
static metric_and_meta_t ip_metric(std::size_t dimensions, accuracy_t accuracy) {
|
|
508
577
|
switch (accuracy) {
|
|
509
|
-
case accuracy_t::i8q100_k: return {};
|
|
578
|
+
case accuracy_t::i8q100_k: return {pun_metric<i8q100_converted_t>(ip_i8q100_converted_t{}), isa_t::auto_k};
|
|
510
579
|
case accuracy_t::f16_k: return ip_metric_f16(dimensions);
|
|
511
580
|
case accuracy_t::f32_k: return ip_metric_f32(dimensions);
|
|
512
581
|
case accuracy_t::f64_k: return {pun_metric<f64_t>(ip_gt<f64_t>{}), isa_t::auto_k};
|
package/src/bench.cpp
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
using namespace unum::usearch;
|
|
27
27
|
using namespace unum;
|
|
28
28
|
|
|
29
|
+
using vector_id_t = std::uint32_t;
|
|
30
|
+
|
|
29
31
|
template <typename element_at>
|
|
30
32
|
std::size_t offset_of(element_at const* begin, element_at const* end, element_at v) noexcept {
|
|
31
33
|
auto iterator = begin;
|
|
@@ -344,6 +346,46 @@ void run_big_or_small(dataset_at& dataset, args_t const& args, config_t config)
|
|
|
344
346
|
run_type_punned<auto_index_gt<std::size_t, neighbor_id_at>>(dataset, args, config);
|
|
345
347
|
}
|
|
346
348
|
|
|
349
|
+
void report_alternative_setups() {
|
|
350
|
+
using set_member_t = std::uint32_t;
|
|
351
|
+
using sets_index_t = index_gt<jaccard_gt<set_member_t>, std::size_t, std::uint32_t, set_member_t>;
|
|
352
|
+
set_member_t set_a[] = {10, 12, 15};
|
|
353
|
+
set_member_t set_b[] = {11, 12, 15, 16};
|
|
354
|
+
sets_index_t index;
|
|
355
|
+
index.reserve(100);
|
|
356
|
+
index.add(10, span_gt<set_member_t const>{&set_a[0], 3});
|
|
357
|
+
index.add(11, span_gt<set_member_t const>{&set_b[0], 4});
|
|
358
|
+
exit(0);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
void report_expected_losses(persisted_dataset_gt<float, vector_id_t> const& dataset) {
|
|
362
|
+
|
|
363
|
+
auto vec1 = dataset.vector(0);
|
|
364
|
+
auto vec2 = dataset.query(0);
|
|
365
|
+
std::vector<f16_converted_t> vec1f16(dataset.dimensions());
|
|
366
|
+
std::vector<f16_converted_t> vec2f16(dataset.dimensions());
|
|
367
|
+
std::transform(vec1, vec1 + dataset.dimensions(), vec1f16.data(), [](float v) { return v; });
|
|
368
|
+
std::transform(vec2, vec2 + dataset.dimensions(), vec2f16.data(), [](float v) { return v; });
|
|
369
|
+
std::vector<i8q100_converted_t> vec1i8(dataset.dimensions());
|
|
370
|
+
std::vector<i8q100_converted_t> vec2i8(dataset.dimensions());
|
|
371
|
+
std::transform(vec1, vec1 + dataset.dimensions(), vec1i8.data(), [](float v) { return v; });
|
|
372
|
+
std::transform(vec2, vec2 + dataset.dimensions(), vec2i8.data(), [](float v) { return v; });
|
|
373
|
+
|
|
374
|
+
#if 0
|
|
375
|
+
auto ip_default = ip_gt<float>{}(vec1, vec2, dataset.dimensions());
|
|
376
|
+
auto ip_sve = 1.f - simsimd_dot_f32sve(vec1, vec2, dataset.dimensions());
|
|
377
|
+
auto ip_neon = 1.f - simsimd_dot_f32x4neon(vec1, vec2, dataset.dimensions());
|
|
378
|
+
auto ip_f16 = ip_gt<f16_converted_t>{}(vec1f16.data(), vec2f16.data(), dataset.dimensions());
|
|
379
|
+
auto ip_f16sve = 1.f - simsimd_dot_f16sve((simsimd_f16_t const*)vec1f16.data(),
|
|
380
|
+
(simsimd_f16_t const*)vec2f16.data(), dataset.dimensions());
|
|
381
|
+
auto ip_f16neon = 1.f - simsimd_dot_f16x8neon((simsimd_f16_t const*)vec1f16.data(),
|
|
382
|
+
(simsimd_f16_t const*)vec2f16.data(), dataset.dimensions());
|
|
383
|
+
auto ip_i8 = ip_i8q100_converted_t{}(vec1i8.data(), vec2i8.data(), dataset.dimensions());
|
|
384
|
+
|
|
385
|
+
exit(0);
|
|
386
|
+
#endif
|
|
387
|
+
}
|
|
388
|
+
|
|
347
389
|
int main(int argc, char** argv) {
|
|
348
390
|
|
|
349
391
|
// Print backtrace if something goes wrong.
|
|
@@ -393,8 +435,6 @@ int main(int argc, char** argv) {
|
|
|
393
435
|
std::printf("-- Query vectors path: %s\n", args.path_queries.c_str());
|
|
394
436
|
std::printf("-- Ground truth neighbors path: %s\n", args.path_neighbors.c_str());
|
|
395
437
|
|
|
396
|
-
using vector_id_t = std::uint32_t;
|
|
397
|
-
|
|
398
438
|
persisted_dataset_gt<float, vector_id_t> dataset{
|
|
399
439
|
args.path_vectors.c_str(),
|
|
400
440
|
args.path_queries.c_str(),
|
|
@@ -405,6 +445,9 @@ int main(int argc, char** argv) {
|
|
|
405
445
|
std::printf("-- Queries count: %zu\n", dataset.queries_count());
|
|
406
446
|
std::printf("-- Neighbors per query: %zu\n", dataset.neighborhood_size());
|
|
407
447
|
|
|
448
|
+
report_alternative_setups();
|
|
449
|
+
report_expected_losses(dataset);
|
|
450
|
+
|
|
408
451
|
config_t config;
|
|
409
452
|
config.connectivity = args.connectivity;
|
|
410
453
|
config.expansion_add = args.expansion_add;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
if(NOT WOLFRAM_PATH)
|
|
3
|
+
set(WOLFRAM_PATH "/usr/local/Wolfram/Mathematica")
|
|
4
|
+
endif()
|
|
5
|
+
|
|
6
|
+
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${WOLFRAM_PATH}/SystemFiles/Links/WSTP/DeveloperKit/Linux-x86-64/CompilerAdditions/")
|
|
7
|
+
include(WSTP)
|
|
8
|
+
include_directories("${WOLFRAM_PATH}/SystemFiles/IncludeFiles/C/")
|
|
9
|
+
link_directories("${WOLFRAM_PATH}/SystemFiles/Libraries/Linux-x86-64/")
|
|
10
|
+
|
|
11
|
+
add_library(usearchWFM SHARED lib.cpp)
|
|
12
|
+
|
|
13
|
+
# Link against the Wolfram LibraryLink library
|
|
14
|
+
target_link_libraries(usearchWFM WSTP::STATIC_LIBRARY)
|
|
15
|
+
|
|
16
|
+
# Set the output name of the library to be compatible with Wolfram Language
|
|
17
|
+
set_target_properties(usearchWFM PROPERTIES PREFIX "" OUTPUT_NAME "usearchWFM")
|