usearch 2.13.3 → 2.13.5

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 CHANGED
@@ -254,7 +254,7 @@ assert!(
254
254
  Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
255
255
  Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
256
256
  Instead, we have focused on high-precision arithmetic over low-precision downcasted vectors.
257
- The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f64_t`, `f32_t`, `f16_t`, `i8_t`, and single-bit representations.
257
+ The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f64_t`, `f32_t`, `f16_t`, `i8_t`, and single-bit `b1x8_t` representations.
258
258
  You can use the following command to check, if hardware acceleration is enabled:
259
259
 
260
260
  ```sh
@@ -264,11 +264,19 @@ $ python -c 'from usearch.index import Index; print(Index(ndim=166, metric="tani
264
264
  > ice
265
265
  ```
266
266
 
267
+ In most cases, it's recommended to use half-precision floating-point numbers on modern hardware.
268
+ When quantization is enabled, the "get"-like functions won't be able to recover the original data, so you may want to replicate the original vectors elsewhere.
269
+ When quantizing to `i8_t` integers, note that it's only valid for cosine-like metrics.
270
+ As part of the quantization process, the vectors are normalized to unit length and later scaled to [-127, 127] range to occupy the full 8-bit range.
271
+ When quantizing to `b1x8_t` single-bit representations, note that it's only valid for binary metrics like Jaccard, Hamming, etc.
272
+ As part of the quantization process, the scalar components greater than zero are set to `true`, and the rest to `false`.
273
+
274
+ ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
275
+
267
276
  Using smaller numeric types will save you RAM needed to store the vectors, but you can also compress the neighbors lists forming our proximity graphs.
268
277
  By default, 32-bit `uint32_t` is used to enumerate those, which is not enough if you need to address over 4 Billion entries.
269
278
  For such cases we provide a custom `uint40_t` type, that will still be 37.5% more space-efficient than the commonly used 8-byte integers, and will scale up to 1 Trillion entries.
270
279
 
271
- ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
272
280
 
273
281
  ## `Indexes` for Multi-Index Lookups
274
282
 
@@ -541,7 +549,7 @@ doi = {10.5281/zenodo.7949416},
541
549
  author = {Vardanian, Ash},
542
550
  title = {{USearch by Unum Cloud}},
543
551
  url = {https://github.com/unum-cloud/usearch},
544
- version = {2.13.3},
552
+ version = {2.13.5},
545
553
  year = {2023},
546
554
  month = oct,
547
555
  }
package/c/usearch.h CHANGED
@@ -72,6 +72,17 @@ USEARCH_EXPORT typedef struct usearch_init_options_t {
72
72
  usearch_metric_t metric;
73
73
  /**
74
74
  * @brief The scalar kind used for quantization of vector data during indexing.
75
+ * In most cases, on modern hardware, it's recommended to use half-precision floating-point numbers.
76
+ * When quantization is enabled, the "get"-like functions won't be able to recover the original data,
77
+ * so you may want to replicate the original vectors elsewhere.
78
+ *
79
+ * Quantizing to integers is also possible, but it's important to note that it's only valid for cosine-like
80
+ * metrics. As part of the quantization process, the vectors are normalized to unit length and later scaled
81
+ * to @b [-127,127] range to occupy the full 8-bit range.
82
+ *
83
+ * Quantizing to 1-bit booleans is also possible, but it's only valid for binary metrics like Jaccard, Hamming,
84
+ * etc. As part of the quantization process, the scalar components greater than zero are set to `true`, and the
85
+ * rest to `false`.
75
86
  */
76
87
  usearch_scalar_kind_t quantization;
77
88
  /**
@@ -9,7 +9,7 @@
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
11
  #define USEARCH_VERSION_MINOR 13
12
- #define USEARCH_VERSION_PATCH 3
12
+ #define USEARCH_VERSION_PATCH 5
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -95,7 +95,7 @@ struct index_dense_head_result_t {
95
95
  * @brief Configuration settings for the construction of dense
96
96
  * equidimensional vector indexes.
97
97
  *
98
- * Unlike the underlying `index_gt` class, incroporates the
98
+ * Unlike the underlying `index_gt` class, incorporates the
99
99
  * `::expansion_add` and `::expansion_search` parameters passed
100
100
  * separately for the lower-level engine.
101
101
  */
@@ -207,8 +207,11 @@ struct index_dense_metadata_result_t {
207
207
  }
208
208
  };
209
209
 
210
+ /**
211
+ * @brief Fixes serialized scalar-kind codes for pre-v2.10 versions, until we can upgrade to v3.
212
+ * The old enum `scalar_kind_t` is defined without explicit constants from 0.
213
+ */
210
214
  inline scalar_kind_t convert_pre_2_10_scalar_kind(scalar_kind_t scalar_kind) noexcept {
211
- // The old enum `scalar_kind_t` is defined without explicit constants from 0
212
215
  switch (static_cast<std::underlying_type<scalar_kind_t>::type>(scalar_kind)) {
213
216
  case 0: return scalar_kind_t::unknown_k;
214
217
  case 1: return scalar_kind_t::b1x8_k;
@@ -229,13 +232,15 @@ inline scalar_kind_t convert_pre_2_10_scalar_kind(scalar_kind_t scalar_kind) noe
229
232
  }
230
233
  }
231
234
 
235
+ /**
236
+ * @brief Fixes the metadata for pre-v2.10 versions, until we can upgrade to v3.
237
+ * Originates from: https://github.com/unum-cloud/usearch/issues/423
238
+ */
232
239
  inline void fix_pre_2_10_metadata(index_dense_head_t& head) {
233
- // deal with incompatibility with pre-2.10 versions: https://github.com/unum-cloud/usearch/issues/423
234
240
  if (head.version_major == 2 && head.version_minor < 10) {
235
241
  head.kind_scalar = convert_pre_2_10_scalar_kind(head.kind_scalar);
236
242
  head.kind_key = convert_pre_2_10_scalar_kind(head.kind_key);
237
243
  head.kind_compressed_slot = convert_pre_2_10_scalar_kind(head.kind_compressed_slot);
238
- // update minor versions so that no repeated conversion occurs
239
244
  head.version_minor = 10;
240
245
  head.version_patch = 0;
241
246
  }
@@ -316,7 +321,7 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
316
321
  /**
317
322
  * @brief Extracts metadata from a pre-constructed index serialized into an in-memory buffer.
318
323
  */
319
- inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_mapped_file_t file,
324
+ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_mapped_file_t const& file,
320
325
  std::size_t offset = 0) noexcept {
321
326
  index_dense_metadata_result_t result;
322
327
 
@@ -324,7 +329,7 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
324
329
  if (offset + sizeof(index_dense_head_buffer_t) >= file.size())
325
330
  return result.failed("End of file reached!");
326
331
 
327
- byte_t* const file_data = file.data() + offset;
332
+ byte_t const* file_data = file.data() + offset;
328
333
  std::size_t const file_size = file.size() - offset;
329
334
  std::memcpy(&result.head_buffer, file_data, sizeof(index_dense_head_buffer_t));
330
335
 
@@ -70,7 +70,6 @@ struct uuid_t {
70
70
  };
71
71
 
72
72
  class f16_bits_t;
73
- class i8_converted_t;
74
73
 
75
74
  #if !USEARCH_USE_FP16LIB
76
75
  #if USEARCH_USE_SIMSIMD
@@ -99,6 +98,9 @@ using i32_t = std::int32_t;
99
98
  using i16_t = std::int16_t;
100
99
  using i8_t = std::int8_t;
101
100
 
101
+ /**
102
+ * @brief Enumerates the most commonly used distance metrics, mostly for dense vector representations.
103
+ */
102
104
  enum class metric_kind_t : std::uint8_t {
103
105
  unknown_k = 0,
104
106
  // Classics:
@@ -111,13 +113,19 @@ enum class metric_kind_t : std::uint8_t {
111
113
  haversine_k = 'h',
112
114
  divergence_k = 'd',
113
115
 
114
- // Sets:
115
- jaccard_k = 'j',
116
+ // Dense Sets:
116
117
  hamming_k = 'b',
117
118
  tanimoto_k = 't',
118
119
  sorensen_k = 's',
120
+
121
+ // Sparse Sets:
122
+ jaccard_k = 'j',
119
123
  };
120
124
 
125
+ /**
126
+ * @brief Enumerates the most commonly used scalar types, mostly for dense vector representations.
127
+ * Doesn't include logical types, like complex numbers or quaternions.
128
+ */
121
129
  enum class scalar_kind_t : std::uint8_t {
122
130
  unknown_k = 0,
123
131
  // Custom:
@@ -198,33 +206,63 @@ inline bool str_equals(char const* begin, std::size_t len, char const* other_beg
198
206
 
199
207
  inline std::size_t bits_per_scalar(scalar_kind_t scalar_kind) noexcept {
200
208
  switch (scalar_kind) {
209
+ case scalar_kind_t::uuid_k: return 128;
210
+ case scalar_kind_t::u40_k: return 40;
211
+ case scalar_kind_t::b1x8_k: return 1;
212
+ case scalar_kind_t::u64_k: return 64;
213
+ case scalar_kind_t::i64_k: return 64;
201
214
  case scalar_kind_t::f64_k: return 64;
215
+ case scalar_kind_t::u32_k: return 32;
216
+ case scalar_kind_t::i32_k: return 32;
202
217
  case scalar_kind_t::f32_k: return 32;
218
+ case scalar_kind_t::u16_k: return 16;
219
+ case scalar_kind_t::i16_k: return 16;
203
220
  case scalar_kind_t::f16_k: return 16;
221
+ case scalar_kind_t::u8_k: return 8;
204
222
  case scalar_kind_t::i8_k: return 8;
205
- case scalar_kind_t::b1x8_k: return 1;
223
+ case scalar_kind_t::f8_k: return 8;
206
224
  default: return 0;
207
225
  }
208
226
  }
209
227
 
210
228
  inline std::size_t bits_per_scalar_word(scalar_kind_t scalar_kind) noexcept {
211
229
  switch (scalar_kind) {
230
+ case scalar_kind_t::uuid_k: return 128;
231
+ case scalar_kind_t::u40_k: return 40;
232
+ case scalar_kind_t::b1x8_k: return 8;
233
+ case scalar_kind_t::u64_k: return 64;
234
+ case scalar_kind_t::i64_k: return 64;
212
235
  case scalar_kind_t::f64_k: return 64;
236
+ case scalar_kind_t::u32_k: return 32;
237
+ case scalar_kind_t::i32_k: return 32;
213
238
  case scalar_kind_t::f32_k: return 32;
239
+ case scalar_kind_t::u16_k: return 16;
240
+ case scalar_kind_t::i16_k: return 16;
214
241
  case scalar_kind_t::f16_k: return 16;
242
+ case scalar_kind_t::u8_k: return 8;
215
243
  case scalar_kind_t::i8_k: return 8;
216
- case scalar_kind_t::b1x8_k: return 8;
244
+ case scalar_kind_t::f8_k: return 8;
217
245
  default: return 0;
218
246
  }
219
247
  }
220
248
 
221
249
  inline char const* scalar_kind_name(scalar_kind_t scalar_kind) noexcept {
222
250
  switch (scalar_kind) {
251
+ case scalar_kind_t::uuid_k: return "uuid";
252
+ case scalar_kind_t::u40_k: return "u40";
253
+ case scalar_kind_t::b1x8_k: return "b1x8";
254
+ case scalar_kind_t::u64_k: return "u64";
255
+ case scalar_kind_t::i64_k: return "i64";
256
+ case scalar_kind_t::f64_k: return "f64";
257
+ case scalar_kind_t::u32_k: return "u32";
258
+ case scalar_kind_t::i32_k: return "i32";
223
259
  case scalar_kind_t::f32_k: return "f32";
260
+ case scalar_kind_t::u16_k: return "u16";
261
+ case scalar_kind_t::i16_k: return "i16";
224
262
  case scalar_kind_t::f16_k: return "f16";
225
- case scalar_kind_t::f64_k: return "f64";
263
+ case scalar_kind_t::u8_k: return "u8";
226
264
  case scalar_kind_t::i8_k: return "i8";
227
- case scalar_kind_t::b1x8_k: return "b1x8";
265
+ case scalar_kind_t::f8_k: return "f8";
228
266
  default: return "";
229
267
  }
230
268
  }
@@ -242,8 +280,8 @@ inline char const* metric_kind_name(metric_kind_t metric) noexcept {
242
280
  case metric_kind_t::hamming_k: return "hamming";
243
281
  case metric_kind_t::tanimoto_k: return "tanimoto";
244
282
  case metric_kind_t::sorensen_k: return "sorensen";
283
+ default: return "";
245
284
  }
246
- return "";
247
285
  }
248
286
  inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::size_t len) {
249
287
  expected_gt<scalar_kind_t> parsed;
@@ -333,8 +371,7 @@ class f16_bits_t {
333
371
  inline operator float() const noexcept { return f16_to_f32(uint16_); }
334
372
  inline explicit operator bool() const noexcept { return f16_to_f32(uint16_) > 0.5f; }
335
373
 
336
- inline f16_bits_t(i8_converted_t) noexcept;
337
- inline f16_bits_t(std::int8_t v) noexcept : uint16_(v) {}
374
+ inline f16_bits_t(std::int8_t v) noexcept : uint16_(f32_to_f16(v)) {}
338
375
  inline f16_bits_t(bool v) noexcept : uint16_(f32_to_f16(v)) {}
339
376
  inline f16_bits_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
340
377
  inline f16_bits_t(double v) noexcept : uint16_(f32_to_f16(static_cast<float>(v))) {}
@@ -345,14 +382,14 @@ class f16_bits_t {
345
382
  inline f16_bits_t operator-(f16_bits_t other) const noexcept { return {float(*this) - float(other)}; }
346
383
  inline f16_bits_t operator*(f16_bits_t other) const noexcept { return {float(*this) * float(other)}; }
347
384
  inline f16_bits_t operator/(f16_bits_t other) const noexcept { return {float(*this) / float(other)}; }
348
- inline f16_bits_t operator+(float other) const noexcept { return {float(*this) + other}; }
349
- inline f16_bits_t operator-(float other) const noexcept { return {float(*this) - other}; }
350
- inline f16_bits_t operator*(float other) const noexcept { return {float(*this) * other}; }
351
- inline f16_bits_t operator/(float other) const noexcept { return {float(*this) / other}; }
352
- inline f16_bits_t operator+(double other) const noexcept { return {float(*this) + other}; }
353
- inline f16_bits_t operator-(double other) const noexcept { return {float(*this) - other}; }
354
- inline f16_bits_t operator*(double other) const noexcept { return {float(*this) * other}; }
355
- inline f16_bits_t operator/(double other) const noexcept { return {float(*this) / other}; }
385
+ inline float operator+(float other) const noexcept { return float(*this) + other; }
386
+ inline float operator-(float other) const noexcept { return float(*this) - other; }
387
+ inline float operator*(float other) const noexcept { return float(*this) * other; }
388
+ inline float operator/(float other) const noexcept { return float(*this) / other; }
389
+ inline double operator+(double other) const noexcept { return float(*this) + other; }
390
+ inline double operator-(double other) const noexcept { return float(*this) - other; }
391
+ inline double operator*(double other) const noexcept { return float(*this) * other; }
392
+ inline double operator/(double other) const noexcept { return float(*this) / other; }
356
393
 
357
394
  inline f16_bits_t& operator+=(float v) noexcept {
358
395
  uint16_ = f32_to_f16(v + f16_to_f32(uint16_));
@@ -907,7 +944,7 @@ template <> struct cast_gt<b1x8_t, b1x8_t> {
907
944
  static bool try_(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
908
945
  };
909
946
 
910
- template <typename from_scalar_at> struct cast_gt<from_scalar_at, b1x8_t> {
947
+ template <typename from_scalar_at> struct cast_to_b1x8_gt {
911
948
  inline static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
912
949
  from_scalar_at const* typed_input = reinterpret_cast<from_scalar_at const*>(input);
913
950
  unsigned char* typed_output = reinterpret_cast<unsigned char*>(output);
@@ -928,7 +965,7 @@ template <typename from_scalar_at> struct cast_gt<from_scalar_at, b1x8_t> {
928
965
  }
929
966
  };
930
967
 
931
- template <typename to_scalar_at> struct cast_gt<b1x8_t, to_scalar_at> {
968
+ template <typename to_scalar_at> struct cast_from_b1x8_gt {
932
969
  static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
933
970
  unsigned char const* typed_input = reinterpret_cast<unsigned char const*>(input);
934
971
  to_scalar_at* typed_output = reinterpret_cast<to_scalar_at*>(output);
@@ -940,53 +977,55 @@ template <typename to_scalar_at> struct cast_gt<b1x8_t, to_scalar_at> {
940
977
  }
941
978
  };
942
979
 
943
- /**
944
- * @brief Numeric type for uniformly-distributed floating point
945
- * values within [-1,1] range, quantized to integers [-100,100].
946
- */
947
- class i8_converted_t {
948
- std::int8_t int8_{};
980
+ template <typename from_scalar_at> struct cast_to_i8_gt {
981
+ inline static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
982
+ from_scalar_at const* typed_input = reinterpret_cast<from_scalar_at const*>(input);
983
+ std::int8_t* typed_output = reinterpret_cast<std::int8_t*>(output);
984
+ // Unlike other casting mechanisms, switching to small range integers is a two step procedure.
985
+ // First we want to estimate the magnitude of the vector to scale into [-1.0, 1.0] interval,
986
+ // instead of clamping. And then we scale the values into the [-127, 127] range.
987
+ // ! This makes an assumption, that the distance metric is dot-product-like, which may not
988
+ // ! be true in many cases, so it's recommended to avoid automatic casting from floats to
989
+ // ! integers.
990
+ double magnitude = 0.0;
991
+ for (std::size_t i = 0; i != dim; ++i)
992
+ magnitude += (double)typed_input[i] * (double)typed_input[i];
993
+ magnitude = std::sqrt(magnitude);
994
+ for (std::size_t i = 0; i != dim; ++i)
995
+ typed_output[i] =
996
+ static_cast<std::int8_t>(usearch::clamp<double>(typed_input[i] * 127.0 / magnitude, -127.0, 127.0));
997
+ return true;
998
+ }
999
+ };
949
1000
 
950
- public:
951
- constexpr static f32_t divisor_k = 100.f;
952
- constexpr static std::int8_t min_k = -100;
953
- constexpr static std::int8_t max_k = 100;
954
-
955
- inline i8_converted_t() noexcept : int8_(0) {}
956
- inline i8_converted_t(bool v) noexcept : int8_(v ? max_k : 0) {}
957
-
958
- inline i8_converted_t(i8_converted_t&&) = default;
959
- inline i8_converted_t& operator=(i8_converted_t&&) = default;
960
- inline i8_converted_t(i8_converted_t const&) = default;
961
- inline i8_converted_t& operator=(i8_converted_t const&) = default;
962
-
963
- inline operator f16_t() const noexcept { return static_cast<f16_t>(f32_t(int8_) / divisor_k); }
964
- inline operator f32_t() const noexcept { return f32_t(int8_) / divisor_k; }
965
- inline operator f64_t() const noexcept { return f64_t(int8_) / divisor_k; }
966
- inline explicit operator bool() const noexcept { return int8_ > (max_k / 2); }
967
- inline explicit operator std::int8_t() const noexcept { return int8_; }
968
- inline explicit operator std::int16_t() const noexcept { return int8_; }
969
- inline explicit operator std::int32_t() const noexcept { return int8_; }
970
- inline explicit operator std::int64_t() const noexcept { return int8_; }
971
-
972
- // Even with `f16_t` arguments we may want to use `f32_t` for clamping and comparions.
973
- inline i8_converted_t(f16_t v)
974
- : int8_(static_cast<std::int8_t>(usearch::clamp<f32_t>(v * divisor_k, min_k, max_k))) {}
975
- inline i8_converted_t(f32_t v)
976
- : int8_(static_cast<std::int8_t>(usearch::clamp<f32_t>(v * divisor_k, min_k, max_k))) {}
977
- inline i8_converted_t(f64_t v)
978
- : int8_(static_cast<std::int8_t>(usearch::clamp<f64_t>(v * divisor_k, min_k, max_k))) {}
1001
+ template <typename to_scalar_at> struct cast_from_i8_gt {
1002
+ static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
1003
+ std::int8_t const* typed_input = reinterpret_cast<std::int8_t const*>(input);
1004
+ to_scalar_at* typed_output = reinterpret_cast<to_scalar_at*>(output);
1005
+ for (std::size_t i = 0; i != dim; ++i)
1006
+ typed_output[i] = static_cast<to_scalar_at>(typed_input[i]) / 127.f;
1007
+ return true;
1008
+ }
979
1009
  };
980
1010
 
981
- f16_bits_t::f16_bits_t(i8_converted_t v) noexcept : uint16_(f32_to_f16(v)) {}
1011
+ template <> struct cast_gt<i8_t, f16_t> : public cast_from_i8_gt<f16_t> {};
1012
+ template <> struct cast_gt<i8_t, f32_t> : public cast_from_i8_gt<f32_t> {};
1013
+ template <> struct cast_gt<i8_t, f64_t> : public cast_from_i8_gt<f64_t> {};
1014
+
1015
+ template <> struct cast_gt<f16_t, i8_t> : public cast_to_i8_gt<f16_t> {};
1016
+ template <> struct cast_gt<f32_t, i8_t> : public cast_to_i8_gt<f32_t> {};
1017
+ template <> struct cast_gt<f64_t, i8_t> : public cast_to_i8_gt<f64_t> {};
1018
+
1019
+ template <> struct cast_gt<b1x8_t, f16_t> : public cast_from_b1x8_gt<f16_t> {};
1020
+ template <> struct cast_gt<b1x8_t, f32_t> : public cast_from_b1x8_gt<f32_t> {};
1021
+ template <> struct cast_gt<b1x8_t, f64_t> : public cast_from_b1x8_gt<f64_t> {};
982
1022
 
983
- template <> struct cast_gt<i8_t, f16_t> : public cast_gt<i8_converted_t, f16_t> {};
984
- template <> struct cast_gt<i8_t, f32_t> : public cast_gt<i8_converted_t, f32_t> {};
985
- template <> struct cast_gt<i8_t, f64_t> : public cast_gt<i8_converted_t, f64_t> {};
1023
+ template <> struct cast_gt<f16_t, b1x8_t> : public cast_to_b1x8_gt<f16_t> {};
1024
+ template <> struct cast_gt<f32_t, b1x8_t> : public cast_to_b1x8_gt<f32_t> {};
1025
+ template <> struct cast_gt<f64_t, b1x8_t> : public cast_to_b1x8_gt<f64_t> {};
986
1026
 
987
- template <> struct cast_gt<f16_t, i8_t> : public cast_gt<f16_t, i8_converted_t> {};
988
- template <> struct cast_gt<f32_t, i8_t> : public cast_gt<f32_t, i8_converted_t> {};
989
- template <> struct cast_gt<f64_t, i8_t> : public cast_gt<f64_t, i8_converted_t> {};
1027
+ template <> struct cast_gt<b1x8_t, i8_t> : public cast_from_b1x8_gt<i8_t> {};
1028
+ template <> struct cast_gt<i8_t, b1x8_t> : public cast_to_b1x8_gt<i8_t> {};
990
1029
 
991
1030
  /* Don't complain if the vectorization of the inner loops fails:
992
1031
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.13.3",
3
+ "version": "2.13.5",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian (https://ashvardanian.com/)",
6
6
  "license": "Apache 2.0",
Binary file
Binary file
@@ -73,7 +73,7 @@ SIMSIMD_PUBLIC void simsimd_jaccard_b8_serial(simsimd_b8_t const* a, simsimd_b8_
73
73
  simsimd_i32_t intersection = 0, union_ = 0;
74
74
  for (simsimd_size_t i = 0; i != n_words; ++i)
75
75
  intersection += simsimd_popcount_b8(a[i] & b[i]), union_ += simsimd_popcount_b8(a[i] | b[i]);
76
- *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 0;
76
+ *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 1;
77
77
  }
78
78
 
79
79
  #if SIMSIMD_TARGET_ARM
@@ -110,7 +110,7 @@ SIMSIMD_PUBLIC void simsimd_jaccard_b8_neon(simsimd_b8_t const* a, simsimd_b8_t
110
110
  // Handle the tail
111
111
  for (; i != n_words; ++i)
112
112
  intersection += simsimd_popcount_b8(a[i] & b[i]), union_ += simsimd_popcount_b8(a[i] | b[i]);
113
- *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 0;
113
+ *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 1;
114
114
  }
115
115
 
116
116
  #pragma clang attribute pop
@@ -148,7 +148,7 @@ SIMSIMD_PUBLIC void simsimd_jaccard_b8_sve(simsimd_b8_t const* a, simsimd_b8_t c
148
148
  union_ += svaddv_u8(svptrue_b8(), svcnt_u8_x(svptrue_b8(), svorr_u8_m(svptrue_b8(), a_vec, b_vec)));
149
149
  i += svcntb();
150
150
  } while (i < n_words);
151
- *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 0;
151
+ *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 1;
152
152
  }
153
153
 
154
154
  #pragma clang attribute pop
@@ -213,7 +213,7 @@ simsimd_jaccard_b8_ice_cycle:
213
213
 
214
214
  simsimd_size_t intersection = _mm512_reduce_add_epi64(intersection_vec),
215
215
  union_ = _mm512_reduce_add_epi64(union_vec);
216
- *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 0;
216
+ *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 1;
217
217
  }
218
218
 
219
219
  #pragma clang attribute pop
@@ -245,7 +245,7 @@ SIMSIMD_PUBLIC void simsimd_jaccard_b8_haswell(simsimd_b8_t const* a, simsimd_b8
245
245
  union_ += _mm_popcnt_u64(*(simsimd_u64_t const*)a | *(simsimd_u64_t const*)b);
246
246
  for (; n_words; --n_words, ++a, ++b)
247
247
  intersection += _mm_popcnt_u32(*a & *b), union_ += _mm_popcnt_u32(*a | *b);
248
- *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 0;
248
+ *result = (union_ != 0) ? 1 - (simsimd_f32_t)intersection / (simsimd_f32_t)union_ : 1;
249
249
  }
250
250
 
251
251
  #pragma clang attribute pop
@@ -858,6 +858,22 @@ inline simsimd_f64_t _mm256_reduce_add_ps_dbl(__m256 vec) {
858
858
  return _mm_cvtsd_f64(sum128);
859
859
  }
860
860
 
861
+ SIMSIMD_PUBLIC void simsimd_dot_f32_haswell(simsimd_f32_t const* a, simsimd_f32_t const* b, simsimd_size_t n,
862
+ simsimd_distance_t* results) {
863
+
864
+ __m256 ab_vec = _mm256_setzero_ps();
865
+ simsimd_size_t i = 0;
866
+ for (; i + 8 <= n; i += 8) {
867
+ __m256 a_vec = _mm256_loadu_ps(a + i);
868
+ __m256 b_vec = _mm256_loadu_ps(b + i);
869
+ ab_vec = _mm256_fmadd_ps(a_vec, b_vec, ab_vec);
870
+ }
871
+ simsimd_f64_t ab = _mm256_reduce_add_ps_dbl(ab_vec);
872
+ for (; i < n; ++i)
873
+ ab += a[i] * b[i];
874
+ *results = ab;
875
+ }
876
+
861
877
  SIMSIMD_PUBLIC void simsimd_dot_f32c_haswell(simsimd_f32_t const* a, simsimd_f32_t const* b, simsimd_size_t n,
862
878
  simsimd_distance_t* results) {
863
879
 
@@ -527,6 +527,16 @@ SIMSIMD_PUBLIC void simsimd_find_metric_punned( //
527
527
  default: break;
528
528
  }
529
529
  #endif
530
+ #if SIMSIMD_TARGET_HASWELL
531
+ if (viable & simsimd_cap_haswell_k)
532
+ switch (kind) {
533
+ case simsimd_metric_dot_k: *m = (m_t)&simsimd_dot_f32_haswell, *c = simsimd_cap_haswell_k; return;
534
+ case simsimd_metric_cos_k: *m = (m_t)&simsimd_cos_f32_haswell, *c = simsimd_cap_haswell_k; return;
535
+ case simsimd_metric_l2sq_k: *m = (m_t)&simsimd_l2sq_f32_haswell, *c = simsimd_cap_haswell_k; return;
536
+ default: break;
537
+ }
538
+ #endif
539
+
530
540
  if (viable & simsimd_cap_serial_k)
531
541
  switch (kind) {
532
542
  case simsimd_metric_dot_k: *m = (m_t)&simsimd_dot_f32_serial, *c = simsimd_cap_serial_k; return;
@@ -1120,6 +1130,8 @@ SIMSIMD_PUBLIC void simsimd_dot_f32(simsimd_f32_t const* a, simsimd_f32_t const*
1120
1130
  simsimd_dot_f32_neon(a, b, n, d);
1121
1131
  #elif SIMSIMD_TARGET_SKYLAKE
1122
1132
  simsimd_dot_f32_skylake(a, b, n, d);
1133
+ #elif SIMSIMD_TARGET_HASWELL
1134
+ simsimd_dot_f32_haswell(a, b, n, d);
1123
1135
  #else
1124
1136
  simsimd_dot_f32_serial(a, b, n, d);
1125
1137
  #endif
@@ -1292,6 +1304,8 @@ SIMSIMD_PUBLIC void simsimd_cos_f32(simsimd_f32_t const* a, simsimd_f32_t const*
1292
1304
  simsimd_cos_f32_neon(a, b, n, d);
1293
1305
  #elif SIMSIMD_TARGET_SKYLAKE
1294
1306
  simsimd_cos_f32_skylake(a, b, n, d);
1307
+ #elif SIMSIMD_TARGET_HASWELL
1308
+ simsimd_cos_f32_haswell(a, b, n, d);
1295
1309
  #else
1296
1310
  simsimd_cos_f32_serial(a, b, n, d);
1297
1311
  #endif
@@ -1338,6 +1352,8 @@ SIMSIMD_PUBLIC void simsimd_l2sq_f32(simsimd_f32_t const* a, simsimd_f32_t const
1338
1352
  simsimd_l2sq_f32_neon(a, b, n, d);
1339
1353
  #elif SIMSIMD_TARGET_SKYLAKE
1340
1354
  simsimd_l2sq_f32_skylake(a, b, n, d);
1355
+ #elif SIMSIMD_TARGET_HASWELL
1356
+ simsimd_l2sq_f32_haswell(a, b, n, d);
1341
1357
  #else
1342
1358
  simsimd_l2sq_f32_serial(a, b, n, d);
1343
1359
  #endif
@@ -27,6 +27,8 @@
27
27
 
28
28
  #include "types.h"
29
29
 
30
+ #include "dot.h" // `_mm256_reduce_add_ps_dbl`
31
+
30
32
  #ifdef __cplusplus
31
33
  extern "C" {
32
34
  #endif
@@ -93,6 +95,8 @@ SIMSIMD_PUBLIC void simsimd_l2sq_f16_haswell(simsimd_f16_t const* a, simsimd_f16
93
95
  SIMSIMD_PUBLIC void simsimd_cos_f16_haswell(simsimd_f16_t const* a, simsimd_f16_t const* b, simsimd_size_t n, simsimd_distance_t*);
94
96
  SIMSIMD_PUBLIC void simsimd_l2sq_bf16_haswell(simsimd_bf16_t const* a, simsimd_bf16_t const* b, simsimd_size_t n, simsimd_distance_t*);
95
97
  SIMSIMD_PUBLIC void simsimd_cos_bf16_haswell(simsimd_bf16_t const* a, simsimd_bf16_t const* b, simsimd_size_t n, simsimd_distance_t*);
98
+ SIMSIMD_PUBLIC void simsimd_l2sq_f32_haswell(simsimd_f32_t const* a, simsimd_f32_t const* b, simsimd_size_t n, simsimd_distance_t*);
99
+ SIMSIMD_PUBLIC void simsimd_cos_f32_haswell(simsimd_f32_t const* a, simsimd_f32_t const* b, simsimd_size_t n, simsimd_distance_t*);
96
100
 
97
101
  /* SIMD-powered backends for AVX512 CPUs of Skylake generation and newer, using 32-bit arithmetic over 512-bit words.
98
102
  * Skylake was launched in 2015, and discontinued in 2019. Skylake had support for F, CD, VL, DQ, and BW extensions,
@@ -839,8 +843,7 @@ SIMSIMD_PUBLIC void simsimd_cos_f16_haswell(simsimd_f16_t const* a, simsimd_f16_
839
843
  __m128 b2_sqrt_recip = _mm_rsqrt_ss(_mm_set_ss((float)b2));
840
844
  __m128 result_vec = _mm_mul_ss(a2_sqrt_recip, b2_sqrt_recip); // Multiply the reciprocal square roots
841
845
  result_vec = _mm_mul_ss(result_vec, _mm_set_ss((float)ab)); // Multiply by ab
842
- result_vec = _mm_sub_ss(_mm_set_ss(1.0f), result_vec); // Subtract from 1
843
- *result = ab != 0 ? _mm_cvtss_f32(result_vec) : 1; // Extract the final result
846
+ *result = ab != 0 ? 1 - _mm_cvtss_f32(result_vec) : 1; // Extract the final result
844
847
  }
845
848
 
846
849
  SIMSIMD_PUBLIC void simsimd_l2sq_bf16_haswell(simsimd_bf16_t const* a, simsimd_bf16_t const* b, simsimd_size_t n,
@@ -946,8 +949,7 @@ SIMSIMD_PUBLIC void simsimd_cos_bf16_haswell(simsimd_bf16_t const* a, simsimd_bf
946
949
  __m128 b2_sqrt_recip = _mm_rsqrt_ss(_mm_set_ss((float)b2));
947
950
  __m128 result_vec = _mm_mul_ss(a2_sqrt_recip, b2_sqrt_recip); // Multiply the reciprocal square roots
948
951
  result_vec = _mm_mul_ss(result_vec, _mm_set_ss((float)ab)); // Multiply by ab
949
- result_vec = _mm_sub_ss(_mm_set_ss(1.0f), result_vec); // Subtract from 1
950
- *result = ab != 0 ? _mm_cvtss_f32(result_vec) : 1; // Extract the final result
952
+ *result = ab != 0 ? 1 - _mm_cvtss_f32(result_vec) : 1; // Extract the final result
951
953
  }
952
954
 
953
955
  SIMSIMD_PUBLIC void simsimd_l2sq_i8_haswell(simsimd_i8_t const* a, simsimd_i8_t const* b, simsimd_size_t n,
@@ -1058,7 +1060,60 @@ SIMSIMD_PUBLIC void simsimd_cos_i8_haswell(simsimd_i8_t const* a, simsimd_i8_t c
1058
1060
  // Compute cosine similarity: ab / sqrt(a2 * b2)
1059
1061
  __m128 denom = _mm_mul_ss(a2_sqrt_recip, b2_sqrt_recip); // Reciprocal of sqrt(a2 * b2)
1060
1062
  __m128 result_vec = _mm_mul_ss(_mm_set_ss((float)ab), denom); // ab * reciprocal of sqrt(a2 * b2)
1061
- *result = ab != 0 ? 1 - _mm_cvtss_f32(result_vec) : 0; // Extract the final result
1063
+ *result = ab != 0 ? 1 - _mm_cvtss_f32(result_vec) : 1; // Extract the final result
1064
+ }
1065
+
1066
+ SIMSIMD_PUBLIC void simsimd_l2sq_f32_haswell(simsimd_f32_t const* a, simsimd_f32_t const* b, simsimd_size_t n,
1067
+ simsimd_distance_t* result) {
1068
+
1069
+ __m256 d2_vec = _mm256_setzero_ps();
1070
+ simsimd_size_t i = 0;
1071
+ for (; i + 8 <= n; i += 8) {
1072
+ __m256 a_vec = _mm256_loadu_ps(a + i);
1073
+ __m256 b_vec = _mm256_loadu_ps(b + i);
1074
+ __m256 d_vec = _mm256_sub_ps(a_vec, b_vec);
1075
+ d2_vec = _mm256_fmadd_ps(d_vec, d_vec, d2_vec);
1076
+ }
1077
+
1078
+ simsimd_f64_t d2 = _mm256_reduce_add_ps_dbl(d2_vec);
1079
+ for (; i < n; ++i) {
1080
+ float d = a[i] - b[i];
1081
+ d2 += d * d;
1082
+ }
1083
+
1084
+ *result = d2;
1085
+ }
1086
+
1087
+ SIMSIMD_PUBLIC void simsimd_cos_f32_haswell(simsimd_f32_t const* a, simsimd_f32_t const* b, simsimd_size_t n,
1088
+ simsimd_distance_t* result) {
1089
+
1090
+ __m256 ab_vec = _mm256_setzero_ps();
1091
+ __m256 a2_vec = _mm256_setzero_ps();
1092
+ __m256 b2_vec = _mm256_setzero_ps();
1093
+ simsimd_size_t i = 0;
1094
+ for (; i + 8 <= n; i += 8) {
1095
+ __m256 a_vec = _mm256_loadu_ps(a + i);
1096
+ __m256 b_vec = _mm256_loadu_ps(b + i);
1097
+ ab_vec = _mm256_fmadd_ps(a_vec, b_vec, ab_vec);
1098
+ a2_vec = _mm256_fmadd_ps(a_vec, a_vec, a2_vec);
1099
+ b2_vec = _mm256_fmadd_ps(b_vec, b_vec, b2_vec);
1100
+ }
1101
+ simsimd_f64_t ab = _mm256_reduce_add_ps_dbl(ab_vec);
1102
+ simsimd_f64_t a2 = _mm256_reduce_add_ps_dbl(a2_vec);
1103
+ simsimd_f64_t b2 = _mm256_reduce_add_ps_dbl(b2_vec);
1104
+ for (; i < n; ++i) {
1105
+ float ai = a[i], bi = b[i];
1106
+ ab += ai * bi, a2 += ai * ai, b2 += bi * bi;
1107
+ }
1108
+
1109
+ // Compute the reciprocal of the square roots
1110
+ __m128 a2_sqrt_recip = _mm_rsqrt_ss(_mm_set_ss((float)a2));
1111
+ __m128 b2_sqrt_recip = _mm_rsqrt_ss(_mm_set_ss((float)b2));
1112
+
1113
+ // Compute cosine similarity: ab / sqrt(a2 * b2)
1114
+ __m128 denom = _mm_mul_ss(a2_sqrt_recip, b2_sqrt_recip); // Reciprocal of sqrt(a2 * b2)
1115
+ __m128 result_vec = _mm_mul_ss(_mm_set_ss((float)ab), denom); // ab * reciprocal of sqrt(a2 * b2)
1116
+ *result = ab != 0 ? 1 - _mm_cvtss_f32(result_vec) : 1; // Extract the final result
1062
1117
  }
1063
1118
 
1064
1119
  #pragma clang attribute pop
@@ -1282,7 +1337,7 @@ simsimd_cos_bf16_genoa_cycle:
1282
1337
  __m128 rsqrts = _mm_rsqrt14_ps(_mm_set_ps(0.f, 0.f, a2 + 1.e-9f, b2 + 1.e-9f));
1283
1338
  simsimd_f32_t rsqrt_a2 = _mm_cvtss_f32(rsqrts);
1284
1339
  simsimd_f32_t rsqrt_b2 = _mm_cvtss_f32(_mm_shuffle_ps(rsqrts, rsqrts, _MM_SHUFFLE(0, 0, 0, 1)));
1285
- *result = ab != 0 ? 1 - ab * rsqrt_a2 * rsqrt_b2 : 0;
1340
+ *result = ab != 0 ? 1 - ab * rsqrt_a2 * rsqrt_b2 : 1;
1286
1341
  }
1287
1342
 
1288
1343
  #pragma clang attribute pop
@@ -1350,7 +1405,7 @@ simsimd_cos_f16_sapphire_cycle:
1350
1405
  __m128 rsqrts = _mm_rsqrt14_ps(_mm_set_ps(0.f, 0.f, a2 + 1.e-9f, b2 + 1.e-9f));
1351
1406
  simsimd_f32_t rsqrt_a2 = _mm_cvtss_f32(rsqrts);
1352
1407
  simsimd_f32_t rsqrt_b2 = _mm_cvtss_f32(_mm_shuffle_ps(rsqrts, rsqrts, _MM_SHUFFLE(0, 0, 0, 1)));
1353
- *result = ab != 0 ? 1 - ab * rsqrt_a2 * rsqrt_b2 : 0;
1408
+ *result = ab != 0 ? 1 - ab * rsqrt_a2 * rsqrt_b2 : 1;
1354
1409
  }
1355
1410
 
1356
1411
  #pragma clang attribute pop
@@ -1440,7 +1495,7 @@ simsimd_cos_i8_ice_cycle:
1440
1495
  __m128 rsqrts = _mm_maskz_rsqrt14_ps(0xFF, _mm_set_ps(0.f, 0.f, a2 + 1.e-9f, b2 + 1.e-9f));
1441
1496
  simsimd_f32_t rsqrt_a2 = _mm_cvtss_f32(rsqrts);
1442
1497
  simsimd_f32_t rsqrt_b2 = _mm_cvtss_f32(_mm_shuffle_ps(rsqrts, rsqrts, _MM_SHUFFLE(0, 0, 0, 1)));
1443
- *result = ab != 0 ? 1 - ab * rsqrt_a2 * rsqrt_b2 : 0;
1498
+ *result = ab != 0 ? 1 - ab * rsqrt_a2 * rsqrt_b2 : 1;
1444
1499
  }
1445
1500
 
1446
1501
  #pragma clang attribute pop