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.
- package/README.md +2 -2
- package/c/lib.cpp +28 -13
- package/cpp/test.cpp +169 -0
- package/include/usearch/index.hpp +364 -58
- package/include/usearch/index_dense.hpp +155 -50
- package/include/usearch/index_plugins.hpp +156 -79
- package/javascript/lib.cpp +4 -2
- package/numkong/CITATION.cff +1 -1
- package/numkong/Cargo.toml +1 -1
- package/numkong/bench/bench_cross_amx.cpp +12 -0
- package/numkong/bench/bench_mesh.cpp +12 -0
- package/numkong/bench/bench_spatial.cpp +0 -3
- package/numkong/binding.gyp +18 -0
- package/numkong/build.rs +1 -0
- package/numkong/c/dispatch_e5m2.c +23 -3
- package/numkong/include/numkong/capabilities.h +1 -1
- package/numkong/include/numkong/cast/README.md +3 -0
- package/numkong/include/numkong/cast/haswell.h +28 -64
- package/numkong/include/numkong/cast/serial.h +17 -0
- package/numkong/include/numkong/cast/skylake.h +67 -52
- package/numkong/include/numkong/cast.h +1 -0
- package/numkong/include/numkong/dot/README.md +1 -0
- package/numkong/include/numkong/dot/haswell.h +92 -13
- package/numkong/include/numkong/dot/serial.h +15 -0
- package/numkong/include/numkong/dot/skylake.h +61 -14
- package/numkong/include/numkong/dots/README.md +2 -0
- package/numkong/include/numkong/dots/graniteamx.h +434 -0
- package/numkong/include/numkong/dots/haswell.h +28 -28
- package/numkong/include/numkong/dots/sapphireamx.h +1 -1
- package/numkong/include/numkong/dots/serial.h +23 -8
- package/numkong/include/numkong/dots/skylake.h +28 -23
- package/numkong/include/numkong/dots.h +12 -0
- package/numkong/include/numkong/each/serial.h +18 -1
- package/numkong/include/numkong/geospatial/serial.h +14 -3
- package/numkong/include/numkong/maxsim/serial.h +15 -0
- package/numkong/include/numkong/mesh/README.md +50 -44
- package/numkong/include/numkong/mesh/genoa.h +462 -0
- package/numkong/include/numkong/mesh/haswell.h +806 -933
- package/numkong/include/numkong/mesh/neon.h +871 -943
- package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
- package/numkong/include/numkong/mesh/neonfhm.h +676 -0
- package/numkong/include/numkong/mesh/rvv.h +404 -319
- package/numkong/include/numkong/mesh/serial.h +204 -162
- package/numkong/include/numkong/mesh/skylake.h +1029 -1585
- package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
- package/numkong/include/numkong/mesh.h +38 -0
- package/numkong/include/numkong/reduce/serial.h +15 -1
- package/numkong/include/numkong/sparse/serial.h +17 -2
- package/numkong/include/numkong/spatial/genoa.h +0 -68
- package/numkong/include/numkong/spatial/haswell.h +98 -56
- package/numkong/include/numkong/spatial/serial.h +15 -0
- package/numkong/include/numkong/spatial/skylake.h +114 -54
- package/numkong/include/numkong/spatial.h +0 -12
- package/numkong/include/numkong/spatials/graniteamx.h +128 -0
- package/numkong/include/numkong/spatials/serial.h +18 -1
- package/numkong/include/numkong/spatials/skylake.h +2 -2
- package/numkong/include/numkong/spatials.h +17 -0
- package/numkong/include/numkong/tensor.hpp +107 -23
- package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
- package/numkong/javascript/numkong.c +3 -2
- package/numkong/package.json +7 -7
- package/numkong/test/test.cu +841 -0
- package/numkong/test/test_cross_amx.cpp +13 -0
- package/numkong/test/test_dlpack.py +492 -0
- package/numkong/test/test_mesh.cpp +12 -0
- package/numkong/test/test_spatial.cpp +0 -3
- package/numkong/test/test_tensor.cpp +87 -27
- package/package.json +2 -2
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
- 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
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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_
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3921
|
+
if (capacity <= this->capacity() && deleted_slots_ == 0)
|
|
3857
3922
|
return true;
|
|
3858
3923
|
|
|
3859
3924
|
// Calculate new sizes
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
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_ =
|
|
3901
|
-
capacity_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
|
-
|
|
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
|
-
|
|
3928
|
-
}
|
|
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
|
-
//
|
|
4192
|
-
|
|
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;
|
package/javascript/lib.cpp
CHANGED
|
@@ -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(
|
|
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;
|
package/numkong/CITATION.cff
CHANGED
|
@@ -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.
|
|
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: >-
|
package/numkong/Cargo.toml
CHANGED
|
@@ -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
|
package/numkong/binding.gyp
CHANGED
|
@@ -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;
|
|
@@ -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
|
|