usearch 0.20.0 → 0.21.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/.github/workflows/prerelease.yml +27 -0
- package/Cargo.toml +1 -1
- package/README.md +20 -1
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +6 -2
- package/cpp/bench.cpp +41 -4
- package/docs/benchmarks.md +14 -12
- package/include/usearch/index.hpp +475 -56
- package/include/usearch/index_punned_dense.hpp +68 -10
- package/include/usearch/index_punned_helpers.hpp +28 -3
- package/package.json +1 -1
- package/python/lib.cpp +57 -5
- package/python/scripts/join.py +151 -0
- package/python/scripts/test.py +19 -0
- package/python/usearch/eval.py +11 -3
- package/python/usearch/index.py +119 -10
- package/python/usearch/join.py +0 -98
|
@@ -258,10 +258,13 @@ template <typename scalar_at> class span_gt {
|
|
|
258
258
|
std::size_t size_;
|
|
259
259
|
|
|
260
260
|
public:
|
|
261
|
-
span_gt(
|
|
261
|
+
span_gt() noexcept : data_(nullptr), size_(0u) {}
|
|
262
|
+
span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
|
|
262
263
|
span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
|
|
263
264
|
scalar_at* data() const noexcept { return data_; }
|
|
264
265
|
std::size_t size() const noexcept { return size_; }
|
|
266
|
+
scalar_at* begin() const noexcept { return data_; }
|
|
267
|
+
scalar_at* end() const noexcept { return data_ + size_; }
|
|
265
268
|
operator scalar_at*() const noexcept { return data(); }
|
|
266
269
|
};
|
|
267
270
|
|
|
@@ -617,8 +620,8 @@ template <typename result_at> struct expected_gt {
|
|
|
617
620
|
}
|
|
618
621
|
result_at const& operator*() const noexcept { return result; }
|
|
619
622
|
explicit operator bool() const noexcept { return !error; }
|
|
620
|
-
expected_gt failed(
|
|
621
|
-
error = message;
|
|
623
|
+
expected_gt failed(error_t message) noexcept {
|
|
624
|
+
error = std::move(message);
|
|
622
625
|
return std::move(*this);
|
|
623
626
|
}
|
|
624
627
|
};
|
|
@@ -1007,7 +1010,7 @@ class usearch_pack_m uint40_t {
|
|
|
1007
1010
|
|
|
1008
1011
|
inline uint40_t& operator+=(std::size_t n) noexcept {
|
|
1009
1012
|
unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
|
|
1010
|
-
std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(
|
|
1013
|
+
std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(n_octets);
|
|
1011
1014
|
std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
|
|
1012
1015
|
octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
|
|
1013
1016
|
tail += n_tail;
|
|
@@ -1042,6 +1045,89 @@ class usearch_pack_m uint40_t {
|
|
|
1042
1045
|
|
|
1043
1046
|
static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
|
|
1044
1047
|
|
|
1048
|
+
template <typename element_at, typename allocator_at = std::allocator<element_at>> //
|
|
1049
|
+
class ring_gt {
|
|
1050
|
+
public:
|
|
1051
|
+
using element_t = element_at;
|
|
1052
|
+
using allocator_t = allocator_at;
|
|
1053
|
+
|
|
1054
|
+
static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
|
|
1055
|
+
static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
|
|
1056
|
+
|
|
1057
|
+
using value_type = element_t;
|
|
1058
|
+
|
|
1059
|
+
private:
|
|
1060
|
+
element_t* elements_;
|
|
1061
|
+
std::size_t capacity_;
|
|
1062
|
+
std::size_t head_;
|
|
1063
|
+
std::size_t tail_;
|
|
1064
|
+
bool empty_;
|
|
1065
|
+
allocator_t allocator_;
|
|
1066
|
+
|
|
1067
|
+
public:
|
|
1068
|
+
explicit ring_gt(allocator_t const& alloc = allocator_t()) noexcept
|
|
1069
|
+
: elements_(nullptr), capacity_(0), head_(0), tail_(0), empty_(true), allocator_(alloc) {}
|
|
1070
|
+
|
|
1071
|
+
ring_gt(ring_gt const&) = delete;
|
|
1072
|
+
ring_gt& operator=(ring_gt const&) = delete;
|
|
1073
|
+
|
|
1074
|
+
~ring_gt() noexcept { reset(); }
|
|
1075
|
+
|
|
1076
|
+
bool empty() const noexcept { return empty_; }
|
|
1077
|
+
size_t size() const noexcept {
|
|
1078
|
+
if (empty_)
|
|
1079
|
+
return 0;
|
|
1080
|
+
else if (head_ >= tail_)
|
|
1081
|
+
return head_ - tail_;
|
|
1082
|
+
else
|
|
1083
|
+
return capacity_ - (tail_ - head_);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
void reset() noexcept {
|
|
1087
|
+
if (elements_)
|
|
1088
|
+
allocator_.deallocate(elements_, capacity_);
|
|
1089
|
+
elements_ = nullptr;
|
|
1090
|
+
capacity_ = 0;
|
|
1091
|
+
head_ = 0;
|
|
1092
|
+
tail_ = 0;
|
|
1093
|
+
empty_ = true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
bool resize(std::size_t n) noexcept {
|
|
1097
|
+
element_t* elements = allocator_.allocate(n);
|
|
1098
|
+
if (!elements)
|
|
1099
|
+
return false;
|
|
1100
|
+
reset();
|
|
1101
|
+
elements_ = elements;
|
|
1102
|
+
capacity_ = n;
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
void push(element_t const& value) noexcept {
|
|
1107
|
+
elements_[head_] = value;
|
|
1108
|
+
head_ = (head_ + 1) % capacity_;
|
|
1109
|
+
empty_ = false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
bool try_push(element_t const& value) noexcept {
|
|
1113
|
+
if (head_ == tail_ && !empty_)
|
|
1114
|
+
return false; // elements_ is full
|
|
1115
|
+
|
|
1116
|
+
return push(value);
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
bool try_pop(element_t& value) noexcept {
|
|
1121
|
+
if (empty_)
|
|
1122
|
+
return false;
|
|
1123
|
+
|
|
1124
|
+
value = std::move(elements_[tail_]);
|
|
1125
|
+
tail_ = (tail_ + 1) % capacity_;
|
|
1126
|
+
empty_ = head_ == tail_;
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1045
1131
|
/// @brief Number of neighbors per graph node.
|
|
1046
1132
|
/// Defaults to 32 in FAISS and 16 in hnswlib.
|
|
1047
1133
|
/// > It is called `M` in the paper.
|
|
@@ -1123,6 +1209,21 @@ struct search_config_t {
|
|
|
1123
1209
|
bool exact = false;
|
|
1124
1210
|
};
|
|
1125
1211
|
|
|
1212
|
+
struct copy_config_t {
|
|
1213
|
+
bool copy_vectors = true;
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
struct join_config_t {
|
|
1217
|
+
/// @brief Controls maximum number of proposals per man during stable marriage.
|
|
1218
|
+
std::size_t max_proposals = 0;
|
|
1219
|
+
/// @brief Hyper-parameter controlling the quality of search.
|
|
1220
|
+
/// Defaults to 16 in FAISS and 10 in hnswlib.
|
|
1221
|
+
/// > It is called `ef` in the paper.
|
|
1222
|
+
std::size_t expansion = default_expansion_search();
|
|
1223
|
+
/// @brief Brute-forces exhaustive search over all entries in the index.
|
|
1224
|
+
bool exact = false;
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1126
1227
|
using file_header_t = byte_t[64];
|
|
1127
1228
|
|
|
1128
1229
|
/**
|
|
@@ -1227,8 +1328,8 @@ struct file_head_result_t {
|
|
|
1227
1328
|
error_t error;
|
|
1228
1329
|
|
|
1229
1330
|
explicit operator bool() const noexcept { return !error; }
|
|
1230
|
-
file_head_result_t failed(
|
|
1231
|
-
error = message;
|
|
1331
|
+
file_head_result_t failed(error_t message) noexcept {
|
|
1332
|
+
error = std::move(message);
|
|
1232
1333
|
return std::move(*this);
|
|
1233
1334
|
}
|
|
1234
1335
|
};
|
|
@@ -1262,6 +1363,37 @@ struct viewed_file_t {
|
|
|
1262
1363
|
};
|
|
1263
1364
|
#endif
|
|
1264
1365
|
|
|
1366
|
+
struct dummy_predicate_t {
|
|
1367
|
+
template <typename match_at> constexpr bool operator()(match_at&&) const noexcept { return true; }
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
struct dummy_progress_t {
|
|
1371
|
+
inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
struct dummy_executor_t {
|
|
1375
|
+
dummy_executor_t() noexcept {}
|
|
1376
|
+
std::size_t size() const noexcept { return 1; }
|
|
1377
|
+
|
|
1378
|
+
template <typename thread_aware_function_at>
|
|
1379
|
+
void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1380
|
+
for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
|
|
1381
|
+
thread_aware_function(0, task_idx);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
template <typename thread_aware_function_at>
|
|
1385
|
+
void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1386
|
+
thread_aware_function(0);
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
struct dummy_label_to_label_mapping_t {
|
|
1391
|
+
struct member_ref_t {
|
|
1392
|
+
template <typename label_at> member_ref_t& operator=(label_at&&) noexcept { return *this; }
|
|
1393
|
+
};
|
|
1394
|
+
template <typename label_at> member_ref_t operator[](label_at&&) const noexcept { return {}; }
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1265
1397
|
/**
|
|
1266
1398
|
* @brief Approximate Nearest Neighbors Search index using the
|
|
1267
1399
|
* Hierarchical Navigable Small World graph algorithm.
|
|
@@ -1552,7 +1684,7 @@ class index_gt {
|
|
|
1552
1684
|
index_config_t config_{};
|
|
1553
1685
|
index_limits_t limits_{};
|
|
1554
1686
|
metric_t metric_{};
|
|
1555
|
-
allocator_t allocator_{};
|
|
1687
|
+
mutable allocator_t allocator_{};
|
|
1556
1688
|
point_allocator_t point_allocator_{};
|
|
1557
1689
|
precomputed_constants_t pre_{};
|
|
1558
1690
|
viewed_file_t viewed_file_{};
|
|
@@ -1606,6 +1738,35 @@ class index_gt {
|
|
|
1606
1738
|
return *this;
|
|
1607
1739
|
}
|
|
1608
1740
|
|
|
1741
|
+
struct copy_result_t {
|
|
1742
|
+
error_t error;
|
|
1743
|
+
index_gt index;
|
|
1744
|
+
|
|
1745
|
+
explicit operator bool() const noexcept { return !error; }
|
|
1746
|
+
copy_result_t failed(error_t message) noexcept {
|
|
1747
|
+
error = std::move(message);
|
|
1748
|
+
return std::move(*this);
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
copy_result_t copy(copy_config_t /*config*/ = {}) const noexcept {
|
|
1753
|
+
copy_result_t result;
|
|
1754
|
+
index_gt& other = result.index;
|
|
1755
|
+
other = index_gt(config_, metric_, allocator_, point_allocator_);
|
|
1756
|
+
if (!other.reserve(limits_))
|
|
1757
|
+
return result.failed("Failed to reserve the contexts");
|
|
1758
|
+
|
|
1759
|
+
// Now all is left - is to allocate new `node_t` instances and populate
|
|
1760
|
+
// the `other.nodes_` array into it.
|
|
1761
|
+
for (std::size_t i = 0; i != size_; ++i)
|
|
1762
|
+
other.nodes_[i] = other.node_make_copy_(node_bytes_split_(nodes_[i]));
|
|
1763
|
+
|
|
1764
|
+
other.size_ = size_.load();
|
|
1765
|
+
other.max_level_ = max_level_;
|
|
1766
|
+
other.entry_id_ = entry_id_;
|
|
1767
|
+
return result;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1609
1770
|
member_citerator_t cbegin() const noexcept { return {this, 0}; }
|
|
1610
1771
|
member_citerator_t cend() const noexcept { return {this, size()}; }
|
|
1611
1772
|
member_citerator_t begin() const noexcept { return {this, 0}; }
|
|
@@ -1657,11 +1818,13 @@ class index_gt {
|
|
|
1657
1818
|
std::swap(limits_, other.limits_);
|
|
1658
1819
|
std::swap(metric_, other.metric_);
|
|
1659
1820
|
std::swap(allocator_, other.allocator_);
|
|
1821
|
+
std::swap(point_allocator_, other.point_allocator_);
|
|
1660
1822
|
std::swap(pre_, other.pre_);
|
|
1661
1823
|
std::swap(viewed_file_, other.viewed_file_);
|
|
1662
1824
|
std::swap(max_level_, other.max_level_);
|
|
1663
1825
|
std::swap(entry_id_, other.entry_id_);
|
|
1664
1826
|
std::swap(nodes_, other.nodes_);
|
|
1827
|
+
std::swap(nodes_mutexes_, other.nodes_mutexes_);
|
|
1665
1828
|
std::swap(contexts_, other.contexts_);
|
|
1666
1829
|
|
|
1667
1830
|
// Non-atomic parts.
|
|
@@ -1760,8 +1923,8 @@ class index_gt {
|
|
|
1760
1923
|
id_t id{};
|
|
1761
1924
|
|
|
1762
1925
|
explicit operator bool() const noexcept { return !error; }
|
|
1763
|
-
add_result_t failed(
|
|
1764
|
-
error = message;
|
|
1926
|
+
add_result_t failed(error_t message) noexcept {
|
|
1927
|
+
error = std::move(message);
|
|
1765
1928
|
return std::move(*this);
|
|
1766
1929
|
}
|
|
1767
1930
|
};
|
|
@@ -1793,14 +1956,17 @@ class index_gt {
|
|
|
1793
1956
|
inline search_result_t& operator=(search_result_t&&) = default;
|
|
1794
1957
|
|
|
1795
1958
|
explicit operator bool() const noexcept { return !error; }
|
|
1796
|
-
search_result_t failed(
|
|
1797
|
-
error = message;
|
|
1959
|
+
search_result_t failed(error_t message) noexcept {
|
|
1960
|
+
error = std::move(message);
|
|
1798
1961
|
return std::move(*this);
|
|
1799
1962
|
}
|
|
1800
1963
|
|
|
1801
1964
|
inline operator std::size_t() const noexcept { return count; }
|
|
1802
1965
|
inline std::size_t size() const noexcept { return count; }
|
|
1966
|
+
inline bool empty() const noexcept { return !count; }
|
|
1803
1967
|
inline match_t operator[](std::size_t i) const noexcept { return at(i); }
|
|
1968
|
+
inline match_t front() const noexcept { return at(0); }
|
|
1969
|
+
inline match_t back() const noexcept { return at(count - 1); }
|
|
1804
1970
|
inline bool contains(label_t label) const noexcept {
|
|
1805
1971
|
for (std::size_t i = 0; i != count; ++i)
|
|
1806
1972
|
if (at(i).member.label == label)
|
|
@@ -1866,7 +2032,7 @@ class index_gt {
|
|
|
1866
2032
|
new_level_lock.unlock();
|
|
1867
2033
|
|
|
1868
2034
|
// Allocate the neighbors
|
|
1869
|
-
node_t node =
|
|
2035
|
+
node_t node = node_make_(label, vector, target_level, config.store_vector);
|
|
1870
2036
|
if (!node)
|
|
1871
2037
|
return result.failed("Out of memory!");
|
|
1872
2038
|
std::size_t old_size = size_.fetch_add(1);
|
|
@@ -1910,10 +2076,6 @@ class index_gt {
|
|
|
1910
2076
|
return result;
|
|
1911
2077
|
}
|
|
1912
2078
|
|
|
1913
|
-
struct dummy_predicate_t {
|
|
1914
|
-
constexpr bool operator()(match_t const&) const noexcept { return true; }
|
|
1915
|
-
};
|
|
1916
|
-
|
|
1917
2079
|
/**
|
|
1918
2080
|
* @brief Searches for the closest elements to the given ::query. Thread-safe.
|
|
1919
2081
|
*
|
|
@@ -2083,8 +2245,8 @@ class index_gt {
|
|
|
2083
2245
|
error_t error;
|
|
2084
2246
|
|
|
2085
2247
|
explicit operator bool() const noexcept { return !error; }
|
|
2086
|
-
serialization_result_t failed(
|
|
2087
|
-
error = message;
|
|
2248
|
+
serialization_result_t failed(error_t message) noexcept {
|
|
2249
|
+
error = std::move(message);
|
|
2088
2250
|
return std::move(*this);
|
|
2089
2251
|
}
|
|
2090
2252
|
};
|
|
@@ -2094,7 +2256,8 @@ class index_gt {
|
|
|
2094
2256
|
* co-locating vectors and neighbors lists.
|
|
2095
2257
|
* Available on Linux, MacOS, Windows.
|
|
2096
2258
|
*/
|
|
2097
|
-
|
|
2259
|
+
template <typename progress_at = dummy_progress_t>
|
|
2260
|
+
serialization_result_t save(char const* file_path, progress_at&& progress = {}) const noexcept {
|
|
2098
2261
|
|
|
2099
2262
|
// Make sure we have right to write to that file
|
|
2100
2263
|
serialization_result_t result;
|
|
@@ -2164,6 +2327,7 @@ class index_gt {
|
|
|
2164
2327
|
write_chunk(node.vector(), node_vector_bytes);
|
|
2165
2328
|
if (result.error)
|
|
2166
2329
|
return result;
|
|
2330
|
+
progress(i, state.size);
|
|
2167
2331
|
}
|
|
2168
2332
|
|
|
2169
2333
|
std::fclose(file);
|
|
@@ -2175,7 +2339,8 @@ class index_gt {
|
|
|
2175
2339
|
* copying both vectors and neighbors lists into RAM.
|
|
2176
2340
|
* Available on Linux, MacOS, Windows.
|
|
2177
2341
|
*/
|
|
2178
|
-
|
|
2342
|
+
template <typename progress_at = dummy_progress_t>
|
|
2343
|
+
serialization_result_t load(char const* file_path, progress_at&& progress = {}) noexcept {
|
|
2179
2344
|
|
|
2180
2345
|
serialization_result_t result;
|
|
2181
2346
|
file_header_t state_buffer{};
|
|
@@ -2223,7 +2388,8 @@ class index_gt {
|
|
|
2223
2388
|
}
|
|
2224
2389
|
|
|
2225
2390
|
// Load nodes one by one
|
|
2226
|
-
|
|
2391
|
+
std::size_t const size = size_;
|
|
2392
|
+
for (std::size_t i = 0; i != size; ++i) {
|
|
2227
2393
|
label_t label;
|
|
2228
2394
|
dim_t dim;
|
|
2229
2395
|
level_t level;
|
|
@@ -2238,11 +2404,15 @@ class index_gt {
|
|
|
2238
2404
|
return result;
|
|
2239
2405
|
|
|
2240
2406
|
std::size_t node_bytes = node_bytes_(dim, level);
|
|
2241
|
-
node_t node = node_malloc_(
|
|
2407
|
+
node_t node = node_malloc_(dim, level);
|
|
2408
|
+
node.label(label);
|
|
2409
|
+
node.dim(dim);
|
|
2410
|
+
node.level(level);
|
|
2242
2411
|
read_chunk(node.tape() + node_head_bytes_(), node_bytes - node_head_bytes_());
|
|
2243
2412
|
if (result.error)
|
|
2244
2413
|
return result;
|
|
2245
2414
|
nodes_[i] = node;
|
|
2415
|
+
progress(i, size);
|
|
2246
2416
|
}
|
|
2247
2417
|
|
|
2248
2418
|
std::fclose(file);
|
|
@@ -2255,7 +2425,8 @@ class index_gt {
|
|
|
2255
2425
|
* @b without copying the vectors and neighbors lists into RAM.
|
|
2256
2426
|
* Available on Linux, MacOS, but @b not on Windows.
|
|
2257
2427
|
*/
|
|
2258
|
-
|
|
2428
|
+
template <typename progress_at = dummy_progress_t>
|
|
2429
|
+
serialization_result_t view(char const* file_path, progress_at&& progress = {}) noexcept {
|
|
2259
2430
|
serialization_result_t result;
|
|
2260
2431
|
|
|
2261
2432
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
@@ -2337,16 +2508,18 @@ class index_gt {
|
|
|
2337
2508
|
}
|
|
2338
2509
|
|
|
2339
2510
|
// Locate every node packed into file
|
|
2340
|
-
std::size_t
|
|
2341
|
-
|
|
2342
|
-
|
|
2511
|
+
std::size_t progress_bytes = sizeof(file_header_t);
|
|
2512
|
+
std::size_t const size = size_;
|
|
2513
|
+
for (std::size_t i = 0; i != size; ++i) {
|
|
2514
|
+
byte_t* tape = (byte_t*)(file + progress_bytes);
|
|
2343
2515
|
dim_t dim = misaligned_load<dim_t>(tape + sizeof(label_t));
|
|
2344
2516
|
level_t level = misaligned_load<level_t>(tape + sizeof(label_t) + sizeof(dim_t));
|
|
2345
2517
|
|
|
2346
2518
|
std::size_t node_bytes = node_bytes_(dim, level);
|
|
2347
2519
|
std::size_t node_vector_bytes = dim * sizeof(scalar_t);
|
|
2348
2520
|
nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
|
|
2349
|
-
|
|
2521
|
+
progress_bytes += node_bytes;
|
|
2522
|
+
progress(i, size);
|
|
2350
2523
|
}
|
|
2351
2524
|
|
|
2352
2525
|
return {};
|
|
@@ -2354,7 +2527,224 @@ class index_gt {
|
|
|
2354
2527
|
|
|
2355
2528
|
#pragma endregion
|
|
2356
2529
|
|
|
2530
|
+
struct join_result_t {
|
|
2531
|
+
error_t error{};
|
|
2532
|
+
std::size_t intersection_size{};
|
|
2533
|
+
std::size_t engagements{};
|
|
2534
|
+
std::size_t cycles{};
|
|
2535
|
+
std::size_t measurements{};
|
|
2536
|
+
|
|
2537
|
+
explicit operator bool() const noexcept { return !error; }
|
|
2538
|
+
join_result_t failed(error_t message) noexcept {
|
|
2539
|
+
error = std::move(message);
|
|
2540
|
+
return std::move(*this);
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
|
|
2546
|
+
* to perform fast one-to-one matching between two large collections
|
|
2547
|
+
* of vectors, using approximate nearest neighbors search.
|
|
2548
|
+
*/
|
|
2549
|
+
template < //
|
|
2550
|
+
typename first_to_second_at = dummy_label_to_label_mapping_t, //
|
|
2551
|
+
typename second_to_first_at = dummy_label_to_label_mapping_t, //
|
|
2552
|
+
typename executor_at = dummy_executor_t, //
|
|
2553
|
+
typename progress_at = dummy_progress_t //
|
|
2554
|
+
>
|
|
2555
|
+
static join_result_t join( //
|
|
2556
|
+
index_gt const& first, index_gt const& second, //
|
|
2557
|
+
join_config_t config = {}, //
|
|
2558
|
+
first_to_second_at&& first_to_second = first_to_second_at{}, //
|
|
2559
|
+
second_to_first_at&& second_to_first = second_to_first_at{}, //
|
|
2560
|
+
executor_at&& executor = executor_at{}, //
|
|
2561
|
+
progress_at&& progress = progress_at{}) noexcept {
|
|
2562
|
+
|
|
2563
|
+
if (second.size() < first.size())
|
|
2564
|
+
return join_small_and_big_( //
|
|
2565
|
+
second, first, //
|
|
2566
|
+
config, //
|
|
2567
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
2568
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
2569
|
+
std::forward<executor_at>(executor), //
|
|
2570
|
+
std::forward<progress_at>(progress));
|
|
2571
|
+
else
|
|
2572
|
+
return join_small_and_big_( //
|
|
2573
|
+
first, second, //
|
|
2574
|
+
config, //
|
|
2575
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
2576
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
2577
|
+
std::forward<executor_at>(executor), //
|
|
2578
|
+
std::forward<progress_at>(progress));
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2357
2581
|
private:
|
|
2582
|
+
template <typename first_to_second_at, typename second_to_first_at, typename executor_at, typename progress_at>
|
|
2583
|
+
static join_result_t join_small_and_big_( //
|
|
2584
|
+
index_gt const& men, index_gt const& women, //
|
|
2585
|
+
join_config_t config, //
|
|
2586
|
+
first_to_second_at&& man_to_woman, //
|
|
2587
|
+
second_to_first_at&& woman_to_man, //
|
|
2588
|
+
executor_at&& executor, //
|
|
2589
|
+
progress_at&& progress) noexcept {
|
|
2590
|
+
|
|
2591
|
+
join_result_t result;
|
|
2592
|
+
|
|
2593
|
+
// Sanity checks and argument validation:
|
|
2594
|
+
if (&men == &women)
|
|
2595
|
+
return result.failed("Can't join with itself, consider copying");
|
|
2596
|
+
if (config.max_proposals == 0)
|
|
2597
|
+
config.max_proposals = std::log(men.size()) + executor.size();
|
|
2598
|
+
config.max_proposals = (std::min)(men.size(), config.max_proposals);
|
|
2599
|
+
|
|
2600
|
+
using proposals_count_t = std::uint16_t;
|
|
2601
|
+
using ids_allocator_t = typename allocator_traits_t::template rebind_alloc<id_t>;
|
|
2602
|
+
allocator_t& alloc = men.allocator_;
|
|
2603
|
+
|
|
2604
|
+
// Create an atomic queue, as a ring structure, from/to which
|
|
2605
|
+
// free men will be added/pulled.
|
|
2606
|
+
std::mutex free_men_mutex{};
|
|
2607
|
+
ring_gt<id_t, ids_allocator_t> free_men;
|
|
2608
|
+
free_men.resize(men.size());
|
|
2609
|
+
for (std::size_t i = 0; i != men.size(); ++i)
|
|
2610
|
+
free_men.push(static_cast<id_t>(i));
|
|
2611
|
+
|
|
2612
|
+
// We are gonna need some temporary memory.
|
|
2613
|
+
proposals_count_t* proposal_counts = (proposals_count_t*)alloc.allocate(sizeof(proposals_count_t) * men.size());
|
|
2614
|
+
id_t* man_to_woman_ids = (id_t*)alloc.allocate(sizeof(id_t) * men.size());
|
|
2615
|
+
id_t* woman_to_man_ids = (id_t*)alloc.allocate(sizeof(id_t) * women.size());
|
|
2616
|
+
if (!proposal_counts || !man_to_woman_ids || !woman_to_man_ids)
|
|
2617
|
+
return result.failed("Can't temporary mappings");
|
|
2618
|
+
|
|
2619
|
+
id_t missing_id;
|
|
2620
|
+
std::memset((void*)&missing_id, 0xFF, sizeof(id_t));
|
|
2621
|
+
std::memset((void*)man_to_woman_ids, 0xFF, sizeof(id_t) * men.size());
|
|
2622
|
+
std::memset((void*)woman_to_man_ids, 0xFF, sizeof(id_t) * women.size());
|
|
2623
|
+
std::memset(proposal_counts, 0, sizeof(proposals_count_t) * men.size());
|
|
2624
|
+
|
|
2625
|
+
// Define locks, to limit concurrent accesses to `man_to_woman_ids` and `woman_to_man_ids`.
|
|
2626
|
+
visits_bitset_t men_locks, women_locks;
|
|
2627
|
+
if (!men_locks.resize(men.size()) || !women_locks.resize(women.size()))
|
|
2628
|
+
return result.failed("Can't allocate locks");
|
|
2629
|
+
|
|
2630
|
+
// Accumulate statistics from all the contexts,
|
|
2631
|
+
// to have a baseline to compare with, by the time the `join` is finished.
|
|
2632
|
+
std::size_t old_measurements{};
|
|
2633
|
+
std::size_t old_cycles{};
|
|
2634
|
+
for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
|
|
2635
|
+
old_measurements += women.contexts_[thread_idx].measurements_count;
|
|
2636
|
+
old_cycles += women.contexts_[thread_idx].iteration_cycles;
|
|
2637
|
+
}
|
|
2638
|
+
std::atomic<std::size_t> rounds{0};
|
|
2639
|
+
std::atomic<std::size_t> engagements{0};
|
|
2640
|
+
|
|
2641
|
+
// Concurrently process all the men
|
|
2642
|
+
executor.execute_bulk([&](std::size_t thread_idx) {
|
|
2643
|
+
context_t& context = women.contexts_[thread_idx];
|
|
2644
|
+
search_config_t search_config;
|
|
2645
|
+
search_config.expansion = config.expansion;
|
|
2646
|
+
search_config.exact = config.exact;
|
|
2647
|
+
search_config.thread = thread_idx;
|
|
2648
|
+
id_t free_man_id;
|
|
2649
|
+
|
|
2650
|
+
// While there exist a free man who still has a woman to propose to.
|
|
2651
|
+
while (true) {
|
|
2652
|
+
std::size_t passed_rounds = 0;
|
|
2653
|
+
std::size_t total_rounds = 0;
|
|
2654
|
+
{
|
|
2655
|
+
std::unique_lock<std::mutex> pop_lock(free_men_mutex);
|
|
2656
|
+
if (!free_men.try_pop(free_man_id))
|
|
2657
|
+
// Primary exit path, we have exhausted the list of candidates
|
|
2658
|
+
break;
|
|
2659
|
+
passed_rounds = ++rounds;
|
|
2660
|
+
total_rounds = passed_rounds + free_men.size();
|
|
2661
|
+
}
|
|
2662
|
+
progress(passed_rounds, total_rounds);
|
|
2663
|
+
while (men_locks.atomic_set(free_man_id))
|
|
2664
|
+
;
|
|
2665
|
+
|
|
2666
|
+
node_t free_man = men.node_with_id_(free_man_id);
|
|
2667
|
+
proposals_count_t& free_man_proposals = proposal_counts[free_man_id];
|
|
2668
|
+
if (free_man_proposals >= config.max_proposals)
|
|
2669
|
+
continue;
|
|
2670
|
+
|
|
2671
|
+
// Find the closest woman, to whom this man hasn't proposed yet.
|
|
2672
|
+
free_man_proposals++;
|
|
2673
|
+
search_result_t candidates = women.search(free_man.vector_view(), free_man_proposals, search_config);
|
|
2674
|
+
if (!candidates) {
|
|
2675
|
+
// TODO:
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
match_t match = candidates.back();
|
|
2679
|
+
member_cref_t woman = match.member;
|
|
2680
|
+
while (women_locks.atomic_set(woman.id))
|
|
2681
|
+
;
|
|
2682
|
+
|
|
2683
|
+
id_t husband_id = woman_to_man_ids[woman.id];
|
|
2684
|
+
bool woman_is_free = husband_id == missing_id;
|
|
2685
|
+
if (woman_is_free) {
|
|
2686
|
+
// Engagement
|
|
2687
|
+
man_to_woman_ids[free_man_id] = woman.id;
|
|
2688
|
+
woman_to_man_ids[woman.id] = free_man_id;
|
|
2689
|
+
engagements++;
|
|
2690
|
+
} else {
|
|
2691
|
+
distance_t distance_from_husband = context.measure(woman.vector, men.node_with_id_(husband_id));
|
|
2692
|
+
distance_t distance_from_candidate = match.distance;
|
|
2693
|
+
if (distance_from_husband > distance_from_candidate) {
|
|
2694
|
+
// Break-up
|
|
2695
|
+
while (men_locks.atomic_set(husband_id))
|
|
2696
|
+
;
|
|
2697
|
+
man_to_woman_ids[husband_id] = missing_id;
|
|
2698
|
+
men_locks.atomic_reset(husband_id);
|
|
2699
|
+
|
|
2700
|
+
// New Engagement
|
|
2701
|
+
man_to_woman_ids[free_man_id] = woman.id;
|
|
2702
|
+
woman_to_man_ids[woman.id] = free_man_id;
|
|
2703
|
+
engagements++;
|
|
2704
|
+
|
|
2705
|
+
std::unique_lock<std::mutex> push_lock(free_men_mutex);
|
|
2706
|
+
free_men.push(husband_id);
|
|
2707
|
+
} else {
|
|
2708
|
+
std::unique_lock<std::mutex> push_lock(free_men_mutex);
|
|
2709
|
+
free_men.push(free_man_id);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
men_locks.atomic_reset(free_man_id);
|
|
2714
|
+
women_locks.atomic_reset(woman.id);
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2717
|
+
|
|
2718
|
+
// Export the IDs into labels:
|
|
2719
|
+
std::size_t intersection_size = 0;
|
|
2720
|
+
for (std::size_t i = 0; i != men.size(); ++i) {
|
|
2721
|
+
id_t woman_id = man_to_woman_ids[i];
|
|
2722
|
+
if (woman_id != missing_id) {
|
|
2723
|
+
label_t man = men.node_with_id_(i).label();
|
|
2724
|
+
label_t woman = women.node_with_id_(woman_id).label();
|
|
2725
|
+
man_to_woman[man] = woman;
|
|
2726
|
+
woman_to_man[woman] = man;
|
|
2727
|
+
intersection_size++;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Deallocate memory
|
|
2732
|
+
alloc.deallocate((byte_t*)proposal_counts, sizeof(proposals_count_t) * men.size());
|
|
2733
|
+
alloc.deallocate((byte_t*)man_to_woman_ids, sizeof(id_t) * men.size());
|
|
2734
|
+
alloc.deallocate((byte_t*)woman_to_man_ids, sizeof(id_t) * women.size());
|
|
2735
|
+
|
|
2736
|
+
// Export stats
|
|
2737
|
+
result.engagements = engagements;
|
|
2738
|
+
result.intersection_size = intersection_size;
|
|
2739
|
+
for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
|
|
2740
|
+
result.measurements += women.contexts_[thread_idx].measurements_count;
|
|
2741
|
+
result.cycles += women.contexts_[thread_idx].iteration_cycles;
|
|
2742
|
+
}
|
|
2743
|
+
result.measurements -= old_measurements;
|
|
2744
|
+
result.cycles -= old_cycles;
|
|
2745
|
+
return result;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2358
2748
|
void reset_view_() noexcept {
|
|
2359
2749
|
if (!viewed_file_)
|
|
2360
2750
|
return;
|
|
@@ -2384,51 +2774,80 @@ class index_gt {
|
|
|
2384
2774
|
return node_head_bytes_() + pre_.neighbors_base_bytes + pre_.neighbors_bytes * level + sizeof(scalar_t) * dim;
|
|
2385
2775
|
}
|
|
2386
2776
|
|
|
2387
|
-
|
|
2777
|
+
using span_bytes_t = span_gt<byte_t>;
|
|
2778
|
+
struct node_bytes_split_t {
|
|
2779
|
+
span_bytes_t tape{};
|
|
2780
|
+
span_bytes_t vector{};
|
|
2781
|
+
|
|
2782
|
+
node_bytes_split_t() {}
|
|
2783
|
+
node_bytes_split_t(span_bytes_t tape, span_bytes_t vector) noexcept : tape(tape), vector(vector) {}
|
|
2784
|
+
|
|
2785
|
+
std::size_t memory_usage() const noexcept { return tape.size() + vector.size(); }
|
|
2786
|
+
bool colocated() const noexcept { return tape.end() == vector.begin(); }
|
|
2787
|
+
operator node_t() const noexcept { return node_t{tape.begin(), reinterpret_cast<scalar_t*>(vector.begin())}; }
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2790
|
+
inline node_bytes_split_t node_bytes_split_(node_t node) const noexcept {
|
|
2388
2791
|
std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * node.level();
|
|
2389
|
-
|
|
2792
|
+
std::size_t bytes_in_tape = node_head_bytes_() + levels_bytes;
|
|
2793
|
+
return {{node.tape(), bytes_in_tape}, {(byte_t*)node.vector(), node_vector_bytes_(node)}};
|
|
2390
2794
|
}
|
|
2391
2795
|
|
|
2392
2796
|
inline std::size_t node_vector_bytes_(dim_t dim) const noexcept { return dim * sizeof(scalar_t); }
|
|
2393
2797
|
inline std::size_t node_vector_bytes_(node_t node) const noexcept { return node_vector_bytes_(node.dim()); }
|
|
2394
2798
|
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
if (viewed_file_)
|
|
2398
|
-
return;
|
|
2399
|
-
|
|
2400
|
-
node_t& node = nodes_[id];
|
|
2401
|
-
std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_stored_(node);
|
|
2402
|
-
point_allocator_.deallocate(node.tape(), node_bytes);
|
|
2403
|
-
node = node_t{};
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
node_t node_malloc_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
|
|
2799
|
+
node_bytes_split_t node_malloc_(dim_t dims_to_store, level_t level) noexcept {
|
|
2407
2800
|
|
|
2408
|
-
std::size_t
|
|
2409
|
-
std::size_t
|
|
2410
|
-
std::size_t
|
|
2411
|
-
node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(!store_vector);
|
|
2801
|
+
std::size_t vector_bytes = node_vector_bytes_(dims_to_store);
|
|
2802
|
+
std::size_t node_bytes = node_bytes_(dims_to_store, level);
|
|
2803
|
+
std::size_t non_vector_bytes = node_bytes - vector_bytes;
|
|
2412
2804
|
|
|
2413
2805
|
byte_t* data = (byte_t*)point_allocator_.allocate(node_bytes);
|
|
2414
2806
|
if (!data)
|
|
2415
|
-
return {};
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (vector.data())
|
|
2419
|
-
std::memcpy(data + node_bytes - stored_vector_bytes, vector.data(), stored_vector_bytes);
|
|
2420
|
-
|
|
2421
|
-
scalar_t* scalars = store_vector //
|
|
2422
|
-
? (scalar_t*)(data + node_bytes - stored_vector_bytes)
|
|
2423
|
-
: (scalar_t*)(vector.data());
|
|
2807
|
+
return node_bytes_split_t{};
|
|
2808
|
+
return {{data, non_vector_bytes}, {data + non_vector_bytes, vector_bytes}};
|
|
2809
|
+
}
|
|
2424
2810
|
|
|
2425
|
-
|
|
2811
|
+
node_t node_make_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
|
|
2812
|
+
node_bytes_split_t node_bytes = node_malloc_(vector.size() * store_vector, level);
|
|
2813
|
+
if (store_vector) {
|
|
2814
|
+
std::memset(node_bytes.tape.data(), 0, node_bytes.tape.size());
|
|
2815
|
+
std::memcpy(node_bytes.vector.data(), vector.data(), node_bytes.vector.size());
|
|
2816
|
+
} else {
|
|
2817
|
+
std::memset(node_bytes.tape.data(), 0, node_bytes.memory_usage());
|
|
2818
|
+
}
|
|
2819
|
+
node_t node = node_bytes;
|
|
2426
2820
|
node.label(label);
|
|
2427
|
-
node.dim(static_cast<dim_t>(
|
|
2821
|
+
node.dim(static_cast<dim_t>(vector.size()));
|
|
2428
2822
|
node.level(level);
|
|
2429
2823
|
return node;
|
|
2430
2824
|
}
|
|
2431
2825
|
|
|
2826
|
+
node_t node_make_copy_(node_bytes_split_t old_bytes) noexcept {
|
|
2827
|
+
if (old_bytes.colocated()) {
|
|
2828
|
+
byte_t* data = (byte_t*)point_allocator_.allocate(old_bytes.memory_usage());
|
|
2829
|
+
std::memcpy(data, old_bytes.tape.data(), old_bytes.memory_usage());
|
|
2830
|
+
return node_t{data, reinterpret_cast<scalar_t*>(data + old_bytes.tape.size())};
|
|
2831
|
+
} else {
|
|
2832
|
+
node_t old_node = old_bytes;
|
|
2833
|
+
node_bytes_split_t node_bytes = node_malloc_(old_node.vector_view().size(), old_node.level());
|
|
2834
|
+
std::memcpy(node_bytes.tape.data(), old_bytes.tape.data(), old_bytes.tape.size());
|
|
2835
|
+
std::memcpy(node_bytes.vector.data(), old_bytes.vector.data(), old_bytes.vector.size());
|
|
2836
|
+
return node_bytes;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
void node_free_(std::size_t id) noexcept {
|
|
2841
|
+
|
|
2842
|
+
if (viewed_file_)
|
|
2843
|
+
return;
|
|
2844
|
+
|
|
2845
|
+
node_t& node = nodes_[id];
|
|
2846
|
+
std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_bytes_split_(node).colocated();
|
|
2847
|
+
point_allocator_.deallocate(node.tape(), node_bytes);
|
|
2848
|
+
node = node_t{};
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2432
2851
|
inline node_t node_with_id_(std::size_t idx) const noexcept { return nodes_[idx]; }
|
|
2433
2852
|
inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
|
|
2434
2853
|
|