usearch 0.1.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.
Files changed (90) hide show
  1. package/.clang-format +10 -0
  2. package/.github/workflows/package.json +82 -0
  3. package/.github/workflows/release.yml +111 -0
  4. package/.gitmodules +6 -0
  5. package/.vscode/launch.json +116 -0
  6. package/.vscode/settings.json +109 -0
  7. package/.vscode/tasks.json +35 -0
  8. package/CMakeLists.txt +84 -0
  9. package/CMakeListsInstructuons.md +109 -0
  10. package/README.md +69 -0
  11. package/VERSION +1 -0
  12. package/assets/unum.png +0 -0
  13. package/binding.gyp +28 -0
  14. package/build.rs +14 -0
  15. package/cargo.toml +15 -0
  16. package/cmake/clipp.cmake +8 -0
  17. package/cmake/jemalloc.cmake +80 -0
  18. package/docs/Makefile +20 -0
  19. package/docs/_static/custom.css +26 -0
  20. package/docs/_static/custom.js +7 -0
  21. package/docs/conf.py +48 -0
  22. package/docs/index.rst +10 -0
  23. package/fp16/CMakeLists.txt +176 -0
  24. package/fp16/LICENSE +11 -0
  25. package/fp16/README.md +20 -0
  26. package/fp16/bench/alt-element.cc +89 -0
  27. package/fp16/bench/from-alt-array.cc +175 -0
  28. package/fp16/bench/from-ieee-array.cc +399 -0
  29. package/fp16/bench/ieee-element.cc +272 -0
  30. package/fp16/bench/to-alt-array.cc +91 -0
  31. package/fp16/bench/to-ieee-array.cc +303 -0
  32. package/fp16/cmake/DownloadGoogleBenchmark.cmake +15 -0
  33. package/fp16/cmake/DownloadGoogleTest.cmake +15 -0
  34. package/fp16/cmake/DownloadPSimd.cmake +15 -0
  35. package/fp16/configure.py +65 -0
  36. package/fp16/confu.yaml +8 -0
  37. package/fp16/include/fp16/__init__.py +0 -0
  38. package/fp16/include/fp16/avx.py +50 -0
  39. package/fp16/include/fp16/avx2.py +53 -0
  40. package/fp16/include/fp16/bitcasts.h +92 -0
  41. package/fp16/include/fp16/fp16.h +451 -0
  42. package/fp16/include/fp16/psimd.h +131 -0
  43. package/fp16/include/fp16.h +11 -0
  44. package/fp16/jni/Android.mk +6 -0
  45. package/fp16/jni/Application.mk +4 -0
  46. package/fp16/test/alt-from-fp32-value.cc +498 -0
  47. package/fp16/test/alt-to-fp32-bits.cc +263 -0
  48. package/fp16/test/alt-to-fp32-psimd.cc +145 -0
  49. package/fp16/test/alt-to-fp32-value.cc +348 -0
  50. package/fp16/test/alt-to-fp32x2-psimd.cc +245 -0
  51. package/fp16/test/bitcasts.cc +56 -0
  52. package/fp16/test/ieee-from-fp32-value.cc +523 -0
  53. package/fp16/test/ieee-to-fp32-bits.cc +333 -0
  54. package/fp16/test/ieee-to-fp32-psimd.cc +312 -0
  55. package/fp16/test/ieee-to-fp32-value.cc +420 -0
  56. package/fp16/test/ieee-to-fp32x2-psimd.cc +541 -0
  57. package/fp16/test/peachpy/alt-xmm-to-fp32-xmm-avx.cc +96 -0
  58. package/fp16/test/peachpy/alt-xmm-to-fp32-ymm-avx2.cc +96 -0
  59. package/fp16/test/peachpy/stubs.py +38 -0
  60. package/fp16/test/tables.cc +530 -0
  61. package/fp16/test/tables.h +13 -0
  62. package/fp16/third-party/THHalf.h +92 -0
  63. package/fp16/third-party/eigen-half.h +170 -0
  64. package/fp16/third-party/float16-compressor.h +80 -0
  65. package/fp16/third-party/half.hpp +2916 -0
  66. package/fp16/third-party/npy-halffloat.h +328 -0
  67. package/include/usearch/usearch.hpp +1345 -0
  68. package/package.json +19 -0
  69. package/pyproject.toml +33 -0
  70. package/setup.py +63 -0
  71. package/simsimd/.clang-format +10 -0
  72. package/simsimd/.vscode/launch.json +24 -0
  73. package/simsimd/.vscode/settings.json +73 -0
  74. package/simsimd/.vscode/tasks.json +17 -0
  75. package/simsimd/CMakeLists.txt +82 -0
  76. package/simsimd/LICENSE +201 -0
  77. package/simsimd/README.md +73 -0
  78. package/simsimd/bench.cxx +82 -0
  79. package/simsimd/include/simsimd/simsimd.h +322 -0
  80. package/simsimd/include/simsimd/simsimd.hpp +443 -0
  81. package/src/advanced.hpp +489 -0
  82. package/src/bench.cpp +415 -0
  83. package/src/bench.py +89 -0
  84. package/src/javascript.cpp +198 -0
  85. package/src/main.rs +42 -0
  86. package/src/python.cpp +226 -0
  87. package/src/rust.cpp +23 -0
  88. package/src/rust.h +22 -0
  89. package/src/test.js +17 -0
  90. package/src/test.py +20 -0
package/src/bench.cpp ADDED
@@ -0,0 +1,415 @@
1
+ /**
2
+ * @brief A benchmark for the construction speed of the USearch index
3
+ * and the resulting accuracy (recall) of the Approximate Nearest Neighbors
4
+ * Search queries.
5
+ */
6
+ #include <execinfo.h>
7
+ #include <fcntl.h> // `open`
8
+ #include <stdlib.h> // `getenv`
9
+ #include <sys/mman.h> // `mmap`
10
+ #include <sys/stat.h> // `stat`
11
+ #include <unistd.h>
12
+
13
+ #include <csignal>
14
+ #include <cstdio>
15
+ #include <iostream> // `std::cerr`
16
+ #include <numeric> // `std::iota`
17
+ #include <stdexcept> // `std::invalid_argument`
18
+ #include <thread> // `std::thread::hardware_concurrency()`
19
+ #include <variant> // `std::monostate`
20
+
21
+ #include <clipp.h> // Command Line Interface
22
+ #include <omp.h> // `omp_get_num_threads()`
23
+
24
+ #include "advanced.hpp"
25
+
26
+ using namespace unum::usearch;
27
+ using namespace unum;
28
+
29
+ template <typename element_at>
30
+ std::size_t offset_of(element_at const* begin, element_at const* end, element_at v) noexcept {
31
+ auto iterator = begin;
32
+ for (; iterator != end; ++iterator)
33
+ if (*iterator == v)
34
+ break;
35
+ return iterator - begin;
36
+ }
37
+
38
+ template <typename element_at> bool contains(element_at const* begin, element_at const* end, element_at v) noexcept {
39
+ return offset_of(begin, end, v) != static_cast<std::size_t>(end - begin);
40
+ }
41
+
42
+ template <typename scalar_at> //
43
+ struct alignas(32) persisted_matrix_gt {
44
+ using scalar_t = scalar_at;
45
+ std::uint8_t const* raw_handle{};
46
+ std::size_t raw_length{};
47
+ std::uint32_t rows{};
48
+ std::uint32_t cols{};
49
+ scalar_t const* scalars{};
50
+
51
+ persisted_matrix_gt(char const* path) noexcept(false) {
52
+ if (!path || !std::strlen(path))
53
+ throw std::invalid_argument("The file path is empty");
54
+ auto file_descriptor = open(path, O_RDONLY | O_CLOEXEC);
55
+ if (file_descriptor == -1)
56
+ throw std::invalid_argument("Couldn't open provided file path");
57
+ struct stat stat_vectors;
58
+ if (fstat(file_descriptor, &stat_vectors) == -1)
59
+ throw std::invalid_argument("Couldn't obtain file stats");
60
+ raw_length = stat_vectors.st_size;
61
+ raw_handle = (std::uint8_t*)mmap(NULL, raw_length, PROT_READ, MAP_PRIVATE, file_descriptor, 0);
62
+ if (raw_handle == nullptr)
63
+ throw std::invalid_argument("Couldn't memory-map the file");
64
+ std::memcpy(&rows, raw_handle, sizeof(rows));
65
+ std::memcpy(&cols, raw_handle + sizeof(rows), sizeof(cols));
66
+ scalars = (scalar_t*)(raw_handle + sizeof(rows) + sizeof(cols));
67
+ }
68
+
69
+ ~persisted_matrix_gt() {
70
+ if (raw_handle != nullptr)
71
+ munmap((void*)raw_handle, raw_length);
72
+ }
73
+
74
+ scalar_t const* row(std::size_t i) const noexcept { return scalars + i * cols; }
75
+ std::size_t row_size_bytes() const noexcept { return cols * sizeof(scalar_t); }
76
+ std::size_t size_bytes() const noexcept { return rows * row_size_bytes(); }
77
+ };
78
+
79
+ template <typename scalar_at> //
80
+ struct vectors_view_gt {
81
+ using scalar_t = scalar_at;
82
+
83
+ scalar_t const* begin_{};
84
+ std::size_t count_{};
85
+ std::size_t stride_{};
86
+
87
+ std::size_t size() const noexcept { return count_; }
88
+ scalar_t const* at(std::size_t i) const noexcept { return begin_ + i * stride_; }
89
+ };
90
+
91
+ template <typename scalar_at, typename vector_id_at> //
92
+ struct persisted_dataset_gt {
93
+ using scalar_t = scalar_at;
94
+ using vector_id_t = vector_id_at;
95
+ persisted_matrix_gt<scalar_t> vectors_;
96
+ persisted_matrix_gt<scalar_t> queries_;
97
+ persisted_matrix_gt<vector_id_t> neighborhoods_;
98
+
99
+ persisted_dataset_gt(char const* path_vectors, char const* path_queries, char const* path_neighbors) noexcept(false)
100
+ : vectors_(path_vectors), queries_(path_queries), neighborhoods_(path_neighbors) {
101
+ if (vectors_.cols != queries_.cols)
102
+ throw std::invalid_argument("Contents and queries have different dimensionality");
103
+ if (queries_.rows != neighborhoods_.rows)
104
+ throw std::invalid_argument("Number of ground-truth neighborhoods doesn't match number of queries");
105
+ }
106
+
107
+ std::size_t dimensions() const noexcept { return vectors_.cols; }
108
+ std::size_t vectors_count() const noexcept { return vectors_.rows; }
109
+ std::size_t queries_count() const noexcept { return queries_.rows; }
110
+ std::size_t neighborhood_size() const noexcept { return neighborhoods_.cols; }
111
+ scalar_t const* vector(std::size_t i) const noexcept { return vectors_.row(i); }
112
+ scalar_t const* query(std::size_t i) const noexcept { return queries_.row(i); }
113
+ vector_id_t const* neighborhood(std::size_t i) const noexcept { return neighborhoods_.row(i); }
114
+
115
+ vectors_view_gt<scalar_t> vectors_view() const noexcept { return {vector(0), vectors_count(), dimensions()}; }
116
+ };
117
+
118
+ template <typename scalar_at, typename vector_id_at> //
119
+ struct in_memory_dataset_gt {
120
+ using scalar_t = scalar_at;
121
+ using vector_id_t = vector_id_at;
122
+
123
+ std::vector<scalar_t> vectors_{};
124
+ std::vector<scalar_t> queries_{};
125
+ std::vector<vector_id_t> neighborhoods_{};
126
+ std::size_t dimensions_{};
127
+ std::size_t vectors_count_{};
128
+ std::size_t neighborhood_size_{};
129
+ std::size_t queries_count_{};
130
+
131
+ in_memory_dataset_gt( //
132
+ std::size_t dimensions, std::size_t vectors_count, std::size_t queries_count,
133
+ std::size_t neighborhood_size) noexcept(false)
134
+ : vectors_(vectors_count * dimensions), queries_(queries_count * dimensions),
135
+ neighborhoods_(queries_count * neighborhood_size), dimensions_(dimensions), vectors_count_(vectors_count),
136
+ queries_count_(queries_count), neighborhood_size_(neighborhood_size) {}
137
+
138
+ std::size_t dimensions() const noexcept { return dimensions_; }
139
+ std::size_t vectors_count() const noexcept { return vectors_count_; }
140
+ std::size_t queries_count() const noexcept { return vectors_count(); }
141
+ std::size_t neighborhood_size() const noexcept { return 1; }
142
+ scalar_t const* vector(std::size_t i) const noexcept { return vectors_.data() + i * dimensions_; }
143
+ scalar_t const* query(std::size_t i) const noexcept { return queries_.data() + i * dimensions_; }
144
+ vector_id_t const* neighborhood(std::size_t i) const noexcept {
145
+ return neighborhoods_.data() + i * neighborhood_size_;
146
+ }
147
+
148
+ scalar_t* vector(std::size_t i) noexcept { return vectors_.data() + i * dimensions_; }
149
+ scalar_t* query(std::size_t i) noexcept { return queries_.data() + i * dimensions_; }
150
+ vector_id_t* neighborhood(std::size_t i) noexcept { return neighborhoods_.data() + i * neighborhood_size_; }
151
+
152
+ vectors_view_gt<scalar_t> vectors_view() const noexcept { return {vector(0), vectors_count(), dimensions()}; }
153
+ };
154
+
155
+ char const* getenv_or(char const* name, char const* default_) { return getenv(name) ? getenv(name) : default_; }
156
+
157
+ template <typename index_at, typename vector_id_at, typename real_at>
158
+ void index_many(index_at& native, std::size_t n, vector_id_at const* ids, real_at const* vectors, std::size_t dims) {
159
+
160
+ using span_t = span_gt<float const>;
161
+
162
+ #pragma omp parallel for
163
+ for (std::size_t i = 0; i < n; ++i) {
164
+ native.add(ids[i], span_t{vectors + dims * i, dims}, omp_get_thread_num(), false);
165
+ if (((i + 1) % 100000) == 0)
166
+ std::printf("-- added point # %zu\n", i + 1);
167
+ }
168
+ }
169
+
170
+ template <typename index_at, typename vector_id_at, typename real_at>
171
+ void search_many(index_at& native, std::size_t n, real_at const* vectors, std::size_t dims, std::size_t wanted,
172
+ vector_id_at* ids, real_at* distances) {
173
+
174
+ using span_t = span_gt<float const>;
175
+
176
+ #pragma omp parallel for
177
+ for (std::size_t i = 0; i < n; ++i) {
178
+ std::size_t found = native.search( //
179
+ span_t{vectors + dims * i, dims}, wanted, //
180
+ ids + wanted * i, distances + wanted * i, //
181
+ omp_get_thread_num());
182
+ }
183
+ }
184
+
185
+ template <typename callback_at> std::size_t nanoseconds(callback_at&& callback) {
186
+ auto t_start = std::chrono::high_resolution_clock::now();
187
+ callback();
188
+ auto t_end = std::chrono::high_resolution_clock::now();
189
+ auto dt_add = std::chrono::duration_cast<std::chrono::nanoseconds>(t_end - t_start).count();
190
+ return dt_add;
191
+ }
192
+
193
+ template <typename dataset_at, typename index_at> //
194
+ static void single_shot(dataset_at& dataset, index_at& index, bool construct = true) {
195
+ using label_t = typename index_at::label_t;
196
+ using distance_t = typename index_at::distance_t;
197
+
198
+ if (construct) {
199
+ // Perform insertions, evaluate speed
200
+ std::vector<label_t> ids(dataset.vectors_count());
201
+ std::iota(ids.begin(), ids.end(), 0);
202
+ auto dt_add = nanoseconds(
203
+ [&] { index_many(index, dataset.vectors_count(), ids.data(), dataset.vector(0), dataset.dimensions()); });
204
+ std::printf("- Added %.2f vectors/sec\n", dataset.vectors_count() * 1e9 / dt_add);
205
+ }
206
+
207
+ // Perform search, evaluate speed
208
+ std::vector<label_t> found_neighbors(dataset.queries_count() * dataset.neighborhood_size());
209
+ std::vector<distance_t> found_distances(dataset.queries_count() * dataset.neighborhood_size());
210
+ std::size_t executed_search_queries = 0;
211
+
212
+ auto dt_search = nanoseconds([&] {
213
+ while (executed_search_queries < dataset.vectors_count()) {
214
+ search_many(index, dataset.queries_count(), dataset.query(0), dataset.dimensions(),
215
+ dataset.neighborhood_size(), found_neighbors.data(), found_distances.data());
216
+ executed_search_queries += dataset.queries_count();
217
+ std::printf("- Searched %zu vectors\n", executed_search_queries);
218
+ }
219
+ });
220
+ std::printf("- Searched %.2f vectors/sec\n", executed_search_queries * 1e9 / dt_search);
221
+
222
+ // Evaluate quality
223
+ std::size_t recall_at_1 = 0, recall_full = 0;
224
+ for (std::size_t i = 0; i != dataset.queries_count(); ++i) {
225
+ auto expected = dataset.neighborhood(i);
226
+ auto received = found_neighbors.data() + i * dataset.neighborhood_size();
227
+ recall_at_1 += expected[0] == received[0];
228
+ recall_full += contains(received, received + dataset.neighborhood_size(), label_t{expected[0]});
229
+ }
230
+ std::printf("- Recall@1 %.2f %%\n", recall_at_1 * 100.f / dataset.queries_count());
231
+ std::printf("- Recall %.2f %%\n", recall_full * 100.f / dataset.queries_count());
232
+ }
233
+
234
+ void handler(int sig) {
235
+ void* array[10];
236
+ size_t size;
237
+
238
+ // get void*'s for all entries on the stack
239
+ size = backtrace(array, 10);
240
+
241
+ // print out all the frames to stderr
242
+ fprintf(stderr, "Error: signal %d:\n", sig);
243
+ backtrace_symbols_fd(array, size, STDERR_FILENO);
244
+ exit(1);
245
+ }
246
+
247
+ bool ends_with(std::string const& value, std::string const& ending) {
248
+ if (ending.size() > value.size())
249
+ return false;
250
+ return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
251
+ }
252
+
253
+ struct args_t {
254
+ std::string path_vectors;
255
+ std::string path_queries;
256
+ std::string path_neighbors;
257
+
258
+ std::size_t connectivity = config_t::connectivity_default_k;
259
+ std::size_t expansion_add = config_t::expansion_construction_default_k;
260
+ std::size_t expansion_search = config_t::expansion_search_default_k;
261
+ std::size_t threads = std::thread::hardware_concurrency();
262
+
263
+ bool help = false;
264
+
265
+ bool big = false;
266
+ bool native = false;
267
+ bool quantize_f16 = false;
268
+ bool quantize_i8 = false;
269
+
270
+ bool metric_ip = true;
271
+ bool metric_l2 = false;
272
+ bool metric_cos = false;
273
+ bool metric_haversine = false;
274
+ };
275
+
276
+ template <typename index_at, typename dataset_at> //
277
+ void run_type_punned(dataset_at& dataset, args_t const& args, config_t config) {
278
+
279
+ accuracy_t accuracy = accuracy_t::f32_k;
280
+ if (args.quantize_f16)
281
+ accuracy = accuracy_t::f16_k;
282
+ if (args.quantize_i8)
283
+ accuracy = accuracy_t::i8q100_k;
284
+
285
+ index_at index;
286
+ if (args.metric_ip)
287
+ index = index_at::ip(dataset.dimensions(), accuracy, config);
288
+ if (args.metric_l2)
289
+ index = index_at::l2(dataset.dimensions(), accuracy, config);
290
+ if (args.metric_cos)
291
+ index = index_at::cos(dataset.dimensions(), accuracy, config);
292
+ if (args.metric_haversine)
293
+ index = index_at::haversine(accuracy, config);
294
+
295
+ std::printf("- Hardware acceleration: %s\n", isa(index.acceleration()));
296
+ std::printf("-- Will benchmark in-memory\n");
297
+
298
+ single_shot(dataset, index, true);
299
+ index.save("tmp.usearch");
300
+
301
+ std::printf("-------\n");
302
+ std::printf("-- Will benchmark an on-disk view\n");
303
+
304
+ index_at index_view = index.fork();
305
+ index_view.view("tmp.usearch");
306
+ single_shot(dataset, index_view, false);
307
+ }
308
+
309
+ template <typename index_at, typename dataset_at> //
310
+ void run_typed(dataset_at& dataset, config_t config) {
311
+
312
+ index_at index(config);
313
+ std::printf("-- Will benchmark in-memory\n");
314
+
315
+ single_shot(dataset, index, true);
316
+ index.save("tmp.usearch");
317
+
318
+ std::printf("-------\n");
319
+ std::printf("-- Will benchmark an on-disk view\n");
320
+
321
+ auto_index_t index_view;
322
+ index_view.view("tmp.usearch");
323
+ single_shot(dataset, index_view, false);
324
+ }
325
+
326
+ template <typename neighbor_id_at, typename dataset_at> //
327
+ void run_big_or_small(dataset_at& dataset, args_t const& args, config_t config) {
328
+ if (args.native) {
329
+ if (args.metric_cos)
330
+ run_typed<index_gt<ip_gt<float>, std::size_t, neighbor_id_at>>(dataset, config);
331
+ else if (args.metric_l2)
332
+ run_typed<index_gt<l2_squared_gt<float>, std::size_t, neighbor_id_at>>(dataset, config);
333
+ else if (args.metric_haversine)
334
+ run_typed<index_gt<haversine_gt<float>, std::size_t, neighbor_id_at>>(dataset, config);
335
+ else
336
+ run_typed<index_gt<ip_gt<float>, std::size_t, neighbor_id_at>>(dataset, config);
337
+ } else
338
+ run_type_punned<auto_index_gt<std::size_t, neighbor_id_at>>(dataset, args, config);
339
+ }
340
+
341
+ int main(int argc, char** argv) {
342
+
343
+ // Print backtrace if something goes wrong.
344
+ signal(SIGSEGV, handler);
345
+
346
+ using namespace clipp;
347
+
348
+ auto args = args_t{};
349
+ auto cli = ( //
350
+ (option("--vectors") & value("path", args.path_vectors)).doc(".fbin file path to construct the index"),
351
+ (option("--queries") & value("path", args.path_queries)).doc(".fbin file path to query the index"),
352
+ (option("--neighbors") & value("path", args.path_neighbors)).doc(".ibin file path with ground truth"),
353
+ (option("-b", "--big").set(args.big)).doc("Will switch to uint40_t for neighbors lists with over 4B entries"),
354
+ (option("-j", "--threads") & value("integer", args.threads)).doc("Uses all available cores by default"),
355
+ (option("-c", "--connectivity") & value("integer", args.connectivity)).doc("Index granularity"),
356
+ (option("--expansion-add") & value("integer", args.expansion_add)).doc("Affects indexing depth"),
357
+ (option("--expansion-search") & value("integer", args.expansion_search)).doc("Affects search depth"),
358
+ ( //
359
+ option("--native").set(args.native).doc("Use raw templates instead of type-punned classes") |
360
+ option("--f16quant").set(args.quantize_f16).doc("Enable `f16_t` quantization") |
361
+ option("--i8quant").set(args.quantize_i8).doc("Enable `int8_t` quantization")),
362
+ ( //
363
+ option("--ip").set(args.metric_ip).doc("Choose Inner Product metric") |
364
+ option("--l2").set(args.metric_l2).doc("Choose L2 Euclidean metric") |
365
+ option("--cos").set(args.metric_cos).doc("Choose Angular metric") |
366
+ option("--haversine").set(args.metric_haversine).doc("Choose Haversine metric")),
367
+ option("-h", "--help").set(args.help).doc("Print this help information on this tool and exit"));
368
+
369
+ if (!parse(argc, argv, cli)) {
370
+ std::cerr << make_man_page(cli, argv[0]);
371
+ exit(1);
372
+ }
373
+ if (args.help) {
374
+ std::cout << make_man_page(cli, argv[0]);
375
+ exit(0);
376
+ }
377
+
378
+ // Instead of relying on `multithreaded` from "advanced.hpp" we will use OpenMP
379
+ // to better estimate statistics between tasks batches, without having to recreate
380
+ // the threads.
381
+ omp_set_dynamic(true);
382
+ omp_set_num_threads(args.threads);
383
+ std::printf("- OpenMP threads: %d\n", omp_get_max_threads());
384
+
385
+ std::printf("- Dataset: \n");
386
+ std::printf("-- Base vectors path: %s\n", args.path_vectors.c_str());
387
+ std::printf("-- Query vectors path: %s\n", args.path_queries.c_str());
388
+ std::printf("-- Ground truth neighbors path: %s\n", args.path_neighbors.c_str());
389
+
390
+ using vector_id_t = std::uint32_t;
391
+
392
+ persisted_dataset_gt<float, vector_id_t> dataset{
393
+ args.path_vectors.c_str(),
394
+ args.path_queries.c_str(),
395
+ args.path_neighbors.c_str(),
396
+ };
397
+ std::printf("-- Dimensions: %zu\n", dataset.dimensions());
398
+ std::printf("-- Vectors count: %zu\n", dataset.vectors_count());
399
+ std::printf("-- Queries count: %zu\n", dataset.queries_count());
400
+ std::printf("-- Neighbors per query: %zu\n", dataset.neighborhood_size());
401
+
402
+ config_t config;
403
+ config.connectivity = args.connectivity;
404
+ config.expansion_add = args.expansion_add;
405
+ config.expansion_search = args.expansion_search;
406
+ config.max_threads_add = config.max_threads_search = args.threads;
407
+ config.max_elements = dataset.vectors_count();
408
+
409
+ if (args.big)
410
+ run_big_or_small<uint40_t>(dataset, args, config);
411
+ else
412
+ run_big_or_small<std::uint32_t>(dataset, args, config);
413
+
414
+ return 0;
415
+ }
package/src/bench.py ADDED
@@ -0,0 +1,89 @@
1
+ import datetime
2
+ import numpy as np
3
+
4
+ import faiss
5
+ import usearch
6
+
7
+
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
+ def measure(f) -> float:
59
+ a = datetime.datetime.now()
60
+ f()
61
+ b = datetime.datetime.now()
62
+ c = b - a
63
+ print(f'Took: {c.seconds} seconds')
64
+ return c.seconds
65
+
66
+
67
+ def construct_both() -> tuple:
68
+
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
+
74
+ index_unum = usearch.make_index(
75
+ expansion_add=efConstruction,
76
+ expansion_search=efSearch,
77
+ connectivity=M,
78
+ )
79
+ dt_unum = measure(lambda: index_unum.add(labels, xb, copy=False))
80
+
81
+ return dt_meta, dt_unum
82
+
83
+
84
+ experiments = 10
85
+ durations = [construct_both() for _ in range(experiments)]
86
+ dt_meta = sum(x[0] for x in durations)
87
+ dt_unum = sum(x[1] for x in durations)
88
+ print(f'- FAISS vectors per second: {xb.shape[0]*experiments/dt_meta:.2f}')
89
+ print(f'- USearch vectors per second: {xb.shape[0]*experiments/dt_unum:.2f}')