usearch 2.15.2 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/binding.gyp +1 -1
  3. package/c/CMakeLists.txt +10 -3
  4. package/cpp/README.md +1 -1
  5. package/cpp/bench.cpp +2 -2
  6. package/cpp/test.cpp +16 -0
  7. package/include/usearch/index.hpp +44 -17
  8. package/include/usearch/index_dense.hpp +51 -89
  9. package/include/usearch/index_plugins.hpp +468 -43
  10. package/javascript/lib.cpp +17 -5
  11. package/javascript/usearch.test.js +205 -34
  12. package/package.json +1 -1
  13. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  14. package/prebuilds/linux-arm64/usearch.node +0 -0
  15. package/prebuilds/linux-x64/usearch.node +0 -0
  16. package/simsimd/.git-blame-ignore-revs +2 -0
  17. package/simsimd/CODE_OF_CONDUCT.md +128 -0
  18. package/simsimd/CONTRIBUTING.md +84 -26
  19. package/simsimd/Cargo.lock +1 -1
  20. package/simsimd/Cargo.toml +4 -4
  21. package/simsimd/MANIFEST.in +3 -0
  22. package/simsimd/Package.swift +15 -3
  23. package/simsimd/README.md +534 -177
  24. package/simsimd/binding.gyp +6 -2
  25. package/simsimd/build.rs +13 -1
  26. package/simsimd/c/lib.c +266 -53
  27. package/simsimd/include/simsimd/binary.h +321 -112
  28. package/simsimd/include/simsimd/curved.h +803 -0
  29. package/simsimd/include/simsimd/dot.h +694 -521
  30. package/simsimd/include/simsimd/elementwise.h +1437 -0
  31. package/simsimd/include/simsimd/geospatial.h +9 -3
  32. package/simsimd/include/simsimd/mesh.h +69 -0
  33. package/simsimd/include/simsimd/probability.h +234 -206
  34. package/simsimd/include/simsimd/simsimd.h +1476 -617
  35. package/simsimd/include/simsimd/sparse.h +1428 -0
  36. package/simsimd/include/simsimd/spatial.h +1556 -742
  37. package/simsimd/include/simsimd/types.h +276 -132
  38. package/simsimd/javascript/lib.c +1 -2
  39. package/simsimd/javascript/simsimd.ts +4 -4
  40. package/simsimd/package.json +4 -4
  41. package/simsimd/rust/lib.rs +179 -1
  42. package/simsimd/scripts/bench.cxx +1070 -0
  43. package/simsimd/scripts/bench.ipynb +187 -0
  44. package/simsimd/{javascript → scripts}/bench.js +1 -1
  45. package/simsimd/{rust/benches/sqeuclidean.rs → scripts/bench_sqeuclidean.rs} +6 -6
  46. package/simsimd/scripts/bench_vectors.py +700 -0
  47. package/simsimd/scripts/bench_vectors_live.py +213 -0
  48. package/simsimd/scripts/test.c +155 -0
  49. package/simsimd/{javascript → scripts}/test.mjs +2 -2
  50. package/simsimd/scripts/test.py +1410 -0
  51. package/simsimd/swift/{source/SimSIMD.swift → SimSIMD.swift} +6 -2
  52. package/simsimd/swift/{tests/SimSIMDTests.swift → Test.swift} +1 -1
  53. /package/simsimd/{rust/benches/cosine.rs → scripts/bench_cosine.rs} +0 -0
  54. /package/simsimd/{rust/benches/native.rs → scripts/bench_native.rs} +0 -0
@@ -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
 
@@ -42,20 +43,38 @@
42
43
 
43
44
  #if USEARCH_USE_SIMSIMD
44
45
  // Propagate the `f16` settings
45
- #if !defined(SIMSIMD_NATIVE_F16)
46
- #define SIMSIMD_NATIVE_F16 !USEARCH_USE_FP16LIB
46
+ #if defined(USEARCH_CAN_COMPILE_FP16) || defined(USEARCH_CAN_COMPILE_FLOAT16)
47
+ #if USEARCH_CAN_COMPILE_FP16 || USEARCH_CAN_COMPILE_FLOAT16
48
+ #define SIMSIMD_NATIVE_F16 1
49
+ #else
50
+ #define SIMSIMD_NATIVE_F16 0
51
+ #endif
52
+ #endif
53
+ // Propagate the `bf16` settings
54
+ #if defined(USEARCH_CAN_COMPILE_BF16) || defined(USEARCH_CAN_COMPILE_BFLOAT16)
55
+ #if USEARCH_CAN_COMPILE_BF16 || USEARCH_CAN_COMPILE_BFLOAT16
56
+ #define SIMSIMD_NATIVE_BF16 1
57
+ #else
58
+ #define SIMSIMD_NATIVE_BF16 0
59
+ #endif
47
60
  #endif
48
-
49
- // Overwrite the dynamic dispatch settings
50
- #undef SIMSIMD_DYNAMIC_DISPATCH
51
- #define SIMSIMD_DYNAMIC_DISPATCH 0
52
61
  // No problem, if some of the functions are unused or undefined
53
62
  #pragma GCC diagnostic push
63
+ #pragma GCC diagnostic ignored "-Wall"
64
+ #pragma GCC diagnostic ignored "-Wunused"
54
65
  #pragma GCC diagnostic ignored "-Wunused-function"
66
+ #pragma GCC diagnostic ignored "-Wunused-parameter"
67
+ #pragma GCC diagnostic ignored "-Wunused-variable"
68
+ #pragma GCC diagnostic ignored "-Wunused-but-set-variable"
69
+ #if defined(_MSC_VER)
55
70
  #pragma warning(push)
56
- #pragma warning(disable : 4101)
71
+ #pragma warning(disable : 4101) // "Unused variables"
72
+ #pragma warning(disable : 4068) // "Unknown pragmas", when MSVC tries to read GCC pragmas
73
+ #endif // _MSC_VER
57
74
  #include <simsimd/simsimd.h>
75
+ #ifdef _MSC_VER
58
76
  #pragma warning(pop)
77
+ #endif // _MSC_VER
59
78
  #pragma GCC diagnostic pop
60
79
  #endif
61
80
 
@@ -380,9 +399,10 @@ inline float f16_to_f32(std::uint16_t u16) noexcept {
380
399
  #if USEARCH_USE_FP16LIB
381
400
  return fp16_ieee_to_fp32_value(u16);
382
401
  #elif USEARCH_USE_SIMSIMD
383
- return simsimd_uncompress_f16(u16);
402
+ return simsimd_f16_to_f32((simsimd_f16_t const*)&u16);
384
403
  #else
385
- f16_native_t f16;
404
+ #warning "It's recommended to use SimSIMD and fp16lib for half-precision numerics"
405
+ _Float16 f16;
386
406
  std::memcpy(&f16, &u16, sizeof(std::uint16_t));
387
407
  return float(f16);
388
408
  #endif
@@ -395,9 +415,12 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
395
415
  #if USEARCH_USE_FP16LIB
396
416
  return fp16_ieee_from_fp32_value(f32);
397
417
  #elif USEARCH_USE_SIMSIMD
398
- return simsimd_compress_f16(f32);
418
+ std::uint16_t result;
419
+ simsimd_f32_to_f16(f32, (simsimd_f16_t*)&result);
420
+ return result;
399
421
  #else
400
- f16_native_t f16 = f16_native_t(f32);
422
+ #warning "It's recommended to use SimSIMD and fp16lib for half-precision numerics"
423
+ _Float16 f16 = _Float16(f32);
401
424
  std::uint16_t u16;
402
425
  std::memcpy(&u16, &f16, sizeof(std::uint16_t));
403
426
  return u16;
@@ -410,15 +433,14 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
410
433
  */
411
434
  inline float bf16_to_f32(std::uint16_t u16) noexcept {
412
435
  #if USEARCH_USE_SIMSIMD
413
- return simsimd_uncompress_bf16(u16);
436
+ return simsimd_bf16_to_f32((simsimd_bf16_t const*)&u16);
414
437
  #else
415
438
  union float_or_unsigned_int_t {
416
439
  float f;
417
440
  unsigned int i;
418
- };
419
- union float_or_unsigned_int_t result_union;
420
- result_union.i = u16 << 16; // Zero extends the mantissa
421
- return result_union.f;
441
+ } conv;
442
+ conv.i = u16 << 16; // Zero extends the mantissa
443
+ return conv.f;
422
444
  #endif
423
445
  }
424
446
 
@@ -428,17 +450,18 @@ inline float bf16_to_f32(std::uint16_t u16) noexcept {
428
450
  */
429
451
  inline std::uint16_t f32_to_bf16(float f32) noexcept {
430
452
  #if USEARCH_USE_SIMSIMD
431
- return simsimd_compress_bf16(f32);
453
+ std::uint16_t result;
454
+ simsimd_f32_to_bf16(f32, (simsimd_bf16_t*)&result);
455
+ return result;
432
456
  #else
433
457
  union float_or_unsigned_int_t {
434
458
  float f;
435
459
  unsigned int i;
436
- };
437
- union float_or_unsigned_int_t value;
438
- value.f = f32;
439
- value.i >>= 16;
440
- value.i &= 0xFFFF;
441
- return (unsigned short)value.i;
460
+ } conv;
461
+ conv.f = f32;
462
+ conv.i >>= 16;
463
+ conv.i &= 0xFFFF;
464
+ return (unsigned short)conv.i;
442
465
  #endif
443
466
  }
444
467
 
@@ -1183,6 +1206,71 @@ template <> struct cast_gt<f64_t, b1x8_t> : public cast_to_b1x8_gt<f64_t> {};
1183
1206
  template <> struct cast_gt<b1x8_t, i8_t> : public cast_from_b1x8_gt<i8_t> {};
1184
1207
  template <> struct cast_gt<i8_t, b1x8_t> : public cast_to_b1x8_gt<i8_t> {};
1185
1208
 
1209
+ /**
1210
+ * @brief Type-punned array casting function.
1211
+ * Arguments: input buffer, bytes in input buffer, output buffer.
1212
+ * Returns `true` if the casting was performed successfully, `false` otherwise.
1213
+ */
1214
+ using cast_punned_t = bool (*)(byte_t const*, std::size_t, byte_t*);
1215
+
1216
+ /**
1217
+ * @brief A collection of casting functions for typical vector types.
1218
+ * Covers to/from conversions for boolean, integer, half-precision,
1219
+ * single-precision, and double-precision scalars.
1220
+ */
1221
+ struct casts_punned_t {
1222
+ struct group_t {
1223
+ cast_punned_t b1x8{};
1224
+ cast_punned_t i8{};
1225
+ cast_punned_t f16{};
1226
+ cast_punned_t f32{};
1227
+ cast_punned_t f64{};
1228
+
1229
+ cast_punned_t operator[](scalar_kind_t scalar_kind) const noexcept {
1230
+ switch (scalar_kind) {
1231
+ case scalar_kind_t::f64_k: return f64;
1232
+ case scalar_kind_t::f32_k: return f32;
1233
+ case scalar_kind_t::f16_k: return f16;
1234
+ case scalar_kind_t::bf16_k: return f16;
1235
+ case scalar_kind_t::i8_k: return i8;
1236
+ case scalar_kind_t::b1x8_k: return b1x8;
1237
+ default: return nullptr;
1238
+ }
1239
+ }
1240
+
1241
+ } from, to;
1242
+
1243
+ template <typename scalar_at> static casts_punned_t make() noexcept {
1244
+ casts_punned_t result;
1245
+
1246
+ result.from.b1x8 = &cast_gt<b1x8_t, scalar_at>::try_;
1247
+ result.from.i8 = &cast_gt<i8_t, scalar_at>::try_;
1248
+ result.from.f16 = &cast_gt<f16_t, scalar_at>::try_;
1249
+ result.from.f32 = &cast_gt<f32_t, scalar_at>::try_;
1250
+ result.from.f64 = &cast_gt<f64_t, scalar_at>::try_;
1251
+
1252
+ result.to.b1x8 = &cast_gt<scalar_at, b1x8_t>::try_;
1253
+ result.to.i8 = &cast_gt<scalar_at, i8_t>::try_;
1254
+ result.to.f16 = &cast_gt<scalar_at, f16_t>::try_;
1255
+ result.to.f32 = &cast_gt<scalar_at, f32_t>::try_;
1256
+ result.to.f64 = &cast_gt<scalar_at, f64_t>::try_;
1257
+
1258
+ return result;
1259
+ }
1260
+
1261
+ static casts_punned_t make(scalar_kind_t scalar_kind) noexcept {
1262
+ switch (scalar_kind) {
1263
+ case scalar_kind_t::f64_k: return casts_punned_t::make<f64_t>();
1264
+ case scalar_kind_t::f32_k: return casts_punned_t::make<f32_t>();
1265
+ case scalar_kind_t::f16_k: return casts_punned_t::make<f16_t>();
1266
+ case scalar_kind_t::bf16_k: return casts_punned_t::make<bf16_t>();
1267
+ case scalar_kind_t::i8_k: return casts_punned_t::make<i8_t>();
1268
+ case scalar_kind_t::b1x8_k: return casts_punned_t::make<b1x8_t>();
1269
+ default: return {};
1270
+ }
1271
+ }
1272
+ };
1273
+
1186
1274
  /* Don't complain if the vectorization of the inner loops fails:
1187
1275
  *
1188
1276
  * > warning: loop not vectorized: the optimizer was unable to perform the requested transformation;
@@ -1910,32 +1998,33 @@ class metric_punned_t {
1910
1998
  * @brief View over a potentially-strided memory buffer, containing a row-major matrix.
1911
1999
  */
1912
2000
  template <typename scalar_at> //
1913
- class vectors_view_gt {
2001
+ class matrix_slice_gt {
1914
2002
  using scalar_t = scalar_at;
2003
+ using byte_addressable_t = typename std::conditional<std::is_const<scalar_t>::value, byte_t const, byte_t>::type;
1915
2004
 
1916
- scalar_t const* begin_{};
2005
+ scalar_t* begin_{};
1917
2006
  std::size_t dimensions_{};
1918
2007
  std::size_t count_{};
1919
2008
  std::size_t stride_bytes_{};
1920
2009
 
1921
2010
  public:
1922
- vectors_view_gt() noexcept = default;
1923
- vectors_view_gt(vectors_view_gt const&) noexcept = default;
1924
- vectors_view_gt& operator=(vectors_view_gt const&) noexcept = default;
2011
+ matrix_slice_gt() noexcept = default;
2012
+ matrix_slice_gt(matrix_slice_gt const&) noexcept = default;
2013
+ matrix_slice_gt& operator=(matrix_slice_gt const&) noexcept = default;
1925
2014
 
1926
- vectors_view_gt(scalar_t const* begin, std::size_t dimensions, std::size_t count = 1) noexcept
1927
- : vectors_view_gt(begin, dimensions, count, dimensions * sizeof(scalar_at)) {}
2015
+ matrix_slice_gt(scalar_t* begin, std::size_t dimensions, std::size_t count = 1) noexcept
2016
+ : matrix_slice_gt(begin, dimensions, count, dimensions * sizeof(scalar_at)) {}
1928
2017
 
1929
- vectors_view_gt(scalar_t const* begin, std::size_t dimensions, std::size_t count, std::size_t stride_bytes) noexcept
2018
+ matrix_slice_gt(scalar_t* begin, std::size_t dimensions, std::size_t count, std::size_t stride_bytes) noexcept
1930
2019
  : begin_(begin), dimensions_(dimensions), count_(count), stride_bytes_(stride_bytes) {}
1931
2020
 
1932
2021
  explicit operator bool() const noexcept { return begin_; }
1933
2022
  std::size_t size() const noexcept { return count_; }
1934
2023
  std::size_t dimensions() const noexcept { return dimensions_; }
1935
- std::size_t stride() const noexcept { return stride_bytes_; }
1936
- scalar_t const* data() const noexcept { return begin_; }
1937
- scalar_t const* at(std::size_t i) const noexcept {
1938
- return reinterpret_cast<scalar_t const*>(reinterpret_cast<byte_t const*>(begin_) + i * stride_bytes_);
2024
+ std::size_t stride_bytes() const noexcept { return stride_bytes_; }
2025
+ scalar_t* data() const noexcept { return begin_; }
2026
+ scalar_t* at(std::size_t i) const noexcept {
2027
+ return reinterpret_cast<scalar_t*>(reinterpret_cast<byte_addressable_t*>(begin_) + i * stride_bytes_);
1939
2028
  }
1940
2029
  };
1941
2030
 
@@ -1944,7 +2033,7 @@ struct exact_offset_and_distance_t {
1944
2033
  f32_t distance;
1945
2034
  };
1946
2035
 
1947
- using exact_search_results_t = vectors_view_gt<exact_offset_and_distance_t>;
2036
+ using exact_search_results_t = matrix_slice_gt<exact_offset_and_distance_t const>;
1948
2037
 
1949
2038
  /**
1950
2039
  * @brief Helper-structure for exact search operations.
@@ -1965,14 +2054,14 @@ class exact_search_t {
1965
2054
 
1966
2055
  public:
1967
2056
  template <typename scalar_at, typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
1968
- exact_search_results_t operator()( //
1969
- vectors_view_gt<scalar_at> dataset, vectors_view_gt<scalar_at> queries, //
1970
- std::size_t wanted, metric_punned_t const& metric, //
2057
+ exact_search_results_t operator()( //
2058
+ matrix_slice_gt<scalar_at const> dataset, matrix_slice_gt<scalar_at const> queries, //
2059
+ std::size_t wanted, metric_punned_t const& metric, //
1971
2060
  executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
1972
- return operator()( //
1973
- metric, //
1974
- reinterpret_cast<byte_t const*>(dataset.data()), dataset.size(), dataset.stride(), //
1975
- reinterpret_cast<byte_t const*>(queries.data()), queries.size(), queries.stride(), //
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(), //
1976
2065
  wanted, executor, progress);
1977
2066
  }
1978
2067
 
@@ -2049,6 +2138,342 @@ class exact_search_t {
2049
2138
  }
2050
2139
  };
2051
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
+
2052
2477
  /**
2053
2478
  * @brief C++11 Multi-Hash-Set with Linear Probing.
2054
2479
  *