usearch 2.5.1 → 2.6.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 +1 -0
- package/CITATION.cff +1 -1
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/conanfile.py +1 -1
- package/csharp/nuget/nuget-package.props +1 -1
- package/include/usearch/index.hpp +2 -2
- package/include/usearch/index_plugins.hpp +1 -1
- package/package.json +1 -1
- package/python/lib.cpp +37 -17
- package/python/scripts/bench_exact.py +44 -0
- package/python/scripts/test_tooling.py +5 -4
- package/python/usearch/index.py +5 -5
- package/wasmer.toml +1 -1
package/.vscode/settings.json
CHANGED
package/CITATION.cff
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
package/conanfile.py
CHANGED
|
@@ -7,7 +7,7 @@ required_conan_version = ">=1.53.0"
|
|
|
7
7
|
class USearchConan(ConanFile):
|
|
8
8
|
|
|
9
9
|
name = "usearch"
|
|
10
|
-
version = '2.
|
|
10
|
+
version = '2.6.0'
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
description = "Smaller & Faster Single-File Vector Search Engine from Unum"
|
|
13
13
|
homepage = "https://github.com/unum-cloud/usearch"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
#define UNUM_USEARCH_HPP
|
|
11
11
|
|
|
12
12
|
#define USEARCH_VERSION_MAJOR 2
|
|
13
|
-
#define USEARCH_VERSION_MINOR
|
|
14
|
-
#define USEARCH_VERSION_PATCH
|
|
13
|
+
#define USEARCH_VERSION_MINOR 6
|
|
14
|
+
#define USEARCH_VERSION_PATCH 0
|
|
15
15
|
|
|
16
16
|
// Inferring C++ version
|
|
17
17
|
// https://stackoverflow.com/a/61552074
|
|
@@ -1602,7 +1602,7 @@ class flat_hash_multi_set_gt {
|
|
|
1602
1602
|
auto slot_pointer = bucket_pointer + sizeof(bucket_header_t) + sizeof(element_t) * in_bucket_index;
|
|
1603
1603
|
return {
|
|
1604
1604
|
*reinterpret_cast<bucket_header_t*>(bucket_pointer),
|
|
1605
|
-
std::uint64_t(1ull) << in_bucket_index,
|
|
1605
|
+
static_cast<std::uint64_t>(1ull) << in_bucket_index,
|
|
1606
1606
|
*reinterpret_cast<element_t*>(slot_pointer),
|
|
1607
1607
|
};
|
|
1608
1608
|
}
|
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -532,49 +532,69 @@ static void search_typed_brute_force( //
|
|
|
532
532
|
threads = std::thread::hardware_concurrency();
|
|
533
533
|
|
|
534
534
|
std::size_t tasks_count = static_cast<std::size_t>(dataset_count * queries_count);
|
|
535
|
+
executor_default_t executor{threads};
|
|
535
536
|
|
|
536
537
|
// Progress status
|
|
537
538
|
progress_t progress_{progress};
|
|
538
539
|
std::atomic<std::size_t> processed{0};
|
|
539
540
|
|
|
540
|
-
// Allocate temporary memory to store the distance matrix
|
|
541
|
-
// Previous version didn't need temporary memory, but the performance was much lower
|
|
542
541
|
struct dense_key_and_distance_t {
|
|
543
542
|
u32_t offset;
|
|
544
543
|
f32_t distance;
|
|
545
544
|
};
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
std::size_t dataset_idx = task_idx / queries_count;
|
|
550
|
-
std::size_t query_idx = task_idx % queries_count;
|
|
545
|
+
auto less = [](dense_key_and_distance_t const& a, dense_key_and_distance_t const& b) {
|
|
546
|
+
return a.distance < b.distance;
|
|
547
|
+
};
|
|
551
548
|
|
|
549
|
+
// Allocate temporary memory to store the distance matrix
|
|
550
|
+
// Previous version didn't need temporary memory, but the performance was much lower.
|
|
551
|
+
// In the new design we keep two buffers - original and transposed, as in-place transpositions
|
|
552
|
+
// of non-rectangular matrixes is expensive.
|
|
553
|
+
std::vector<dense_key_and_distance_t> keys_and_distances(tasks_count * 2);
|
|
554
|
+
dense_key_and_distance_t* keys_and_distances_per_dataset = keys_and_distances.data();
|
|
555
|
+
dense_key_and_distance_t* keys_and_distances_per_query = keys_and_distances_per_dataset + tasks_count;
|
|
556
|
+
executor.dynamic(dataset_count, [&](std::size_t thread_idx, std::size_t dataset_idx) {
|
|
552
557
|
byte_t const* dataset = dataset_data + dataset_idx * dataset_info.strides[0];
|
|
558
|
+
for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
|
|
553
559
|
byte_t const* query = queries_data + query_idx * queries_info.strides[0];
|
|
554
560
|
distance_t distance = metric(dataset, query);
|
|
555
|
-
|
|
556
|
-
|
|
561
|
+
std::size_t task_idx = dataset_idx * queries_count + query_idx;
|
|
562
|
+
keys_and_distances_per_dataset[task_idx].offset = static_cast<u32_t>(dataset_idx);
|
|
563
|
+
keys_and_distances_per_dataset[task_idx].distance = static_cast<f32_t>(distance);
|
|
564
|
+
}
|
|
557
565
|
|
|
558
566
|
// We don't want to check for signals from multiple threads
|
|
559
|
-
|
|
567
|
+
processed += queries_count;
|
|
560
568
|
if (thread_idx == 0)
|
|
561
569
|
if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), tasks_count))
|
|
562
570
|
return false;
|
|
563
571
|
return true;
|
|
564
572
|
});
|
|
573
|
+
if (processed.load() != tasks_count)
|
|
574
|
+
return;
|
|
575
|
+
|
|
576
|
+
// Transpose in a single thread to avoid contention writing into the same memory buffers
|
|
577
|
+
for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
|
|
578
|
+
for (std::size_t dataset_idx = 0; dataset_idx != dataset_count; ++dataset_idx) {
|
|
579
|
+
std::size_t from_idx = queries_count * dataset_idx + query_idx;
|
|
580
|
+
std::size_t to_idx = dataset_count * query_idx + dataset_idx;
|
|
581
|
+
keys_and_distances_per_query[to_idx] = keys_and_distances_per_dataset[from_idx];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
565
584
|
|
|
566
585
|
// Partial-sort every query result
|
|
567
|
-
|
|
568
|
-
auto start = keys_and_distances.data() + query_idx * dataset_count;
|
|
569
|
-
std::partial_sort(start, start + wanted, start + dataset_count,
|
|
570
|
-
[](dense_key_and_distance_t const& a, dense_key_and_distance_t const& b) {
|
|
571
|
-
return a.distance < b.distance;
|
|
572
|
-
});
|
|
573
|
-
|
|
586
|
+
executor.fixed(queries_count, [&](std::size_t, std::size_t query_idx) {
|
|
574
587
|
dense_key_t* keys = &keys_py2d(query_idx, 0);
|
|
575
588
|
distance_t* distances = &distances_py2d(query_idx, 0);
|
|
589
|
+
auto start = keys_and_distances_per_query + dataset_count * query_idx;
|
|
590
|
+
if (wanted > 1) {
|
|
591
|
+
std::partial_sort(start, start + wanted, start + dataset_count, less);
|
|
576
592
|
for (std::size_t i = 0; i != wanted; ++i)
|
|
577
593
|
keys[i] = static_cast<dense_key_t>(start[i].offset), distances[i] = start[i].distance;
|
|
594
|
+
} else {
|
|
595
|
+
auto max_it = std::max_element(start, start + dataset_count, less);
|
|
596
|
+
keys[0] = static_cast<dense_key_t>(max_it->offset), distances[0] = max_it->distance;
|
|
597
|
+
}
|
|
578
598
|
});
|
|
579
599
|
|
|
580
600
|
// At the end report the latest numbers, because the reporter thread may be finished earlier
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
|
|
3
|
+
from usearch.index import search, MetricKind, ScalarKind
|
|
4
|
+
from usearch.compiled import hardware_acceleration
|
|
5
|
+
from faiss import IndexFlatL2, knn
|
|
6
|
+
import numpy as np
|
|
7
|
+
import fire
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(
|
|
11
|
+
n: int = 10**5,
|
|
12
|
+
q: int = 10,
|
|
13
|
+
k: int = 100,
|
|
14
|
+
ndim: int = 256,
|
|
15
|
+
half: bool = False,
|
|
16
|
+
):
|
|
17
|
+
usearch_dtype = ScalarKind.F16 if half else ScalarKind.F32
|
|
18
|
+
acceleration = hardware_acceleration(
|
|
19
|
+
dtype=usearch_dtype,
|
|
20
|
+
ndim=ndim,
|
|
21
|
+
metric_kind=MetricKind.L2sq,
|
|
22
|
+
)
|
|
23
|
+
print("Hardware acceleration in USearch: ", acceleration)
|
|
24
|
+
|
|
25
|
+
x = np.random.random((n, ndim))
|
|
26
|
+
x = x.astype(np.float16) if half else x.astype(np.float32)
|
|
27
|
+
|
|
28
|
+
start = time()
|
|
29
|
+
_ = search(
|
|
30
|
+
x,
|
|
31
|
+
x[:q],
|
|
32
|
+
k,
|
|
33
|
+
MetricKind.L2sq,
|
|
34
|
+
exact=True,
|
|
35
|
+
).keys
|
|
36
|
+
print("USearch: ", time() - start)
|
|
37
|
+
|
|
38
|
+
start = time()
|
|
39
|
+
_ = knn(x, x[:q], k)[1]
|
|
40
|
+
print("FAISS: ", time() - start)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
fire.Fire(run)
|
|
@@ -11,7 +11,7 @@ from usearch.index import Match, Matches, BatchMatches, Index, Indexes
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
dimensions = [3, 97, 256]
|
|
14
|
-
batch_sizes = [1, 77]
|
|
14
|
+
batch_sizes = [1, 77, 100]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@pytest.mark.parametrize("rows", batch_sizes)
|
|
@@ -56,15 +56,16 @@ def test_exact_search(rows: int, cols: int):
|
|
|
56
56
|
:param int cols: The number of columns in the matrix.
|
|
57
57
|
"""
|
|
58
58
|
original = np.random.rand(rows, cols)
|
|
59
|
+
keys = np.arange(rows)
|
|
59
60
|
matches: BatchMatches = search(original, original, min(10, rows), exact=True)
|
|
60
61
|
top_matches = (
|
|
61
62
|
[int(m.keys[0]) for m in matches] if rows > 1 else int(matches.keys[0])
|
|
62
63
|
)
|
|
63
|
-
assert np.all(top_matches ==
|
|
64
|
+
assert np.all(top_matches == keys)
|
|
64
65
|
|
|
65
|
-
matches: Matches = search(original, original[
|
|
66
|
+
matches: Matches = search(original, original[-1], min(10, rows), exact=True)
|
|
66
67
|
top_match = int(matches.keys[0])
|
|
67
|
-
assert top_match ==
|
|
68
|
+
assert top_match == keys[-1]
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def test_matches_creation_and_methods():
|
package/python/usearch/index.py
CHANGED
|
@@ -180,7 +180,7 @@ def _search_in_compiled(
|
|
|
180
180
|
vectors = vectors.reshape(1, len(vectors))
|
|
181
181
|
count_vectors = vectors.shape[0]
|
|
182
182
|
|
|
183
|
-
def
|
|
183
|
+
def distill_batch(
|
|
184
184
|
batch_matches: BatchMatches,
|
|
185
185
|
) -> Union[BatchMatches, Matches]:
|
|
186
186
|
return batch_matches[0] if count_vectors == 1 else batch_matches
|
|
@@ -188,22 +188,22 @@ def _search_in_compiled(
|
|
|
188
188
|
# Create progress bar if needed
|
|
189
189
|
if log:
|
|
190
190
|
name = log if isinstance(log, str) else "Search"
|
|
191
|
-
|
|
191
|
+
progress_bar = tqdm(
|
|
192
192
|
desc=name,
|
|
193
193
|
total=count_vectors,
|
|
194
194
|
unit="vector",
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
def update_progress_bar(processed: int, total: int) -> bool:
|
|
198
|
-
|
|
198
|
+
progress_bar.update(processed - progress_bar.n)
|
|
199
199
|
return progress if progress else True
|
|
200
200
|
|
|
201
201
|
tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
|
|
202
|
-
|
|
202
|
+
progress_bar.close()
|
|
203
203
|
else:
|
|
204
204
|
tuple_ = compiled_callable(vectors, **kwargs)
|
|
205
205
|
|
|
206
|
-
return
|
|
206
|
+
return distill_batch(BatchMatches(*tuple_))
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def _add_to_compiled(
|