usearch 2.25.0 → 2.25.3

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 (80) hide show
  1. package/README.md +43 -47
  2. package/c/README.md +1 -1
  3. package/c/lib.cpp +28 -13
  4. package/c/test.c +41 -0
  5. package/cpp/test.cpp +99 -0
  6. package/include/usearch/index.hpp +338 -57
  7. package/include/usearch/index_dense.hpp +155 -50
  8. package/include/usearch/index_plugins.hpp +94 -35
  9. package/javascript/README.md +1 -1
  10. package/javascript/lib.cpp +4 -2
  11. package/javascript/usearch.test.js +117 -94
  12. package/numkong/CITATION.cff +1 -1
  13. package/numkong/Cargo.toml +1 -1
  14. package/numkong/bench/bench_cross_amx.cpp +12 -0
  15. package/numkong/bench/bench_mesh.cpp +12 -0
  16. package/numkong/bench/bench_spatial.cpp +0 -3
  17. package/numkong/binding.gyp +18 -0
  18. package/numkong/build.rs +1 -0
  19. package/numkong/c/dispatch_e5m2.c +23 -3
  20. package/numkong/include/numkong/capabilities.h +1 -1
  21. package/numkong/include/numkong/cast/README.md +3 -0
  22. package/numkong/include/numkong/cast/haswell.h +28 -64
  23. package/numkong/include/numkong/cast/serial.h +17 -0
  24. package/numkong/include/numkong/cast/skylake.h +67 -52
  25. package/numkong/include/numkong/cast.h +1 -0
  26. package/numkong/include/numkong/dot/README.md +1 -0
  27. package/numkong/include/numkong/dot/haswell.h +92 -13
  28. package/numkong/include/numkong/dot/serial.h +15 -0
  29. package/numkong/include/numkong/dot/skylake.h +61 -14
  30. package/numkong/include/numkong/dots/README.md +2 -0
  31. package/numkong/include/numkong/dots/graniteamx.h +434 -0
  32. package/numkong/include/numkong/dots/haswell.h +28 -28
  33. package/numkong/include/numkong/dots/sapphireamx.h +1 -1
  34. package/numkong/include/numkong/dots/serial.h +23 -8
  35. package/numkong/include/numkong/dots/skylake.h +28 -23
  36. package/numkong/include/numkong/dots.h +12 -0
  37. package/numkong/include/numkong/each/serial.h +18 -1
  38. package/numkong/include/numkong/geospatial/serial.h +14 -3
  39. package/numkong/include/numkong/maxsim/serial.h +15 -0
  40. package/numkong/include/numkong/mesh/README.md +50 -44
  41. package/numkong/include/numkong/mesh/genoa.h +462 -0
  42. package/numkong/include/numkong/mesh/haswell.h +806 -933
  43. package/numkong/include/numkong/mesh/neon.h +871 -943
  44. package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
  45. package/numkong/include/numkong/mesh/neonfhm.h +676 -0
  46. package/numkong/include/numkong/mesh/rvv.h +404 -319
  47. package/numkong/include/numkong/mesh/serial.h +204 -162
  48. package/numkong/include/numkong/mesh/skylake.h +1029 -1585
  49. package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
  50. package/numkong/include/numkong/mesh.h +38 -0
  51. package/numkong/include/numkong/reduce/serial.h +15 -1
  52. package/numkong/include/numkong/sparse/serial.h +17 -2
  53. package/numkong/include/numkong/spatial/genoa.h +0 -68
  54. package/numkong/include/numkong/spatial/haswell.h +98 -56
  55. package/numkong/include/numkong/spatial/serial.h +15 -0
  56. package/numkong/include/numkong/spatial/skylake.h +114 -54
  57. package/numkong/include/numkong/spatial.h +0 -12
  58. package/numkong/include/numkong/spatials/graniteamx.h +128 -0
  59. package/numkong/include/numkong/spatials/serial.h +18 -1
  60. package/numkong/include/numkong/spatials/skylake.h +2 -2
  61. package/numkong/include/numkong/spatials.h +17 -0
  62. package/numkong/include/numkong/tensor.hpp +107 -23
  63. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  64. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  65. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  66. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  67. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  68. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  69. package/numkong/javascript/numkong.c +3 -2
  70. package/numkong/package.json +7 -7
  71. package/numkong/test/test.cu +841 -0
  72. package/numkong/test/test_cross_amx.cpp +13 -0
  73. package/numkong/test/test_dlpack.py +492 -0
  74. package/numkong/test/test_mesh.cpp +12 -0
  75. package/numkong/test/test_spatial.cpp +0 -3
  76. package/numkong/test/test_tensor.cpp +87 -27
  77. package/package.json +1 -1
  78. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  79. package/prebuilds/linux-arm64/usearch.node +0 -0
  80. package/prebuilds/linux-x64/usearch.node +0 -0
@@ -276,15 +276,22 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
276
276
 
277
277
  std::uint32_t dimensions_u32[2]{0};
278
278
  std::memcpy(dimensions_u32, result.head_buffer, sizeof(dimensions_u32));
279
- std::size_t offset_if_u32 = std::size_t(dimensions_u32[0]) * dimensions_u32[1] + sizeof(dimensions_u32);
279
+ checked_size_result_t offset_if_u32 =
280
+ checked_mul_add(std::size_t(dimensions_u32[0]), std::size_t(dimensions_u32[1]), sizeof(dimensions_u32));
280
281
 
281
282
  std::uint64_t dimensions_u64[2]{0};
282
283
  std::memcpy(dimensions_u64, result.head_buffer, sizeof(dimensions_u64));
283
- std::size_t offset_if_u64 = std::size_t(dimensions_u64[0]) * dimensions_u64[1] + sizeof(dimensions_u64);
284
+ checked_size_result_t rows_if_u64 = checked_size_from_u64(dimensions_u64[0]);
285
+ checked_size_result_t columns_if_u64 = checked_size_from_u64(dimensions_u64[1]);
286
+ checked_size_result_t offset_if_u64 =
287
+ rows_if_u64 && columns_if_u64 ? checked_mul_add(rows_if_u64.value, columns_if_u64.value, sizeof(dimensions_u64))
288
+ : checked_size_overflow();
284
289
 
285
290
  // Check if it starts with 32-bit
286
- if (offset_if_u32 + sizeof(index_dense_head_buffer_t) < file_size) {
287
- if (std::fseek(file.get(), static_cast<long>(offset_if_u32), SEEK_SET) != 0)
291
+ checked_size_result_t head_offset_if_u32 =
292
+ offset_if_u32 ? checked_add(offset_if_u32.value, sizeof(index_dense_head_buffer_t)) : offset_if_u32;
293
+ if (head_offset_if_u32 && head_offset_if_u32.value < file_size) {
294
+ if (std::fseek(file.get(), static_cast<long>(offset_if_u32.value), SEEK_SET) != 0)
288
295
  return result.failed(std::strerror(errno));
289
296
  read = std::fread(result.head_buffer, sizeof(index_dense_head_buffer_t), 1, file.get());
290
297
  if (!read)
@@ -299,8 +306,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
299
306
  }
300
307
 
301
308
  // Check if it starts with 64-bit
302
- if (offset_if_u64 + sizeof(index_dense_head_buffer_t) < file_size) {
303
- if (std::fseek(file.get(), static_cast<long>(offset_if_u64), SEEK_SET) != 0)
309
+ checked_size_result_t head_offset_if_u64 =
310
+ offset_if_u64 ? checked_add(offset_if_u64.value, sizeof(index_dense_head_buffer_t)) : offset_if_u64;
311
+ if (head_offset_if_u64 && head_offset_if_u64.value < file_size) {
312
+ if (std::fseek(file.get(), static_cast<long>(offset_if_u64.value), SEEK_SET) != 0)
304
313
  return result.failed(std::strerror(errno));
305
314
  read = std::fread(result.head_buffer, sizeof(index_dense_head_buffer_t), 1, file.get());
306
315
  if (!read)
@@ -341,15 +350,22 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
341
350
  // Check if it starts with 32-bit
342
351
  std::uint32_t dimensions_u32[2]{0};
343
352
  std::memcpy(dimensions_u32, result.head_buffer, sizeof(dimensions_u32));
344
- std::size_t offset_if_u32 = std::size_t(dimensions_u32[0]) * dimensions_u32[1] + sizeof(dimensions_u32);
353
+ checked_size_result_t offset_if_u32 =
354
+ checked_mul_add(std::size_t(dimensions_u32[0]), std::size_t(dimensions_u32[1]), sizeof(dimensions_u32));
345
355
 
346
356
  std::uint64_t dimensions_u64[2]{0};
347
357
  std::memcpy(dimensions_u64, result.head_buffer, sizeof(dimensions_u64));
348
- std::size_t offset_if_u64 = std::size_t(dimensions_u64[0]) * dimensions_u64[1] + sizeof(dimensions_u64);
358
+ checked_size_result_t rows_if_u64 = checked_size_from_u64(dimensions_u64[0]);
359
+ checked_size_result_t columns_if_u64 = checked_size_from_u64(dimensions_u64[1]);
360
+ checked_size_result_t offset_if_u64 =
361
+ rows_if_u64 && columns_if_u64 ? checked_mul_add(rows_if_u64.value, columns_if_u64.value, sizeof(dimensions_u64))
362
+ : checked_size_overflow();
349
363
 
350
364
  // Check if it starts with 32-bit
351
- if (offset_if_u32 + sizeof(index_dense_head_buffer_t) < file_size) {
352
- std::memcpy(&result.head_buffer, file_data + offset_if_u32, sizeof(index_dense_head_buffer_t));
365
+ checked_size_result_t head_offset_if_u32 =
366
+ offset_if_u32 ? checked_add(offset_if_u32.value, sizeof(index_dense_head_buffer_t)) : offset_if_u32;
367
+ if (head_offset_if_u32 && head_offset_if_u32.value < file_size) {
368
+ std::memcpy(&result.head_buffer, file_data + offset_if_u32.value, sizeof(index_dense_head_buffer_t));
353
369
  result.config.exclude_vectors = false;
354
370
  result.config.use_64_bit_dimensions = false;
355
371
  if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
@@ -357,8 +373,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
357
373
  }
358
374
 
359
375
  // Check if it starts with 64-bit
360
- if (offset_if_u64 + sizeof(index_dense_head_buffer_t) < file_size) {
361
- std::memcpy(&result.head_buffer, file_data + offset_if_u64, sizeof(index_dense_head_buffer_t));
376
+ checked_size_result_t head_offset_if_u64 =
377
+ offset_if_u64 ? checked_add(offset_if_u64.value, sizeof(index_dense_head_buffer_t)) : offset_if_u64;
378
+ if (head_offset_if_u64 && head_offset_if_u64.value < file_size) {
379
+ std::memcpy(&result.head_buffer, file_data + offset_if_u64.value, sizeof(index_dense_head_buffer_t));
362
380
  result.config.exclude_vectors = false;
363
381
  result.config.use_64_bit_dimensions = true;
364
382
  if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
@@ -532,6 +550,9 @@ class index_dense_gt {
532
550
  : parent(other.parent), thread_id(other.thread_id), engaged(other.engaged) {
533
551
  other.engaged = false;
534
552
  }
553
+ explicit operator bool() const noexcept {
554
+ return parent.typed_ && thread_id != any_thread() && thread_id < parent.typed_->limits().threads();
555
+ }
535
556
  };
536
557
 
537
558
  public:
@@ -635,16 +656,14 @@ class index_dense_gt {
635
656
  * @param[in] metric One of the provided or an @b ad-hoc metric, type-punned.
636
657
  * @param[in] config The index configuration (optional).
637
658
  * @param[in] free_key The key used for freed vectors (optional).
659
+ * @param[in] limits Initial reservation. Default sizes the thread pool to
660
+ * @c hardware_concurrency(); pass @c {unreserved} to skip.
638
661
  * @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
639
- *
640
- * ! If the `metric` isn't provided in this method, it has to be set with
641
- * ! the `change_metric` method before the index can be used. Alternatively,
642
- * ! if you are loading an existing index, the metric will be set automatically.
643
662
  */
644
663
  static state_result_t make( //
645
664
  metric_t metric = {}, //
646
665
  index_dense_config_t config = {}, //
647
- vector_key_t free_key = default_free_value<vector_key_t>()) {
666
+ vector_key_t free_key = default_free_value<vector_key_t>(), index_limits_t limits = {}) {
648
667
 
649
668
  if (metric.missing())
650
669
  return state_result_t{}.failed("Metric won't be initialized!");
@@ -659,16 +678,15 @@ class index_dense_gt {
659
678
  index_dense_gt& index = result.index;
660
679
  index.config_ = config;
661
680
  index.free_key_ = free_key;
662
-
663
- // In some cases the metric is not provided, and will be set later.
664
- if (metric) {
665
- scalar_kind_t scalar_kind = metric.scalar_kind();
666
- index.casts_ = casts_punned_t::make(scalar_kind);
667
- index.metric_ = metric;
668
- }
681
+ index.casts_ = casts_punned_t::make(metric.scalar_kind());
682
+ index.metric_ = metric;
669
683
 
670
684
  new (raw) index_t(config);
671
685
  index.typed_ = raw;
686
+
687
+ if (!index.try_reserve(limits))
688
+ return state_result_t{}.failed("Failed to reserve memory for the index!");
689
+
672
690
  return result;
673
691
  }
674
692
 
@@ -704,7 +722,34 @@ class index_dense_gt {
704
722
 
705
723
  // The metric and its properties
706
724
  metric_t const& metric() const { return metric_; }
707
- void change_metric(metric_t metric) { metric_ = std::move(metric); }
725
+
726
+ /**
727
+ * @brief Replaces the active distance metric, resizing the per-thread cast
728
+ * buffer and rebuilding the cast dispatch table if the new metric
729
+ * changes @c bytes_per_vector() or @c scalar_kind().
730
+ * @return @c false if the cast buffer can't be re-allocated; the metric is
731
+ * left untouched in that case so the index stays consistent.
732
+ */
733
+ bool try_change_metric(metric_t metric) noexcept {
734
+ checked_size_result_t needed_bytes = checked_mul(limits().threads(), metric.bytes_per_vector());
735
+ if (!needed_bytes)
736
+ return false;
737
+ if (needed_bytes.value > cast_buffer_.size()) {
738
+ cast_buffer_t new_buffer(needed_bytes.value);
739
+ if (!new_buffer)
740
+ return false;
741
+ cast_buffer_ = std::move(new_buffer);
742
+ }
743
+ casts_ = casts_punned_t::make(metric.scalar_kind());
744
+ metric_ = std::move(metric);
745
+ return true;
746
+ }
747
+
748
+ /// @brief Throwing counterpart of @ref try_change_metric.
749
+ void change_metric(metric_t metric) {
750
+ if (!try_change_metric(std::move(metric)))
751
+ usearch_raise_runtime_error("failed to grow cast buffer for the new metric");
752
+ }
708
753
 
709
754
  scalar_kind_t scalar_kind() const { return metric_.scalar_kind(); }
710
755
  metric_kind_t metric_kind() const { return metric_.metric_kind(); }
@@ -729,6 +774,45 @@ class index_dense_gt {
729
774
  return typed_->stats(stats_per_level, max_level);
730
775
  }
731
776
 
777
+ using neighbors_view_t = typename index_t::neighbors_view_t;
778
+
779
+ /**
780
+ * @brief Returns a read-only range over the neighbors of @p key at the given
781
+ * graph @p level.
782
+ *
783
+ * For multi-key indexes, returns the neighbors of the first matching
784
+ * slot; iterate through `cbegin()`/`cend()` and call the underlying
785
+ * `index_gt::neighbors(member_citerator_t, std::size_t)` overload to
786
+ * inspect every copy of a duplicated key.
787
+ *
788
+ * @return An empty view if the key is not present, or if @p level exceeds
789
+ * the node's level.
790
+ * @warning The view aliases the node's adjacency tape. Hold no concurrent
791
+ * `add`/`update`/`remove` for the lifetime of the view.
792
+ */
793
+ neighbors_view_t neighbors(vector_key_t key, std::size_t level) const {
794
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
795
+ shared_lock_t lookup_lock(slot_lookup_mutex_);
796
+ auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
797
+ if (matching_slots.first == matching_slots.second)
798
+ return {};
799
+ compressed_slot_t slot = (*matching_slots.first).slot;
800
+ return typed_->neighbors(slot, level);
801
+ }
802
+
803
+ /**
804
+ * @brief Returns the top graph level at which @p key is present, or zero
805
+ * if the key is not in the index.
806
+ */
807
+ std::size_t level_of(vector_key_t key) const {
808
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
809
+ shared_lock_t lookup_lock(slot_lookup_mutex_);
810
+ auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
811
+ if (matching_slots.first == matching_slots.second)
812
+ return 0;
813
+ return typed_->level_of((*matching_slots.first).slot);
814
+ }
815
+
732
816
  dynamic_allocator_t const& allocator() const { return typed_->dynamic_allocator(); }
733
817
  vector_key_t const& free_key() const { return free_key_; }
734
818
 
@@ -935,6 +1019,8 @@ class index_dense_gt {
935
1019
 
936
1020
  index_cluster_config_t cluster_config;
937
1021
  thread_lock_t lock = thread_lock_(thread);
1022
+ if (!lock)
1023
+ return cluster_result_t{}.failed("Reserve capacity ahead of searches!");
938
1024
  cluster_config.thread = lock.thread_id;
939
1025
  cluster_config.expansion = config_.expansion_search;
940
1026
  metric_proxy_t metric{*this};
@@ -994,7 +1080,10 @@ class index_dense_gt {
994
1080
  available_threads_.push(i);
995
1081
 
996
1082
  // Allocate a buffer for the casted vectors.
997
- cast_buffer_t cast_buffer(limits.threads() * metric_.bytes_per_vector());
1083
+ checked_size_result_t cast_buffer_bytes = checked_mul(limits.threads(), metric_.bytes_per_vector());
1084
+ if (!cast_buffer_bytes)
1085
+ return false;
1086
+ cast_buffer_t cast_buffer(cast_buffer_bytes.value);
998
1087
  if (!cast_buffer)
999
1088
  return false;
1000
1089
  cast_buffer_ = std::move(cast_buffer);
@@ -1019,7 +1108,8 @@ class index_dense_gt {
1019
1108
  std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
1020
1109
  typed_->clear();
1021
1110
  slot_lookup_.clear();
1022
- vectors_lookup_.reset();
1111
+ // Tape pointers are about to be invalidated by the reset below.
1112
+ std::fill(vectors_lookup_.begin(), vectors_lookup_.end(), nullptr);
1023
1113
  free_keys_.clear();
1024
1114
  vectors_tape_allocator_.reset();
1025
1115
  }
@@ -1144,8 +1234,9 @@ class index_dense_gt {
1144
1234
  serialization_config_t config = {}, //
1145
1235
  progress_at&& progress = {}) {
1146
1236
 
1147
- // Discard all previous memory allocations of `vectors_tape_allocator_`
1148
- index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
1237
+ // Preserve any explicit thread counts from the prior index state; fall
1238
+ // back to library defaults when they were never set (e.g. `unreserved`).
1239
+ index_limits_t new_limits = typed_ ? typed_->limits().with_thread_defaults() : index_limits_t{};
1149
1240
  reset();
1150
1241
 
1151
1242
  // Infer the new index size
@@ -1206,9 +1297,10 @@ class index_dense_gt {
1206
1297
 
1207
1298
  config_.multi = head.multi;
1208
1299
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1209
- // available_threads_.size() will be updated to old_limits.threads() later in this
1210
- // method, so use that as the number of threads to prepare for.
1211
- cast_buffer_ = cast_buffer_t(old_limits.threads() * metric_.bytes_per_vector());
1300
+ checked_size_result_t cast_buffer_bytes = checked_mul(new_limits.threads(), metric_.bytes_per_vector());
1301
+ if (!cast_buffer_bytes)
1302
+ return result.failed("Failed to allocate memory for the casts");
1303
+ cast_buffer_ = cast_buffer_t(cast_buffer_bytes.value);
1212
1304
  if (!cast_buffer_)
1213
1305
  return result.failed("Failed to allocate memory for the casts");
1214
1306
  casts_ = casts_punned_t::make(head.kind_scalar);
@@ -1227,14 +1319,13 @@ class index_dense_gt {
1227
1319
  return result;
1228
1320
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
1229
1321
  return result.failed("Index size and the number of vectors doesn't match");
1230
- old_limits.members = static_cast<std::size_t>(matrix_rows);
1231
- if (!typed_->try_reserve(old_limits))
1322
+ new_limits.members = static_cast<std::size_t>(matrix_rows);
1323
+ if (!typed_->try_reserve(new_limits))
1232
1324
  return result.failed("Failed to reserve memory for the index");
1233
1325
 
1234
- // After the index is loaded, we may have to resize the `available_threads_` to
1235
- // match the limits of the underlying engine.
1326
+ // After the index is loaded, resize `available_threads_` to match the new limits.
1236
1327
  available_threads_t available_threads;
1237
- std::size_t max_threads = old_limits.threads();
1328
+ std::size_t max_threads = new_limits.threads();
1238
1329
  if (!available_threads.reserve(max_threads))
1239
1330
  return result.failed("Failed to allocate memory for the available threads!");
1240
1331
  for (std::size_t i = 0; i < max_threads; i++)
@@ -1258,8 +1349,9 @@ class index_dense_gt {
1258
1349
  std::size_t offset = 0, serialization_config_t config = {}, //
1259
1350
  progress_at&& progress = {}) {
1260
1351
 
1261
- // Discard all previous memory allocations of `vectors_tape_allocator_`
1262
- index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
1352
+ // Preserve any explicit thread counts from the prior index state; fall
1353
+ // back to library defaults when they were never set (e.g. `unreserved`).
1354
+ index_limits_t new_limits = typed_ ? typed_->limits().with_thread_defaults() : index_limits_t{};
1263
1355
  reset();
1264
1356
 
1265
1357
  serialization_result_t result = file.open_if_not();
@@ -1323,8 +1415,10 @@ class index_dense_gt {
1323
1415
  config_.multi = head.multi;
1324
1416
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1325
1417
  // available_threads_.size() will be updated to old_limits.threads() later in this
1326
- // method, so use that as the number of threads to prepare for.
1327
- cast_buffer_ = cast_buffer_t(old_limits.threads() * metric_.bytes_per_vector());
1418
+ checked_size_result_t cast_buffer_bytes = checked_mul(new_limits.threads(), metric_.bytes_per_vector());
1419
+ if (!cast_buffer_bytes)
1420
+ return result.failed("Failed to allocate memory for the casts");
1421
+ cast_buffer_ = cast_buffer_t(cast_buffer_bytes.value);
1328
1422
  if (!cast_buffer_)
1329
1423
  return result.failed("Failed to allocate memory for the casts");
1330
1424
  casts_ = casts_punned_t::make(head.kind_scalar);
@@ -1344,8 +1438,8 @@ class index_dense_gt {
1344
1438
  return result;
1345
1439
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
1346
1440
  return result.failed("Index size and the number of vectors doesn't match");
1347
- old_limits.members = static_cast<std::size_t>(matrix_rows);
1348
- if (!typed_->try_reserve(old_limits))
1441
+ new_limits.members = static_cast<std::size_t>(matrix_rows);
1442
+ if (!typed_->try_reserve(new_limits))
1349
1443
  return result.failed("Failed to reserve memory for the index");
1350
1444
 
1351
1445
  // Address the vectors
@@ -1356,10 +1450,9 @@ class index_dense_gt {
1356
1450
  for (std::uint64_t slot = 0; slot != matrix_rows; ++slot)
1357
1451
  vectors_lookup_[slot] = (byte_t*)vectors_buffer.data() + matrix_cols * slot;
1358
1452
 
1359
- // After the index is loaded, we may have to resize the `available_threads_` to
1360
- // match the limits of the underlying engine.
1453
+ // After the index is viewed, resize `available_threads_` to match the new limits.
1361
1454
  available_threads_t available_threads;
1362
- std::size_t max_threads = old_limits.threads();
1455
+ std::size_t max_threads = new_limits.threads();
1363
1456
  if (!available_threads.reserve(max_threads))
1364
1457
  return result.failed("Failed to allocate memory for the available threads!");
1365
1458
  for (std::size_t i = 0; i < max_threads; i++)
@@ -1624,6 +1717,11 @@ class index_dense_gt {
1624
1717
  */
1625
1718
  labeling_result_t rename(vector_key_t from, vector_key_t to) {
1626
1719
  labeling_result_t result;
1720
+ if (from == to) {
1721
+ result.completed = count(from);
1722
+ return result;
1723
+ }
1724
+
1627
1725
  unique_lock_t lookup_lock(slot_lookup_mutex_);
1628
1726
 
1629
1727
  if (!multi() && slot_lookup_.contains(key_and_slot_t::any_slot(to)))
@@ -2043,10 +2141,9 @@ class index_dense_gt {
2043
2141
  if (thread_id != any_thread())
2044
2142
  return {*this, thread_id, false};
2045
2143
 
2046
- available_threads_mutex_.lock();
2047
- usearch_assert_m(available_threads_.size(), "No available threads to lock");
2048
- available_threads_.try_pop(thread_id);
2049
- available_threads_mutex_.unlock();
2144
+ std::unique_lock<std::mutex> lock(available_threads_mutex_);
2145
+ if (!available_threads_.try_pop(thread_id))
2146
+ return {*this, any_thread(), false};
2050
2147
  return {*this, thread_id, true};
2051
2148
  }
2052
2149
 
@@ -2067,6 +2164,8 @@ class index_dense_gt {
2067
2164
 
2068
2165
  // Cast the vector, if needed for compatibility with `metric_`
2069
2166
  thread_lock_t lock = thread_lock_(thread);
2167
+ if (!lock)
2168
+ return add_result_t{}.failed("Reserve capacity ahead of insertions!");
2070
2169
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2071
2170
  {
2072
2171
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2120,6 +2219,8 @@ class index_dense_gt {
2120
2219
 
2121
2220
  // Cast the vector, if needed for compatibility with `metric_`
2122
2221
  thread_lock_t lock = thread_lock_(thread);
2222
+ if (!lock)
2223
+ return search_result_t{*this}.failed("Reserve capacity ahead of searches!");
2123
2224
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2124
2225
  {
2125
2226
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2156,6 +2257,8 @@ class index_dense_gt {
2156
2257
 
2157
2258
  // Cast the vector, if needed for compatibility with `metric_`
2158
2259
  thread_lock_t lock = thread_lock_(thread);
2260
+ if (!lock)
2261
+ return cluster_result_t{}.failed("Reserve capacity ahead of searches!");
2159
2262
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2160
2263
  {
2161
2264
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2180,6 +2283,8 @@ class index_dense_gt {
2180
2283
 
2181
2284
  // Cast the vector, if needed for compatibility with `metric_`
2182
2285
  thread_lock_t lock = thread_lock_(thread);
2286
+ if (!lock)
2287
+ return {};
2183
2288
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2184
2289
  {
2185
2290
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -1572,21 +1572,24 @@ class aligned_allocator_gt {
1572
1572
  constexpr std::size_t alignment() const { return alignment_ak; }
1573
1573
 
1574
1574
  pointer allocate(size_type length) const {
1575
- std::size_t length_bytes = alignment_ak * divide_round_up<alignment_ak>(length * sizeof(value_type));
1576
- // Avoid overflow
1577
- if (length > length_bytes)
1575
+ checked_size_result_t bytes = checked_mul(length, sizeof(value_type));
1576
+ if (!bytes)
1578
1577
  return nullptr;
1578
+ checked_size_result_t length_bytes = checked_round_up(bytes.value, alignment_ak);
1579
+ if (!length_bytes)
1580
+ return nullptr;
1581
+
1579
1582
  std::size_t alignment = alignment_ak;
1580
1583
  #if defined(USEARCH_DEFINED_WINDOWS)
1581
- return (pointer)_aligned_malloc(length_bytes, alignment);
1584
+ return (pointer)_aligned_malloc(length_bytes.value, alignment);
1582
1585
  #elif defined(USEARCH_DEFINED_APPLE) || defined(USEARCH_DEFINED_ANDROID)
1583
1586
  // Apple Clang keeps complaining that `aligned_alloc` is only available
1584
1587
  // with macOS 10.15 and newer or Android API >= 28, so let's use `posix_memalign` there.
1585
1588
  void* result = nullptr;
1586
- int status = posix_memalign(&result, alignment, length_bytes);
1589
+ int status = posix_memalign(&result, alignment, length_bytes.value);
1587
1590
  return status == 0 ? (pointer)result : nullptr;
1588
1591
  #else
1589
- return (pointer)aligned_alloc(alignment, length_bytes);
1592
+ return (pointer)aligned_alloc(alignment, length_bytes.value);
1590
1593
  #endif
1591
1594
  }
1592
1595
 
@@ -1615,7 +1618,10 @@ class page_allocator_t {
1615
1618
  * @return A pointer to the allocated memory block, or `nullptr` if allocation fails.
1616
1619
  */
1617
1620
  byte_t* allocate(std::size_t count_bytes) const noexcept {
1618
- count_bytes = divide_round_up(count_bytes, page_size()) * page_size();
1621
+ checked_size_result_t rounded_bytes = checked_round_up(count_bytes, page_size());
1622
+ if (!rounded_bytes)
1623
+ return nullptr;
1624
+ count_bytes = rounded_bytes.value;
1619
1625
  #if defined(USEARCH_DEFINED_WINDOWS)
1620
1626
  return (byte_t*)(::VirtualAlloc(NULL, count_bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
1621
1627
  #else
@@ -1628,7 +1634,10 @@ class page_allocator_t {
1628
1634
  #if defined(USEARCH_DEFINED_WINDOWS)
1629
1635
  ::VirtualFree(page_pointer, 0, MEM_RELEASE);
1630
1636
  #else
1631
- count_bytes = divide_round_up(count_bytes, page_size()) * page_size();
1637
+ checked_size_result_t rounded_bytes = checked_round_up(count_bytes, page_size());
1638
+ if (!rounded_bytes)
1639
+ return;
1640
+ count_bytes = rounded_bytes.value;
1632
1641
  munmap(page_pointer, count_bytes);
1633
1642
  #endif
1634
1643
  }
@@ -1724,25 +1733,39 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
1724
1733
  * @return A pointer to the allocated memory block, or `nullptr` if allocation fails.
1725
1734
  */
1726
1735
  inline byte_t* allocate(std::size_t count_bytes) noexcept {
1727
- std::size_t extended_bytes = divide_round_up<alignment_ak>(count_bytes) * alignment_ak;
1736
+ checked_size_result_t extended_bytes = checked_round_up(count_bytes, alignment_ak);
1737
+ if (!extended_bytes)
1738
+ return nullptr;
1728
1739
  std::unique_lock<std::mutex> lock(mutex_);
1729
- if (!last_arena_ || (last_usage_ + extended_bytes >= last_capacity_)) {
1730
- std::size_t new_cap = (std::max)(last_capacity_, ceil2(extended_bytes)) * capacity_multiplier();
1731
- byte_t* new_arena = page_allocator_t{}.allocate(new_cap);
1740
+ checked_size_result_t next_usage = checked_add(last_usage_, extended_bytes.value);
1741
+ if (!next_usage)
1742
+ return nullptr;
1743
+ if (!last_arena_ || (next_usage.value >= last_capacity_)) {
1744
+ checked_size_result_t rounded_bytes = checked_ceil2(extended_bytes.value);
1745
+ if (!rounded_bytes)
1746
+ return nullptr;
1747
+ checked_size_result_t new_cap =
1748
+ checked_mul((std::max)(last_capacity_, rounded_bytes.value), capacity_multiplier());
1749
+ if (!new_cap)
1750
+ return nullptr;
1751
+ checked_size_result_t new_total_allocated = checked_add(total_allocated_, new_cap.value);
1752
+ if (!new_total_allocated)
1753
+ return nullptr;
1754
+ byte_t* new_arena = page_allocator_t{}.allocate(new_cap.value);
1732
1755
  if (!new_arena)
1733
1756
  return nullptr;
1734
1757
  std::memcpy(new_arena, &last_arena_, sizeof(byte_t*));
1735
- std::memcpy(new_arena + sizeof(byte_t*), &new_cap, sizeof(std::size_t));
1758
+ std::memcpy(new_arena + sizeof(byte_t*), &new_cap.value, sizeof(std::size_t));
1736
1759
 
1737
1760
  wasted_space_ += total_reserved();
1738
1761
  last_arena_ = new_arena;
1739
- last_capacity_ = new_cap;
1762
+ last_capacity_ = new_cap.value;
1740
1763
  last_usage_ = head_size();
1741
- total_allocated_ += new_cap;
1764
+ total_allocated_ = new_total_allocated.value;
1742
1765
  }
1743
1766
 
1744
- wasted_space_ += extended_bytes - count_bytes;
1745
- return last_arena_ + exchange(last_usage_, last_usage_ + extended_bytes);
1767
+ wasted_space_ += extended_bytes.value - count_bytes;
1768
+ return last_arena_ + exchange(last_usage_, last_usage_ + extended_bytes.value);
1746
1769
  }
1747
1770
 
1748
1771
  /**
@@ -1948,6 +1971,11 @@ template <typename from_scalar_at> struct cast_to_i8_gt {
1948
1971
  for (std::size_t i = 0; i != dim; ++i)
1949
1972
  magnitude += (double)typed_input[i] * (double)typed_input[i];
1950
1973
  magnitude = std::sqrt(magnitude);
1974
+ // `!(x > 0)` also catches NaN; cast-to-int of NaN is UB.
1975
+ if (!(magnitude > 0.0)) {
1976
+ std::fill_n(typed_output, dim, std::int8_t{0});
1977
+ return true;
1978
+ }
1951
1979
  for (std::size_t i = 0; i != dim; ++i)
1952
1980
  typed_output[i] =
1953
1981
  static_cast<std::int8_t>(usearch::clamp<double>(typed_input[i] * 127.0 / magnitude, -127.0, 127.0));
@@ -1973,6 +2001,11 @@ template <typename from_scalar_at> struct cast_to_u8_gt {
1973
2001
  for (std::size_t i = 0; i != dim; ++i)
1974
2002
  magnitude += (double)typed_input[i] * (double)typed_input[i];
1975
2003
  magnitude = std::sqrt(magnitude);
2004
+ // `!(x > 0)` also catches NaN; cast-to-int of NaN is UB.
2005
+ if (!(magnitude > 0.0)) {
2006
+ std::fill_n(typed_output, dim, std::uint8_t{0});
2007
+ return true;
2008
+ }
1976
2009
  for (std::size_t i = 0; i != dim; ++i)
1977
2010
  typed_output[i] =
1978
2011
  static_cast<std::uint8_t>(usearch::clamp<double>(typed_input[i] * 255.0 / magnitude, 0.0, 255.0));
@@ -3750,7 +3783,8 @@ class flat_hash_multi_set_gt {
3750
3783
 
3751
3784
  public:
3752
3785
  std::size_t size() const noexcept { return populated_slots_; }
3753
- std::size_t capacity() const noexcept { return capacity_slots_; }
3786
+ std::size_t capacity() const noexcept { return capacity_slots_ * 2u / 3u; }
3787
+ std::size_t capacity_slots() const noexcept { return capacity_slots_; }
3754
3788
 
3755
3789
  flat_hash_multi_set_gt() noexcept {}
3756
3790
  ~flat_hash_multi_set_gt() noexcept { reset(); }
@@ -3764,7 +3798,10 @@ class flat_hash_multi_set_gt {
3764
3798
  }
3765
3799
 
3766
3800
  // Allocate new memory
3767
- data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
3801
+ checked_size_result_t bytes = checked_mul(other.buckets_, bytes_per_bucket());
3802
+ if (!bytes)
3803
+ usearch_raise_runtime_error("failed memory allocation");
3804
+ data_ = (char*)allocator_t{}.allocate(bytes.value);
3768
3805
  if (!data_)
3769
3806
  usearch_raise_runtime_error("failed memory allocation");
3770
3807
 
@@ -3804,7 +3841,10 @@ class flat_hash_multi_set_gt {
3804
3841
  allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
3805
3842
 
3806
3843
  // Allocate new memory
3807
- data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
3844
+ checked_size_result_t bytes = checked_mul(other.buckets_, bytes_per_bucket());
3845
+ if (!bytes)
3846
+ usearch_raise_runtime_error("failed memory allocation");
3847
+ data_ = (char*)allocator_t{}.allocate(bytes.value);
3808
3848
  if (!data_)
3809
3849
  usearch_raise_runtime_error("failed memory allocation");
3810
3850
 
@@ -3853,22 +3893,37 @@ class flat_hash_multi_set_gt {
3853
3893
  }
3854
3894
 
3855
3895
  bool try_reserve(std::size_t capacity) noexcept {
3856
- if (capacity * 3u <= capacity_slots_ * 2u)
3896
+ if (capacity <= this->capacity())
3857
3897
  return true;
3858
3898
 
3859
3899
  // Calculate new sizes
3860
- std::size_t new_slots = ceil2((capacity * 3ul) / 2ul);
3861
- std::size_t new_buckets = divide_round_up<slots_per_bucket()>(new_slots);
3862
- new_slots = new_buckets * slots_per_bucket(); // This must be a power of two!
3863
- std::size_t new_bytes = new_buckets * bytes_per_bucket();
3900
+ checked_size_result_t scaled_capacity = checked_mul(capacity, std::size_t{3});
3901
+ if (!scaled_capacity)
3902
+ return false;
3903
+ checked_size_result_t slots_needed = checked_divide_round_up(scaled_capacity.value, std::size_t{2});
3904
+ if (!slots_needed)
3905
+ return false;
3906
+ checked_size_result_t new_slots_checked = checked_ceil2(slots_needed.value);
3907
+ if (!new_slots_checked)
3908
+ return false;
3909
+ checked_size_result_t new_buckets_checked =
3910
+ checked_divide_round_up(new_slots_checked.value, slots_per_bucket());
3911
+ if (!new_buckets_checked)
3912
+ return false;
3913
+ checked_size_result_t new_slots = checked_mul(new_buckets_checked.value, slots_per_bucket());
3914
+ if (!new_slots)
3915
+ return false;
3916
+ checked_size_result_t new_bytes = checked_mul(new_buckets_checked.value, bytes_per_bucket());
3917
+ if (!new_bytes)
3918
+ return false;
3864
3919
 
3865
3920
  // Allocate new memory
3866
- char* new_data = (char*)allocator_t{}.allocate(new_bytes);
3921
+ char* new_data = (char*)allocator_t{}.allocate(new_bytes.value);
3867
3922
  if (!new_data)
3868
3923
  return false;
3869
3924
 
3870
3925
  // Initialize new buckets to empty
3871
- std::memset(new_data, 0, new_bytes);
3926
+ std::memset(new_data, 0, new_bytes.value);
3872
3927
 
3873
3928
  // Rehash and copy existing elements to new_data
3874
3929
  hash_t hasher;
@@ -3879,7 +3934,7 @@ class flat_hash_multi_set_gt {
3879
3934
 
3880
3935
  // Rehash
3881
3936
  std::size_t hash_value = hasher(old_slot.element);
3882
- std::size_t new_slot_index = hash_value & (new_slots - 1);
3937
+ std::size_t new_slot_index = hash_value & (new_slots.value - 1);
3883
3938
 
3884
3939
  // Linear probing to find an empty slot in new_data
3885
3940
  while (true) {
@@ -3889,7 +3944,7 @@ class flat_hash_multi_set_gt {
3889
3944
  new_slot.header.populated |= new_slot.mask;
3890
3945
  break;
3891
3946
  }
3892
- new_slot_index = (new_slot_index + 1) & (new_slots - 1);
3947
+ new_slot_index = (new_slot_index + 1) & (new_slots.value - 1);
3893
3948
  }
3894
3949
  }
3895
3950
 
@@ -3897,8 +3952,8 @@ class flat_hash_multi_set_gt {
3897
3952
  if (data_)
3898
3953
  allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
3899
3954
  data_ = new_data;
3900
- buckets_ = new_buckets;
3901
- capacity_slots_ = new_slots;
3955
+ buckets_ = new_buckets_checked.value;
3956
+ capacity_slots_ = new_slots.value;
3902
3957
 
3903
3958
  return true;
3904
3959
  }
@@ -3916,16 +3971,20 @@ class flat_hash_multi_set_gt {
3916
3971
  : index_(index), parent_(parent), query_(query), equals_(equals) {}
3917
3972
 
3918
3973
  // Pre-increment: advance past tombstones and non-matching entries,
3919
- // stopping at the next matching live entry or an empty slot.
3974
+ // stopping at the next matching live entry or an empty slot. When every
3975
+ // slot is either live or tombstoned (no empty slot exists), the probe
3976
+ // saturates after `capacity_slots_` steps and the iterator becomes
3977
+ // `end()` - otherwise the loop would spin forever.
3920
3978
  equal_iterator_gt& operator++() {
3921
- do {
3979
+ for (std::size_t remaining = parent_->capacity_slots_; remaining; --remaining) {
3922
3980
  index_ = (index_ + 1) & (parent_->capacity_slots_ - 1);
3923
3981
  auto slot = parent_->slot_ref(index_);
3924
3982
  bool is_empty = ~slot.header.populated & slot.mask;
3925
3983
  bool is_match = !(slot.header.deleted & slot.mask) && equals_(slot.element, query_);
3926
3984
  if (is_empty || is_match)
3927
- break;
3928
- } while (true);
3985
+ return *this;
3986
+ }
3987
+ index_ = parent_->capacity_slots_; // saturated probe -> end()
3929
3988
  return *this;
3930
3989
  }
3931
3990
 
@@ -48,7 +48,7 @@ Customize your index with additional configuration options:
48
48
  const index = new usearch.Index({
49
49
  dimensions: 128,
50
50
  metric: 'ip',
51
- quantization: 'f32',
51
+ quantization: 'f32', // or 'bf16', 'f16', 'e5m2', 'e4m3', 'e3m2', 'e2m3', 'u8', 'i8', 'b1'
52
52
  connectivity: 10,
53
53
  expansion_add: 5,
54
54
  expansion_search: 3,