usearch 2.15.3 → 2.16.1
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 +1 -1
- package/cpp/test.cpp +16 -0
- package/include/usearch/index.hpp +21 -5
- package/include/usearch/index_dense.hpp +6 -1
- package/include/usearch/index_plugins.hpp +345 -8
- package/javascript/lib.cpp +17 -5
- package/javascript/usearch.test.js +190 -21
- package/package.json +1 -1
- 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/simsimd/.git-blame-ignore-revs +2 -0
- package/simsimd/CONTRIBUTING.md +71 -23
- package/simsimd/Cargo.lock +1 -1
- package/simsimd/Cargo.toml +4 -4
- package/simsimd/MANIFEST.in +2 -1
- package/simsimd/Package.swift +15 -3
- package/simsimd/README.md +104 -35
- package/simsimd/binding.gyp +6 -2
- package/simsimd/build.rs +2 -0
- package/simsimd/c/lib.c +180 -99
- package/simsimd/include/simsimd/binary.h +311 -102
- package/simsimd/include/simsimd/curved.h +134 -135
- package/simsimd/include/simsimd/dot.h +448 -274
- package/simsimd/include/simsimd/elementwise.h +1437 -0
- package/simsimd/include/simsimd/geospatial.h +2 -2
- package/simsimd/include/simsimd/mesh.h +2 -2
- package/simsimd/include/simsimd/probability.h +88 -88
- package/simsimd/include/simsimd/simsimd.h +1157 -772
- package/simsimd/include/simsimd/sparse.h +768 -147
- package/simsimd/include/simsimd/spatial.h +955 -376
- package/simsimd/include/simsimd/types.h +145 -71
- package/simsimd/javascript/lib.c +1 -2
- package/simsimd/package.json +4 -4
- package/simsimd/rust/lib.rs +14 -1
- package/simsimd/scripts/bench.cxx +1070 -0
- package/simsimd/scripts/bench.ipynb +187 -0
- package/simsimd/{javascript → scripts}/bench.js +1 -1
- package/simsimd/{rust/benches/sqeuclidean.rs → scripts/bench_sqeuclidean.rs} +6 -6
- package/simsimd/scripts/bench_vectors.py +700 -0
- package/simsimd/scripts/bench_vectors_live.py +213 -0
- package/simsimd/scripts/test.c +155 -0
- package/simsimd/{javascript → scripts}/test.mjs +2 -2
- package/simsimd/scripts/test.py +1410 -0
- package/simsimd/swift/{source/SimSIMD.swift → SimSIMD.swift} +6 -2
- package/simsimd/swift/{tests/SimSIMDTests.swift → Test.swift} +1 -1
- /package/simsimd/{rust/benches/cosine.rs → scripts/bench_cosine.rs} +0 -0
- /package/simsimd/{rust/benches/native.rs → scripts/bench_native.rs} +0 -0
package/README.md
CHANGED
package/cpp/test.cpp
CHANGED
|
@@ -1100,6 +1100,22 @@ int main(int, char**) {
|
|
|
1100
1100
|
test_uint40();
|
|
1101
1101
|
test_cosine<float, std::int64_t, uint40_t>(10, 10);
|
|
1102
1102
|
|
|
1103
|
+
// Test plugins, like K-Means clustering.
|
|
1104
|
+
{
|
|
1105
|
+
std::size_t vectors_count = 1000, centroids_count = 10, dimensions = 256;
|
|
1106
|
+
kmeans_clustering_t clustering;
|
|
1107
|
+
clustering.max_iterations = 2;
|
|
1108
|
+
std::vector<float> vectors(vectors_count * dimensions), centroids(centroids_count * dimensions);
|
|
1109
|
+
matrix_slice_gt<float const> vectors_slice(vectors.data(), dimensions, vectors_count);
|
|
1110
|
+
matrix_slice_gt<float> centroids_slice(centroids.data(), dimensions, centroids_count);
|
|
1111
|
+
std::generate(vectors.begin(), vectors.end(), [] { return float(std::rand()) / float(INT_MAX); });
|
|
1112
|
+
std::vector<std::size_t> assignments(vectors_count);
|
|
1113
|
+
std::vector<distance_punned_t> distances(vectors_count);
|
|
1114
|
+
auto clustering_result = clustering(vectors_slice, centroids_slice, {assignments.data(), assignments.size()},
|
|
1115
|
+
{distances.data(), distances.size()});
|
|
1116
|
+
expect(clustering_result);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1103
1119
|
// Exact search without constructing indexes.
|
|
1104
1120
|
// Great for validating the distance functions.
|
|
1105
1121
|
std::printf("Testing exact search\n");
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
#define UNUM_USEARCH_HPP
|
|
9
9
|
|
|
10
10
|
#define USEARCH_VERSION_MAJOR 2
|
|
11
|
-
#define USEARCH_VERSION_MINOR
|
|
12
|
-
#define USEARCH_VERSION_PATCH
|
|
11
|
+
#define USEARCH_VERSION_MINOR 16
|
|
12
|
+
#define USEARCH_VERSION_PATCH 1
|
|
13
13
|
|
|
14
14
|
// Inferring C++ version
|
|
15
15
|
// https://stackoverflow.com/a/61552074
|
|
@@ -370,7 +370,7 @@ template <typename scalar_at, typename allocator_at = std::allocator<scalar_at>>
|
|
|
370
370
|
void reset() noexcept {
|
|
371
371
|
if (!std::is_trivially_destructible<scalar_at>::value)
|
|
372
372
|
for (std::size_t i = 0; i != size_; ++i)
|
|
373
|
-
destroy_at(data_ + i);
|
|
373
|
+
unum::usearch::destroy_at(data_ + i); //< Facing some symbol visibility/ambiguity issues
|
|
374
374
|
allocator_at{}.deallocate(data_, size_);
|
|
375
375
|
data_ = nullptr;
|
|
376
376
|
size_ = 0;
|
|
@@ -960,6 +960,10 @@ class sorted_buffer_gt {
|
|
|
960
960
|
|
|
961
961
|
/**
|
|
962
962
|
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
963
|
+
*
|
|
964
|
+
* 40 bits is enough to address a @b Trillion entries potentially colocated on 1 machine.
|
|
965
|
+
* At roughly 5 bytes * 20 neighbors + 100 bytes per entry, this translates to 200 TB of data,
|
|
966
|
+
* which is similar to a single-server capacity of modern NVME arrays.
|
|
963
967
|
*/
|
|
964
968
|
class usearch_pack_m uint40_t {
|
|
965
969
|
unsigned char octets[5];
|
|
@@ -1214,8 +1218,8 @@ class ring_gt {
|
|
|
1214
1218
|
using element_t = element_at;
|
|
1215
1219
|
using allocator_t = allocator_at;
|
|
1216
1220
|
|
|
1217
|
-
static_assert(std::is_trivially_destructible<element_t>(), "This
|
|
1218
|
-
static_assert(std::is_trivially_copy_constructible<element_t>(), "This
|
|
1221
|
+
static_assert(std::is_trivially_destructible<element_t>(), "This ring is designed for trivial structs");
|
|
1222
|
+
static_assert(std::is_trivially_copy_constructible<element_t>(), "This ring is designed for trivial structs");
|
|
1219
1223
|
|
|
1220
1224
|
using value_type = element_t;
|
|
1221
1225
|
|
|
@@ -3175,6 +3179,18 @@ class index_gt {
|
|
|
3175
3179
|
|
|
3176
3180
|
std::size_t memory_usage_per_node(level_t level) const noexcept { return node_bytes_(level); }
|
|
3177
3181
|
|
|
3182
|
+
double inverse_log_connectivity() const {
|
|
3183
|
+
return pre_.inverse_log_connectivity;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
std::size_t neighbors_base_bytes() const {
|
|
3187
|
+
return pre_.neighbors_base_bytes;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
std::size_t neighbors_bytes() const {
|
|
3191
|
+
return pre_.neighbors_bytes;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3178
3194
|
#if defined(USEARCH_USE_PRAGMA_REGION)
|
|
3179
3195
|
#pragma endregion
|
|
3180
3196
|
|
|
@@ -693,6 +693,9 @@ class index_dense_gt {
|
|
|
693
693
|
std::size_t max_level() const { return typed_->max_level(); }
|
|
694
694
|
index_dense_config_t const& config() const { return config_; }
|
|
695
695
|
index_limits_t const& limits() const { return typed_->limits(); }
|
|
696
|
+
double inverse_log_connectivity() const { return typed_->inverse_log_connectivity(); }
|
|
697
|
+
std::size_t neighbors_base_bytes() const { return typed_->neighbors_base_bytes(); }
|
|
698
|
+
std::size_t neighbors_bytes() const { return typed_->neighbors_bytes(); }
|
|
696
699
|
bool multi() const { return config_.multi; }
|
|
697
700
|
std::size_t currently_available_threads() const {
|
|
698
701
|
std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
|
|
@@ -1460,6 +1463,8 @@ class index_dense_gt {
|
|
|
1460
1463
|
labeling_result_t remove(vector_key_t key) {
|
|
1461
1464
|
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1462
1465
|
labeling_result_t result;
|
|
1466
|
+
if (typed_->is_immutable())
|
|
1467
|
+
return result.failed("Can't remove from an immutable index");
|
|
1463
1468
|
|
|
1464
1469
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
1465
1470
|
auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
|
|
@@ -2249,4 +2254,4 @@ static join_result_t join( //
|
|
|
2249
2254
|
}
|
|
2250
2255
|
|
|
2251
2256
|
} // namespace usearch
|
|
2252
|
-
} // namespace unum
|
|
2257
|
+
} // namespace unum
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include <stdlib.h> // `aligned_alloc`
|
|
5
5
|
|
|
6
6
|
#include <atomic> // `std::atomic`
|
|
7
|
+
#include <chrono> // `std::chrono`
|
|
7
8
|
#include <cstring> // `std::strncmp`
|
|
8
9
|
#include <thread> // `std::thread`
|
|
9
10
|
|
|
@@ -65,11 +66,11 @@
|
|
|
65
66
|
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
|
66
67
|
#pragma GCC diagnostic ignored "-Wunused-variable"
|
|
67
68
|
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
|
|
68
|
-
#
|
|
69
|
+
#if defined(_MSC_VER)
|
|
69
70
|
#pragma warning(push)
|
|
70
71
|
#pragma warning(disable : 4101) // "Unused variables"
|
|
71
72
|
#pragma warning(disable : 4068) // "Unknown pragmas", when MSVC tries to read GCC pragmas
|
|
72
|
-
#endif
|
|
73
|
+
#endif // _MSC_VER
|
|
73
74
|
#include <simsimd/simsimd.h>
|
|
74
75
|
#ifdef _MSC_VER
|
|
75
76
|
#pragma warning(pop)
|
|
@@ -2020,7 +2021,7 @@ class matrix_slice_gt {
|
|
|
2020
2021
|
explicit operator bool() const noexcept { return begin_; }
|
|
2021
2022
|
std::size_t size() const noexcept { return count_; }
|
|
2022
2023
|
std::size_t dimensions() const noexcept { return dimensions_; }
|
|
2023
|
-
std::size_t
|
|
2024
|
+
std::size_t stride_bytes() const noexcept { return stride_bytes_; }
|
|
2024
2025
|
scalar_t* data() const noexcept { return begin_; }
|
|
2025
2026
|
scalar_t* at(std::size_t i) const noexcept {
|
|
2026
2027
|
return reinterpret_cast<scalar_t*>(reinterpret_cast<byte_addressable_t*>(begin_) + i * stride_bytes_);
|
|
@@ -2032,7 +2033,7 @@ struct exact_offset_and_distance_t {
|
|
|
2032
2033
|
f32_t distance;
|
|
2033
2034
|
};
|
|
2034
2035
|
|
|
2035
|
-
using exact_search_results_t = matrix_slice_gt<exact_offset_and_distance_t>;
|
|
2036
|
+
using exact_search_results_t = matrix_slice_gt<exact_offset_and_distance_t const>;
|
|
2036
2037
|
|
|
2037
2038
|
/**
|
|
2038
2039
|
* @brief Helper-structure for exact search operations.
|
|
@@ -2057,10 +2058,10 @@ class exact_search_t {
|
|
|
2057
2058
|
matrix_slice_gt<scalar_at const> dataset, matrix_slice_gt<scalar_at const> queries, //
|
|
2058
2059
|
std::size_t wanted, metric_punned_t const& metric, //
|
|
2059
2060
|
executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
|
|
2060
|
-
return operator()(
|
|
2061
|
-
metric,
|
|
2062
|
-
reinterpret_cast<byte_t const*>(dataset.data()), dataset.size(), dataset.
|
|
2063
|
-
reinterpret_cast<byte_t const*>(queries.data()), queries.size(), queries.
|
|
2061
|
+
return operator()( //
|
|
2062
|
+
metric, //
|
|
2063
|
+
reinterpret_cast<byte_t const*>(dataset.data()), dataset.size(), dataset.stride_bytes(), //
|
|
2064
|
+
reinterpret_cast<byte_t const*>(queries.data()), queries.size(), queries.stride_bytes(), //
|
|
2064
2065
|
wanted, executor, progress);
|
|
2065
2066
|
}
|
|
2066
2067
|
|
|
@@ -2137,6 +2138,342 @@ class exact_search_t {
|
|
|
2137
2138
|
}
|
|
2138
2139
|
};
|
|
2139
2140
|
|
|
2141
|
+
struct kmeans_clustering_result_t {
|
|
2142
|
+
error_t error{};
|
|
2143
|
+
std::size_t computed_distances{};
|
|
2144
|
+
/// @brief The number of iterations the algorithm took to converge.
|
|
2145
|
+
std::size_t iterations{};
|
|
2146
|
+
/// @brief The number of points that changed clusters in the last iteration.
|
|
2147
|
+
std::size_t last_iteration_points_shifted{};
|
|
2148
|
+
/// @brief The inertia of the last iteration (sum of squared distances to centroids).
|
|
2149
|
+
f64_t last_iteration_inertia{};
|
|
2150
|
+
/// @brief The total elapsed runtime of the algorithm in seconds.
|
|
2151
|
+
f64_t runtime_seconds{};
|
|
2152
|
+
/// @brief The total distance between the points and their assigned centroids.
|
|
2153
|
+
f64_t aggregate_distance{};
|
|
2154
|
+
|
|
2155
|
+
explicit operator bool() const noexcept { return !error; }
|
|
2156
|
+
kmeans_clustering_result_t failed(error_t message) noexcept {
|
|
2157
|
+
error = std::move(message);
|
|
2158
|
+
return std::move(*this);
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* @brief Helper-class for K-Means clustering of dense vectors.
|
|
2164
|
+
* Doesn't require constructing the index, but benefits from mixed-precision logic.
|
|
2165
|
+
* ! Doesn't guarantee that the clusters are balanced in size.
|
|
2166
|
+
*
|
|
2167
|
+
* The algorithm is as follows:
|
|
2168
|
+
* - Initialization: Select K initial centroids (randomly or with a heuristic).
|
|
2169
|
+
* - Assignment: Assign each data point to the nearest centroid based on the Euclidean distance.
|
|
2170
|
+
* - Update: Recalculate the centroids as the mean of all points assigned to each centroid.
|
|
2171
|
+
* - Repeat: Repeat the assignment and update steps until the centroids no longer change significantly
|
|
2172
|
+
* or an early-exit condition is met.
|
|
2173
|
+
*/
|
|
2174
|
+
template <typename allocator_at = std::allocator<char>> class kmeans_clustering_gt {
|
|
2175
|
+
public:
|
|
2176
|
+
using distance_t = distance_punned_t;
|
|
2177
|
+
|
|
2178
|
+
metric_kind_t metric_kind{metric_kind_t::l2sq_k};
|
|
2179
|
+
scalar_kind_t quantization_kind{scalar_kind_t::bf16_k};
|
|
2180
|
+
|
|
2181
|
+
static constexpr std::size_t max_iterations_default_k = 300;
|
|
2182
|
+
static constexpr f64_t inertia_threshold_default_k = 1e-4;
|
|
2183
|
+
static constexpr f64_t max_seconds_default_k = 60.0;
|
|
2184
|
+
static constexpr f64_t min_shifts_default_k = 0.01;
|
|
2185
|
+
|
|
2186
|
+
/// @brief Early-exit parameter - the maximum number of iterations to perform.
|
|
2187
|
+
std::size_t max_iterations{max_iterations_default_k};
|
|
2188
|
+
/// @brief Early-exit parameter - the threshold for the final inertia to terminate early.
|
|
2189
|
+
f64_t inertia_threshold{inertia_threshold_default_k};
|
|
2190
|
+
/// @brief Early-exit parameter - the maximum runtime allowed in seconds.
|
|
2191
|
+
f64_t max_seconds{max_seconds_default_k};
|
|
2192
|
+
/// @brief Early-exit parameter - the minimum share of points that must change clusters per iteration.
|
|
2193
|
+
f64_t min_shifts{min_shifts_default_k};
|
|
2194
|
+
/// @brief The random seed to use for centroid initialization.
|
|
2195
|
+
std::uint64_t seed{0};
|
|
2196
|
+
|
|
2197
|
+
kmeans_clustering_gt(std::uint64_t seed) noexcept : seed(seed) {}
|
|
2198
|
+
kmeans_clustering_gt() noexcept(false) {
|
|
2199
|
+
std::random_device random_device;
|
|
2200
|
+
seed = random_device();
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
kmeans_clustering_gt(kmeans_clustering_gt const&) = default;
|
|
2204
|
+
kmeans_clustering_gt& operator=(kmeans_clustering_gt const&) = default;
|
|
2205
|
+
|
|
2206
|
+
template <typename scalar_at, typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
|
|
2207
|
+
kmeans_clustering_result_t operator()( //
|
|
2208
|
+
matrix_slice_gt<scalar_at const> points, matrix_slice_gt<scalar_at> centroids,
|
|
2209
|
+
span_gt<std::size_t> point_to_centroid_index, span_gt<distance_t> point_to_centroid_distance, //
|
|
2210
|
+
executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
|
|
2211
|
+
return operator()( //
|
|
2212
|
+
reinterpret_cast<byte_t const*>(points.data()), points.size(), points.stride_bytes(), //
|
|
2213
|
+
reinterpret_cast<byte_t*>(centroids.data()), centroids.size(), centroids.stride_bytes(),
|
|
2214
|
+
point_to_centroid_index.data(), point_to_centroid_distance.data(), //
|
|
2215
|
+
scalar_kind<scalar_at>(), points.dimensions(), executor, progress);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
template <typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
|
|
2219
|
+
kmeans_clustering_result_t operator()( //
|
|
2220
|
+
byte_t const* points_data, std::size_t points_count, std::size_t points_stride_bytes, //
|
|
2221
|
+
byte_t* centroids_data, std::size_t wanted_clusters, std::size_t centroids_stride_bytes, //
|
|
2222
|
+
std::size_t* point_to_centroid_index, distance_t* point_to_centroid_distance, //
|
|
2223
|
+
scalar_kind_t original_scalar_kind, std::size_t dimensions, executor_at&& executor = executor_at{},
|
|
2224
|
+
progress_at&& progress = progress_at{}) {
|
|
2225
|
+
|
|
2226
|
+
// Perform sanity checks for algorithm settings.
|
|
2227
|
+
kmeans_clustering_result_t result;
|
|
2228
|
+
if (max_iterations < 1)
|
|
2229
|
+
return result.failed("The number of iterations must be at least 1");
|
|
2230
|
+
|
|
2231
|
+
// Perform sanity checks for input arguments.
|
|
2232
|
+
if (wanted_clusters < 2)
|
|
2233
|
+
return result.failed("The number of clusters must be at least 2");
|
|
2234
|
+
if (wanted_clusters >= points_count)
|
|
2235
|
+
return result.failed("The number of clusters must be less than the number of vectors");
|
|
2236
|
+
|
|
2237
|
+
metric_punned_t metric = metric_punned_t::builtin(dimensions, metric_kind, quantization_kind);
|
|
2238
|
+
if (!metric)
|
|
2239
|
+
return result.failed("Unsupported metric or scalar kind");
|
|
2240
|
+
|
|
2241
|
+
// Let's allocate memory for the centroids coordinates and make sure it's
|
|
2242
|
+
// rows are aligned to cache lines to avoid false sharing.
|
|
2243
|
+
buffer_gt<distance_t, aligned_allocator_gt<distance_t, 64>> point_to_centroid_distance_buffer(points_count);
|
|
2244
|
+
buffer_gt<std::size_t, aligned_allocator_gt<std::size_t, 64>> point_to_centroid_index_buffer(points_count);
|
|
2245
|
+
buffer_gt<std::atomic<std::size_t>, aligned_allocator_gt<std::atomic<std::size_t>, 64>> cluster_sizes_buffer(
|
|
2246
|
+
wanted_clusters);
|
|
2247
|
+
|
|
2248
|
+
// For a mixed precision computation, we keep the centroids represented in two forms -
|
|
2249
|
+
// double precision and quantized the same way as in the index, to avoid paying conversion penalties.
|
|
2250
|
+
// Double precision is needed to avoid accumulating errors when aggregating too many entries.
|
|
2251
|
+
std::size_t const bytes_per_vector_original =
|
|
2252
|
+
divide_round_up<CHAR_BIT>(dimensions * bits_per_scalar(original_scalar_kind));
|
|
2253
|
+
std::size_t const bytes_per_vector_quantized = metric.bytes_per_vector();
|
|
2254
|
+
std::size_t const stride_per_vector_quantized = divide_round_up<64>(bytes_per_vector_quantized) * 64;
|
|
2255
|
+
buffer_gt<byte_t, aligned_allocator_gt<byte_t, 64>> points_quantized_buffer( //
|
|
2256
|
+
points_count * stride_per_vector_quantized);
|
|
2257
|
+
buffer_gt<byte_t, aligned_allocator_gt<byte_t, 64>> centroids_quantized_buffer( //
|
|
2258
|
+
wanted_clusters * stride_per_vector_quantized);
|
|
2259
|
+
|
|
2260
|
+
// When aggregating centroids, we want to parallelize the operation and need more memory.
|
|
2261
|
+
// For every thread we keep two double-precision vectors. One is the up-casting output buffer for quantized
|
|
2262
|
+
// coordinates, and the other is the temporary buffer for the partial sums of the double-precision coordinates.
|
|
2263
|
+
// The ordering:
|
|
2264
|
+
//
|
|
2265
|
+
// - thread 0: [centroid 0, centroid 1, centroid 2, centroid 3, ...]
|
|
2266
|
+
// - thread 1: [centroid 0, centroid 1, centroid 2, centroid 3, ...]
|
|
2267
|
+
// - thread 2: [centroid 0, centroid 1, centroid 2, centroid 3, ...]
|
|
2268
|
+
//
|
|
2269
|
+
std::size_t const thread_count = executor.size();
|
|
2270
|
+
buffer_gt<f64_t, aligned_allocator_gt<f64_t, 64>> centroids_precise_buffer( //
|
|
2271
|
+
wanted_clusters * dimensions * thread_count);
|
|
2272
|
+
buffer_gt<f64_t, aligned_allocator_gt<f64_t, 64>> points_precise_buffer( //
|
|
2273
|
+
wanted_clusters * dimensions * thread_count);
|
|
2274
|
+
|
|
2275
|
+
// Check if all memory allocations were successful.
|
|
2276
|
+
if (!centroids_precise_buffer || !points_precise_buffer || !centroids_quantized_buffer ||
|
|
2277
|
+
!point_to_centroid_index_buffer || !cluster_sizes_buffer || !point_to_centroid_distance_buffer ||
|
|
2278
|
+
!points_quantized_buffer)
|
|
2279
|
+
return result.failed("No memory for result outputs!");
|
|
2280
|
+
|
|
2281
|
+
std::fill_n(point_to_centroid_index_buffer.data(), points_count, wanted_clusters);
|
|
2282
|
+
std::fill_n(point_to_centroid_distance_buffer.data(), points_count, std::numeric_limits<distance_t>::max());
|
|
2283
|
+
|
|
2284
|
+
// Initialize the casting kernel for quantization and export.
|
|
2285
|
+
casts_punned_t casts = casts_punned_t::make(quantization_kind);
|
|
2286
|
+
cast_punned_t const& compress_points = casts.from[original_scalar_kind];
|
|
2287
|
+
cast_punned_t const& decompress_points = casts.to[original_scalar_kind];
|
|
2288
|
+
cast_punned_t const& compress_precise = casts.from.f64;
|
|
2289
|
+
cast_punned_t const& decompress_precise = casts.to.f64;
|
|
2290
|
+
for (std::size_t i = 0; i < points_count; i++) {
|
|
2291
|
+
byte_t const* vector = points_data + i * points_stride_bytes;
|
|
2292
|
+
byte_t* quantized = points_quantized_buffer.data() + i * stride_per_vector_quantized;
|
|
2293
|
+
if (!compress_points(vector, dimensions, quantized))
|
|
2294
|
+
std::memcpy(quantized, vector, bytes_per_vector_original);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// Initialize centroids with random points vectors.
|
|
2298
|
+
std::mt19937_64 random_engine;
|
|
2299
|
+
random_engine.seed(seed);
|
|
2300
|
+
for (std::size_t i = 0; i < wanted_clusters; i++) {
|
|
2301
|
+
// Generate the random index of the points vector,
|
|
2302
|
+
// that is unique and not already used as a centroid.
|
|
2303
|
+
std::size_t random_index;
|
|
2304
|
+
do {
|
|
2305
|
+
random_index = random_engine() % points_count;
|
|
2306
|
+
bool is_unique = true;
|
|
2307
|
+
for (std::size_t j = 0; j < i; j++) {
|
|
2308
|
+
if (point_to_centroid_index_buffer[j] == random_index) {
|
|
2309
|
+
is_unique = false;
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (is_unique)
|
|
2314
|
+
break;
|
|
2315
|
+
} while (true);
|
|
2316
|
+
|
|
2317
|
+
// Copy the vector to the centroid and quantize it.
|
|
2318
|
+
byte_t const* quantized_point = points_quantized_buffer.data() + random_index * stride_per_vector_quantized;
|
|
2319
|
+
byte_t* quantized_centroid = centroids_quantized_buffer.data() + i * stride_per_vector_quantized;
|
|
2320
|
+
std::memcpy(quantized_centroid, quantized_point, bytes_per_vector_quantized);
|
|
2321
|
+
point_to_centroid_index_buffer[random_index] = i;
|
|
2322
|
+
point_to_centroid_distance_buffer[random_index] = 0;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
auto start_time = std::chrono::high_resolution_clock::now();
|
|
2326
|
+
std::size_t iterations = 0;
|
|
2327
|
+
std::size_t const min_points_shifted_per_iteration = static_cast<std::size_t>(min_shifts * points_count);
|
|
2328
|
+
f64_t last_aggregate_distance = std::numeric_limits<f64_t>::max();
|
|
2329
|
+
|
|
2330
|
+
while (iterations < max_iterations) {
|
|
2331
|
+
iterations++;
|
|
2332
|
+
|
|
2333
|
+
// For every point, find the closest centroid.
|
|
2334
|
+
std::atomic<std::size_t> points_shifted{0};
|
|
2335
|
+
executor.dynamic(points_count, [&](std::size_t thread_idx, std::size_t points_idx) {
|
|
2336
|
+
byte_t const* quantized_point =
|
|
2337
|
+
points_quantized_buffer.data() + points_idx * stride_per_vector_quantized;
|
|
2338
|
+
byte_t const* quantized_centroids = centroids_quantized_buffer.data();
|
|
2339
|
+
distance_t closest_distance_local = std::numeric_limits<distance_t>::max();
|
|
2340
|
+
std::size_t closest_idx_local = 0;
|
|
2341
|
+
for (std::size_t centroid_idx = 0; centroid_idx < wanted_clusters; centroid_idx++) {
|
|
2342
|
+
byte_t const* quantized_centroid = quantized_centroids + centroid_idx * stride_per_vector_quantized;
|
|
2343
|
+
distance_t distance = metric(quantized_point, quantized_centroid);
|
|
2344
|
+
if (distance < closest_distance_local) {
|
|
2345
|
+
closest_distance_local = distance;
|
|
2346
|
+
closest_idx_local = centroid_idx;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
distance_t& closest_distance_ref = point_to_centroid_distance_buffer[points_idx];
|
|
2351
|
+
std::size_t& closest_idx_ref = point_to_centroid_index_buffer[points_idx];
|
|
2352
|
+
if (closest_idx_local != closest_idx_ref) {
|
|
2353
|
+
closest_idx_ref = closest_idx_local;
|
|
2354
|
+
points_shifted.fetch_add(1, std::memory_order_relaxed);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
closest_distance_ref = closest_distance_local;
|
|
2358
|
+
return true;
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
f64_t aggregate_distance = 0.0;
|
|
2362
|
+
for (std::size_t i = 0; i < points_count; i++)
|
|
2363
|
+
aggregate_distance += point_to_centroid_distance_buffer[i];
|
|
2364
|
+
f64_t aggregate_distance_change =
|
|
2365
|
+
std::abs(aggregate_distance - last_aggregate_distance) / last_aggregate_distance;
|
|
2366
|
+
|
|
2367
|
+
auto current_time = std::chrono::high_resolution_clock::now();
|
|
2368
|
+
std::chrono::duration<f64_t> elapsed_time = current_time - start_time;
|
|
2369
|
+
result.runtime_seconds = elapsed_time.count();
|
|
2370
|
+
result.last_iteration_inertia = aggregate_distance_change;
|
|
2371
|
+
result.last_iteration_points_shifted = points_shifted.load(std::memory_order_relaxed);
|
|
2372
|
+
|
|
2373
|
+
// Check for early-exit conditions
|
|
2374
|
+
if (last_aggregate_distance != 0.0 && inertia_threshold != 0.0)
|
|
2375
|
+
if (aggregate_distance_change <= inertia_threshold)
|
|
2376
|
+
break;
|
|
2377
|
+
if (min_points_shifted_per_iteration != 0 || result.last_iteration_points_shifted == 0)
|
|
2378
|
+
if (result.last_iteration_points_shifted <= min_points_shifted_per_iteration)
|
|
2379
|
+
break;
|
|
2380
|
+
if (max_seconds != 0)
|
|
2381
|
+
if (result.runtime_seconds >= max_seconds)
|
|
2382
|
+
break;
|
|
2383
|
+
|
|
2384
|
+
// For every centroid, recalculate the mean of all points assigned to it.
|
|
2385
|
+
// That part is problematic to parallelize on many-core-systems, because of the contention.
|
|
2386
|
+
// Alternatively, a tree-like approach can be used, where every core accumulates it's own partial sums.
|
|
2387
|
+
// And those are later aggregated by a single thread.
|
|
2388
|
+
std::memset(centroids_precise_buffer.data(), 0,
|
|
2389
|
+
wanted_clusters * dimensions * thread_count * sizeof(f64_t));
|
|
2390
|
+
std::memset(reinterpret_cast<byte_t*>(cluster_sizes_buffer.data()), 0,
|
|
2391
|
+
wanted_clusters * sizeof(std::atomic<std::size_t>));
|
|
2392
|
+
executor.dynamic(points_count, [&](std::size_t thread_idx, std::size_t points_idx) {
|
|
2393
|
+
std::size_t centroid_idx = point_to_centroid_index_buffer[points_idx];
|
|
2394
|
+
byte_t const* quantized_point =
|
|
2395
|
+
points_quantized_buffer.data() + points_idx * stride_per_vector_quantized;
|
|
2396
|
+
f64_t* centroid_precise = centroids_precise_buffer.data() + wanted_clusters * dimensions * thread_idx +
|
|
2397
|
+
centroid_idx * dimensions;
|
|
2398
|
+
|
|
2399
|
+
// Upcast the points point into a buffer of double-precision floats.
|
|
2400
|
+
f64_t* point_precise = points_precise_buffer.data() + wanted_clusters * dimensions * thread_idx +
|
|
2401
|
+
centroid_idx * dimensions;
|
|
2402
|
+
if (!decompress_precise(quantized_point, dimensions, reinterpret_cast<byte_t*>(point_precise)))
|
|
2403
|
+
std::memcpy(reinterpret_cast<byte_t*>(point_precise), quantized_point, bytes_per_vector_quantized);
|
|
2404
|
+
|
|
2405
|
+
// Now add the vector from the points into the centroid partial sum.
|
|
2406
|
+
for (std::size_t i = 0; i < dimensions; i++)
|
|
2407
|
+
centroid_precise[i] += point_precise[i];
|
|
2408
|
+
|
|
2409
|
+
cluster_sizes_buffer[centroid_idx].fetch_add(1, std::memory_order_relaxed);
|
|
2410
|
+
return true;
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// Aggregate the partial sums into the final centroids - storing them in the high-precision
|
|
2414
|
+
// buffer of the first thread. Normalization procedure is different for different metrics.
|
|
2415
|
+
for (std::size_t centroid_idx = 0; centroid_idx < wanted_clusters; centroid_idx++) {
|
|
2416
|
+
f64_t* centroid_precise_aggregated = centroids_precise_buffer.data() + centroid_idx * dimensions;
|
|
2417
|
+
for (std::size_t thread_idx = 1; thread_idx < thread_count; thread_idx++) {
|
|
2418
|
+
f64_t* centroid_precise = centroids_precise_buffer.data() +
|
|
2419
|
+
wanted_clusters * dimensions * thread_idx + centroid_idx * dimensions;
|
|
2420
|
+
for (std::size_t i = 0; i < dimensions; i++)
|
|
2421
|
+
centroid_precise_aggregated[i] += centroid_precise[i];
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// Normalize based on the metric kind
|
|
2425
|
+
if (metric_kind == metric_kind_t::l2sq_k) {
|
|
2426
|
+
// Normalize for Euclidean distance (L2)
|
|
2427
|
+
std::size_t cluster_size = cluster_sizes_buffer[centroid_idx].load(std::memory_order_relaxed);
|
|
2428
|
+
if (cluster_size > 0)
|
|
2429
|
+
for (std::size_t i = 0; i < dimensions; i++)
|
|
2430
|
+
centroid_precise_aggregated[i] /= static_cast<f64_t>(cluster_size);
|
|
2431
|
+
|
|
2432
|
+
} else if (metric_kind == metric_kind_t::cos_k) {
|
|
2433
|
+
// Normalize for Cosine distance
|
|
2434
|
+
f64_t norm = 0.0;
|
|
2435
|
+
for (std::size_t i = 0; i < dimensions; i++)
|
|
2436
|
+
norm += centroid_precise_aggregated[i] * centroid_precise_aggregated[i];
|
|
2437
|
+
norm = std::sqrt(norm);
|
|
2438
|
+
if (norm > 0.0)
|
|
2439
|
+
for (std::size_t i = 0; i < dimensions; i++)
|
|
2440
|
+
centroid_precise_aggregated[i] /= norm;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// Quantize the centroid after normalization for further iterations
|
|
2444
|
+
byte_t* centroid_quantized =
|
|
2445
|
+
centroids_quantized_buffer.data() + centroid_idx * stride_per_vector_quantized;
|
|
2446
|
+
if (!compress_precise(reinterpret_cast<byte_t*>(centroid_precise_aggregated), dimensions,
|
|
2447
|
+
centroid_quantized))
|
|
2448
|
+
std::memcpy(centroid_quantized, reinterpret_cast<byte_t*>(centroid_precise_aggregated),
|
|
2449
|
+
bytes_per_vector_quantized);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// Export stats.
|
|
2454
|
+
result.iterations = iterations;
|
|
2455
|
+
result.computed_distances = points_count * wanted_clusters * iterations;
|
|
2456
|
+
result.aggregate_distance = 0;
|
|
2457
|
+
for (distance_t distance : point_to_centroid_distance_buffer)
|
|
2458
|
+
result.aggregate_distance += distance;
|
|
2459
|
+
|
|
2460
|
+
// We've finished all the iterations, now we can export the centroids back to the original precision.
|
|
2461
|
+
std::memcpy(point_to_centroid_index, point_to_centroid_index_buffer.data(), points_count * sizeof(std::size_t));
|
|
2462
|
+
std::memcpy(point_to_centroid_distance, point_to_centroid_distance_buffer.data(),
|
|
2463
|
+
points_count * sizeof(distance_t));
|
|
2464
|
+
for (std::size_t i = 0; i < wanted_clusters; i++) {
|
|
2465
|
+
byte_t const* quantized_centroid = centroids_quantized_buffer.data() + i * stride_per_vector_quantized;
|
|
2466
|
+
byte_t* centroid = centroids_data + i * centroids_stride_bytes;
|
|
2467
|
+
if (!decompress_points(quantized_centroid, dimensions, centroid))
|
|
2468
|
+
std::memcpy(centroid, quantized_centroid, bytes_per_vector_quantized);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
|
|
2475
|
+
using kmeans_clustering_t = kmeans_clustering_gt<>;
|
|
2476
|
+
|
|
2140
2477
|
/**
|
|
2141
2478
|
* @brief C++11 Multi-Hash-Set with Linear Probing.
|
|
2142
2479
|
*
|
package/javascript/lib.cpp
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
using namespace unum::usearch;
|
|
21
21
|
using namespace unum;
|
|
22
22
|
|
|
23
|
+
using add_result_t = typename index_dense_t::add_result_t;
|
|
24
|
+
|
|
23
25
|
class CompiledIndex : public Napi::ObjectWrap<CompiledIndex> {
|
|
24
26
|
public:
|
|
25
27
|
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
@@ -129,6 +131,8 @@ void CompiledIndex::Load(Napi::CallbackInfo const& ctx) {
|
|
|
129
131
|
auto result = native_->load(path.c_str());
|
|
130
132
|
if (!result)
|
|
131
133
|
Napi::TypeError::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
|
|
134
|
+
if (!native_->try_reserve(ceil2(native_->size())))
|
|
135
|
+
Napi::Error::New(ctx.Env(), "Failed to reserve memory").ThrowAsJavaScriptException();
|
|
132
136
|
|
|
133
137
|
} catch (...) {
|
|
134
138
|
Napi::TypeError::New(ctx.Env(), "Loading failed").ThrowAsJavaScriptException();
|
|
@@ -142,6 +146,8 @@ void CompiledIndex::View(Napi::CallbackInfo const& ctx) {
|
|
|
142
146
|
auto result = native_->view(path.c_str());
|
|
143
147
|
if (!result)
|
|
144
148
|
Napi::TypeError::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
|
|
149
|
+
if (!native_->try_reserve(ceil2(native_->size())))
|
|
150
|
+
Napi::Error::New(ctx.Env(), "Failed to reserve memory").ThrowAsJavaScriptException();
|
|
145
151
|
|
|
146
152
|
} catch (...) {
|
|
147
153
|
Napi::TypeError::New(ctx.Env(), "Memory-mapping failed").ThrowAsJavaScriptException();
|
|
@@ -161,7 +167,10 @@ void CompiledIndex::Add(Napi::CallbackInfo const& ctx) {
|
|
|
161
167
|
auto run_parallel = [&](auto vectors) {
|
|
162
168
|
executor_stl_t executor;
|
|
163
169
|
executor.fixed(tasks, [&](std::size_t /*thread_idx*/, std::size_t task_idx) {
|
|
164
|
-
native_->add(static_cast<default_key_t>(keys[task_idx]),
|
|
170
|
+
add_result_t result = native_->add(static_cast<default_key_t>(keys[task_idx]),
|
|
171
|
+
vectors + task_idx * native_->dimensions());
|
|
172
|
+
if (!result)
|
|
173
|
+
Napi::Error::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
|
|
165
174
|
});
|
|
166
175
|
};
|
|
167
176
|
|
|
@@ -245,11 +254,14 @@ Napi::Value CompiledIndex::Remove(Napi::CallbackInfo const& ctx) {
|
|
|
245
254
|
Napi::Env env = ctx.Env();
|
|
246
255
|
Napi::BigUint64Array keys = ctx[0].As<Napi::BigUint64Array>();
|
|
247
256
|
std::size_t length = keys.ElementLength();
|
|
248
|
-
Napi::Array
|
|
257
|
+
Napi::Array results = Napi::Array::New(env, length);
|
|
249
258
|
for (std::size_t i = 0; i < length; ++i) {
|
|
250
|
-
result
|
|
259
|
+
auto result = native_->remove(static_cast<default_key_t>(keys[i]));
|
|
260
|
+
if (!result)
|
|
261
|
+
Napi::Error::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
|
|
262
|
+
results[i] = Napi::Number::New(env, result.completed);
|
|
251
263
|
}
|
|
252
|
-
return
|
|
264
|
+
return results;
|
|
253
265
|
}
|
|
254
266
|
|
|
255
267
|
Napi::Value CompiledIndex::Contains(Napi::CallbackInfo const& ctx) {
|
|
@@ -268,7 +280,7 @@ Napi::Value CompiledIndex::Count(Napi::CallbackInfo const& ctx) {
|
|
|
268
280
|
std::size_t length = keys.ElementLength();
|
|
269
281
|
Napi::Array result = Napi::Array::New(env, length);
|
|
270
282
|
for (std::size_t i = 0; i < length; ++i)
|
|
271
|
-
result[i] = Napi::
|
|
283
|
+
result[i] = Napi::Number::New(env, native_->count(static_cast<default_key_t>(keys[i])));
|
|
272
284
|
return result;
|
|
273
285
|
}
|
|
274
286
|
|