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.
@@ -145,6 +145,7 @@
145
145
  "SLOC",
146
146
  "sorensen",
147
147
  "tanimoto",
148
+ "tqdm",
148
149
  "uninitialize",
149
150
  "unumusearch",
150
151
  "usearch",
package/CITATION.cff CHANGED
@@ -5,7 +5,7 @@ authors:
5
5
  given-names: "Ash"
6
6
  orcid: "https://orcid.org/0000-0002-4882-1815"
7
7
  title: "USearch by Unum Cloud"
8
- version: 2.5.1
8
+ version: 2.6.0
9
9
  doi: 10.5281/zenodo.7949416
10
10
  date-released: 2022-06-12
11
11
  url: "https://github.com/unum-cloud/usearch"
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "2.5.1"
3
+ version = "2.6.0"
4
4
  authors = ["Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -392,7 +392,7 @@ doi = {10.5281/zenodo.7949416},
392
392
  author = {Vardanian, Ash},
393
393
  title = {{USearch by Unum Cloud}},
394
394
  url = {https://github.com/unum-cloud/usearch},
395
- version = {2.5.1},
395
+ version = {2.6.0},
396
396
  year = {2022},
397
397
  month = jun,
398
398
  }
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.5.1'
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"
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <Project>
3
3
  <PropertyGroup>
4
- <Version Condition="'$(Version)' == ''">2.5.1</Version>
4
+ <Version Condition="'$(Version)' == ''">2.6.0</Version>
5
5
 
6
6
  <Authors>Unum</Authors>
7
7
  <Company>Unum</Company>
@@ -10,8 +10,8 @@
10
10
  #define UNUM_USEARCH_HPP
11
11
 
12
12
  #define USEARCH_VERSION_MAJOR 2
13
- #define USEARCH_VERSION_MINOR 5
14
- #define USEARCH_VERSION_PATCH 1
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",
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
- std::vector<dense_key_and_distance_t> keys_and_distances(tasks_count);
547
-
548
- executor_default_t{threads}.dynamic(tasks_count, [&](std::size_t thread_idx, std::size_t task_idx) {
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
- keys_and_distances[task_idx] = {static_cast<u32_t>(query_idx), static_cast<f32_t>(distance)};
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
- ++processed;
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
- executor_default_t{threads}.fixed(queries_count, [&](std::size_t, std::size_t query_idx) {
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 == np.arange(rows))
64
+ assert np.all(top_matches == keys)
64
65
 
65
- matches: Matches = search(original, original[0], min(10, rows), exact=True)
66
+ matches: Matches = search(original, original[-1], min(10, rows), exact=True)
66
67
  top_match = int(matches.keys[0])
67
- assert top_match == 0
68
+ assert top_match == keys[-1]
68
69
 
69
70
 
70
71
  def test_matches_creation_and_methods():
@@ -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 distil_batch(
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
- pbar = tqdm(
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
- pbar.update(processed - pbar.n)
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
- pbar.close()
202
+ progress_bar.close()
203
203
  else:
204
204
  tuple_ = compiled_callable(vectors, **kwargs)
205
205
 
206
- return distil_batch(BatchMatches(*tuple_))
206
+ return distill_batch(BatchMatches(*tuple_))
207
207
 
208
208
 
209
209
  def _add_to_compiled(
package/wasmer.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name="unum/usearch"
3
- version="2.5.1"
3
+ version="2.6.0"
4
4
  description="Smaller & Faster Single-File Vector Search Engine from Unum"
5
5
  license="Apache-2.0"
6
6
  readme="README.md"