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.
- package/README.md +1 -1
- package/binding.gyp +1 -1
- package/c/CMakeLists.txt +10 -3
- package/cpp/README.md +1 -1
- package/cpp/bench.cpp +2 -2
- package/cpp/test.cpp +16 -0
- package/include/usearch/index.hpp +44 -17
- package/include/usearch/index_dense.hpp +51 -89
- package/include/usearch/index_plugins.hpp +468 -43
- package/javascript/lib.cpp +17 -5
- package/javascript/usearch.test.js +205 -34
- 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/CODE_OF_CONDUCT.md +128 -0
- package/simsimd/CONTRIBUTING.md +84 -26
- package/simsimd/Cargo.lock +1 -1
- package/simsimd/Cargo.toml +4 -4
- package/simsimd/MANIFEST.in +3 -0
- package/simsimd/Package.swift +15 -3
- package/simsimd/README.md +534 -177
- package/simsimd/binding.gyp +6 -2
- package/simsimd/build.rs +13 -1
- package/simsimd/c/lib.c +266 -53
- package/simsimd/include/simsimd/binary.h +321 -112
- package/simsimd/include/simsimd/curved.h +803 -0
- package/simsimd/include/simsimd/dot.h +694 -521
- package/simsimd/include/simsimd/elementwise.h +1437 -0
- package/simsimd/include/simsimd/geospatial.h +9 -3
- package/simsimd/include/simsimd/mesh.h +69 -0
- package/simsimd/include/simsimd/probability.h +234 -206
- package/simsimd/include/simsimd/simsimd.h +1476 -617
- package/simsimd/include/simsimd/sparse.h +1428 -0
- package/simsimd/include/simsimd/spatial.h +1556 -742
- package/simsimd/include/simsimd/types.h +276 -132
- package/simsimd/javascript/lib.c +1 -2
- package/simsimd/javascript/simsimd.ts +4 -4
- package/simsimd/package.json +4 -4
- package/simsimd/rust/lib.rs +179 -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
|
@@ -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
|
|
46
|
-
#
|
|
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
|
|
402
|
+
return simsimd_f16_to_f32((simsimd_f16_t const*)&u16);
|
|
384
403
|
#else
|
|
385
|
-
|
|
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
|
-
|
|
418
|
+
std::uint16_t result;
|
|
419
|
+
simsimd_f32_to_f16(f32, (simsimd_f16_t*)&result);
|
|
420
|
+
return result;
|
|
399
421
|
#else
|
|
400
|
-
|
|
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
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
1927
|
-
:
|
|
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
|
-
|
|
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
|
|
1936
|
-
scalar_t
|
|
1937
|
-
scalar_t
|
|
1938
|
-
return reinterpret_cast<scalar_t
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
1975
|
-
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(), //
|
|
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
|
*
|