usearch 2.25.1 → 2.26.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 (77) hide show
  1. package/README.md +2 -2
  2. package/c/lib.cpp +28 -13
  3. package/cpp/test.cpp +169 -0
  4. package/include/usearch/index.hpp +364 -58
  5. package/include/usearch/index_dense.hpp +155 -50
  6. package/include/usearch/index_plugins.hpp +156 -79
  7. package/javascript/lib.cpp +4 -2
  8. package/numkong/CITATION.cff +1 -1
  9. package/numkong/Cargo.toml +1 -1
  10. package/numkong/bench/bench_cross_amx.cpp +12 -0
  11. package/numkong/bench/bench_mesh.cpp +12 -0
  12. package/numkong/bench/bench_spatial.cpp +0 -3
  13. package/numkong/binding.gyp +18 -0
  14. package/numkong/build.rs +1 -0
  15. package/numkong/c/dispatch_e5m2.c +23 -3
  16. package/numkong/include/numkong/capabilities.h +1 -1
  17. package/numkong/include/numkong/cast/README.md +3 -0
  18. package/numkong/include/numkong/cast/haswell.h +28 -64
  19. package/numkong/include/numkong/cast/serial.h +17 -0
  20. package/numkong/include/numkong/cast/skylake.h +67 -52
  21. package/numkong/include/numkong/cast.h +1 -0
  22. package/numkong/include/numkong/dot/README.md +1 -0
  23. package/numkong/include/numkong/dot/haswell.h +92 -13
  24. package/numkong/include/numkong/dot/serial.h +15 -0
  25. package/numkong/include/numkong/dot/skylake.h +61 -14
  26. package/numkong/include/numkong/dots/README.md +2 -0
  27. package/numkong/include/numkong/dots/graniteamx.h +434 -0
  28. package/numkong/include/numkong/dots/haswell.h +28 -28
  29. package/numkong/include/numkong/dots/sapphireamx.h +1 -1
  30. package/numkong/include/numkong/dots/serial.h +23 -8
  31. package/numkong/include/numkong/dots/skylake.h +28 -23
  32. package/numkong/include/numkong/dots.h +12 -0
  33. package/numkong/include/numkong/each/serial.h +18 -1
  34. package/numkong/include/numkong/geospatial/serial.h +14 -3
  35. package/numkong/include/numkong/maxsim/serial.h +15 -0
  36. package/numkong/include/numkong/mesh/README.md +50 -44
  37. package/numkong/include/numkong/mesh/genoa.h +462 -0
  38. package/numkong/include/numkong/mesh/haswell.h +806 -933
  39. package/numkong/include/numkong/mesh/neon.h +871 -943
  40. package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
  41. package/numkong/include/numkong/mesh/neonfhm.h +676 -0
  42. package/numkong/include/numkong/mesh/rvv.h +404 -319
  43. package/numkong/include/numkong/mesh/serial.h +204 -162
  44. package/numkong/include/numkong/mesh/skylake.h +1029 -1585
  45. package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
  46. package/numkong/include/numkong/mesh.h +38 -0
  47. package/numkong/include/numkong/reduce/serial.h +15 -1
  48. package/numkong/include/numkong/sparse/serial.h +17 -2
  49. package/numkong/include/numkong/spatial/genoa.h +0 -68
  50. package/numkong/include/numkong/spatial/haswell.h +98 -56
  51. package/numkong/include/numkong/spatial/serial.h +15 -0
  52. package/numkong/include/numkong/spatial/skylake.h +114 -54
  53. package/numkong/include/numkong/spatial.h +0 -12
  54. package/numkong/include/numkong/spatials/graniteamx.h +128 -0
  55. package/numkong/include/numkong/spatials/serial.h +18 -1
  56. package/numkong/include/numkong/spatials/skylake.h +2 -2
  57. package/numkong/include/numkong/spatials.h +17 -0
  58. package/numkong/include/numkong/tensor.hpp +107 -23
  59. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  60. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  61. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  62. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  63. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  64. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  65. package/numkong/javascript/numkong.c +3 -2
  66. package/numkong/package.json +7 -7
  67. package/numkong/test/test.cu +841 -0
  68. package/numkong/test/test_cross_amx.cpp +13 -0
  69. package/numkong/test/test_dlpack.py +492 -0
  70. package/numkong/test/test_mesh.cpp +12 -0
  71. package/numkong/test/test_spatial.cpp +0 -3
  72. package/numkong/test/test_tensor.cpp +87 -27
  73. package/package.json +2 -2
  74. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  75. package/prebuilds/linux-arm64/usearch.node +0 -0
  76. package/prebuilds/linux-x64/usearch.node +0 -0
  77. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -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));
@@ -3713,6 +3746,8 @@ class flat_hash_multi_set_gt {
3713
3746
  char* data_ = nullptr;
3714
3747
  std::size_t buckets_ = 0;
3715
3748
  std::size_t populated_slots_ = 0;
3749
+ /// @brief Number of tombstones (slots marked deleted but not yet reclaimed)
3750
+ std::size_t deleted_slots_ = 0;
3716
3751
  /// @brief Number of slots
3717
3752
  std::size_t capacity_slots_ = 0;
3718
3753
 
@@ -3748,9 +3783,38 @@ class flat_hash_multi_set_gt {
3748
3783
  }
3749
3784
  }
3750
3785
 
3786
+ /**
3787
+ * @brief Copies every live entry of @p source into the freshly zeroed @p target.
3788
+ *
3789
+ * Tombstones are never carried over, so probe chains must be rebuilt from the hash
3790
+ * rather than reproduced slot-for-slot: a live entry displaced past a tombstone by
3791
+ * linear probing would otherwise become unreachable once that tombstone reads empty.
3792
+ * The target holds no tombstones, so an unpopulated slot is always a free slot.
3793
+ */
3794
+ void rehash_into(char* source, std::size_t source_slots, char* target, std::size_t target_slots) const noexcept {
3795
+ hash_t hasher;
3796
+ for (std::size_t i = 0; i != source_slots; ++i) {
3797
+ slot_ref_t source_slot = slot_ref(source, i);
3798
+ if (!(source_slot.header.populated & source_slot.mask) || (source_slot.header.deleted & source_slot.mask))
3799
+ continue;
3800
+
3801
+ std::size_t target_index = hasher(source_slot.element) & (target_slots - 1);
3802
+ while (true) {
3803
+ slot_ref_t target_slot = slot_ref(target, target_index);
3804
+ if (!(target_slot.header.populated & target_slot.mask)) {
3805
+ new (&target_slot.element) element_t(source_slot.element);
3806
+ target_slot.header.populated |= target_slot.mask;
3807
+ break;
3808
+ }
3809
+ target_index = (target_index + 1) & (target_slots - 1);
3810
+ }
3811
+ }
3812
+ }
3813
+
3751
3814
  public:
3752
3815
  std::size_t size() const noexcept { return populated_slots_; }
3753
- std::size_t capacity() const noexcept { return capacity_slots_; }
3816
+ std::size_t capacity() const noexcept { return capacity_slots_ * 2u / 3u; }
3817
+ std::size_t capacity_slots() const noexcept { return capacity_slots_; }
3754
3818
 
3755
3819
  flat_hash_multi_set_gt() noexcept {}
3756
3820
  ~flat_hash_multi_set_gt() noexcept { reset(); }
@@ -3764,26 +3828,22 @@ class flat_hash_multi_set_gt {
3764
3828
  }
3765
3829
 
3766
3830
  // Allocate new memory
3767
- data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
3831
+ checked_size_result_t bytes = checked_mul(other.buckets_, bytes_per_bucket());
3832
+ if (!bytes)
3833
+ usearch_raise_runtime_error("failed memory allocation");
3834
+ data_ = (char*)allocator_t{}.allocate(bytes.value);
3768
3835
  if (!data_)
3769
3836
  usearch_raise_runtime_error("failed memory allocation");
3770
3837
 
3771
- // Copy metadata
3838
+ // Copy metadata. Only live entries are rehashed below, so the copy has no tombstones.
3772
3839
  buckets_ = other.buckets_;
3773
3840
  populated_slots_ = other.populated_slots_;
3841
+ deleted_slots_ = 0;
3774
3842
  capacity_slots_ = other.capacity_slots_;
3775
3843
 
3776
3844
  // Initialize new buckets to empty
3777
3845
  std::memset(data_, 0, buckets_ * bytes_per_bucket());
3778
-
3779
- // Copy elements and bucket headers
3780
- for (std::size_t i = 0; i < capacity_slots_; ++i) {
3781
- slot_ref_t old_slot = other.slot_ref(i);
3782
- if ((old_slot.header.populated & old_slot.mask) && !(old_slot.header.deleted & old_slot.mask)) {
3783
- slot_ref_t new_slot = slot_ref(i);
3784
- populate_slot(new_slot, old_slot.element);
3785
- }
3786
- }
3846
+ rehash_into(other.data_, other.capacity_slots_, data_, capacity_slots_);
3787
3847
  }
3788
3848
 
3789
3849
  flat_hash_multi_set_gt& operator=(flat_hash_multi_set_gt const& other) {
@@ -3804,26 +3864,22 @@ class flat_hash_multi_set_gt {
3804
3864
  allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
3805
3865
 
3806
3866
  // Allocate new memory
3807
- data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
3867
+ checked_size_result_t bytes = checked_mul(other.buckets_, bytes_per_bucket());
3868
+ if (!bytes)
3869
+ usearch_raise_runtime_error("failed memory allocation");
3870
+ data_ = (char*)allocator_t{}.allocate(bytes.value);
3808
3871
  if (!data_)
3809
3872
  usearch_raise_runtime_error("failed memory allocation");
3810
3873
 
3811
- // Copy metadata
3874
+ // Copy metadata. Only live entries are rehashed below, so the copy has no tombstones.
3812
3875
  buckets_ = other.buckets_;
3813
3876
  populated_slots_ = other.populated_slots_;
3877
+ deleted_slots_ = 0;
3814
3878
  capacity_slots_ = other.capacity_slots_;
3815
3879
 
3816
3880
  // Initialize new buckets to empty
3817
3881
  std::memset(data_, 0, buckets_ * bytes_per_bucket());
3818
-
3819
- // Copy elements and bucket headers
3820
- for (std::size_t i = 0; i < capacity_slots_; ++i) {
3821
- slot_ref_t old_slot = other.slot_ref(i);
3822
- if ((old_slot.header.populated & old_slot.mask) && !(old_slot.header.deleted & old_slot.mask)) {
3823
- slot_ref_t new_slot = slot_ref(i);
3824
- populate_slot(new_slot, old_slot.element);
3825
- }
3826
- }
3882
+ rehash_into(other.data_, other.capacity_slots_, data_, capacity_slots_);
3827
3883
 
3828
3884
  return *this;
3829
3885
  }
@@ -3840,6 +3896,7 @@ class flat_hash_multi_set_gt {
3840
3896
  if (data_)
3841
3897
  std::memset(data_, 0, buckets_ * bytes_per_bucket());
3842
3898
  populated_slots_ = 0;
3899
+ deleted_slots_ = 0;
3843
3900
  }
3844
3901
 
3845
3902
  void reset() noexcept {
@@ -3849,56 +3906,66 @@ class flat_hash_multi_set_gt {
3849
3906
  data_ = nullptr;
3850
3907
  buckets_ = 0;
3851
3908
  populated_slots_ = 0;
3909
+ deleted_slots_ = 0;
3852
3910
  capacity_slots_ = 0;
3853
3911
  }
3854
3912
 
3913
+ /**
3914
+ * @brief Grows the table to fit @p capacity live entries, reclaiming tombstones.
3915
+ *
3916
+ * Tombstones are reclaimed even when no growth is requested. They only ever stop
3917
+ * being created once reclaimed, and a table with no empty slot left cannot
3918
+ * terminate a probe, silently stranding live entries.
3919
+ */
3855
3920
  bool try_reserve(std::size_t capacity) noexcept {
3856
- if (capacity * 3u <= capacity_slots_ * 2u)
3921
+ if (capacity <= this->capacity() && deleted_slots_ == 0)
3857
3922
  return true;
3858
3923
 
3859
3924
  // 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();
3925
+ checked_size_result_t scaled_capacity = checked_mul(capacity, std::size_t{3});
3926
+ if (!scaled_capacity)
3927
+ return false;
3928
+ checked_size_result_t slots_needed = checked_divide_round_up(scaled_capacity.value, std::size_t{2});
3929
+ if (!slots_needed)
3930
+ return false;
3931
+ checked_size_result_t new_slots_checked = checked_ceil2(slots_needed.value);
3932
+ if (!new_slots_checked)
3933
+ return false;
3934
+ checked_size_result_t new_buckets_checked =
3935
+ checked_divide_round_up(new_slots_checked.value, slots_per_bucket());
3936
+ if (!new_buckets_checked)
3937
+ return false;
3938
+ checked_size_result_t new_slots = checked_mul(new_buckets_checked.value, slots_per_bucket());
3939
+ if (!new_slots)
3940
+ return false;
3941
+
3942
+ // Never shrink: reclaiming tombstones alone needs no more than the current capacity
3943
+ std::size_t target_buckets = new_buckets_checked.value;
3944
+ std::size_t target_slots = new_slots.value;
3945
+ if (target_slots < capacity_slots_) {
3946
+ target_buckets = buckets_;
3947
+ target_slots = capacity_slots_;
3948
+ }
3949
+ checked_size_result_t new_bytes = checked_mul(target_buckets, bytes_per_bucket());
3950
+ if (!new_bytes)
3951
+ return false;
3864
3952
 
3865
3953
  // Allocate new memory
3866
- char* new_data = (char*)allocator_t{}.allocate(new_bytes);
3954
+ char* new_data = (char*)allocator_t{}.allocate(new_bytes.value);
3867
3955
  if (!new_data)
3868
3956
  return false;
3869
3957
 
3870
3958
  // Initialize new buckets to empty
3871
- std::memset(new_data, 0, new_bytes);
3872
-
3873
- // Rehash and copy existing elements to new_data
3874
- hash_t hasher;
3875
- for (std::size_t i = 0; i < capacity_slots_; ++i) {
3876
- slot_ref_t old_slot = slot_ref(i);
3877
- if ((~old_slot.header.populated & old_slot.mask) | (old_slot.header.deleted & old_slot.mask))
3878
- continue;
3879
-
3880
- // Rehash
3881
- std::size_t hash_value = hasher(old_slot.element);
3882
- std::size_t new_slot_index = hash_value & (new_slots - 1);
3883
-
3884
- // Linear probing to find an empty slot in new_data
3885
- while (true) {
3886
- slot_ref_t new_slot = slot_ref(new_data, new_slot_index);
3887
- if (!(new_slot.header.populated & new_slot.mask) || (new_slot.header.deleted & new_slot.mask)) {
3888
- populate_slot(new_slot, std::move(old_slot.element));
3889
- new_slot.header.populated |= new_slot.mask;
3890
- break;
3891
- }
3892
- new_slot_index = (new_slot_index + 1) & (new_slots - 1);
3893
- }
3894
- }
3959
+ std::memset(new_data, 0, new_bytes.value);
3960
+ rehash_into(data_, capacity_slots_, new_data, target_slots);
3895
3961
 
3896
3962
  // Deallocate old data and update pointers and sizes
3897
3963
  if (data_)
3898
3964
  allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
3899
3965
  data_ = new_data;
3900
- buckets_ = new_buckets;
3901
- capacity_slots_ = new_slots;
3966
+ buckets_ = target_buckets;
3967
+ capacity_slots_ = target_slots;
3968
+ deleted_slots_ = 0;
3902
3969
 
3903
3970
  return true;
3904
3971
  }
@@ -3916,16 +3983,20 @@ class flat_hash_multi_set_gt {
3916
3983
  : index_(index), parent_(parent), query_(query), equals_(equals) {}
3917
3984
 
3918
3985
  // Pre-increment: advance past tombstones and non-matching entries,
3919
- // stopping at the next matching live entry or an empty slot.
3986
+ // stopping at the next matching live entry or an empty slot. When every
3987
+ // slot is either live or tombstoned (no empty slot exists), the probe
3988
+ // saturates after `capacity_slots_` steps and the iterator becomes
3989
+ // `end()` - otherwise the loop would spin forever.
3920
3990
  equal_iterator_gt& operator++() {
3921
- do {
3991
+ for (std::size_t remaining = parent_->capacity_slots_; remaining; --remaining) {
3922
3992
  index_ = (index_ + 1) & (parent_->capacity_slots_ - 1);
3923
3993
  auto slot = parent_->slot_ref(index_);
3924
3994
  bool is_empty = ~slot.header.populated & slot.mask;
3925
3995
  bool is_match = !(slot.header.deleted & slot.mask) && equals_(slot.element, query_);
3926
3996
  if (is_empty || is_match)
3927
- break;
3928
- } while (true);
3997
+ return *this;
3998
+ }
3999
+ index_ = parent_->capacity_slots_; // saturated probe -> end()
3929
4000
  return *this;
3930
4001
  }
3931
4002
 
@@ -4023,6 +4094,7 @@ class flat_hash_multi_set_gt {
4023
4094
  // Found a match, mark as deleted
4024
4095
  slot.header.deleted |= slot.mask;
4025
4096
  --populated_slots_;
4097
+ ++deleted_slots_;
4026
4098
  popped_value = slot.element;
4027
4099
  return true; // Successfully removed
4028
4100
  }
@@ -4058,6 +4130,7 @@ class flat_hash_multi_set_gt {
4058
4130
  // Found a match, mark as deleted
4059
4131
  slot.header.deleted |= slot.mask;
4060
4132
  --populated_slots_;
4133
+ ++deleted_slots_;
4061
4134
  ++count; // Increment count of elements removed
4062
4135
  }
4063
4136
  } else {
@@ -4188,8 +4261,10 @@ class flat_hash_multi_set_gt {
4188
4261
  }
4189
4262
 
4190
4263
  bool try_emplace(element_t const& element) noexcept {
4191
- // Check if we need to resize
4192
- if (populated_slots_ * 3u >= capacity_slots_ * 2u)
4264
+ // Both live entries and tombstones consume slots a probe must walk past, so the
4265
+ // load factor counts them together. Under churn the live count alone stays flat
4266
+ // and would never trigger the rehash that reclaims the tombstones.
4267
+ if ((populated_slots_ + deleted_slots_) * 3u >= capacity_slots_ * 2u)
4193
4268
  if (!try_reserve(populated_slots_ + 1))
4194
4269
  return false;
4195
4270
 
@@ -4201,7 +4276,9 @@ class flat_hash_multi_set_gt {
4201
4276
  while (true) {
4202
4277
  slot_ref_t slot = slot_ref(slot_index);
4203
4278
  if ((~slot.header.populated & slot.mask) | (slot.header.deleted & slot.mask)) {
4204
- // Found an empty or deleted slot
4279
+ // Found an empty or deleted slot; reusing a tombstone reclaims it.
4280
+ // Read the tombstone bit before `populate_slot` clears it.
4281
+ deleted_slots_ -= (slot.header.deleted & slot.mask) != 0;
4205
4282
  populate_slot(slot, element);
4206
4283
  ++populated_slots_;
4207
4284
  return true;
@@ -320,8 +320,10 @@ Napi::Value CompiledIndex::Remove(Napi::CallbackInfo const& ctx) {
320
320
  Napi::Array results = Napi::Array::New(env, length);
321
321
  for (std::size_t i = 0; i < length; ++i) {
322
322
  auto result = native_->remove(static_cast<default_key_t>(keys[i]));
323
- if (!result)
324
- Napi::Error::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
323
+ if (!result) {
324
+ Napi::Error::New(env, result.error.release()).ThrowAsJavaScriptException();
325
+ return env.Null();
326
+ }
325
327
  results[i] = Napi::Number::New(env, result.completed);
326
328
  }
327
329
  return results;
@@ -3,7 +3,7 @@ title: "NumKong: 2000 Mixed Precision Kernels For All"
3
3
  message: "If you use this software, please cite it as below."
4
4
  type: software
5
5
  license: Apache-2.0
6
- version: 7.5.0
6
+ version: 7.6.0
7
7
  repository-code: https://github.com/ashvardanian/NumKong
8
8
  url: https://ashvardanian.com/posts/numkong/
9
9
  abstract: >-
@@ -19,7 +19,7 @@ name = "numkong"
19
19
  readme = "rust/README.md"
20
20
  repository = "https://github.com/ashvardanian/NumKong"
21
21
  rust-version = "1.64" # Introduced Core C FFI in stable Rust
22
- version = "7.5.0"
22
+ version = "7.6.0"
23
23
 
24
24
  [lib]
25
25
  name = "numkong"
@@ -58,5 +58,17 @@ void bench_cross_amx() {
58
58
  run_euclideans_packed<f16_k>("euclideans_packed_f16_graniteamx", nk_dots_packed_size_f16_graniteamx,
59
59
  nk_dots_pack_f16_graniteamx, nk_euclideans_packed_f16_graniteamx);
60
60
  run_euclideans_symmetric<f16_k>("euclideans_symmetric_f16_graniteamx", nk_euclideans_symmetric_f16_graniteamx);
61
+
62
+ run_dots_packed<e5m2_k>("dots_packed_e5m2_graniteamx", nk_dots_packed_size_e5m2_graniteamx,
63
+ nk_dots_pack_e5m2_graniteamx, nk_dots_packed_e5m2_graniteamx);
64
+ run_dots_symmetric<e5m2_k>("dots_symmetric_e5m2_graniteamx", nk_dots_symmetric_e5m2_graniteamx);
65
+
66
+ run_angulars_packed<e5m2_k>("angulars_packed_e5m2_graniteamx", nk_dots_packed_size_e5m2_graniteamx,
67
+ nk_dots_pack_e5m2_graniteamx, nk_angulars_packed_e5m2_graniteamx);
68
+ run_angulars_symmetric<e5m2_k>("angulars_symmetric_e5m2_graniteamx", nk_angulars_symmetric_e5m2_graniteamx);
69
+
70
+ run_euclideans_packed<e5m2_k>("euclideans_packed_e5m2_graniteamx", nk_dots_packed_size_e5m2_graniteamx,
71
+ nk_dots_pack_e5m2_graniteamx, nk_euclideans_packed_e5m2_graniteamx);
72
+ run_euclideans_symmetric<e5m2_k>("euclideans_symmetric_e5m2_graniteamx", nk_euclideans_symmetric_e5m2_graniteamx);
61
73
  #endif
62
74
  }
@@ -84,6 +84,12 @@ void bench_mesh() {
84
84
  run_mesh<bf16_k>("umeyama_bf16_neonbfdot", nk_umeyama_bf16_neonbfdot);
85
85
  #endif
86
86
 
87
+ #if NK_TARGET_NEONFHM
88
+ run_mesh<f16_k>("rmsd_f16_neonfhm", nk_rmsd_f16_neonfhm);
89
+ run_mesh<f16_k>("kabsch_f16_neonfhm", nk_kabsch_f16_neonfhm);
90
+ run_mesh<f16_k>("umeyama_f16_neonfhm", nk_umeyama_f16_neonfhm);
91
+ #endif
92
+
87
93
  #if NK_TARGET_HASWELL
88
94
  run_mesh<f32_k>("rmsd_f32_haswell", nk_rmsd_f32_haswell);
89
95
  run_mesh<f32_k>("kabsch_f32_haswell", nk_kabsch_f32_haswell);
@@ -114,6 +120,12 @@ void bench_mesh() {
114
120
  run_mesh<bf16_k>("umeyama_bf16_skylake", nk_umeyama_bf16_skylake);
115
121
  #endif
116
122
 
123
+ #if NK_TARGET_GENOA
124
+ run_mesh<bf16_k>("rmsd_bf16_genoa", nk_rmsd_bf16_genoa);
125
+ run_mesh<bf16_k>("kabsch_bf16_genoa", nk_kabsch_bf16_genoa);
126
+ run_mesh<bf16_k>("umeyama_bf16_genoa", nk_umeyama_bf16_genoa);
127
+ #endif
128
+
117
129
  #if NK_TARGET_RVV
118
130
  run_mesh<f32_k>("rmsd_f32_rvv", nk_rmsd_f32_rvv);
119
131
  run_mesh<f32_k>("kabsch_f32_rvv", nk_kabsch_f32_rvv);
@@ -232,9 +232,6 @@ void bench_spatial() {
232
232
  run_dense<bf16_k, f32_k>("angular_bf16_genoa", nk_angular_bf16_genoa);
233
233
  run_dense<bf16_k, f32_k>("sqeuclidean_bf16_genoa", nk_sqeuclidean_bf16_genoa);
234
234
  run_dense<bf16_k, f32_k>("euclidean_bf16_genoa", nk_euclidean_bf16_genoa);
235
- run_dense<e5m2_k, f32_k>("angular_e5m2_genoa", nk_angular_e5m2_genoa);
236
- run_dense<e5m2_k, f32_k>("sqeuclidean_e5m2_genoa", nk_sqeuclidean_e5m2_genoa);
237
- run_dense<e5m2_k, f32_k>("euclidean_e5m2_genoa", nk_euclidean_e5m2_genoa);
238
235
  #endif
239
236
 
240
237
  #if NK_TARGET_DIAMOND
@@ -70,6 +70,7 @@
70
70
  },
71
71
  "conditions": [
72
72
  # Pin TU baseline to each arch's ABI floor; SIMD kernels use per-function pragmas.
73
+ # Keep per-arch table in sync with cmake/nk_compiler_flags.cmake, build.rs, setup.py.
73
74
  [
74
75
  "OS!='win' and target_arch=='arm64'",
75
76
  {
@@ -94,6 +95,23 @@
94
95
  ]
95
96
  }
96
97
  ],
98
+ [
99
+ "OS!='win' and target_arch=='ppc64'",
100
+ {
101
+ "cflags": [
102
+ "-mcpu=power8"
103
+ ]
104
+ }
105
+ ],
106
+ [
107
+ "OS!='win' and target_arch=='loong64'",
108
+ {
109
+ "cflags": [
110
+ "-march=loongarch64",
111
+ "-mlasx"
112
+ ]
113
+ }
114
+ ],
97
115
  # Forbid auto-vectorization so serial fallbacks don't get silently
98
116
  # promoted to NEON/SSE2/VSX. SIMD kernels use explicit intrinsics
99
117
  # and per-function `target` pragmas; unaffected. MSVC has no
package/numkong/build.rs CHANGED
@@ -376,6 +376,7 @@ fn build_numkong() -> Result<HashMap<String, bool>, String> {
376
376
 
377
377
  // Pin TU baseline to each arch's ABI floor; SIMD kernels carry per-function pragmas.
378
378
  // `NK_MARCH_NATIVE=1` opts into a host-tuned, non-portable build (ignored on MSVC).
379
+ // Keep per-arch table in sync with cmake/nk_compiler_flags.cmake, setup.py, binding.gyp.
379
380
  let march_native = env::var("NK_MARCH_NATIVE").is_ok_and(|v| v == "1" || v == "true");
380
381
  // Portable baseline: pin TU ISA floor + forbid auto-vectorization so serial
381
382
  // fallbacks don't get silently promoted to NEON/SSE2/VSX. SIMD kernels use
@@ -113,6 +113,29 @@ void nk_dispatch_e5m2_find_(nk_capability_t v, nk_kernel_kind_t k, nk_kernel_pun
113
113
  default: break;
114
114
  }
115
115
  #endif
116
+ #if NK_TARGET_GRANITEAMX
117
+ if (v & nk_cap_graniteamx_k) switch (k) {
118
+ case nk_kernel_dots_packed_size_k:
119
+ *m = (m_t)&nk_dots_packed_size_e5m2_graniteamx, *c = nk_cap_graniteamx_k;
120
+ return;
121
+ case nk_kernel_dots_pack_k: *m = (m_t)&nk_dots_pack_e5m2_graniteamx, *c = nk_cap_graniteamx_k; return;
122
+ case nk_kernel_dots_packed_k: *m = (m_t)&nk_dots_packed_e5m2_graniteamx, *c = nk_cap_graniteamx_k; return;
123
+ case nk_kernel_angulars_packed_k:
124
+ *m = (m_t)&nk_angulars_packed_e5m2_graniteamx, *c = nk_cap_graniteamx_k;
125
+ return;
126
+ case nk_kernel_euclideans_packed_k:
127
+ *m = (m_t)&nk_euclideans_packed_e5m2_graniteamx, *c = nk_cap_graniteamx_k;
128
+ return;
129
+ case nk_kernel_dots_symmetric_k: *m = (m_t)&nk_dots_symmetric_e5m2_graniteamx, *c = nk_cap_graniteamx_k; return;
130
+ case nk_kernel_angulars_symmetric_k:
131
+ *m = (m_t)&nk_angulars_symmetric_e5m2_graniteamx, *c = nk_cap_graniteamx_k;
132
+ return;
133
+ case nk_kernel_euclideans_symmetric_k:
134
+ *m = (m_t)&nk_euclideans_symmetric_e5m2_graniteamx, *c = nk_cap_graniteamx_k;
135
+ return;
136
+ default: break;
137
+ }
138
+ #endif
116
139
  #if NK_TARGET_SAPPHIREAMX
117
140
  if (v & nk_cap_sapphireamx_k) switch (k) {
118
141
  case nk_kernel_dots_packed_size_k:
@@ -162,9 +185,6 @@ void nk_dispatch_e5m2_find_(nk_capability_t v, nk_kernel_kind_t k, nk_kernel_pun
162
185
  #if NK_TARGET_GENOA
163
186
  if (v & nk_cap_genoa_k) switch (k) {
164
187
  case nk_kernel_dot_k: *m = (m_t)&nk_dot_e5m2_genoa, *c = nk_cap_genoa_k; return;
165
- case nk_kernel_euclidean_k: *m = (m_t)&nk_euclidean_e5m2_genoa, *c = nk_cap_genoa_k; return;
166
- case nk_kernel_sqeuclidean_k: *m = (m_t)&nk_sqeuclidean_e5m2_genoa, *c = nk_cap_genoa_k; return;
167
- case nk_kernel_angular_k: *m = (m_t)&nk_angular_e5m2_genoa, *c = nk_cap_genoa_k; return;
168
188
  case nk_kernel_dots_packed_size_k: *m = (m_t)&nk_dots_packed_size_e5m2_genoa, *c = nk_cap_genoa_k; return;
169
189
  case nk_kernel_dots_pack_k: *m = (m_t)&nk_dots_pack_e5m2_genoa, *c = nk_cap_genoa_k; return;
170
190
  case nk_kernel_dots_packed_k: *m = (m_t)&nk_dots_packed_e5m2_genoa, *c = nk_cap_genoa_k; return;
@@ -95,7 +95,7 @@
95
95
  #include "numkong/types.h" // `nk_u64_t`, `NK_DEFINED_LINUX_`
96
96
 
97
97
  #define NK_VERSION_MAJOR 7
98
- #define NK_VERSION_MINOR 5
98
+ #define NK_VERSION_MINOR 6
99
99
  #define NK_VERSION_PATCH 0
100
100
 
101
101
  /**
@@ -93,6 +93,9 @@ NEON backend uses `vreinterpretq_u16_u8` + `vzip` for zero-extension; Haswell us
93
93
  `nk_f16_to_f32_haswell`, `nk_f32_to_f16_haswell` use the F16C extension instructions `VCVTPH2PS` / `VCVTPS2PH` — single-instruction conversion of 8 elements with correct denormal handling, NaN propagation, and RNE rounding.
94
94
  The serial fallback (`nk_f16_to_f32_serial`) must handle denormals via explicit exponent/mantissa extraction and conditional re-normalization — ~15 integer ops per element vs 1 instruction with F16C.
95
95
  AVX-512 (`nk_cast_skylake`) doubles throughput to 16 elements per instruction.
96
+ F16C also unlocks a cheaper FP8 → F32 path that bypasses i32-lane bit math: `nk_e5m2x16_to_f32x16_skylake_` and `nk_e5m2x8_to_f32x8_haswell_` widen u8 → u16 and left-shift by 8 (E5M2 shares F16's bias 15, so the result is a bit-exact F16 encoding of every input including subnormals and NaN), then feed `VCVTPH2PS` — three ops total.
97
+ E4M3 can't use a plain shift (bias 7 vs 15), but the Giesen-style fake-F16 `((byte & 0x7F) << 7) | ((byte & 0x80) << 8)` gives an F16 whose value differs from the E4M3 magnitude by exactly 2⁸; `nk_e4m3x16_to_f32x16_skylake_` and `nk_e4m3x8_to_f32x8_haswell_` widen through `VCVTPH2PS`, multiply by 256 in F32 to correct, and blend in F32 NaN for the lone `|byte|==0x7F` encoding.
98
+ For E4M3 GEMM specifically, `nk_e4m3x16_to_f16x16_skylake_` produces TRUE F16 (bias-corrected, with a small subnormal LUT and NaN blend) so the packed buffer stores 2 bytes/element instead of 4 — the inner loop reads F16 and widens to F32 once per B-load, trading ~10% compute for 50% pack memory.
96
99
 
97
100
  ## Performance
98
101